From fb8b737efeeb825a66c124dc1cbfc834a02b8088 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 23 Oct 2023 18:03:16 +0900 Subject: [PATCH 001/412] Created a use case diagram for veYMT. --- doc/v1.5/veYMT/usecase.md | 156 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 doc/v1.5/veYMT/usecase.md diff --git a/doc/v1.5/veYMT/usecase.md b/doc/v1.5/veYMT/usecase.md new file mode 100644 index 00000000..0b397557 --- /dev/null +++ b/doc/v1.5/veYMT/usecase.md @@ -0,0 +1,156 @@ +```mermaid +graph LR + classDef transparent fill:none,stroke:none; + owner{{オーナー}} + holder{{YMTホルダー}} + yamato{{Yamato操作}} + user{{ユーザ}} + + create_lock[YMT をロックする] + increase_amount[YMT ロック量を増額する] + increase_unlock_time[YMT ロック期間を延長する] + withdraw[YMTを引き出す] + uck[ユーザのポイント履歴を更新] + ck[全体のポイント履歴を更新] + ve_total_supply[ポイント履歴を取得] + + mint[Tokenをmint] + rate[インフレーションレートの取得] + future_epoch_time_write[次回のインフレーションレート変更タイムスタンプ取得] + update_rate[インフレーションレート更新] + update_minted[ユーザのミント済みYMTトークン額を更新] + minted[ユーザのミント済みYMTトークン額を取得] + claim_YMT[YMTの報酬をクレーム] + + change_admin[管理者を変更する] + change_smchecker[スマートウォレットチェッカーを変更する] + + + claimable_tokens[ + YMTのトータルmint額を取得 + integrate_fraction + ] + user_checkpoint[ + ユーザーの報酬を更新 + user_checkpoint + ] + + gauge_relative_weight[ゲージの重みを取得する] + + collateral_ratio[担保率を取得] + balance_issued[CJPYの発行額を取得] + first_checkpoint[チェックポイントを更新する] + depositor + borrower + repayer + withdrawer + redeemer + sweeper + + create_lock -.->|include| uck + increase_amount -.->|include| uck + increase_unlock_time -.->|include| uck + withdraw -.->|include| uck + uck -.->|include| ck + user --- ck + holder --- create_lock + holder --- increase_amount + holder --- increase_unlock_time + holder --- withdraw + + user ---> holder + user --- claim_YMT + user ---|View関数として実行| claimable_tokens + + claimable_tokens -.->|include| claim_YMT + claim_YMT -.->|include| mint + claim_YMT -.-|view| minted + claim_YMT -.->|include| update_minted + + future_epoch_time_write -.->|include| update_rate + + user ---> |v1→v1.5の初回実行する必要あり| first_checkpoint + user_checkpoint -.->|include| claimable_tokens + ve_total_supply -.-|view| user_checkpoint + rate -.-|view| user_checkpoint + future_epoch_time_write -.-|include| user_checkpoint + + owner --- change_admin + owner --- change_smchecker + + user --- yamato + yamato --- depositor + yamato --- borrower + yamato --- repayer + yamato --- withdrawer + yamato --- redeemer + yamato --- sweeper + + depositor -.->|include| user_checkpoint + borrower -.->|include| user_checkpoint + repayer -.->|include| user_checkpoint + withdrawer -.->|include| user_checkpoint + redeemer -.->|include| user_checkpoint + sweeper -.->|include| user_checkpoint + first_checkpoint -.->|include| user_checkpoint + collateral_ratio -.-|view| user_checkpoint + balance_issued -.-|view| user_checkpoint + gauge_relative_weight -.-|view| user_checkpoint + + subgraph Admin[ ] + direction LR + end + + class Admin transparent + + subgraph Gauge + claimable_tokens + user_checkpoint + end + + subgraph Gauge Controller + gauge_relative_weight + end + + subgraph veYMT + ve_total_supply + direction LR + create_lock + increase_amount + increase_unlock_time + withdraw + uck + ck + change_admin + change_smchecker + end + + subgraph YMT + direction LR + mint + rate + future_epoch_time_write + update_rate + end + + subgraph Minter + direction LR + claim_YMT + minted + update_minted + end + + subgraph Yamato + collateral_ratio + balance_issued + first_checkpoint + depositor + borrower + repayer + withdrawer + redeemer + sweeper + end + + +``` \ No newline at end of file From c424185bb8d68ec25f52a2b20e5c428836c4bb55 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 24 Oct 2023 11:15:43 +0900 Subject: [PATCH 002/412] Documentation of v1.5 --- doc/v1.5/Controller/index.md | 245 ++++++++++++++++++++++++ doc/v1.5/FeeDistribution/usecase.md | 81 ++++++++ doc/v1.5/Gauge/index.md | 163 ++++++++++++++++ doc/v1.5/GaugeController/class.md | 70 +++++++ doc/v1.5/Minter/index.md | 81 ++++++++ doc/v1.5/YMT/index.md | 167 +++++++++++++++++ doc/v1.5/veYMT/index.md | 276 ++++++++++++++++++++++++++++ doc/v1.5/veYMT/usecase.md | 2 +- 8 files changed, 1084 insertions(+), 1 deletion(-) create mode 100644 doc/v1.5/Controller/index.md create mode 100644 doc/v1.5/FeeDistribution/usecase.md create mode 100644 doc/v1.5/Gauge/index.md create mode 100644 doc/v1.5/GaugeController/class.md create mode 100644 doc/v1.5/Minter/index.md create mode 100644 doc/v1.5/YMT/index.md create mode 100644 doc/v1.5/veYMT/index.md diff --git a/doc/v1.5/Controller/index.md b/doc/v1.5/Controller/index.md new file mode 100644 index 00000000..dcd85056 --- /dev/null +++ b/doc/v1.5/Controller/index.md @@ -0,0 +1,245 @@ + + +## 定数 + +### `WEEK: constant(uint256) = 604800` +- **説明**: 1週間の秒数。現在の値は604800秒です。 + +### `WEIGHT_VOTE_DELAY: constant(uint256) = 10 * 86400` +- **説明**: 重みの投票を変更することができるのは10日に1回だけです。現在の値は10日(864,000秒)です。 + +### `MULTIPLIER: constant(uint256) = 10 ** 18` +- **説明**: 多数の固定小数点数を扱うための乗数。現在の値は10の18乗です。 + +--- + +## 変数 + +### `admin: public(address)` +- **説明**: 管理者のアドレス。このアドレスはスマートコントラクトに許可されています。 + +### `future_admin: public(address)` +- **説明**: 未来の管理者のアドレス。所有権の移転が計画されています。 + +### `token: public(address)` +- **説明**: CRVトークンのアドレス。 + +### `voting_escrow: public(address)` +- **説明**: 投票エスクローのアドレス。 + +### `n_gauge_types: public(int128)` +- **説明**: 使用可能なゲージの種類の数。 + +### `n_gauges: public(int128)` +- **説明**: 現在のゲージの数。 + +### `gauge_type_names: public(HashMap[int128, String[64]])` +- **説明**: 各ゲージの種類の名前を格納するマップ。 + +### `gauges: public(address[1000000000])` +- **説明**: ゲージのアドレスを格納するリスト。 + +### `gauge_types_: HashMap[address, int128]` +- **説明**: 各ゲージアドレスとそのタイプを関連付けるマップ。 + +### `vote_user_slopes: public(HashMap[address, HashMap[address, VotedSlope]])` +- **説明**: ユーザーの各ゲージアドレスに対する投票傾斜を格納するマップ。 + +### `vote_user_power: public(HashMap[address, uint256])` +- **説明**: ユーザーが使用する総投票パワー。 + +### `last_user_vote: public(HashMap[address, HashMap[address, uint256]])` +- **説明**: 各ゲージアドレスの最後のユーザー投票のタイムスタンプ。 + +### `points_weight: public(HashMap[address, HashMap[uint256, Point]])` +- **説明**: 各ゲージアドレスと時間に対する点数の重みを格納するマップ。 + +### `changes_weight: HashMap[address, HashMap[uint256, uint256]]` +- **説明**: 各ゲージアドレスと時間に対する重みの変更を格納するマップ。 + +### `time_weight: public(HashMap[address, uint256])` +- **説明**: 各ゲージアドレスの最後のスケジュールされた時間。 + +### `points_sum: public(HashMap[int128, HashMap[uint256, Point]])` +- **説明**: ゲージタイプと時間に対する点の合計を格納するマップ。 + +### `changes_sum: HashMap[int128, HashMap[uint256, uint256]]` +- **説明**: ゲージタイプと時間に対する合計の変更を格納するマップ。 + +### `time_sum: public(uint256[1000000000])` +- **説明**: ゲージタイプの最後のスケジュールされた時間。 + +### `points_total: public(HashMap[uint256, uint256])` +- **説明**: 時間ごとの総重みを格納するマップ。 + +### `time_total: public(uint256)` +- **説明**: 総重みの最後のスケジュールされた時間。 + +--- + +## イベント + +### `CommitOwnership` +- **説明**: 所有権のコミットイベント。 +- **パラメータ**: + - `admin`: 管理者のアドレス。 + +### `ApplyOwnership` +- **説明**: 所有権の適用イベント。 +- **パラメータ**: + - `admin`: 管理者のアドレス。 + +### `AddType` +- **説明**: 新しいタイプの追加イベント。 +- **パラメータ**: + - `name`: タイプの名前。 + - `type_id`: タイプID。 + +### `NewTypeWeight` +- **説明**: 新しいタイプの重みイベント。 +- **パラメータ**: + - `type_id`: タイプID。 + - `time`: タイムスタンプ。 + - `weight`: 重み。 + - `total_weight`: 総重量。 + +### `NewGaugeWeight` +- **説明**: 新しいゲージの重みイベント。 +- **パラメータ**: + - `gauge_address`: ゲージのアドレス。 + - `time`: タイムスタンプ。 + - `weight`: 重み。 + - `total_weight`: 総重量。 + +### `VoteForGauge` +- **説明**: ゲージへの投票イベント。 +- **パラメータ**: + - `time`: タイムスタンプ。 + - `user`: ユーザーのアドレス。 + - `gauge_addr`: ゲージのアドレス。 + - `weight`: 重み。 + +### `NewGauge` +- **説明**: 新しいゲージのイベント。 +- **パラメータ**: + - `addr`: ゲージのアドレス。 + - `gauge_type`: ゲージのタイプ。 + - `weight`: 重み。 + +--- + +## 関数 + +### `__init__(_token: address, _voting_escrow: address)` +- **説明**: コントラクトの初期化関数。初期設定を行います。 +- **パラメータ**: + - `_token`: `ERC20CRV` コントラクトのアドレス。 + - `_voting_escrow`: `VotingEscrow` コントラクトのアドレス。 + +### `commit_transfer_ownership(addr: address)` +- **説明**: GaugeControllerの所有権を`addr`に転送するためのコミット関数。 +- **パラメータ**: + - `addr`: 所有権が転送されるアドレス。 + +### `apply_transfer_ownership()` +- **説明**: 保留中の所有権転送を適用する関数。 + +### `gauge_types(_addr: address) -> int128` +- **説明**: 指定したアドレスのゲージタイプを取得する関数。 +- **パラメータ**: + - `_addr`: ゲージのアドレス。 + +### `_get_type_weight(gauge_type: int128) -> uint256` +- **説明**: 歴史的なタイプの重みを計算し、未チェックインの週に記入します。また、次の週のタイプの重みを返します。 +- **パラメータ**: + - `gauge_type`: ゲージタイプのID。 + +### `_get_sum(gauge_type: int128) -> uint256` +- **説明**: 同じタイプのゲージの重みの合計を計算し、未チェックインの週に記入します。また、次の週の合計を返します。 +- **パラメータ**: + - `gauge_type`: ゲージタイプのID。 + +### `_get_total() -> uint256` +- **説明**: 歴史的な全体の重みを計算し、未チェックインの週に記入します。また、次の週の合計を返します。 + +### `_get_weight(gauge_addr: address) -> uint256` +- **説明**: 歴史的なゲージの重みを計算し、未チェックインの週に記入します。また、次の週のゲージの重みを返します。 +- **パラメータ**: + - `gauge_addr`: ゲージのアドレス。 + +### `add_gauge(addr: address, gauge_type: int128, weight: uint256 = 0)` +- **説明**: 指定されたタイプと重みでゲージを追加します。 +- **パラメータ**: + - `addr`: ゲージのアドレス。 + - `gauge_type`: ゲージタイプ。 + - `weight`: ゲージの重み。 + +### `checkpoint()` +- **説明**: すべてのゲージに共通のデータを記入するためのチェックポイント。 + +### `checkpoint_gauge(addr: address)` +- **説明**: 特定のゲージとすべてのゲージに共通のデータを記入するためのチェックポイント。 +- **パラメータ**: + - `addr`: ゲージのアドレス。 + +### `_gauge_relative_weight(addr: address, time: uint256) -> uint256` +- **説明**: 指定されたタイムスタンプでのゲージの相対重み(1.0を超えない)を取得します。 +- **パラメータ**: + - `addr`: ゲージのアドレス。 + - `time`: 指定したタイムスタンプ。 + +### `gauge_relative_weight(addr: address, time: uint256 = block.timestamp) -> uint256` +- **説明**: 指定されたタイムスタンプでのゲージの相対重みを取得します。 +- **パラメータ**: + - `addr`: ゲージのアドレス。 + - `time`: 指定したタイムスタンプ。 + +### `gauge_relative_weight_write(addr: address, time: uint256 = block.timestamp) -> uint256` +- **説明**: + + 指定されたタイムスタンプでのゲージの相対重みを書き込みます。 +- **パラメータ**: + - `addr`: ゲージのアドレス。 + - `time`: 指定したタイムスタンプ。 + +### `add_type(_name: String[64], weight: uint256 = 0)` +- **説明**: ゲージタイプを追加します。ゲージタイプ名 `_name` と重み `weight` を指定して追加します。 +- **パラメータ**: + - `_name`: ゲージタイプの名前。 + - `weight`: ゲージタイプの重み。 + +### `change_type_weight(type_id: int128, weight: uint256)` +- **説明**: ゲージタイプの重みを変更します。 +- **パラメータ**: + - `type_id`: ゲージタイプID。 + - `weight`: 新しいゲージの重み。 + +### `change_gauge_weight(addr: address, weight: uint256)` +- **説明**: ゲージの重みを変更します。 +- **パラメータ**: + - `addr`: `GaugeController` コントラクトのアドレス。 + - `weight`: 新しいゲージの重み。 + +### `vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256)` +- **説明**: プールの重みを変更するための投票パワーを割り当てます。 +- **パラメータ**: + - `_gauge_addr`: 投票対象のゲージアドレス。 + - `_user_weight`: ゲージの重み (bps単位、0.01%の単位)。 + +### `get_gauge_weight(addr: address) -> uint256` +- **説明**: 現在のゲージの重みを取得します。 +- **パラメータ**: + - `addr`: ゲージのアドレス。 + +### `get_type_weight(type_id: int128) -> uint256` +- **説明**: 現在のタイプの重みを取得します。 +- **パラメータ**: + - `type_id`: タイプID。 + +### `get_total_weight() -> uint256` +- **説明**: 現在の合計 (タイプごとの重み付け) 重みを取得します。 + +### `get_weights_sum_per_type(type_id: int128) -> uint256` +- **説明**: タイプごとのゲージの重みの合計を取得します。 +- **パラメータ**: + - `type_id`: タイプID。 diff --git a/doc/v1.5/FeeDistribution/usecase.md b/doc/v1.5/FeeDistribution/usecase.md new file mode 100644 index 00000000..9816d5ae --- /dev/null +++ b/doc/v1.5/FeeDistribution/usecase.md @@ -0,0 +1,81 @@ +# 未完成 + + +```mermaid +graph LR + owner{{コントラクトオーナー}} + user{{ユーザ}} + deposit_reward_ether[ETHを入金する] + send_fee_eth[ETHを入金する] + claim[報酬をクレームする] + claim_many[複数アドレス(最大20アドレス)の報酬をまとめてクレームする] + claim_multiple_tokens[複数トークン(最大20トークン)の報酬をまとめてクレームする] + claimable_token[報酬額を取得する] + user_point_history[ユーザのポイント履歴を取得] + point_history[全体のポイント履歴を取得] + sync_ve[最大20週分のve履歴を取得・保存] + sync_user_ve[最大50エポック分のユーザve履歴を取得・保存] + checkpoint_token[入金された手数料を週ごとに配分] + set_admin[管理者を変更する] + kill[FeeDistributorを非アクティブにする] + evacuate[緊急時トークン送金先にトークンを送金する] + borrowing[借入] + + owner --- kill + owner --- evacuate + owner --- set_admin + + + send_fee_eth -.->|include| deposit_reward_ether + borrowing -.->|include| send_fee_eth + + deposit_reward_ether -.->|include| sync_ve + deposit_reward_ether -.->|include| checkpoint_token + + user --- claim + user --- claim_many + user --- claim_multiple_tokens + user ---|View関数| claimable_token + user --- borrowing + + claim -.->|include| claimable_token + claim_many -.->|include| claimable_token + claim_multiple_tokens -.->|include| claimable_token + claimable_token -.->|include| sync_ve + claimable_token -.->|include| checkpoint_token + claimable_token -.->|include| sync_user_ve + + sync_user_ve -.->|include| user_point_history + sync_ve -.->|include| point_history + + + subgraph FeePool + direction LR + send_fee_eth + end + + subgraph FeeDistributor + direction LR + kill + evacuate + set_admin + deposit_reward_ether + claim + claim_many + claim_multiple_tokens + claimable_token + sync_ve + sync_user_ve + checkpoint_token + end + + subgraph VotinEscrow + direction LR + user_point_history + point_history + end + + subgraph Yamato + borrowing + end +``` \ No newline at end of file diff --git a/doc/v1.5/Gauge/index.md b/doc/v1.5/Gauge/index.md new file mode 100644 index 00000000..a5ef3f8c --- /dev/null +++ b/doc/v1.5/Gauge/index.md @@ -0,0 +1,163 @@ +# Gauge + +## 概要 + +Gaugeは、Yamato Protocolによって提供される報酬ゲージを計算・保存するコントラクトです。 + +--- + +## 定数 + +### `VERSION: constant(String[8]) = "v1"` +- **説明**: コントラクトのバージョン情報。 + +### `MAX_REWARDS` +- **説明**: 同時に追跡できる報酬の最大数。現在は8です。 + +### `TOKENLESS_PRODUCTION` +- **説明**: トークンなしでの生産可能な最大値。現在の値は40です。 + +### `WEEK` +- **説明**: 1週間の秒数。現在の値は604800秒です。 + +### 各アドレス定数 (`MINTER`, `YMT`, `veYMT`) +- **説明**: 特定の役割を持つ外部コントラクトのアドレスを定義しています。 + +--- + +## 変数 + +### `NAME` +- **説明**: ゲージの名前。 + +### `TOKEN_ADDRESS` +- **説明**: 対象となるトークンのアドレス。 + +### `nonces` +- **説明**: 各アドレスのnonceのマッピング。 + +### `future_epoch_time` +- **説明**: 未来のエポック時間。 + +### `balanceOf` +- **説明**: 各アドレスのバランスのマッピング。 + +### `totalSupply` +- **説明**: 合計供給量。 + +### `working_balances` +- **説明**: 各アドレスの動作中のバランス。 + +### `working_supply` +- **説明**: 動作中の供給量。 + +### `reward_count`, `reward_tokens`, `reward_data` +- **説明**: 外部報酬の追跡のための変数。 + +### `rewards_receiver` +- **説明**: 請求者からデフォルトの報酬受取人へのマッピング。 + +### `reward_integral_for` +- **説明**: 報酬トークンに関する請求アドレスの積分のマッピング。 + +### `claim_data` +- **説明**: ユーザーに関する請求データのマッピング。 + +### `admin`, `future_admin`, `is_killed` +- **説明**: 管理者関連の変数。 + +### `integrate_inv_supply_of`, `integrate_checkpoint_of`, `integrate_fraction` +- **説明**: 供給や報酬に関する積分の計算のための変数。 + +### `inflation_rate` +- **説明**: インフレ率。 + +### `period`, `period_timestamp`, `integrate_inv_supply` +- **説明**: インフレ率の変化や供給の積分に関する変数。 + +--- + +## イベント + +### `UpdateLimit` +- **説明**: ユーザーの情報が更新された際に発火。 +- 各変数はユーザーの情報や更新後の情報を示します。 + +### その他のイベント(`CommitOwnership`, `ApplyOwnership`) +- **説明**: 各イベントはゲージの管理やトークンの移転、許可の変更に関連する操作を示します。 + +--- + +## 関数 + +### `__init__(_lp_token: address, _admin: address)` +- **説明**: コントラクトの初期化関数。初期設定を行います。 +- **パラメータ**: + - `_lp_token`: 流動性プールトークンのアドレス。 + - `_admin`: 管理者のアドレス。 + +### `integrate_checkpoint() -> uint256` +- **説明**: 現在の期間のタイムスタンプを取得する関数。 + +### `_update_liquidity_limit(addr: address, l: uint256, L: uint256)` +- **説明**: ユーザーの流動性の限界を更新する内部関数。CRVトークンの量に基づいてワーキングバランスを計算します。 +- **パラメータ**: + - `addr`: ユーザーのアドレス。 + - `l`: ユーザーの流動性の量。 + - `L`: 全体の流動性の量。 + +### `user_checkpoint(addr: address) -> bool` +- **説明**: ユーザー`addr`のチェックポイントを記録します。 +- **パラメータ**: + - `addr`: チェックポイントを記録するユーザーのアドレス。 + +### `claimable_tokens(addr: address) -> uint256` +- **説明**: チェックポイントを更新して、ユーザー`addr`に対して請求可能なトークンの数を取得します。 +- **パラメータ**: + - `addr`: 請求可能なトークンの数を取得するユーザーのアドレス。 + +### `claimed_reward(_addr: address, _token: address) -> uint256` +- **説明**: ユーザー`_addr`が既に請求した報酬トークンの数を取得します。 +- **パラメータ**: + - `_addr`: 既に請求した報酬トークンの数を取得するユーザーのアドレス。 + - `_token`: 既に請求した報酬トークンのアドレス。 + +### `claimable_reward(_user: address, _reward_token: address) -> uint256` +- **説明**: ユーザー`_user`に対して請求可能な報酬トークンの数を取得します。 +- **パラメータ**: + - `_user`: 請求可能な報酬トークンの数を取得するユーザーのアドレス。 + - `_reward_token`: 請求可能な報酬トークンのアドレス。 + +### `set_rewards_receiver(_receiver: address)` +- **説明**: 呼び出し元のデフォルトの報酬受信者を設定します。 +- **パラメータ**: + - `_receiver`: `claim_rewards`で報酬を受け取るアドレス。 + +### `claim_rewards(_addr: address = msg.sender, _receiver: address = ZERO_ADDRESS)` +- **説明**: `_addr`の利用可能な報酬トークンを請求します。 +- **パラメータ**: + - `_addr`: 報酬を請求するアドレス。 + - `_receiver`: 報酬を転送するアドレス。ZERO_ADDRESSに設定されている場合、呼び出し元のデフォルトの報酬受信者を使用します。 + +### `kick(addr: address)` +- **説明**: そのブーストを乱用している`addr`をキックします。 +- **パラメータ**: + - `addr`: キックするアドレス。 + +### `set_killed(_is_killed: bool)` +- **説明**: このコントラクトの`killed`ステータスを設定します。`killed`としてマークされた場合、このゲージは常にレート0を返すため、CRVを生成することはできません。 +- **パラメータ**: + - `_is_killed`: 設定する`killed`のステータス。 + +### `commit_transfer_ownership(addr: address)` +- **説明**: `Gauge`の所有権を`addr`に転送します。 +- **パラメータ**: + - `addr`: 所有権を転送するアドレス。 + +### `accept_transfer_ownership()` +- **説明**: 所有権転送を受け入れます。 + +### `version() -> String[8]` +- **説明**: このゲージのバージョンを取得します。 + + diff --git a/doc/v1.5/GaugeController/class.md b/doc/v1.5/GaugeController/class.md new file mode 100644 index 00000000..085ea049 --- /dev/null +++ b/doc/v1.5/GaugeController/class.md @@ -0,0 +1,70 @@ + +# GaugeController + + +```mermaid +classDiagram + + class GaugeController { + -admin: address + -future_admin: address + -token: address + -voting_escrow: address + -n_gauge_types: int128 + -n_gauges: int128 + -gauge_type_names: HashMap[int128, String[64]] + -gauges: address[1000000000] + -gauge_types_: HashMap[address, int128] + -vote_user_slopes: HashMap[address, HashMap[address, VotedSlope]] + -vote_user_power: HashMap[address, uint256] + -last_user_vote: HashMap[address, HashMap[address, uint256]] + -points_weight: HashMap[address, HashMap[uint256, Point]] + -changes_weight: HashMap[address, HashMap[uint256, uint256]] + -time_weight: HashMap[address, uint256] + -points_sum: HashMap[int128, HashMap[uint256, Point]] + -changes_sum: HashMap[int128, HashMap[uint256, uint256]] + -time_sum: uint256[1000000000] + -points_total: HashMap[uint256, uint256] + -time_total: uint256 + -points_type_weight: HashMap[int128, HashMap[uint256, uint256]] + -time_type_weight: uint256[1000000000] + +__init__(_token: address, _voting_escrow: address) + +commit_transfer_ownership(addr: address) + +apply_transfer_ownership() + +gauge_types(_addr: address): int128 + +add_gauge(addr: address, gauge_type: int128, weight: uint256) + +checkpoint() + +checkpoint_gauge(addr: address) + +gauge_relative_weight(addr: address, time: uint256): uint256 + +add_type(_name: String[64], weight: uint256) + +change_type_weight(type_id: int128, weight: uint256) + +change_gauge_weight(addr: address, weight: uint256) + +vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256) + +get_gauge_weight(addr: address): uint256 + +get_type_weight(type_id: int128): uint256 + +get_total_weight(): uint256 + +get_weights_sum_per_type(type_id: int128): uint256 + } + + class Point { + bias: uint256 + slope: uint256 + } + + class VotedSlope { + slope: uint256 + power: uint256 + end: uint256 + } + + class VotingEscrow { + +get_last_user_slope(addr: address): int128 + +locked__end(addr: address): uint256 + } + + GaugeController --|> Point + GaugeController --|> VotedSlope + GaugeController --|> VotingEscrow: Uses > + + +``` \ No newline at end of file diff --git a/doc/v1.5/Minter/index.md b/doc/v1.5/Minter/index.md new file mode 100644 index 00000000..e2a996cd --- /dev/null +++ b/doc/v1.5/Minter/index.md @@ -0,0 +1,81 @@ +# Token Minter + +## 概要 + +Token Minterは、特定のゲージからトークンをmintするためのコントラクトです。YMTトークンをmintする機能を提供します。また、ユーザーが他のユーザーに代わってmintする権限を与えることもできます。 + +--- + +## インターフェース + +### `Gauge`: +- **説明**: 流動性のゲージから情報を取得するためのインターフェース。 +- **関数**: + - `integrate_fraction(addr: address) -> uint256`: 指定したアドレスの積分値を取得する。 + - `user_checkpoint(addr: address) -> bool`: ユーザーチェックポイントを更新する。 + +### `YMT`: +- **説明**: トークンをmintするためのERC20トークンインターフェース。 +- **関数**: + - `mint(_to: address, _value: uint256) -> bool`: トークンをmintする。 + +--- + +## イベント + +### `Minted`: +- **説明**: トークンがmintされたときにトリガーされるイベント。 +- **パラメータ**: + - `recipient`: 受取人のアドレス。 + - `gauge`: 使用されたゲージのアドレス。 + - `minted`: mintされたトークンの量。 + +--- + +## 変数 + +### `token: public(address)` +- **説明**: mintするMERC20トークンのアドレス。 + +### `minted: public(HashMap[address, HashMap[address, uint256]])` +- **説明**: すでにmintされたトークンの量を追跡するためのマップ。 + +### `allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]])` +- **説明**: あるユーザーが別のユーザーに代わってトークンをmintすることを許可するためのマップ。 + +--- + +## 関数 + +### `__init__(_token: address, _controller: address)` +- **説明**: コントラクトの初期化関数。トークンとコントローラーのアドレスを設定する。 +- **パラメータ**: + - `_token`: MERC20トークンのアドレス。 + - `_controller`: GaugeControllerのアドレス。 + +### `_mint_for(gauge_addr: address, _for: address)` +- **説明**: 内部関数。指定したゲージからトークンをmintする。 +- **パラメータ**: + - `gauge_addr`: ゲージのアドレス。 + - `_for`: トークンを受け取るアドレス。 + +### `mint(gauge_addr: address)` +- **説明**: `msg.sender`に属するすべてのトークンをmintして送信する。 +- **パラメータ**: + - `gauge_addr`: ゲージのアドレス。 + +### `mint_many(gauge_addrs: address[8])` +- **説明**: 複数のゲージから`msg.sender`に属するすべてのトークンをmintする。 +- **パラメータ**: + - `gauge_addrs`: ゲージのアドレスのリスト。 + +### `mint_for(gauge_addr: address, _for: address)` +- **説明**: `_for`のためのトークンをmintする。`msg.sender`が承認されている場合のみ可能。 +- **パラメータ**: + - `gauge_addr`: ゲージのアドレス。 + - `_for`: トークンを受け取るアドレス。 + +### `toggle_approve_mint(minting_user: address)` +- **説明**: `minting_user`が`msg.sender`の代わりにトークンをmintすることを許可または禁止する。 +- **パラメータ**: + - `minting_user`: 許可または禁止を切り替えるユーザーのアドレス。 diff --git a/doc/v1.5/YMT/index.md b/doc/v1.5/YMT/index.md new file mode 100644 index 00000000..5e8555f2 --- /dev/null +++ b/doc/v1.5/YMT/index.md @@ -0,0 +1,167 @@ +# YMT Token + +## 概要 + +Curve DAO Tokenは、piecewise-linear mining supplyを持つERC20トークンです。このトークンのサプライは線形に増加しており、マイニングパラメータは定期的に更新される。初期供給は一定であり、その後の供給レートは時間の経過とともに減少していく。 + +--- + +## 定数 + +### `YEAR: constant(uint256) = 86400 * 365` +- **説明**: 1年の秒数を表す定数。具体的には、1日の秒数(86400秒)に365を掛けた値。 + +### `INITIAL_SUPPLY: constant(uint256) ` +- **説明**: トークンの初期供給量。 + +### `INITIAL_RATE: constant(uint256)` +- **説明**: 初期のマイニングレート。 + +### `RATE_REDUCTION_TIME: constant(uint256) = YEAR` +- **説明**: マイニングレートが減少する時間の間隔。 + +### `RATE_REDUCTION_COEFFICIENT: constant(uint256)` +- **説明**: マイニングレートの減少係数。 + +### `RATE_DENOMINATOR: constant(uint256) = 10 ** 18` +- **説明**: レートの分母として使用される定数。 + +### `INFLATION_DELAY: constant(uint256) = 86400` +- **説明**: インフレーションが適用されるまでの遅延時間(秒)。 + +--- + +## 変数 + +### `name: public(String[64])` +- **説明**: トークンの名前。 + +### `symbol: public(String[32])` +- **説明**: トークンのシンボル。 + +### `decimals: public(uint256)` +- **説明**: トークンの小数点以下の桁数。 + +### `balanceOf: public(HashMap[address, uint256])` +- **説明**: 各アドレスのトークン残高を保持するマップ。 + +### `allowances: HashMap[address, HashMap[address, uint256]]` +- **説明**: トークンの所有者が他のユーザーに許可したトークンの移動量。 + +### `total_supply: uint256` +- **説明**: 現在のトークンの総供給量。 + +### `minter: public(address)` +- **説明**: トークンをmintできるアドレス。 + +### `admin: public(address)` +- **説明**: このスマートコントラクトの管理者アドレス。 + +### `mining_epoch: public(int128)` +- **説明**: 現在のマイニングエポック。 + +### `start_epoch_time: public(uint256)` +- **説明**: 現在のエポックの開始タイムスタンプ。 + +### `rate: public(uint256)` +- **説明**: 現在のマイニングレート。 + +### `start_epoch_supply: uint256` +- **説明**: エポック開始時の供給量。 + +--- + +## イベント + +### Transfer: +- _from: indexed(address) +- _to: indexed(address) +- _value: uint256 + +### Approval: +- _owner: indexed(address) +- _spender: indexed(address) +- _value: uint256 + +### UpdateMiningParameters: +- time: uint256 +- rate: uint256 +- supply: uint256 + +### SetMinter: +- minter: address + +### SetAdmin: +- admin: address + +--- + +## 関数 + +### `__init__(_name: String[64], _symbol: String[32], _decimals: uint256)` +- **説明**:コントラクトのコンストラクタ。初期設定を行います。 +- **パラメータ**: + - `_name`:トークンの名前。 + - `_symbol`:トークンのシンボル。 + - `_decimals`:トークンの小数点以下の桁数。 + +### `_update_mining_parameters()` +- **説明**:採掘のレートと供給をエポックの開始時に更新します。 +- **制限**:内部関数。 + +### `update_mining_parameters()` +- **説明**:エポックの開始時に採掘のレートと供給を更新します。 + +### `start_epoch_time_write() -> uint256` +- **説明**:現在の採掘エポックの開始タイムスタンプを取得し、同時に採掘パラメータを更新します。 + +### `future_epoch_time_write() -> uint256` +- **説明**:次の採掘エポックの開始タイムスタンプを取得し、同時に採掘パラメータを更新します。 + +### `_available_supply() -> uint256` +- **説明**:存在するトークンの数(請求済みまたは未請求)を返します。 +- **制限**:内部関数。 + +### `available_supply() -> uint256` +- **説明**:存在するトークンの数(請求済みまたは未請求)を返します。 + +### `mintable_in_timeframe(start: uint256, end: uint256) -> uint256` +- **説明**:指定されたタイムフレームで発行可能なトークンの量を計算します。 +- **パラメータ**: + - `start`:タイムフレームの開始タイムスタンプ。 + - `end`:タイムフレームの終了タイムスタンプ。 + +### `set_minter(_minter: address)` +- **説明**:minterのアドレスを設定します。 +- **パラメータ**: + - `_minter`:minterのアドレス。 + +### `set_admin(_admin: address)` +- **説明**:新しい管理者のアドレスを設定します。 +- **パラメータ**: + - `_admin`:新しい管理者のアドレス。 + +### `totalSupply() -> uint256` +- **説明**:存在するトークンの総量を返します。 + +### `allowance(_owner: address, _spender: address) -> uint256` +- **説明**:_ownerが_spenderに許可したトークンの量を返します。 +- **パラメータ**: + - `_owner`:資金の所有者のアドレス。 + - `_spender`:資金を使うアドレス。 + +### `transfer(_to: address, _value: uint256) -> bool` +- **説明**:`msg.sender`から`_to`へのトークンの転送。 +- **パラメータ**: + - `_to`:受取人のアドレス。 + - `_value`:転送するトークンの量。 + +### `transferFrom(_from: address, _to: address, _value: uint256) -> bool` +- **説明**:`_from`から`_to`へのトークンの転送。 +- **パラメータ**: + - `_from`:送信者のアドレス。 + - `_to`:受取人のアドレス。 + - `_value`:転送するトークンの量。 + +### `approve(_spender: address, _value: uint256) -> bool` +- **説明**:`msg.sender`が`_spender`にトークンを使わせ \ No newline at end of file diff --git a/doc/v1.5/veYMT/index.md b/doc/v1.5/veYMT/index.md new file mode 100644 index 00000000..03dbab8d --- /dev/null +++ b/doc/v1.5/veYMT/index.md @@ -0,0 +1,276 @@ +# veYMT (VotingEscrow) + +## 概要 + +YMT トークンをロックし、移転不可のveYMT トークンを発行する。 +ロック期間は最大4年間、最小単位は1週間で、veYMTはロック後の時間経過により線形に減衰する。1YMTを4年間ロックすると1veYMTが発行される。 + +#### 参考 + +- [Curve DAO: Vote-Escrowed CRV](https://etherscan.io/address/0x5f3b5dfeb7b28cdbd7faba78963ee202a494e2a2#readContract) +- [Curve VotingEscrow Contract](https://curve.readthedocs.io/dao-vecrv.html) +- [The Curve DAO: Liquidity Gauges and Minting CRV](https://curve.readthedocs.io/dao-gauges.html) +- [LiquidityGaugeV6 Contract](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/LiquidityGauge.vy) + +## Struct + +### Point + +veYMT のある時点での状態を格納するための構造体 + +- bias(int128) + - veYMT の残高 +- slope(int128) + - veYMT の減り方を表す傾き。ロック額 / 最大ロック期間 +- ts(uint256) + - タイムスタンプ +- blk(uint256) + - ブロック高 + +### LockedBalance + +ロックの情報を格納するための構造体 + +- amount(int128) + - ロック量 +- end(uint256) + - ロックが終了する時点のタイムスタンプ + +## 定数 + +### `DEPOSIT_FOR_TYPE(int128)` + +イベント識別用 + +### `CREATE_LOCK_TYPE(int128)` + +イベント識別用 + +### `INCREASE_LOCK_AMOUNT(int128)` + +イベント識別用 + +### `INCREASE_UNLOCK_TIME(int128)` + +イベント識別用 + +### `WEEK(uint256)` + +1 週間(7 \* 86400) + +### `MAXTIME(uint256)` + +4 年間(4 \* 365 \* 86400) + +### `MULTIPLIER(uint256)` + +除算時の丸め誤差防止に使用する定数 (10^18) + +## プロパティ + +### `token: public(address)` + +ロック対象のトークンアドレス(YMT のコントラクトアドレス) + +### `supply: public(uint256)` + +ロック対象トークンの総ロック量。デポジット、引き出し時に変化 + +### `locked: public(address => LockedBalance)` + +ユーザごとのトークンロック情報(量、終了タイムスタンプ)を格納 + +### `epoch: public(uint256)` + +全てのユーザのアクションごとにインクリメントするグローバルなインデックス + +### `point_history: public(Point[])` + +veYMTのグローバルな状態を epoch ごとに記録する配列 + +### `user_point_history: public(address => Point[])` + +veYMTのユーザごとの状態を user epoch ごとに記録する配列 + +### `user_point_epoch: public(address => uint256)` + +各ユーザのアクションごとにインクリメントするローカルなインデックス + +### `slope_changes: public(uint256 => uint128)` + +ある時点で予定されている slope の変化を記録する。ユーザのデポジットやロック期間変更時に更新される。週単位のタイムスタンプ(WEEK の倍数)がキーになり、ユーザのアクション時に該当する slope の変化がある場合は Point の slope にこの変化を適用する。 + +### `controller: public(address)` + +Aragon互換性のため + +### `transfersEnabled: public(bool)` + +Aragon互換性のため + +### `name: public(string)` + +ve トークン名 + +### `symbol: public(string)` + +ve トークンシンボル + +### `version: public(string)` + +ve トークンバージョン + +### `decimals: public(uint256)` + +ve トークンデシマル + +### `future_smart_wallet_checker: public(address)` + +Checker for whitelisted (smart contract) wallets which are allowed to deposit +The goal is to prevent tokenizing the escrow + +### `smart_wallet_checker: public(address)` + +Checker for whitelisted (smart contract) wallets which are allowed to deposit +The goal is to prevent tokenizing the escrow + +CurveのVotingEscrowでは現在下記コントラクトが登録されている +https://etherscan.io/address/0xca719728Ef172d0961768581fdF35CB116e0B7a4#readContract + +### `admin: public(address)` + +管理者アドレス + +### `future_admin: public(address)` + +次期管理者アドレス + +# 機能 + +### `**init**(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]) external` + +初期化 + +- adminにmsg.senderを設定 +- tokenにtoken_addrを設定 +- point_history[0].blkにblock.numberを設定 +- point_history[0].tsにblock.timestampを設定 +- controllerにmsg.senderを設定 +- transfersEnabledにTrueを設定 +- decimalsにtokenのdecimalsと同じ値を設定 +- nameに\_nameを設定 +- symbolに\_symbolを設定 +- versionに\_versionを設定 + +### `commit_transfer_ownership(addr: address) external` + +- 次期管理者アドレスを設定 +- 管理者のみ + +### `apply_transfer_ownership() external` + +- 管理者アドレスに次期管理者アドレスを設定 +- 管理者のみ + +### `commit_smart_wallet_checker(addr: address) external` + +- 次期スマートウォレットチェッカーを設定 +- 管理者のみ + +### `apply_smart_wallet_checker() external` + +- スマートウォレットチェッカーに次期スマートウォレットチェッカーを設定 +- 管理者のみ + +### `assert_not_contract(addr: address) internal` + +- スマートウォレットチェッカーを使用して対象アドレスがホワイトリストされたスマコンかどうかチェックする + +### `get_last_user_slope(addr: address) -> int128 external view` + +- 指定アドレスの最新の slope を返す + +### `user_point_history__ts(_addr: address, _idx: uint256) -> uint256 external view` + +- 指定アドレスの指定インデックス(user epoch)のタイムスタンプを返す + +### `locked__end(_addr: address) -> uint256 external view` + +- 指定アドレスのロック終了時点タイムスタンプを返す + +### `_checkpoint(addr: address, old_locked: LockedBalance, new_locked: LockedBalance) internal` + +各ユーザアクションごとにコールされ、ポイント履歴、報酬情報を更新する + +- addr が ZERO_ADDRESS でない場合 + - addr の新旧 slope と bias を計算 + - slope の変化(slope_changes)を計算 + - ユーザのポイント履歴を更新 + - ユーザごとの報酬の計算をする + - integrate_inv_supply_of + - integrate_checkpoint_of + - integrate_fraction +- ポイント履歴の最後の時点から 最大 255 週分の履歴を作成する。255 週以上の期間に渡って履歴がない場合(=ユーザ操作がない場合)は正しい計算ができなくなる +- integrate_inv_supplyを更新する + +### `_deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128) internal` + +- 任意の addr に代わって YMT を任意の量ロックする + +### `checkpoint() external` + +- \_checkpoint を呼び、veYMT のグローバルな状態を更新する + +### `deposit_for(_addr: address, _value: uint256) external` + +- \_deposit_for を呼び、任意の addr に代わって YMT を任意の量ロックする +- 既存のロックがない場合はリバート + +### `create_lock(_value: uint256, _unlock_time: uint256) external` + +- 新規にロックを作成する +- 既存のロックがある場合はリバート + +### `increase_amount(_value: uint256) external` + +- ロック量を増額する + +### `increase_unlock_time(_unlock_time: uint256) external` + +- ロック期間を延長する + +### `withdraw() external` + +- ロック期間が終了した YMT を引き出す + +### `find_block_epoch(_block: uint256, max_epoch: uint256) -> uint256 internal view` + +- 指定したブロック高に一番近い epoch を検索して返す + +### `balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256 external view` + +- 指定したアドレスの指定したタイムスタンプ時点でのveYMT残高を返す +- \_t が最後に記録されたユーザのポイント履歴より前の場合は失敗する + +### `balanceOfAt(addr: address, _block: uint256) -> uint256 external view` + +- 指定したアドレスの指定したブロック高時点でのveYMT残高を返す + +### `supply_at(point: Point, t: uint256) -> uint256 internal view` + +- 指定したポイントを起点に指定したタイムスタンプ時点での 総veYMT残高を返す +- 255 週以上ポイントが記録されていない状況の場合は正しい計算ができなくなる + +### `totalSupply(t: uint256 = block.timestamp) -> uint256 external view` + +- 最後に記録されたポイントを起点に、指定したタイムスタンプ時点の総veYMT残高を返す + +### `totalSupplyAt(_block: uint256) -> uint256 external view` + +- 指定したブロック高時点での総veYMT残高を返す + +### `changeController(_newController: address) external` + +- controller のみ +- controller を変更する +- Aragon互換性のためのダミー関数 \ No newline at end of file diff --git a/doc/v1.5/veYMT/usecase.md b/doc/v1.5/veYMT/usecase.md index 0b397557..c9f0045f 100644 --- a/doc/v1.5/veYMT/usecase.md +++ b/doc/v1.5/veYMT/usecase.md @@ -37,7 +37,7 @@ graph LR gauge_relative_weight[ゲージの重みを取得する] - collateral_ratio[担保率を取得] + collateral_ratio[預入額を取得] balance_issued[CJPYの発行額を取得] first_checkpoint[チェックポイントを更新する] depositor From 6c4462d8a51ab629effcd29f73c9c78da2294a9b Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 24 Oct 2023 19:54:37 +0900 Subject: [PATCH 003/412] Minter and Gauge Controller in Solidity --- contracts/curveFork/CRV.sol | 220 ++++++ contracts/curveFork/GaugeController.sol | 693 ++++++++++++++++ contracts/curveFork/Minter.sol | 100 +++ contracts/curveFork/VotingEscrow.sol | 739 ++++++++++++++++++ contracts/curveFork/interfaces/ICRV.sol | 7 + .../curveFork/interfaces/IGaugeController.sol | 8 + .../curveFork/interfaces/ILiquidityGauge.sol | 9 + .../curveFork/interfaces/IVotingEscrow.sol | 18 + 8 files changed, 1794 insertions(+) create mode 100644 contracts/curveFork/CRV.sol create mode 100644 contracts/curveFork/GaugeController.sol create mode 100644 contracts/curveFork/Minter.sol create mode 100644 contracts/curveFork/VotingEscrow.sol create mode 100644 contracts/curveFork/interfaces/ICRV.sol create mode 100644 contracts/curveFork/interfaces/IGaugeController.sol create mode 100644 contracts/curveFork/interfaces/ILiquidityGauge.sol create mode 100644 contracts/curveFork/interfaces/IVotingEscrow.sol diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol new file mode 100644 index 00000000..33ed87f2 --- /dev/null +++ b/contracts/curveFork/CRV.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.18; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @title Curve DAO Token +/// @notice ERC20 with piecewise-linear mining supply. +contract CRV is ERC20 { + event UpdateMiningParameters(uint256 time, uint256 rate, uint256 supply); + event SetMinter(address minter); + event SetAdmin(address admin); + + address public minter; + address public admin; + + // General constants + uint256 constant YEAR = 365 days; + + // Allocation: + // ========= + // * shareholders - 30% + // * emplyees - 3% + // * DAO-controlled reserve - 5% + // * Early users - 5% + // == 43% == + // left for inflation: 57% + + // Supply parameters + uint256 constant INITIAL_SUPPLY = 1_303_030_303; + uint256 constant INITIAL_RATE = (274_815_283 * 10 ** 18) / YEAR; // leading to 43% premine + uint256 constant RATE_REDUCTION_TIME = YEAR; + uint256 constant RATE_REDUCTION_COEFFICIENT = 1189207115002721024; // 2 ** (1/4) * 1e18 + uint256 constant RATE_DENOMINATOR = 10 ** 18; + uint256 constant INFLATION_DELAY = 1 days; + + // Supply variables + int128 public mining_epoch; + uint256 public start_epoch_time; + uint256 public rate; + + uint256 start_epoch_supply; + + constructor() ERC20("Curve", "CRV") { + uint256 init_supply = INITIAL_SUPPLY * 10 ** decimals(); + _mint(msg.sender, init_supply); + + admin = msg.sender; + + start_epoch_time = + block.timestamp + + INFLATION_DELAY - + RATE_REDUCTION_TIME; + mining_epoch = -1; + rate = 0; + start_epoch_supply = init_supply; + } + + // @dev Update mining rate and supply at the start of the epoch + // Any modifying mining call must also call this + function _update_mining_parameters() internal { + uint256 _rate = rate; + uint256 _start_epoch_supply = start_epoch_supply; + + start_epoch_time += RATE_REDUCTION_TIME; + mining_epoch += 1; + + if (_rate == 0) { + _rate = INITIAL_RATE; + } else { + _start_epoch_supply += _rate * RATE_REDUCTION_TIME; + start_epoch_supply = _start_epoch_supply; + _rate = (_rate * RATE_DENOMINATOR) / RATE_REDUCTION_COEFFICIENT; + } + + rate = _rate; + + emit UpdateMiningParameters( + block.timestamp, + _rate, + _start_epoch_supply + ); + } + + // @notice Update mining rate and supply at the start of the epoch + // @dev Callable by any address, but only once per epoch + // Total supply becomes slightly larger if(this function is called late + function update_mining_parameters() external { + require(block.timestamp >= start_epoch_time + RATE_REDUCTION_TIME); // dev: too soon! + _update_mining_parameters(); + } + + // @notice Get timestamp of the current mining epoch start + // while simultaneously updating mining parameters + // @return Timestamp of the epoch + function start_epoch_time_write() external returns (uint256) { + uint256 _start_epoch_time = start_epoch_time; + if (block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME) { + _update_mining_parameters(); + return start_epoch_time; + } else { + return _start_epoch_time; + } + } + + // @notice Get timestamp of the next mining epoch start + // while simultaneously updating mining parameters + // @return Timestamp of the next epoch + function future_epoch_time_write() external returns (uint256) { + uint256 _start_epoch_time = start_epoch_time; + if (block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME) { + _update_mining_parameters(); + return start_epoch_time + RATE_REDUCTION_TIME; + } else { + return _start_epoch_time + RATE_REDUCTION_TIME; + } + } + + function _available_supply() internal view returns (uint256) { + return start_epoch_supply + (block.timestamp - start_epoch_time) * rate; + } + + // @notice Current number of tokens in existence (claimed or unclaimed) + function available_supply() external view returns (uint256) { + return _available_supply(); + } + + // @notice How much supply is mintable from start timestamp till end timestamp + // @param start Start of the time interval (timestamp) + // @param end End of the time interval (timestamp) + // @return Tokens mintable from `start` till `end` + function mintable_in_timeframe( + uint256 start, + uint256 end + ) external view returns (uint256) { + require(start <= end); // dev: start > end + uint256 to_mint = 0; + uint256 current_epoch_time = start_epoch_time; + uint256 current_rate = rate; + + // Special case if(end is in future (not yet minted) epoch + if (end > current_epoch_time + RATE_REDUCTION_TIME) { + current_epoch_time += RATE_REDUCTION_TIME; + current_rate = + (current_rate * RATE_DENOMINATOR) / + RATE_REDUCTION_COEFFICIENT; + } + + require(end <= current_epoch_time + RATE_REDUCTION_TIME); // dev: too far in future + + // Curve will not work in 1000 years. Darn! + for (uint i; i < 999; ) { + if (end >= current_epoch_time) { + uint256 current_end = end; + if (current_end > current_epoch_time + RATE_REDUCTION_TIME) { + current_end = current_epoch_time + RATE_REDUCTION_TIME; + } + + uint256 current_start = start; + if (current_start >= current_epoch_time + RATE_REDUCTION_TIME) { + break; // We should never get here but what if... + } else if (current_start < current_epoch_time) { + current_start = current_epoch_time; + } + to_mint += current_rate * (current_end - current_start); + if (start >= current_epoch_time) { + break; + } + } + + current_epoch_time -= RATE_REDUCTION_TIME; + current_rate = + (current_rate * RATE_REDUCTION_COEFFICIENT) / + RATE_DENOMINATOR; // double-division with rounding made rate a bit less => good + require(current_rate <= INITIAL_RATE); // This should never happen + + unchecked { + i++; + } + } + + return to_mint; + } + + // @notice Set the minter address + // @dev Only callable once, when minter has not yet been set + // @param _minter Address of the minter + function set_minter(address _minter) external { + require(msg.sender == admin); // dev: admin only + require(minter == address(0)); // dev: can set the minter only once, at creation + minter = _minter; + emit SetMinter(_minter); + } + + // @notice Set the new admin. + // @dev After all is set up, admin only can change the token name + // @param _admin New admin address + function set_admin(address _admin) external { + require(msg.sender == admin); // dev: admin only + admin = _admin; + emit SetAdmin(_admin); + } + + // @notice Mint `_value` tokens and assign them to `_to` + // @dev Emits a Transfer event originating from 0x00 + // @param _to The account that will receive the created tokens + // @param _value The amount that will be created + // @return bool success + function mint(address _to, uint256 _value) external returns (bool) { + require(msg.sender == minter); // dev: minter only + require(_to != address(0)); // dev: zero address + + if (block.timestamp >= start_epoch_time + RATE_REDUCTION_TIME) { + _update_mining_parameters(); + } + + _mint(_to, _value); + + return true; + } +} \ No newline at end of file diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol new file mode 100644 index 00000000..defe9bde --- /dev/null +++ b/contracts/curveFork/GaugeController.sol @@ -0,0 +1,693 @@ +pragma solidity 0.8.18; + +/*** + *@title Gauge Controller + * SPDX-License-Identifier: MIT + *@notice Controls liquidity gauges and the issuance of INSURE token through the gauges + */ + +//dao-contracts +import "./interfaces/dao/ICRV.sol"; +import "./interfaces/dao/IVotingEscrow.sol"; +import "./interfaces/pool/IOwnership.sol"; + +contract GaugeController { + // 7 * 86400 seconds - all future times are rounded by week + // uint256 constant WEEK = 604800; + uint256 constant WEEK = 7 days; + + // Cannot change weight votes more often than once in 10 days. + uint256 constant WEIGHT_VOTE_DELAY = 10 days; + + struct Point { + uint256 bias; + uint256 slope; + } + + struct VotedSlope { + uint256 slope; + uint256 power; + uint256 end; + } + + event CommitOwnership(address admin); + event ApplyOwnership(address admin); + event AddType( + string name, + int128 typeId + ); + event NewTypeWeight( + int128 typeId, + uint256 time, + uint256 weight, + uint256 totalWeight + ); + + event NewGaugeWeight( + address gaugeAddress, + uint256 time, + uint256 weight, + uint256 totalWeight + ); + event VoteForGauge( + uint256 time, + address user, + address gaugeAddr, + uint256 weight + ); + event NewGauge( + address addr, + int128 gaugeType, + uint256 weight + ); + + uint256 constant MULTIPLIER = 10**18; + + // Can and will be a smart contract + address public admin; + // Can and will be a smart contract + address public futureAdmin; + // CRV token + address public token; + // Voting escrow + address public votingEscrow; + + + int128 public nGaugeTypes; + int128 public nGauges; //number of gauges + mapping(int128 => string) public gaugeTypeNames; + + // Needed for enumeration + address[1000000000] public gauges; + + // we increment values by 1 prior to storing them here so we can rely on a value + // of zero as meaning the gauge has not been set mapping(address => int128) gaugeTypes; + mapping(address => int128) public gaugeTypes_; + + mapping(address => mapping(address => VotedSlope)) public voteUserSlopes; // user -> gauge_addr -> VotedSlope + + mapping(address => uint256) public voteUserPower; // Total vote power used by user + mapping(address => mapping(address => uint256)) public lastUserVote; // Last user vote's timestamp for each gauge address + + // Past and scheduled points for gauge weight, sum of weights per type, total weight + // Point is for bias+slope + // changes_* are for changes in slope + // time_* are for the last change timestamp + // timestamps are rounded to whole weeks + + mapping(address => mapping(uint256 => Point)) public pointsWeight; // gauge_addr -> time -> Point + mapping(address => mapping(uint256 => uint256)) public changesWeight; // gauge_addr -> time -> slope + mapping(address => uint256) public timeWeight; // gauge_addr -> last scheduled time (next week) + + mapping(int128 => mapping(uint256 => Point)) public pointsSum; // type_id -> time -> Point + mapping(int128 => mapping(uint256 => uint256)) public changesSum; // type_id -> time -> slope + uint256[1000000000] public timeSum; // type_id -> last scheduled time (next week) + + mapping(uint256 => uint256) public pointsTotal; // time -> total weight + uint256 public timeTotal; // last scheduled time + + mapping(int128 => mapping(uint256 => uint256)) public pointsTypeWeight; // type_id -> time -> type weight + uint256[1000000000] public timeTypeWeight; // type_id -> last scheduled time (next week) + + /*** + *@notice Contract constructor + *@param _token `InsureToken` contract address + *@param _votingEscrow `VotingEscrow` contract address + */ + constructor( + address token_, + address votingEscrow_, + ) { + require(token_ != address(0)); + require(votingEscrow_ != address(0)); + + admin = msg.sender; + token = token_; + votingEscrow = votingEscrow_; + timeTotal = (block.timestamp / WEEK) * WEEK; + } + + /*** + * @notice Transfer ownership of GaugeController to `addr` + * @param addr_ Address to have ownership transferred to + */ + function commitTransferOwnership(address addr_) external onlyAdmin { + futureAdmin = addr_; + emit CommitOwnership(addr_); + } + + /*** + * @notice Apply pending ownership transfer + */ + function applyTransferOwnership() external onlyAdmin{ + address _admin = futureAdmin; + require(_admin != address(0), "admin not set"); + admin = _admin; + emit ApplyOwnership(_admin); + } + + /*** + *@notice Get gauge type for address + *@param addr_ Gauge address + *@return Gauge type id + */ + function gaugeTypes(address addr_) external view returns (int128) { + int128 _gaugeType = gaugeTypes_[addr_]; + require(qaugeType != 0); + + return _gaugeType - 1; + } + + /*** + *@notice Fill historic type weights week-over-week for missed checkins + * and return the type weight for the future week + *@param _gauge_type Gauge type id + *@return Type weight + */ + function _getTypeWeight(int128 gaugeType_) internal returns (uint256) { + uint256 _t = timeTypeWeight[gaugeType_]; + if (_t > 0) { + uint256 _w = pointsTypeWeight[gaugeType_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + pointsTypeWeight[gaugeType_][_t] = _w; + if (_t > block.timestamp) { + timeTypeWeight[gaugeType_] = _t; + } + unchecked { ++i; } + } + return _w; + }else{ + return 0; + } + } + + /*** + *@notice Fill sum of gauge weights for the same type week-over-week for + * missed checkins and return the sum for the future week + *@param gaugeType_ Gauge type id + *@return Sum of weights + */ + function _getSum(int128 gaugeType_) internal returns (uint256) { + uint256 _t = timeSum[gaugeType_]; + if (_t > 0) { + Point memory _pt = pointsSum[gaugeType_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + uint256 _dBias = _pt.slope * WEEK; + if (_pt.bias > _dBias) { + _pt.bias -= _dBias; + uint256 _dSlope = changesSum[gaugeType_][_t]; + _pt.slope -= _dSlope; + } else { + _pt.bias = 0; + _pt.slope = 0; + } + pointsSum[gaugeType_][_t] = _pt; + if (_t > block.timestamp) { + timeSum[gaugeType_] = _t; + } + unchecked { ++i; } + } + return _pt.bias; + }else{ + return 0; + } + } + + /*** + *@notice Fill historic total weights week-over-week for missed checkins + * and return the total for the future week + *@return Total weight + */ + function _getTotal() internal returns (uint256) { + uint256 _t = timeTotal; + int128 _nGaugeTypes = nGaugeTypes; + if (_t > block.timestamp) { + // If we have already checkpointed - still need to change the value + _t -= WEEK; + } + uint256 _pt = pointsTotal[_t]; + + for (uint256 _gaugeType = 1; _gaugeType < 100; ) { + if (_gaugeType == _nGaugeTypes) { + break; + } + _getSum(_gaugeType); + _getTypeWeight(_gaugeType); + unchecked { ++_gaugeType; } + } + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + _pt = 0; + // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) + for (uint256 _gaugeType = 1; _gaugeType < 100; ) { + if (_gaugeType == _nGaugeTypes) { + break; + } + uint256 _typeSum = pointsSum[_gaugeType][_t].bias; + uint256 _typeWeight = pointsTypeWeight[_gaugeType][_t]; + _pt += _typeSum * _typeWeight; + unchecked { ++_gaugeType; } + } + pointsTotal[_t] = _pt; + + if (_t > block.timestamp) { + timeTotal = _t; + } + unchecked { ++i; } + } + return _pt; + } + + /*** + *@notice Fill historic gauge weights week-over-week for missed checkins + * and return the total for the future week + *@param gaugeAddr_ Address of the gauge + *@return Gauge weight + */ + function _getWeight(address gaugeAddr_) internal returns (uint256) { + uint256 _t = timeWeight[gaugeAddr_]; + if (_t > 0) { + Point memory _pt = pointsWeight[gaugeAddr_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + uint256 _dBias = _pt.slope * WEEK; + if (_pt.bias > _dBias) { + _pt.bias -= _dBias; + uint256 _dSlope = changesWeight[gaugeAddr_][_t]; + _pt.slope -= _dSlope; + } else { + _pt.bias = 0; + _pt.slope = 0; + } + pointsWeight[gaugeAddr_][_t] = _pt; + if (_t > block.timestamp) { + timeWeight[gaugeAddr_] = _t; + } + unchecked { ++i; } + } + return _pt.bias; + }else{ + return 0; + } + } + + /*** + *@notice Add gauge `addr` of type `gauge_type` with weight `weight` + *@param addr_ Gauge address + *@param gaugeType_ Gauge type + */ + function addGauge( + address addr_, + int128 gaugeType_, + uint256 weight_, + ) external onlyAdmin { + require((gaugeType_ >= 0) && (gaugeType_ < nGaugeTypes)); //gauge_type 0 means unset + require(gaugeTypes_[addr_] == 0, "cannot add the same gauge twice"); + int128 _n = nGauges; + unchecked { + nGauges = _n + 1; + } + gauges[_n] = addr_; + + gaugeTypes_[addr_] = gaugeType_ + 1; + uint256 _nextTime; + unchecked { + _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + + if (weight_ > 0) { + uint256 _typeWeight = _getTypeWeight(gaugeType_); + uint256 _oldSum = _getSum(gaugeType_); + uint256 _oldTotal = _getTotal(); + + pointsSum[gaugeType_][_nextTime].bias = weight_ + _oldSum; + timeSum[gaugeType_] = _nextTime; + pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); + timeTotal = _nextTime; + + pointsWeight[addr_][_nextTime].bias = weight_; + } + if (timeSum[gaugeType_] == 0) { + timeSum[gaugeType_] = _nextTime; + } + timeWeight[addr_] = _nextTime; + + emit NewGauge(addr_, gaugeType_, weight_); + } + + /*** + * @notice Checkpoint to fill data common for all gauges + */ + function checkpoint() external { + _getTotal(); + } + + /*** + *@notice Checkpoint to fill data for both a specific gauge and common for all gauges + *@param addr_ Gauge address + */ + function checkpointGauge(address addr_) external { + _getWeight(addr_); + _getTotal(); + } + + /*** + *@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18 + * (e.g. 1.0 == 1e18). Inflation which will be received by it is + * inflation_rate * relative_weight / 1e18 + *@param addr_ Gauge address + *@param time_ Relative weight at the specified timestamp in the past or present + *@return Value of relative weight normalized to 1e18 + */ + function _gaugeRelativeWeight(address addr_, uint256 time_) + internal + view + returns (uint256) + { + uint256 _t = (time_ / WEEK) * WEEK; + uint256 _totalWeight = pointsTotal[_t]; + + if (_totalWeight > 0) { + int128 _gaugeType = gaugeTypes_[addr_] - 1; + uint256 _typeWeight = pointsTypeWeight[_gaugeType][_t]; + uint256 _gaugeWeight = pointsWeight[addr_][_t].bias; + + return (MULTIPLIER * _typeWeight * _gaugeWeight) / _totalWeight; + } else { + return 0; + } + } + + /*** + *@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18 + * (e.g. 1.0 == 1e18). Inflation which will be received by it is + * inflation_rate * relative_weight / 1e18 + *@param addr_ Gauge address + *@param time_ Relative weight at the specified timestamp in the past or present + *@return Value of relative weight normalized to 1e18 + */ + function gaugeRelativeWeight(address addr_, uint256 time_) + external + view + returns (uint256) + { + //default value + if (time_ == 0) { + time_ = block.timestamp; + } + + return _gaugeRelativeWeight(addr_, time_); + } + + function gaugeRelativeWeightWrite(address addr_, uint256 time_) + external + returns (uint256) + { + //default value + if (time_ == 0) { + time_ = block.timestamp; + } + + _getWeight(addr_); + _getTotal(); // Also calculates get_sum + return _gaugeRelativeWeight(addr_, time_); + } + + /*** + *@notice Change type weight + *@param typeId_ Type id + *@param weight_ New type weight + */ + function _changeTypeWeight(int128 typeId_, uint256 weight_) internal { + uint256 _oldWeight = _getTypeWeight(typeId_); + uint256 _oldSum = _getSum(typeId_); + uint256 _totalWeight = _getTotal(); + uint256 _nextTime; + unchecked { + _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + + _totalWeight = + _totalWeight + + (_oldSum * weight_) - + (_oldSum * _oldWeight); + pointsTotal[_nextTime] = _totalWeight; + pointsTypeWeight[typeId_][_nextTime] = weight_; + timeTotal = _nextTime; + timeTypeWeight[typeId_] = _nextTime; + + emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); + } + + /*** + *@notice Add gauge type with name `name_` and weight `weight` //ex. type=1, Liquidity, 1*1e18 + *@param name_ Name of gauge type + *@param weight_ Weight of gauge type + */ + function addType(string memory name_, uint256 weight_) external onlyAdmin { + int128 _typeId = nGaugeTypes; + gaugeTypeNames[_typeId] = name_; + unchecked { + nGaugeTypes = _typeId + 1; + } + if (weight_ != 0) { + _changeTypeWeight(_typeId, weight_); + emit AddType(name_, _typeId); + } + } + + /*** + *@notice Change gauge type `type_id` weight to `weight` + *@param typeId_ Gauge type id + *@param weight_ New Gauge weight + */ + function changeTypeWeight(int128 typeId_, uint256 weight_) + external onlyAdmin + { + _changeTypeWeight(typeId_, weight_); + } + + function _changeGaugeWeight(address addr_, uint256 weight_) internal { + // Change gauge weight + // Only needed when testing in reality + int128 _gaugeType = gaugeTypes_[addr_]; + uint256 _oldGaugeWeight = _getWeight(addr_); + uint256 _typeWeight = _getTypeWeight(_gaugeType); + uint256 _oldSum = _getSum(_gaugeType); + uint256 _totalWeight = _getTotal(); + uint256 _nextTime; + unchecked { + _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + + pointsWeight[addr_][_nextTime].bias = weight_; + timeWeight[addr_] = _nextTime; + + uint256 newSum = _oldSum + weight_ - _oldGaugeWeight; + pointsSum[_gaugeType][_nextTime].bias = newSum; + timeSum[_gaugeType] = _nextTime; + + _totalWeight = + _totalWeight + + (newSum * _typeWeight) - + (_oldSum * _typeWeight); + pointsTotal[_nextTime] = _totalWeight; + timeTotal = _nextTime; + + emit NewGaugeWeight(addr_, block.timestamp, weight_, _totalWeight); + } + + /*** + *@notice Change weight of gauge `addr` to `weight` + *@param addr_ `GaugeController` contract address + *@param weight_ New Gauge weight + */ + function changeGaugeWeight(address addr_, uint256 weight_) + external onlyAdmin + { + _changeGaugeWeight(addr_, weight_); + } + + struct VotingParameter { + //to avoid "Stack too deep" issue + uint256 slope; + uint256 lockEnd; + uint256 _nGauges; + uint256 nextTime; + int128 gaugeType; + uint256 oldDt; + uint256 oldBias; + } + + /**** + *@notice Allocate voting power for changing pool weights + *@param gaugeAddr_ Gauge which `msg.sender` votes for + *@param userWeight_ Weight for a gauge in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0. bps = basis points + */ + function voteForGaugeWeights(address gaugeAddr_, uint256 userWeight_) + external + { + VotingParameter memory _vp; + _vp.slope = uint256(IvotingEscrow(votingEscrow).getLastUserSlope(msg.sender)); + _vp.lockEnd = IvotingEscrow(votingEscrow).lockedEnd(msg.sender); + _vp._nGauges = nGauges; + unchecked { + _vp.nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + require( + _vp.lockEnd > _vp.nextTime, + "Your token lock expires too soon" + ); + require( + (userWeight_ >= 0) && (userWeight_ <= 10000), + "You used all your voting power" + ); + unchecked { + require( + block.timestamp >= + lastUserVote[msg.sender][gaugeAddr_] + WEIGHT_VOTE_DELAY, + "Cannot vote so often" + ); + } + + _vp.gaugeType = gaugeTypes_[gaugeAddr_] - 1; + require(_vp.gaugeType >= 0, "Gauge not added"); + // Prepare slopes and biases in memory + VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][ + gaugeAddr_ + ]; + _vp.oldDt = 0; + if (_oldSlope.end > _vp.nextTime) { + _vp.oldDt = _oldSlope.end - _vp.nextTime; + } + _vp.oldBias = _oldSlope.slope * _vp.oldDt; + VotedSlope memory _newSlope = VotedSlope({ + slope: (_vp.slope * userWeight_) / 10000, + end: _vp.lockEnd + power: userWeight_, + }); + uint256 _newDt = _vp.lockEnd - _vp.nextTime; // dev: raises when expired + uint256 _newBias = _newSlope.slope * _newDt; + + // Check and update powers (weights) used + uint256 _powerUsed = voteUserPower[msg.sender]; + _powerUsed = _powerUsed + _newSlope.power - _oldSlope.power; + voteUserPower[msg.sender] = _powerUsed; + require( + (_powerUsed >= 0) && (_powerUsed <= 10000), + "Used too much power" + ); + + //// Remove old and schedule new slope changes + // Remove slope changes for old slopes + // Schedule recording of initial slope for nextTime + uint256 _oldWeightBias = _getWeight(gaugeAddr_); + uint256 _oldWeightSlope = pointsWeight[gaugeAddr_][_vp.nextTime] + .slope; + uint256 _oldSumBias = _getSum(_vp.gaugeType); + uint256 _oldSumSlope = pointsSum[_vp.gaugeType][_vp.nextTime] + .slope; + + pointsWeight[gaugeAddr_][_vp.nextTime].bias = + max(_oldWeightBias + _newBias, _vp.oldBias) - + _vp.oldBias; + pointsSum[_vp.gaugeType][_vp.nextTime].bias = + max(_oldSumBias + _newBias, _vp.oldBias) - + _vp.oldBias; + if (_oldSlope.end > _vp.nextTime) { + pointsWeight[gaugeAddr_][_vp.nextTime].slope = + max(_oldWeightSlope + _newSlope.slope, _oldSlope.slope) - + _oldSlope.slope; + pointsSum[_vp.gaugeType][_vp.nextTime].slope = + max(_oldSumSlope + _newSlope.slope, _oldSlope.slope) - + _oldSlope.slope; + } else { + pointsWeight[gaugeAddr_][_vp.nextTime].slope += _newSlope.slope; + pointsSum[_vp.gaugeType][_vp.nextTime].slope += _newSlope.slope; + } + if (_oldSlope.end > block.timestamp) { + // Cancel old slope changes if they still didn't happen + changesWeight[gaugeAddr_][_oldSlope.end] -= _oldSlope.slope; + changesSum[_vp.gaugeType][_oldSlope.end] -= _oldSlope.slope; + } + // Add slope changes for new slopes + changesWeight[gaugeAddr_][_newSlope.end] += _newSlope.slope; + changesSum[_vp.gaugeType][_newSlope.end] += _newSlope.slope; + + _getTotal(); + + voteUserSlopes[msg.sender][gaugeAddr_] = _newSlope; + + // Record last action time + lastUserVote[msg.sender][gaugeAddr_] = block.timestamp; + + emit VoteForGauge( + block.timestamp, + msg.sender, + gaugeAddr_, + userWeight_ + ); + } + + /*** + *@notice Get current gauge weight + *@param addr_ Gauge address + *@return Gauge weight + */ + function getGaugeWeight(address addr_) external view returns (uint256) { + return pointsWeight[addr_][timeWeight[addr_]].bias; + } + + /*** + *@notice Get current type weight + *@param typeId_ Type id + *@return Type weight + */ + function getTypeWeight(int128 typeId_) external view returns (uint256) { + return pointsTypeWeight[typeId_][timeTypeWeight[typeId_]]; + } + + /*** + *@notice Get current total (type-weighted) weight + *@return Total weight + */ + function getTotalWeight() external view returns (uint256) { + return pointsTotal[timeTotal]; + } + + /*** + *@notice Get sum of gauge weights per type + *@param typeId_ Type id + *@return Sum of gauge weights + */ + function getWeightsSumPerType(int128 typeId_) + external + view + returns (uint256) + { + return pointsSum[typeId_][timeSum[typeId_]].bias; + } + + function max(uint256 _a, uint256 _b) internal pure returns (uint256) { + return _a >= _b ? _a : _b; + } + + modifier onlyAdmin() { + require(admin == msg.sender, "admin only"); + _; + } +} \ No newline at end of file diff --git a/contracts/curveFork/Minter.sol b/contracts/curveFork/Minter.sol new file mode 100644 index 00000000..1b973483 --- /dev/null +++ b/contracts/curveFork/Minter.sol @@ -0,0 +1,100 @@ +pragma solidity 0.8.18; + +/*** + *@title Token Minter + * SPDX-License-Identifier: MIT + */ + +import "./interfaces/ICRV.sol"; +import "./interfaces/ILiquidityGauge.sol"; +import "./interfaces/IGaugeController.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +contract Minter is ReentrancyGuard { + event Minted(address indexed recipient, address gauge, uint256 minted); + + address public token; + address public controller; + + // user -> gauge -> value + mapping(address => mapping(address => uint256)) public minted; //INSURE minted amount of user from specific gauge. + + // minter -> user -> can mint? + mapping(address => mapping(address => bool)) public allowedToMintFor; // A can mint for B if [A => B => true]. + + constructor( + address _token, + address _controller + ) { + token = _token; + controller = _controller; + } + + function _mintFor(address gaugeAddr_, address for_) internal { + require( + IGaugeController(controller).gauge_types(gaugeAddr_) >= 0, + "dev: gauge is not added" + ); + + ILiquidityGauge(gaugeAddr_).user_checkpoint(for_); + uint256 totalMint = ILiquidityGauge(gaugeAddr_).integrate_fraction( + for_ + ); + uint256 _toMint = totalMint - minted[for_][gaugeAddr_]; + + if (_toMint != 0) { + ICRV(token).mint(for_, _toMint); + minted[for_][gaugeAddr_] = totalMint; + + emit Minted(for_, gaugeAddr_, totalMint); + } + } + + /*** + *@notice Mint everything which belongs to `msg.sender` and send to them + *@param gaugeAddr_ `LiquidityGauge` address to get mintable amount from + */ + function mint(address gaugeAddr_) external nonReentrant { + _mintFor(gaugeAddr_, msg.sender); + } + + /*** + *@notice Mint everything which belongs to `msg.sender` across multiple gauges + *@param gaugeAddrs_ List of `LiquidityGauge` addresses + *@dev address[8]: 8 has randomly decided and has no meaning. + */ + function mintMany(address[8] memory gaugeAddrs_) external nonReentrant { + for (uint256 i; i < 8; ) { + if (gaugeAddrs_[i] == address(0)) { + break; + } + _mintFor(gaugeAddrs_[i], msg.sender); + unchecked { + ++i; + } + } + } + + /*** + *@notice Mint tokens for `for_` + *@dev Only possible when `msg.sender` has been approved via `toggle_approve_mint` + *@param gaugeAddr_ `LiquidityGauge` address to get mintable amount from + *@param for_ Address to mint to + */ + function mintFor(address gaugeAddr_, address for_) external nonReentrant { + if (allowedToMintFor[msg.sender][for_]) { + _mintFor(gaugeAddr_, for_); + } + } + + /*** + *@notice allow `mintingUser` to mint for `msg.sender` + *@param mintingUser_ Address to toggle permission for + */ + function toggleApproveMint(address mintingUser_) external { + allowedToMintFor[mintingUser_][msg.sender] = !allowedToMintFor[ + mintingUser_ + ][msg.sender]; + } + +} \ No newline at end of file diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol new file mode 100644 index 00000000..39186c24 --- /dev/null +++ b/contracts/curveFork/VotingEscrow.sol @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "./interfaces/ISmartWalletChecker.sol"; + +contract VotingEscrow is ReentrancyGuard { + struct Point { + int128 bias; + int128 slope; // - dweight / dt + uint256 ts; //timestamp + uint256 blk; // block + } + + struct LockedBalance { + int128 amount; + uint256 end; + } + + int128 constant DEPOSIT_FOR_TYPE = 0; + int128 constant CREATE_LOCK_TYPE = 1; + int128 constant INCREASE_LOCK_AMOUNT = 2; + int128 constant INCREASE_UNLOCK_TIME = 3; + + event CommitOwnership(address admin); + event ApplyOwnership(address admin); + event Deposit( + address indexed provider, + uint256 value, + uint256 indexed locktime, + int128 _type, + uint256 ts + ); + event Withdraw(address indexed provider, uint256 value, uint256 ts); + event Supply(uint256 prevSupply, uint256 supply); + + uint256 public constant WEEK = 7 * 86400; // all future times are rounded by week + uint256 public constant MAXTIME = 4 * 365 * 86400; // 4 years + uint256 public constant MULTIPLIER = 1e18; + + address public token; + uint256 public supply; + + mapping(address => LockedBalance) public locked; + + uint256 public epoch; + + mapping(uint256 => Point) public pointHistory; + mapping(address => mapping(uint256 => Point)) public userPointHistory; + mapping(address => uint256) public userPointEpoch; + mapping(uint256 => int128) public slopeChanges; + + // Aragon's view methods for compatibility + address public controller; + bool public transfersEnabled; + + string public name; + string public symbol; + string public version; + uint256 public decimals; + + // Checker for whitelisted (smart contract) wallets which are allowed to deposit + // The goal is to prevent tokenizing the escrow + address public futureSmartWalletChecker; + address public smartWalletChecker; + + address public admin; + address public futureAdmin; + + /*** + * @notice Contract constructor + * @param tokenAddr_ `YMWK` token address + * @param name_ Token name + * @param symbol_ Token symbol + * @param version_ Contract version - required for Aragon compatibility + */ + constructor( + address tokenAddr_, + string memory name_, + string memory symbol_, + string memory version_ + ) { + admin = msg.sender; + token = tokenAddr_; + pointHistory[0].blk = block.number; + pointHistory[0].ts = block.timestamp; + controller = msg.sender; + transfersEnabled = true; + + uint256 decimalsFromToken = ERC20(tokenAddr_).decimals(); + require( + decimalsFromToken <= 255, + "Decimals should be less than or equal to 255" + ); + decimals = decimalsFromToken; + + name = name_; + symbol = symbol_; + version = version_; + } + + /*** + * @notice Transfer ownership of VotingEscrow contract to `addr_` + * @param addr Address to have ownership transferred to + */ + function commitTransferOwnership(address addr_) external { + require(msg.sender == admin, "admin only"); + futureAdmin = addr_; + emit CommitOwnership(addr_); + } + + /*** + * @notice Apply ownership transfer + */ + function applyTransferOwnership() external { + require(msg.sender == admin, "admin only"); + address _admin = futureAdmin; + require(_admin != address(0), "admin not set"); + admin = _admin; + emit ApplyOwnership(_admin); + } + + /*** + * @notice Set an external contract to check for approved smart contract wallets + * @param addr_ Address of Smart contract checker + */ + function commitSmartWalletChecker(address addr_) external { + require(msg.sender == admin); + futureSmartWalletChecker = addr_; + } + + /*** + * @notice Apply setting external contract to check approved smart contract wallets + */ + function applySmartWalletChecker() external { + require(msg.sender == admin); + smartWalletChecker = futureSmartWalletChecker; + } + + /*** + *@notice Check if the call is from a whitelisted smart contract, revert if not + *@param addr_ Address to be checked + */ + function assertNotContract(address addr_) internal { + if (addr_ != tx.origin) { + address _checker = smartWalletChecker; + if ( + _checker != address(0) && + ISmartWalletChecker(_checker).check(addr_) + ) { + return; + } + revert("Smart contract depositors not allowed"); + } + } + + /*** + * @notice Get the most recently recorded rate of voting power decrease for `addr` + * @param addr_ Address of the user wallet + * @return Value of the slope + */ + function getLastUserSlope(address addr_) external view returns (int128) { + uint256 _uEpoch = userPointEpoch[addr_]; + return userPointHistory[addr_][_uEpoch].slope; + } + + /*** + * @notice Get the timestamp for checkpoint `idx_` for `addr_` + * @param addr_ User wallet address + * @param idx_ User epoch number + * @return Epoch time of the checkpoint + */ + function userPointHistoryTs( + address addr_, + uint256 idx_ + ) external view returns (uint256) { + return userPointHistory[addr_][idx_].ts; + } + + /*** + * @notice Get timestamp when `addr_`'s lock finishes + * @param addr_ User wallet + * @return Epoch time of the lock end + */ + function lockedEnd(address addr_) external view returns (uint256) { + return locked[addr_].end; + } + + /*** + * @notice Record global and per-user data to checkpoint + * @param addr_ User's wallet address. No user checkpoint if 0x0 + * @param oldLocked_ Pevious locked amount / end lock time for the user + * @param newLocked_ New locked amount / end lock time for the user + */ + function _checkpoint( + address addr_, + LockedBalance memory oldLocked_, + LockedBalance memory newLocked_ + ) internal { + Point memory _uOld; + Point memory _uNew; + int128 _oldDSlope = 0; + int128 _newDSlope = 0; + uint256 _epoch = epoch; + + int128( + int128(uint128(oldLocked_.end)) - int128(uint128(block.timestamp)) + ); + if (addr_ != address(0)) { + if (oldLocked_.end > block.timestamp && oldLocked_.amount > 0) { + _uOld.slope = int128(oldLocked_.amount / int256(MAXTIME)); + _uOld.bias = + _uOld.slope * + int128(uint128(oldLocked_.end) - uint128(block.timestamp)); + } + + if (newLocked_.end > block.timestamp && newLocked_.amount > 0) { + _uNew.slope = int128( + uint128(newLocked_.amount) / uint128(MAXTIME) + ); + _uNew.bias = + _uNew.slope * + int128(uint128(newLocked_.end) - uint128(block.timestamp)); + } + + _oldDSlope = slopeChanges[oldLocked_.end]; + if (newLocked_.end != 0) { + if (newLocked_.end == oldLocked_.end) { + _newDSlope = _oldDSlope; + } else { + _newDSlope = slopeChanges[newLocked_.end]; + } + } + } + + Point memory _lastPoint = Point({ + bias: 0, + slope: 0, + ts: block.timestamp, + blk: block.number + }); + if (_epoch > 0) { + _lastPoint = pointHistory[_epoch]; + } + uint256 _lastCheckpoint = _lastPoint.ts; + Point memory _initialLastPoint = _lastPoint; + uint256 _blockSlope = 0; + if (block.timestamp > _lastPoint.ts) { + _blockSlope = + (MULTIPLIER * (block.number - _lastPoint.blk)) / + (block.timestamp - _lastPoint.ts); + } + + uint256 _ti = (_lastCheckpoint / WEEK) * WEEK; + for (uint256 i = 0; i < 255; i++) { + _ti += WEEK; + int128 _dSlope = 0; + if (_ti > block.timestamp) { + _ti = block.timestamp; + } else { + _dSlope = slopeChanges[_ti]; + } + + _lastPoint.bias -= + _lastPoint.slope * + int128(uint128(_ti) - uint128(_lastCheckpoint)); + _lastPoint.slope += _dSlope; + + if (_lastPoint.bias < 0) { + _lastPoint.bias = 0; + } + if (_lastPoint.slope < 0) { + _lastPoint.slope = 0; + } + + _lastCheckpoint = _ti; + _lastPoint.ts = _ti; + _lastPoint.blk = + _initialLastPoint.blk + + (_blockSlope * (_ti - _initialLastPoint.ts)) / + MULTIPLIER; + + _epoch += 1; + + if (_ti == block.timestamp) { + _lastPoint.blk = block.number; + break; + } else { + pointHistory[_epoch] = _lastPoint; + } + } + + epoch = _epoch; + + if (addr_ != address(0)) { + _lastPoint.slope += (_uNew.slope - _uOld.slope); + _lastPoint.bias += (_uNew.bias - _uOld.bias); + if (_lastPoint.slope < 0) { + _lastPoint.slope = 0; + } + if (_lastPoint.bias < 0) { + _lastPoint.bias = 0; + } + } + + pointHistory[_epoch] = _lastPoint; + + address _addr = addr_; // To avoid being "Stack Too Deep" + + if (_addr != address(0)) { + if (oldLocked_.end > block.timestamp) { + _oldDSlope += _uOld.slope; + if (newLocked_.end == oldLocked_.end) { + _oldDSlope -= _uNew.slope; + } + slopeChanges[oldLocked_.end] = _oldDSlope; + } + + if (newLocked_.end > block.timestamp) { + if (newLocked_.end > oldLocked_.end) { + _newDSlope -= _uNew.slope; + slopeChanges[newLocked_.end] = _newDSlope; + } + } + + uint256 _userEpoch = userPointEpoch[_addr] + 1; + userPointEpoch[_addr] = _userEpoch; + + _uNew.ts = block.timestamp; + _uNew.blk = block.number; + userPointHistory[_addr][_userEpoch] = _uNew; + } + } + + /*** + * @notice Deposit and lock tokens for a user + * @param addr_ User's wallet address + * @param value_ Amount to deposit + * @param unlockTime_ New time when to unlock the tokens, or 0 if unchanged + * @param lockedBalance_ Previous locked amount / timestamp + */ + function _depositFor( + address addr_, + uint256 value_, + uint256 unlockTime_, + LockedBalance memory lockedBalance_, + int128 type_ + ) internal { + LockedBalance memory _locked = lockedBalance_; + uint256 supplyBefore = supply; + + supply = supplyBefore + value_; + LockedBalance memory oldLocked = _locked; + _locked.amount += int128(uint128(value_)); + if (unlockTime_ != 0) { + _locked.end = unlockTime_; + } + locked[addr_] = _locked; + + _checkpoint(addr_, oldLocked, _locked); + + if (value_ != 0) { + require( + ERC20(token).transferFrom(addr_, address(this), value_), + "Transfer failed" + ); + } + + emit Deposit(addr_, value_, _locked.end, type_, block.timestamp); + emit Supply(supplyBefore, supplyBefore + value_); + } + + /*** + * @notice Record global data to checkpoint + */ + function checkpoint() external { + LockedBalance memory _old; + LockedBalance memory _new; + _checkpoint(address(0), _old, _new); + } + + /*** + * @notice Deposit `_value` tokens for `_addr` and add to the lock + * @dev Anyone (even a smart contract) can deposit for someone else, but + cannot extend their locktime and deposit for a brand new user + * @param addr_ User's wallet address + * @param value_ Amount to add to user's lock + */ + function depositFor(address addr_, uint256 value_) external nonReentrant { + LockedBalance memory _locked = locked[addr_]; + + require(value_ > 0, "Need non-zero value"); + require(_locked.amount > 0, "No existing lock found"); + require( + _locked.end > block.timestamp, + "Cannot add to expired lock. Withdraw" + ); + + _depositFor(addr_, value_, 0, locked[addr_], DEPOSIT_FOR_TYPE); + } + + /*** + * @notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time` + * @param value_ Amount to deposit + * @param unlockTime_ Epoch time when tokens unlock, rounded down to whole weeks + */ + function createLock( + uint256 value_, + uint256 unlockTime_ + ) external nonReentrant { + assertNotContract(msg.sender); + + uint256 _unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; + LockedBalance memory _locked = locked[msg.sender]; + + require(value_ > 0, "Need non-zero value"); + require(_locked.amount == 0, "Withdraw old tokens first"); + require( + _unlockTimeRounded > block.timestamp, + "Can only lock until time in the future" + ); + require( + _unlockTimeRounded <= block.timestamp + MAXTIME, + "Voting lock can be 4 years max" + ); + + _depositFor( + msg.sender, + value_, + _unlockTimeRounded, + _locked, + CREATE_LOCK_TYPE + ); + } + + /*** + * @notice Deposit `_value` additional tokens for `msg.sender` + without modifying the unlock time + * @param value_ Amount of tokens to deposit and add to the lock + */ + function increaseAmount(uint256 value_) external nonReentrant { + assertNotContract(msg.sender); + + LockedBalance memory _locked = locked[msg.sender]; + + require(value_ > 0, "Need non-zero value"); + require(_locked.amount > 0, "No existing lock found"); + require( + _locked.end > block.timestamp, + "Cannot add to expired lock. Withdraw" + ); + + _depositFor(msg.sender, value_, 0, _locked, INCREASE_LOCK_AMOUNT); + } + + /*** + * @notice Extend the unlock time for `msg.sender` to `_unlock_time` + * @param unlockTime_ New epoch time for unlocking + */ + function increaseUnlockTime(uint256 unlockTime_) external nonReentrant { + assertNotContract(msg.sender); + + LockedBalance memory _locked = locked[msg.sender]; + uint256 unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; + + require(_locked.end > block.timestamp, "Lock expired"); + require(_locked.amount > 0, "Nothing is locked"); + require( + unlockTimeRounded > _locked.end, + "Can only increase lock duration" + ); + require( + unlockTimeRounded <= block.timestamp + MAXTIME, + "Voting lock can be 4 years max" + ); + + _depositFor( + msg.sender, + 0, + unlockTimeRounded, + _locked, + INCREASE_UNLOCK_TIME + ); + } + + /*** + * @notice Withdraw all tokens for `msg.sender` + * @dev Only possible if the lock has expired + */ + function withdraw() external nonReentrant { + LockedBalance memory _locked = locked[msg.sender]; + require(block.timestamp >= _locked.end, "The lock didn't expire"); + uint256 _value = uint256(int256(_locked.amount)); + LockedBalance memory oldLocked = _locked; + _locked.end = 0; + _locked.amount = 0; + locked[msg.sender] = _locked; + uint256 _supplyBefore = supply; + supply = _supplyBefore - _value; + _checkpoint(msg.sender, oldLocked, _locked); + require(ERC20(token).transfer(msg.sender, _value), "Transfer failed"); + emit Withdraw(msg.sender, _value, block.timestamp); + emit Supply(_supplyBefore, _supplyBefore - _value); + } + /*** + * @notice Binary search to estimate timestamp for block number + * @param block_ Block to find + * @param maxEpoch_ Don't go beyond this epoch + * @return Approximate timestamp for block + */ + function findBlockEpoch( + uint256 block_, + uint256 maxEpoch_ + ) internal view returns (uint256) { + // Binary search + uint256 _min = 0; + uint256 _max = maxEpoch_; + for (uint256 i = 0; i < 128; i++) { + // Will be always enough for 128-bit numbers + if (_min >= _max) { + break; + } + uint256 _mid = (_min + _max + 1) / 2; + if (pointHistory[_mid].blk <= block_) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + return _min; + } + + /*** + * @notice Get the current voting power for `msg.sender` + * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility + * @param addr_ User wallet address + * @param t_ Epoch time to return voting power at + * @return User voting power + */ + function balanceOf( + address addr_, + uint256 t_ + ) external view returns (uint256) { + uint256 epoch_ = userPointEpoch[addr_]; + if (epoch_ == 0) { + return 0; + } else { + Point memory lastPoint = userPointHistory[addr_][epoch_]; + lastPoint.bias -= + lastPoint.slope * + int128(int256(t_) - int256(lastPoint.ts)); + if (lastPoint.bias < 0) { + lastPoint.bias = 0; + } + return uint256(int256(lastPoint.bias)); + } + } + + /*** + * @notice Get the current voting power for `msg.sender` + * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility + * @param addr_ User wallet address + * @return User voting power + */ + function balanceOf(address addr_) external view returns (uint256) { + uint256 epoch_ = userPointEpoch[addr_]; + if (epoch_ == 0) { + return 0; + } else { + Point memory lastPoint = userPointHistory[addr_][epoch_]; + lastPoint.bias -= + lastPoint.slope * + int128(int256(block.timestamp) - int256(lastPoint.ts)); + if (lastPoint.bias < 0) { + lastPoint.bias = 0; + } + return uint256(int256(lastPoint.bias)); + } + } + + /*** + * @notice Measure voting power of `addr` at block height `_block` + * @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime + * @param addr_ User's wallet address + * @param block_ Block to calculate the voting power at + * @return Voting power + */ + function balanceOfAt( + address addr_, + uint256 block_ + ) external view returns (uint256) { + require(block_ <= block.number, "Cannot look up future block"); + + uint256 min_ = 0; + uint256 max_ = userPointEpoch[addr_]; + + for (uint i = 0; i < 128; i++) { + if (min_ >= max_) { + break; + } + uint256 mid_ = (min_ + max_ + 1) / 2; + if (userPointHistory[addr_][mid_].blk <= block_) { + min_ = mid_; + } else { + max_ = mid_ - 1; + } + } + + Point memory upoint = userPointHistory[addr_][min_]; + uint256 maxEpoch = epoch; + uint256 epoch_ = findBlockEpoch(block_, maxEpoch); + Point memory point0 = pointHistory[epoch_]; + uint256 dBlock = 0; + uint256 dt = 0; + + if (epoch_ < maxEpoch) { + Point memory point1 = pointHistory[epoch_ + 1]; + dBlock = point1.blk - point0.blk; + dt = point1.ts - point0.ts; + } else { + dBlock = block.number - point0.blk; + dt = block.timestamp - point0.ts; + } + + uint256 blockTime = point0.ts; + if (dBlock != 0) { + blockTime += (dt * (block_ - point0.blk)) / dBlock; + } + + upoint.bias -= + upoint.slope * + int128(int256(blockTime) - int256(upoint.ts)); + if (upoint.bias >= 0) { + return uint256(int256(upoint.bias)); + } else { + return 0; + } + } + + /*** + * @notice Calculate total voting power at some point in the past + * @param point_ The point (bias/slope) to start search from + * @param t_ Time to calculate the total voting power at + * @return Total voting power at that time + */ + function supplyAt( + Point memory point_, + uint256 t_ + ) internal view returns (uint256) { + Point memory lastPoint = point_; + uint256 _ti = (lastPoint.ts / WEEK) * WEEK; + for (uint256 i = 0; i < 255; i++) { + _ti += WEEK; + int128 d_slope = 0; + if (_ti > t_) { + _ti = t_; + } else { + d_slope = slopeChanges[_ti]; + } + lastPoint.bias -= + lastPoint.slope * + int128(int256(_ti) - int256(lastPoint.ts)); + if (_ti == t_) { + break; + } + lastPoint.slope += d_slope; + lastPoint.ts = _ti; + } + + if (lastPoint.bias < 0) { + lastPoint.bias = 0; + } + return uint256(int256(lastPoint.bias)); + } + + /*** + * @notice Calculate total voting power + * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + * @param t_ Time to calculate the total voting power at + * @return Total voting power + */ + function totalSupply(uint256 t_) external view returns (uint256) { + uint256 _epoch = epoch; + Point memory lastPoint = pointHistory[_epoch]; + return supplyAt(lastPoint, t_); + } + + /*** + * @notice Calculate total voting power + * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + * @return Total voting power + */ + function totalSupply() external view returns (uint256) { + uint256 _epoch = epoch; + Point memory lastPoint = pointHistory[_epoch]; + return supplyAt(lastPoint, block.timestamp); + } + + /*** + * @notice Calculate total voting power at some point in the past + * @param _block Block to calculate the total voting power at + * @return Total voting power at `_block` + */ + function totalSupplyAt(uint256 block_) external view returns (uint256) { + require(block_ <= block.number, "Invalid block number"); + uint256 _epoch = epoch; + uint256 targetEpoch = findBlockEpoch(block_, _epoch); + + Point memory point = pointHistory[targetEpoch]; + uint256 dt = 0; + if (targetEpoch < _epoch) { + Point memory pointNext = pointHistory[targetEpoch + 1]; + if (point.blk != pointNext.blk) { + dt = + ((block_ - point.blk) * (pointNext.ts - point.ts)) / + (pointNext.blk - point.blk); + } + } else { + if (point.blk != block.number) { + dt = + ((block_ - point.blk) * (block.timestamp - point.ts)) / + (block.number - point.blk); + } + } + return supplyAt(point, point.ts + dt); + } + + /*** + * @dev Dummy method required for Aragon compatibility + */ + function changeController(address newController) external { + require( + msg.sender == controller, + "Only the controller can call this function" + ); + controller = newController; + } +} \ No newline at end of file diff --git a/contracts/curveFork/interfaces/ICRV.sol b/contracts/curveFork/interfaces/ICRV.sol new file mode 100644 index 00000000..c27412a5 --- /dev/null +++ b/contracts/curveFork/interfaces/ICRV.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +interface ICRV { + function mint(address to_, uint256 value_) external returns (bool); +} \ No newline at end of file diff --git a/contracts/curveFork/interfaces/IGaugeController.sol b/contracts/curveFork/interfaces/IGaugeController.sol new file mode 100644 index 00000000..ae599e3d --- /dev/null +++ b/contracts/curveFork/interfaces/IGaugeController.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +interface IGaugeController { + function gaugeTypes(address addr_) external view returns (uint256); + +} \ No newline at end of file diff --git a/contracts/curveFork/interfaces/ILiquidityGauge.sol b/contracts/curveFork/interfaces/ILiquidityGauge.sol new file mode 100644 index 00000000..27223268 --- /dev/null +++ b/contracts/curveFork/interfaces/ILiquidityGauge.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +interface ILiquidityGauge { + function userCheckpoint(address addr_) external returns (bool); + + function integrateFraction(address addr_) external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/curveFork/interfaces/IVotingEscrow.sol b/contracts/curveFork/interfaces/IVotingEscrow.sol new file mode 100644 index 00000000..afe0cff7 --- /dev/null +++ b/contracts/curveFork/interfaces/IVotingEscrow.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +interface IVotingEscrow { + function getLastUserSlope(address addr_) external view returns (int128); + + function lockedEnd(address addr_) external view returns (uint256); + + function balanceOf(address addr_, uint256 t_) + external + view + returns (uint256); + + //function balanceOf(address addr)external view returns (uint256); + function totalSupply(uint256 t_) external view returns (uint256); + +} \ No newline at end of file From 0c221cd312005c8aeb8f0f2739d39d60f61640a2 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 25 Oct 2023 09:56:44 +0900 Subject: [PATCH 004/412] Compilation error resolution --- contracts/curveFork/CRV.sol | 2 +- contracts/curveFork/GaugeController.sol | 58 ++++++++++--------- contracts/curveFork/Minter.sol | 8 +-- contracts/curveFork/VotingEscrow.sol | 2 +- contracts/curveFork/interfaces/ICRV.sol | 2 +- .../curveFork/interfaces/IGaugeController.sol | 2 +- .../curveFork/interfaces/ILiquidityGauge.sol | 2 +- .../interfaces/ISmartWalletChecker.sol | 10 ++++ .../curveFork/interfaces/IVotingEscrow.sol | 2 +- 9 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 contracts/curveFork/interfaces/ISmartWalletChecker.sol diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol index 33ed87f2..2c6064b9 100644 --- a/contracts/curveFork/CRV.sol +++ b/contracts/curveFork/CRV.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol index defe9bde..c525f05c 100644 --- a/contracts/curveFork/GaugeController.sol +++ b/contracts/curveFork/GaugeController.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.18; +pragma solidity 0.8.4; /*** *@title Gauge Controller @@ -7,9 +7,8 @@ pragma solidity 0.8.18; */ //dao-contracts -import "./interfaces/dao/ICRV.sol"; -import "./interfaces/dao/IVotingEscrow.sol"; -import "./interfaces/pool/IOwnership.sol"; +import "./interfaces/ICRV.sol"; +import "./interfaces/IVotingEscrow.sol"; contract GaugeController { // 7 * 86400 seconds - all future times are rounded by week @@ -116,7 +115,7 @@ contract GaugeController { */ constructor( address token_, - address votingEscrow_, + address votingEscrow_ ) { require(token_ != address(0)); require(votingEscrow_ != address(0)); @@ -153,7 +152,7 @@ contract GaugeController { */ function gaugeTypes(address addr_) external view returns (int128) { int128 _gaugeType = gaugeTypes_[addr_]; - require(qaugeType != 0); + require(_gaugeType != 0); return _gaugeType - 1; } @@ -165,7 +164,8 @@ contract GaugeController { *@return Type weight */ function _getTypeWeight(int128 gaugeType_) internal returns (uint256) { - uint256 _t = timeTypeWeight[gaugeType_]; + uint256 _gaugeType = uint256(uint128(gaugeType_)); + uint256 _t = timeTypeWeight[_gaugeType]; if (_t > 0) { uint256 _w = pointsTypeWeight[gaugeType_][_t]; for (uint256 i; i < 500; ) { @@ -175,7 +175,7 @@ contract GaugeController { _t += WEEK; pointsTypeWeight[gaugeType_][_t] = _w; if (_t > block.timestamp) { - timeTypeWeight[gaugeType_] = _t; + timeTypeWeight[_gaugeType] = _t; } unchecked { ++i; } } @@ -192,7 +192,8 @@ contract GaugeController { *@return Sum of weights */ function _getSum(int128 gaugeType_) internal returns (uint256) { - uint256 _t = timeSum[gaugeType_]; + uint256 _gaugeType = uint256(uint128(gaugeType_)); + uint256 _t = timeSum[_gaugeType]; if (_t > 0) { Point memory _pt = pointsSum[gaugeType_][_t]; for (uint256 i; i < 500; ) { @@ -211,7 +212,7 @@ contract GaugeController { } pointsSum[gaugeType_][_t] = _pt; if (_t > block.timestamp) { - timeSum[gaugeType_] = _t; + timeSum[_gaugeType] = _t; } unchecked { ++i; } } @@ -235,7 +236,7 @@ contract GaugeController { } uint256 _pt = pointsTotal[_t]; - for (uint256 _gaugeType = 1; _gaugeType < 100; ) { + for (int128 _gaugeType = 1; _gaugeType < 100; ) { if (_gaugeType == _nGaugeTypes) { break; } @@ -250,7 +251,7 @@ contract GaugeController { _t += WEEK; _pt = 0; // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) - for (uint256 _gaugeType = 1; _gaugeType < 100; ) { + for (int128 _gaugeType = 1; _gaugeType < 100; ) { if (_gaugeType == _nGaugeTypes) { break; } @@ -313,7 +314,7 @@ contract GaugeController { function addGauge( address addr_, int128 gaugeType_, - uint256 weight_, + uint256 weight_ ) external onlyAdmin { require((gaugeType_ >= 0) && (gaugeType_ < nGaugeTypes)); //gauge_type 0 means unset require(gaugeTypes_[addr_] == 0, "cannot add the same gauge twice"); @@ -321,8 +322,8 @@ contract GaugeController { unchecked { nGauges = _n + 1; } - gauges[_n] = addr_; - + gauges[uint256(uint128(_n))] = addr_; + uint256 _gaugeType = uint256(uint128(gaugeType_)); gaugeTypes_[addr_] = gaugeType_ + 1; uint256 _nextTime; unchecked { @@ -335,14 +336,14 @@ contract GaugeController { uint256 _oldTotal = _getTotal(); pointsSum[gaugeType_][_nextTime].bias = weight_ + _oldSum; - timeSum[gaugeType_] = _nextTime; + timeSum[_gaugeType] = _nextTime; pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); timeTotal = _nextTime; pointsWeight[addr_][_nextTime].bias = weight_; } - if (timeSum[gaugeType_] == 0) { - timeSum[gaugeType_] = _nextTime; + if (timeSum[_gaugeType] == 0) { + timeSum[_gaugeType] = _nextTime; } timeWeight[addr_] = _nextTime; @@ -440,6 +441,7 @@ contract GaugeController { unchecked { _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; } + uint256 _typeId = uint256(uint128(typeId_)); _totalWeight = _totalWeight + @@ -448,7 +450,7 @@ contract GaugeController { pointsTotal[_nextTime] = _totalWeight; pointsTypeWeight[typeId_][_nextTime] = weight_; timeTotal = _nextTime; - timeTypeWeight[typeId_] = _nextTime; + timeTypeWeight[_typeId] = _nextTime; emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); } @@ -499,7 +501,7 @@ contract GaugeController { uint256 newSum = _oldSum + weight_ - _oldGaugeWeight; pointsSum[_gaugeType][_nextTime].bias = newSum; - timeSum[_gaugeType] = _nextTime; + timeSum[uint256(uint128(_gaugeType))] = _nextTime; _totalWeight = _totalWeight + @@ -542,9 +544,9 @@ contract GaugeController { external { VotingParameter memory _vp; - _vp.slope = uint256(IvotingEscrow(votingEscrow).getLastUserSlope(msg.sender)); - _vp.lockEnd = IvotingEscrow(votingEscrow).lockedEnd(msg.sender); - _vp._nGauges = nGauges; + _vp.slope = uint256(uint128(IVotingEscrow(votingEscrow).getLastUserSlope(msg.sender))); + _vp.lockEnd = IVotingEscrow(votingEscrow).lockedEnd(msg.sender); + _vp._nGauges = uint256(uint128(nGauges)); unchecked { _vp.nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; } @@ -577,8 +579,8 @@ contract GaugeController { _vp.oldBias = _oldSlope.slope * _vp.oldDt; VotedSlope memory _newSlope = VotedSlope({ slope: (_vp.slope * userWeight_) / 10000, - end: _vp.lockEnd - power: userWeight_, + end: _vp.lockEnd, + power: userWeight_ }); uint256 _newDt = _vp.lockEnd - _vp.nextTime; // dev: raises when expired uint256 _newBias = _newSlope.slope * _newDt; @@ -658,7 +660,8 @@ contract GaugeController { *@return Type weight */ function getTypeWeight(int128 typeId_) external view returns (uint256) { - return pointsTypeWeight[typeId_][timeTypeWeight[typeId_]]; + uint256 _typeId = uint256(uint128(typeId_)); + return pointsTypeWeight[typeId_][timeTypeWeight[_typeId]]; } /*** @@ -679,7 +682,8 @@ contract GaugeController { view returns (uint256) { - return pointsSum[typeId_][timeSum[typeId_]].bias; + uint256 _typeId = uint256(uint128(typeId_)); + return pointsSum[typeId_][timeSum[_typeId]].bias; } function max(uint256 _a, uint256 _b) internal pure returns (uint256) { diff --git a/contracts/curveFork/Minter.sol b/contracts/curveFork/Minter.sol index 1b973483..30eca9f5 100644 --- a/contracts/curveFork/Minter.sol +++ b/contracts/curveFork/Minter.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.18; +pragma solidity 0.8.4; /*** *@title Token Minter @@ -32,12 +32,12 @@ contract Minter is ReentrancyGuard { function _mintFor(address gaugeAddr_, address for_) internal { require( - IGaugeController(controller).gauge_types(gaugeAddr_) >= 0, + IGaugeController(controller).gaugeTypes(gaugeAddr_) >= 0, "dev: gauge is not added" ); - ILiquidityGauge(gaugeAddr_).user_checkpoint(for_); - uint256 totalMint = ILiquidityGauge(gaugeAddr_).integrate_fraction( + ILiquidityGauge(gaugeAddr_).userCheckpoint(for_); + uint256 totalMint = ILiquidityGauge(gaugeAddr_).integrateFraction( for_ ); uint256 _toMint = totalMint - minted[for_][gaugeAddr_]; diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 39186c24..84e5f93f 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; +pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; diff --git a/contracts/curveFork/interfaces/ICRV.sol b/contracts/curveFork/interfaces/ICRV.sol index c27412a5..94cae1c8 100644 --- a/contracts/curveFork/interfaces/ICRV.sol +++ b/contracts/curveFork/interfaces/ICRV.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity 0.8.4; interface ICRV { function mint(address to_, uint256 value_) external returns (bool); diff --git a/contracts/curveFork/interfaces/IGaugeController.sol b/contracts/curveFork/interfaces/IGaugeController.sol index ae599e3d..049f6b9d 100644 --- a/contracts/curveFork/interfaces/IGaugeController.sol +++ b/contracts/curveFork/interfaces/IGaugeController.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity 0.8.4; interface IGaugeController { function gaugeTypes(address addr_) external view returns (uint256); diff --git a/contracts/curveFork/interfaces/ILiquidityGauge.sol b/contracts/curveFork/interfaces/ILiquidityGauge.sol index 27223268..0c1cda58 100644 --- a/contracts/curveFork/interfaces/ILiquidityGauge.sol +++ b/contracts/curveFork/interfaces/ILiquidityGauge.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity 0.8.4; interface ILiquidityGauge { function userCheckpoint(address addr_) external returns (bool); diff --git a/contracts/curveFork/interfaces/ISmartWalletChecker.sol b/contracts/curveFork/interfaces/ISmartWalletChecker.sol new file mode 100644 index 00000000..94e4cc32 --- /dev/null +++ b/contracts/curveFork/interfaces/ISmartWalletChecker.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +// Interface for checking whether address belongs to a whitelisted type of a smart wallet. +// When new types are added - the whole contract is changed +// The check() method is modifying to be able to use caching +// for individual wallet addresses +interface ISmartWalletChecker { + function check(address addr_) external returns (bool); +} \ No newline at end of file diff --git a/contracts/curveFork/interfaces/IVotingEscrow.sol b/contracts/curveFork/interfaces/IVotingEscrow.sol index afe0cff7..be43cd59 100644 --- a/contracts/curveFork/interfaces/IVotingEscrow.sol +++ b/contracts/curveFork/interfaces/IVotingEscrow.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity 0.8.4; interface IVotingEscrow { function getLastUserSlope(address addr_) external view returns (int128); From ea277412d0ee97c45693c8354ab61683a6e082c4 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 09:52:57 +0900 Subject: [PATCH 005/412] Voting escrow updated. --- contracts/curveFork/VotingEscrow.sol | 201 ++++++++++++++++++++------- 1 file changed, 148 insertions(+), 53 deletions(-) diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 84e5f93f..1b0c9f19 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -1,8 +1,27 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; +pragma solidity 0.8.4; + +// Voting escrow to have time-weighted votes +// Votes have a weight depending on time, so that users are committed +// to the future of (whatever they are voting for). +// The weight in this implementation is linear, and lock cannot be more than maxtime: +// w ^ +// 1 + / +// | / +// | / +// | / +// |/ +// 0 +--------+------> time +// maxtime (4 years?) import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +// Interface for checking whether address belongs to a whitelisted +// type of a smart wallet. +// When new types are added - the whole contract is changed +// The check() method is modifying to be able to use caching +// for individual wallet addresses import "./interfaces/ISmartWalletChecker.sol"; contract VotingEscrow is ReentrancyGuard { @@ -13,6 +32,9 @@ contract VotingEscrow is ReentrancyGuard { uint256 blk; // block } + // We cannot really do block numbers per se b/c slope is per time, not per block + // and per block could be fairly bad b/c Ethereum changes blocktimes. + // What we can do is to extrapolate ***At functions struct LockedBalance { int128 amount; uint256 end; @@ -44,12 +66,13 @@ contract VotingEscrow is ReentrancyGuard { mapping(address => LockedBalance) public locked; + //everytime user deposit/withdraw/change_locktime, these values will be updated; uint256 public epoch; - mapping(uint256 => Point) public pointHistory; - mapping(address => mapping(uint256 => Point)) public userPointHistory; + mapping(uint256 => Point) public pointHistory; // epoch -> unsigned point. + mapping(address => mapping(uint256 => Point)) public userPointHistory; // user -> Point[user_epoch] mapping(address => uint256) public userPointEpoch; - mapping(uint256 => int128) public slopeChanges; + mapping(uint256 => int128) public slopeChanges; // time -> signed slope change // Aragon's view methods for compatibility address public controller; @@ -104,8 +127,7 @@ contract VotingEscrow is ReentrancyGuard { * @notice Transfer ownership of VotingEscrow contract to `addr_` * @param addr Address to have ownership transferred to */ - function commitTransferOwnership(address addr_) external { - require(msg.sender == admin, "admin only"); + function commitTransferOwnership(address addr_) external onlyAdmin { futureAdmin = addr_; emit CommitOwnership(addr_); } @@ -113,8 +135,7 @@ contract VotingEscrow is ReentrancyGuard { /*** * @notice Apply ownership transfer */ - function applyTransferOwnership() external { - require(msg.sender == admin, "admin only"); + function applyTransferOwnership() external onlyAdmin { address _admin = futureAdmin; require(_admin != address(0), "admin not set"); admin = _admin; @@ -125,16 +146,14 @@ contract VotingEscrow is ReentrancyGuard { * @notice Set an external contract to check for approved smart contract wallets * @param addr_ Address of Smart contract checker */ - function commitSmartWalletChecker(address addr_) external { - require(msg.sender == admin); + function commitSmartWalletChecker(address addr_) external onlyAdmin { futureSmartWalletChecker = addr_; } /*** * @notice Apply setting external contract to check approved smart contract wallets */ - function applySmartWalletChecker() external { - require(msg.sender == admin); + function applySmartWalletChecker() external onlyAdmin { smartWalletChecker = futureSmartWalletChecker; } @@ -208,17 +227,26 @@ contract VotingEscrow is ReentrancyGuard { int128(uint128(oldLocked_.end)) - int128(uint128(block.timestamp)) ); if (addr_ != address(0)) { + // Calculate slopes and biases + // Kept at zero when they have to if (oldLocked_.end > block.timestamp && oldLocked_.amount > 0) { - _uOld.slope = int128(oldLocked_.amount / int256(MAXTIME)); + unchecked { + _uOld.slope = int128(oldLocked_.amount / int256(MAXTIME)); + } _uOld.bias = _uOld.slope * int128(uint128(oldLocked_.end) - uint128(block.timestamp)); } + // Read values of scheduled changes in the slope + // old_locked.end can be in the past and in the future + // new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros if (newLocked_.end > block.timestamp && newLocked_.amount > 0) { - _uNew.slope = int128( - uint128(newLocked_.amount) / uint128(MAXTIME) - ); + unchecked { + _uNew.slope = int128( + uint128(newLocked_.amount) / uint128(MAXTIME) + ); + } _uNew.bias = _uNew.slope * int128(uint128(newLocked_.end) - uint128(block.timestamp)); @@ -244,6 +272,10 @@ contract VotingEscrow is ReentrancyGuard { _lastPoint = pointHistory[_epoch]; } uint256 _lastCheckpoint = _lastPoint.ts; + + // initial_last_point is used for extrapolation to calculate block number + // (approximately, for *At methods) and save them + // as we cannot figure that out exactly from inside the contract Point memory _initialLastPoint = _lastPoint; uint256 _blockSlope = 0; if (block.timestamp > _lastPoint.ts) { @@ -251,9 +283,17 @@ contract VotingEscrow is ReentrancyGuard { (MULTIPLIER * (block.number - _lastPoint.blk)) / (block.timestamp - _lastPoint.ts); } + // If last point is already recorded in this block, slope=0 + // But that's ok b/c we know the block in such case - uint256 _ti = (_lastCheckpoint / WEEK) * WEEK; - for (uint256 i = 0; i < 255; i++) { + // Go over weeks to fill history and calculate what the current point is + uint256 _ti; + unchecked { + _ti = (_lastCheckpoint / WEEK) * WEEK; + } + for (uint256 i; i < 255; ) { + // Hopefully it won't happen that this won't get used in 5 years! + // If it does, users will be able to withdraw but vote weight will be broken _ti += WEEK; int128 _dSlope = 0; if (_ti > block.timestamp) { @@ -268,9 +308,11 @@ contract VotingEscrow is ReentrancyGuard { _lastPoint.slope += _dSlope; if (_lastPoint.bias < 0) { + // This can happen _lastPoint.bias = 0; } if (_lastPoint.slope < 0) { + // This cannot happen - just in case _lastPoint.slope = 0; } @@ -289,11 +331,17 @@ contract VotingEscrow is ReentrancyGuard { } else { pointHistory[_epoch] = _lastPoint; } + unchecked { + ++i; + } } epoch = _epoch; + // Now point_history is filled until t=now if (addr_ != address(0)) { + // If last point was in this block, the slope change has been applied already + // But in such case we have 0 slope(s) _lastPoint.slope += (_uNew.slope - _uOld.slope); _lastPoint.bias += (_uNew.bias - _uOld.bias); if (_lastPoint.slope < 0) { @@ -304,27 +352,37 @@ contract VotingEscrow is ReentrancyGuard { } } + // Record the changed point into history pointHistory[_epoch] = _lastPoint; address _addr = addr_; // To avoid being "Stack Too Deep" if (_addr != address(0)) { + // Schedule the slope changes (slope is going down) + // We subtract new_user_slope from [new_locked.end] + // and add old_user_slope to [old_locked.end] if (oldLocked_.end > block.timestamp) { + // old_dslope was - u_old.slope, so we cancel that _oldDSlope += _uOld.slope; if (newLocked_.end == oldLocked_.end) { - _oldDSlope -= _uNew.slope; + _oldDSlope -= _uNew.slope; // It was a new deposit, not extension } slopeChanges[oldLocked_.end] = _oldDSlope; } if (newLocked_.end > block.timestamp) { if (newLocked_.end > oldLocked_.end) { - _newDSlope -= _uNew.slope; + _newDSlope -= _uNew.slope; // old slope disappeared at this point slopeChanges[newLocked_.end] = _newDSlope; } + // else: we recorded it already in old_dslope } - uint256 _userEpoch = userPointEpoch[_addr] + 1; + // Now handle user history + uint256 _userEpoch; + unchecked { + _userEpoch = userPointEpoch[_addr] + 1; + } userPointEpoch[_addr] = _userEpoch; _uNew.ts = block.timestamp; @@ -352,12 +410,17 @@ contract VotingEscrow is ReentrancyGuard { supply = supplyBefore + value_; LockedBalance memory oldLocked = _locked; + // Adding to existing lock, or if a lock is expired - creating a new one _locked.amount += int128(uint128(value_)); if (unlockTime_ != 0) { _locked.end = unlockTime_; } locked[addr_] = _locked; + // Possibilities: + // Both old_locked.end could be current or expired (>/< block.timestamp) + // value == 0 (extend lock) or value > 0 (add to lock or extend lock) + // _locked.end > block.timestamp (always) _checkpoint(addr_, oldLocked, _locked); if (value_ != 0) { @@ -462,23 +525,26 @@ contract VotingEscrow is ReentrancyGuard { assertNotContract(msg.sender); LockedBalance memory _locked = locked[msg.sender]; - uint256 unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; + uint256 _unlockTimeRounded; + unchecked { + _unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; + } require(_locked.end > block.timestamp, "Lock expired"); require(_locked.amount > 0, "Nothing is locked"); require( - unlockTimeRounded > _locked.end, + _unlockTimeRounded > _locked.end, "Can only increase lock duration" ); require( - unlockTimeRounded <= block.timestamp + MAXTIME, + _unlockTimeRounded <= block.timestamp + MAXTIME, "Voting lock can be 4 years max" ); _depositFor( msg.sender, 0, - unlockTimeRounded, + _unlockTimeRounded, _locked, INCREASE_UNLOCK_TIME ); @@ -492,22 +558,34 @@ contract VotingEscrow is ReentrancyGuard { LockedBalance memory _locked = locked[msg.sender]; require(block.timestamp >= _locked.end, "The lock didn't expire"); uint256 _value = uint256(int256(_locked.amount)); + LockedBalance memory oldLocked = _locked; _locked.end = 0; _locked.amount = 0; locked[msg.sender] = _locked; uint256 _supplyBefore = supply; supply = _supplyBefore - _value; + + // old_locked can have either expired <= timestamp or zero end + // _locked has only 0 end + // Both can have >= 0 amount _checkpoint(msg.sender, oldLocked, _locked); + require(ERC20(token).transfer(msg.sender, _value), "Transfer failed"); + emit Withdraw(msg.sender, _value, block.timestamp); emit Supply(_supplyBefore, _supplyBefore - _value); } + + // The following ERC20/minime-compatible methods are not real balanceOf and supply! + // They measure the weights for the purpose of voting, so they don't represent + // real coins. + /*** - * @notice Binary search to estimate timestamp for block number + * @notice Binary search to estimate epoch for block number * @param block_ Block to find * @param maxEpoch_ Don't go beyond this epoch - * @return Approximate timestamp for block + * @return Approximate epoch for block */ function findBlockEpoch( uint256 block_, @@ -516,16 +594,18 @@ contract VotingEscrow is ReentrancyGuard { // Binary search uint256 _min = 0; uint256 _max = maxEpoch_; - for (uint256 i = 0; i < 128; i++) { - // Will be always enough for 128-bit numbers - if (_min >= _max) { - break; - } - uint256 _mid = (_min + _max + 1) / 2; - if (pointHistory[_mid].blk <= block_) { - _min = _mid; - } else { - _max = _mid - 1; + unchecked { + for (uint256 i; i < 128; i++) { + // Will be always enough for 128-bit numbers + if (_min >= _max) { + break; + } + uint256 _mid = (_min + _max + 1) / 2; + if (pointHistory[_mid].blk <= block_) { + _min = _mid; + } else { + _max = _mid - 1; + } } } return _min; @@ -590,20 +670,26 @@ contract VotingEscrow is ReentrancyGuard { address addr_, uint256 block_ ) external view returns (uint256) { + // Copying and pasting totalSupply code because Vyper cannot pass by + // reference yet require(block_ <= block.number, "Cannot look up future block"); + // Binary search uint256 min_ = 0; uint256 max_ = userPointEpoch[addr_]; - for (uint i = 0; i < 128; i++) { - if (min_ >= max_) { - break; - } - uint256 mid_ = (min_ + max_ + 1) / 2; - if (userPointHistory[addr_][mid_].blk <= block_) { - min_ = mid_; - } else { - max_ = mid_ - 1; + unchecked { + for (uint i = 0; i < 128; i++) { + // Will be always enough for 128-bit numbers + if (min_ >= max_) { + break; + } + uint256 mid_ = (min_ + max_ + 1) / 2; + if (userPointHistory[addr_][mid_].blk <= block_) { + min_ = mid_; + } else { + max_ = mid_ - 1; + } } } @@ -650,7 +736,7 @@ contract VotingEscrow is ReentrancyGuard { ) internal view returns (uint256) { Point memory lastPoint = point_; uint256 _ti = (lastPoint.ts / WEEK) * WEEK; - for (uint256 i = 0; i < 255; i++) { + for (uint256 i; i < 255; ) { _ti += WEEK; int128 d_slope = 0; if (_ti > t_) { @@ -666,6 +752,9 @@ contract VotingEscrow is ReentrancyGuard { } lastPoint.slope += d_slope; lastPoint.ts = _ti; + unchecked { + ++i; + } } if (lastPoint.bias < 0) { @@ -699,7 +788,7 @@ contract VotingEscrow is ReentrancyGuard { /*** * @notice Calculate total voting power at some point in the past - * @param _block Block to calculate the total voting power at + * @param block_ Block to calculate the total voting power at * @return Total voting power at `_block` */ function totalSupplyAt(uint256 block_) external view returns (uint256) { @@ -708,22 +797,23 @@ contract VotingEscrow is ReentrancyGuard { uint256 targetEpoch = findBlockEpoch(block_, _epoch); Point memory point = pointHistory[targetEpoch]; - uint256 dt = 0; + uint256 _dt = 0; if (targetEpoch < _epoch) { Point memory pointNext = pointHistory[targetEpoch + 1]; if (point.blk != pointNext.blk) { - dt = + _dt = ((block_ - point.blk) * (pointNext.ts - point.ts)) / (pointNext.blk - point.blk); } } else { if (point.blk != block.number) { - dt = + _dt = ((block_ - point.blk) * (block.timestamp - point.ts)) / (block.number - point.blk); } } - return supplyAt(point, point.ts + dt); + // Now _dt contains info on how far are we beyond point + return supplyAt(point, point.ts + _dt); } /*** @@ -736,4 +826,9 @@ contract VotingEscrow is ReentrancyGuard { ); controller = newController; } -} \ No newline at end of file + + modifier onlyAdmin() { + require(admin == msg.sender, "admin only"); + _; + } +} From 86472ba14c8e89e28e0cc6df74ec8e823a41ab26 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 09:53:48 +0900 Subject: [PATCH 006/412] Fix Gauge Controller. --- contracts/curveFork/GaugeController.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol index c525f05c..daaa11de 100644 --- a/contracts/curveFork/GaugeController.sol +++ b/contracts/curveFork/GaugeController.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.4; /*** *@title Gauge Controller * SPDX-License-Identifier: MIT - *@notice Controls liquidity gauges and the issuance of INSURE token through the gauges + *@notice Controls liquidity gauges and the issuance of token through the gauges */ //dao-contracts @@ -110,7 +110,7 @@ contract GaugeController { /*** *@notice Contract constructor - *@param _token `InsureToken` contract address + *@param _token `Token` contract address *@param _votingEscrow `VotingEscrow` contract address */ constructor( @@ -152,7 +152,7 @@ contract GaugeController { */ function gaugeTypes(address addr_) external view returns (int128) { int128 _gaugeType = gaugeTypes_[addr_]; - require(_gaugeType != 0); + require(_gaugeType != 0, "dev: gauge is not added"); return _gaugeType - 1; } @@ -316,7 +316,7 @@ contract GaugeController { int128 gaugeType_, uint256 weight_ ) external onlyAdmin { - require((gaugeType_ >= 0) && (gaugeType_ < nGaugeTypes)); //gauge_type 0 means unset + require((gaugeType_ >= 0) && (gaugeType_ < nGaugeTypes), "gauge_type 0 means unset"); //gauge_type 0 means unset require(gaugeTypes_[addr_] == 0, "cannot add the same gauge twice"); int128 _n = nGauges; unchecked { From a7286f2209529c1c4d1e708c7b9fb7d61a627c58 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 09:55:11 +0900 Subject: [PATCH 007/412] Updated interface --- contracts/curveFork/interfaces/ICRV.sol | 6 ++++++ .../curveFork/interfaces/IGaugeController.sol | 12 ++++++++++++ .../curveFork/interfaces/ILiquidityGauge.sol | 3 +++ contracts/curveFork/interfaces/IMinter.sol | 15 +++++++++++++++ .../curveFork/interfaces/ISmartWalletChecker.sol | 2 +- contracts/curveFork/interfaces/IVotingEscrow.sol | 10 ++++++++++ 6 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 contracts/curveFork/interfaces/IMinter.sol diff --git a/contracts/curveFork/interfaces/ICRV.sol b/contracts/curveFork/interfaces/ICRV.sol index 94cae1c8..c8710f56 100644 --- a/contracts/curveFork/interfaces/ICRV.sol +++ b/contracts/curveFork/interfaces/ICRV.sol @@ -4,4 +4,10 @@ pragma solidity 0.8.4; interface ICRV { function mint(address to_, uint256 value_) external returns (bool); + + function approve(address spender_, uint256 value_) external; + + function rate() external view returns (uint256); + + function futureEpochTimeWrite() external returns (uint256); } \ No newline at end of file diff --git a/contracts/curveFork/interfaces/IGaugeController.sol b/contracts/curveFork/interfaces/IGaugeController.sol index 049f6b9d..95a8ae41 100644 --- a/contracts/curveFork/interfaces/IGaugeController.sol +++ b/contracts/curveFork/interfaces/IGaugeController.sol @@ -5,4 +5,16 @@ pragma solidity 0.8.4; interface IGaugeController { function gaugeTypes(address addr_) external view returns (uint256); + function votingEscrow() external view returns (address); + + function checkpointGauge(address addr) external; + + function addType(string memory name_, uint256 weight_) external; + + function addGauge(address addr_, int128 gaugeType_, uint256 weight_) external; + + function gaugeRelativeWeight(address addr, uint256 time) + external + view + returns (uint256); } \ No newline at end of file diff --git a/contracts/curveFork/interfaces/ILiquidityGauge.sol b/contracts/curveFork/interfaces/ILiquidityGauge.sol index 0c1cda58..b62c362a 100644 --- a/contracts/curveFork/interfaces/ILiquidityGauge.sol +++ b/contracts/curveFork/interfaces/ILiquidityGauge.sol @@ -6,4 +6,7 @@ interface ILiquidityGauge { function userCheckpoint(address addr_) external returns (bool); function integrateFraction(address addr_) external view returns (uint256); + + function deposit(uint256 _value, address _addr) external; + function withdraw(uint256 _value)external; } \ No newline at end of file diff --git a/contracts/curveFork/interfaces/IMinter.sol b/contracts/curveFork/interfaces/IMinter.sol new file mode 100644 index 00000000..a80dc607 --- /dev/null +++ b/contracts/curveFork/interfaces/IMinter.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.4; + + +interface IMinter { + function token() external view returns (address); + + function controller() external view returns (address); + + function minted(address user_, address gauge_) + external + view + returns (uint256); +} diff --git a/contracts/curveFork/interfaces/ISmartWalletChecker.sol b/contracts/curveFork/interfaces/ISmartWalletChecker.sol index 94e4cc32..60bb6422 100644 --- a/contracts/curveFork/interfaces/ISmartWalletChecker.sol +++ b/contracts/curveFork/interfaces/ISmartWalletChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; +pragma solidity 0.8.4; // Interface for checking whether address belongs to a whitelisted type of a smart wallet. // When new types are added - the whole contract is changed diff --git a/contracts/curveFork/interfaces/IVotingEscrow.sol b/contracts/curveFork/interfaces/IVotingEscrow.sol index be43cd59..92a68826 100644 --- a/contracts/curveFork/interfaces/IVotingEscrow.sol +++ b/contracts/curveFork/interfaces/IVotingEscrow.sol @@ -15,4 +15,14 @@ interface IVotingEscrow { //function balanceOf(address addr)external view returns (uint256); function totalSupply(uint256 t_) external view returns (uint256); + function getUserPointEpoch(address _user) + external + view + returns (uint256); + + function userPointHistoryTs(address _addr, uint256 _idx) + external + view + returns (uint256); + } \ No newline at end of file From 2c71a7424a7ea78754efa997f3152bbf219e48b1 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 09:55:51 +0900 Subject: [PATCH 008/412] Token contract rewritten into a cames case --- contracts/curveFork/CRV.sol | 76 ++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol index 2c6064b9..f058234e 100644 --- a/contracts/curveFork/CRV.sol +++ b/contracts/curveFork/CRV.sol @@ -34,11 +34,11 @@ contract CRV is ERC20 { uint256 constant INFLATION_DELAY = 1 days; // Supply variables - int128 public mining_epoch; - uint256 public start_epoch_time; + int128 public miningEpoch; + uint256 public startEpochTime; uint256 public rate; - uint256 start_epoch_supply; + uint256 startEpochSupply; constructor() ERC20("Curve", "CRV") { uint256 init_supply = INITIAL_SUPPLY * 10 ** decimals(); @@ -46,29 +46,29 @@ contract CRV is ERC20 { admin = msg.sender; - start_epoch_time = + startEpochTime = block.timestamp + INFLATION_DELAY - RATE_REDUCTION_TIME; - mining_epoch = -1; + miningEpoch = -1; rate = 0; - start_epoch_supply = init_supply; + startEpochSupply = init_supply; } // @dev Update mining rate and supply at the start of the epoch // Any modifying mining call must also call this - function _update_mining_parameters() internal { + function _updateMiningParameters() internal { uint256 _rate = rate; - uint256 _start_epoch_supply = start_epoch_supply; + uint256 _startEpochSupply = startEpochSupply; - start_epoch_time += RATE_REDUCTION_TIME; - mining_epoch += 1; + startEpochTime += RATE_REDUCTION_TIME; + miningEpoch += 1; if (_rate == 0) { _rate = INITIAL_RATE; } else { - _start_epoch_supply += _rate * RATE_REDUCTION_TIME; - start_epoch_supply = _start_epoch_supply; + _startEpochSupply += _rate * RATE_REDUCTION_TIME; + startEpochSupply = _startEpochSupply; _rate = (_rate * RATE_DENOMINATOR) / RATE_REDUCTION_COEFFICIENT; } @@ -77,64 +77,64 @@ contract CRV is ERC20 { emit UpdateMiningParameters( block.timestamp, _rate, - _start_epoch_supply + _startEpochSupply ); } // @notice Update mining rate and supply at the start of the epoch // @dev Callable by any address, but only once per epoch // Total supply becomes slightly larger if(this function is called late - function update_mining_parameters() external { - require(block.timestamp >= start_epoch_time + RATE_REDUCTION_TIME); // dev: too soon! - _update_mining_parameters(); + function updateMiningParameters() external { + require(block.timestamp >= startEpochTime + RATE_REDUCTION_TIME); // dev: too soon! + _updateMiningParameters(); } // @notice Get timestamp of the current mining epoch start // while simultaneously updating mining parameters // @return Timestamp of the epoch - function start_epoch_time_write() external returns (uint256) { - uint256 _start_epoch_time = start_epoch_time; - if (block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME) { - _update_mining_parameters(); - return start_epoch_time; + function startEpochTimeWrite() external returns (uint256) { + uint256 _startEpochTime = startEpochTime; + if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); + return startEpochTime; } else { - return _start_epoch_time; + return _startEpochTime; } } // @notice Get timestamp of the next mining epoch start // while simultaneously updating mining parameters // @return Timestamp of the next epoch - function future_epoch_time_write() external returns (uint256) { - uint256 _start_epoch_time = start_epoch_time; - if (block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME) { - _update_mining_parameters(); - return start_epoch_time + RATE_REDUCTION_TIME; + function futureEpochTimeWrite() external returns (uint256) { + uint256 _startEpochTime = startEpochTime; + if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); + return startEpochTime + RATE_REDUCTION_TIME; } else { - return _start_epoch_time + RATE_REDUCTION_TIME; + return _startEpochTime + RATE_REDUCTION_TIME; } } - function _available_supply() internal view returns (uint256) { - return start_epoch_supply + (block.timestamp - start_epoch_time) * rate; + function _availableSupply() internal view returns (uint256) { + return startEpochSupply + (block.timestamp - startEpochTime) * rate; } // @notice Current number of tokens in existence (claimed or unclaimed) - function available_supply() external view returns (uint256) { - return _available_supply(); + function availableSupply() external view returns (uint256) { + return _availableSupply(); } // @notice How much supply is mintable from start timestamp till end timestamp // @param start Start of the time interval (timestamp) // @param end End of the time interval (timestamp) // @return Tokens mintable from `start` till `end` - function mintable_in_timeframe( + function mintableInTimeframe( uint256 start, uint256 end ) external view returns (uint256) { require(start <= end); // dev: start > end uint256 to_mint = 0; - uint256 current_epoch_time = start_epoch_time; + uint256 current_epoch_time = startEpochTime; uint256 current_rate = rate; // Special case if(end is in future (not yet minted) epoch @@ -184,7 +184,7 @@ contract CRV is ERC20 { // @notice Set the minter address // @dev Only callable once, when minter has not yet been set // @param _minter Address of the minter - function set_minter(address _minter) external { + function setMinter(address _minter) external { require(msg.sender == admin); // dev: admin only require(minter == address(0)); // dev: can set the minter only once, at creation minter = _minter; @@ -194,7 +194,7 @@ contract CRV is ERC20 { // @notice Set the new admin. // @dev After all is set up, admin only can change the token name // @param _admin New admin address - function set_admin(address _admin) external { + function setAdmin(address _admin) external { require(msg.sender == admin); // dev: admin only admin = _admin; emit SetAdmin(_admin); @@ -209,8 +209,8 @@ contract CRV is ERC20 { require(msg.sender == minter); // dev: minter only require(_to != address(0)); // dev: zero address - if (block.timestamp >= start_epoch_time + RATE_REDUCTION_TIME) { - _update_mining_parameters(); + if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); } _mint(_to, _value); From 220c950cd4b27960171489548d10775d0aee2cda Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 09:57:58 +0900 Subject: [PATCH 009/412] Corrected names and comments --- contracts/curveFork/Minter.sol | 2 +- doc/v1.5/YMT/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/curveFork/Minter.sol b/contracts/curveFork/Minter.sol index 30eca9f5..c0da1d64 100644 --- a/contracts/curveFork/Minter.sol +++ b/contracts/curveFork/Minter.sol @@ -17,7 +17,7 @@ contract Minter is ReentrancyGuard { address public controller; // user -> gauge -> value - mapping(address => mapping(address => uint256)) public minted; //INSURE minted amount of user from specific gauge. + mapping(address => mapping(address => uint256)) public minted; // minted amount of user from specific gauge. // minter -> user -> can mint? mapping(address => mapping(address => bool)) public allowedToMintFor; // A can mint for B if [A => B => true]. diff --git a/doc/v1.5/YMT/index.md b/doc/v1.5/YMT/index.md index 5e8555f2..73917d96 100644 --- a/doc/v1.5/YMT/index.md +++ b/doc/v1.5/YMT/index.md @@ -2,7 +2,7 @@ ## 概要 -Curve DAO Tokenは、piecewise-linear mining supplyを持つERC20トークンです。このトークンのサプライは線形に増加しており、マイニングパラメータは定期的に更新される。初期供給は一定であり、その後の供給レートは時間の経過とともに減少していく。 +YMT Tokenは、piecewise-linear mining supplyを持つERC20トークンです。このトークンのサプライは線形に増加しており、マイニングパラメータは定期的に更新される。初期供給は一定であり、その後の供給レートは時間の経過とともに減少していく。 --- From deac06ef9c6d97eec97faffbec483048f5f57268 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 09:58:29 +0900 Subject: [PATCH 010/412] Add test contracts. --- contracts/Tester/TestLP.sol | 141 +++++++++ contracts/curveFork/LiquidityGauge.sol | 398 +++++++++++++++++++++++++ 2 files changed, 539 insertions(+) create mode 100644 contracts/Tester/TestLP.sol create mode 100644 contracts/curveFork/LiquidityGauge.sol diff --git a/contracts/Tester/TestLP.sol b/contracts/Tester/TestLP.sol new file mode 100644 index 00000000..20bde5da --- /dev/null +++ b/contracts/Tester/TestLP.sol @@ -0,0 +1,141 @@ +pragma solidity 0.8.4; +//SPDX-License-Identifier: MIT + +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +contract TestLP { + using SafeMath for uint256; + + string public name; + string public symbol; + uint256 public _decimals; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) allowances; + uint256 public total_supply; + address public minter; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); + + constructor( + string memory _name, + string memory _symbol, + uint256 _decimal, + uint256 _supply + ) { + uint256 init_supply = _supply.mul(10**_decimals); + name = _name; + symbol = _symbol; + _decimals = _decimal; + balanceOf[msg.sender] = init_supply; + total_supply = init_supply; + minter = msg.sender; + + emit Transfer(address(0), msg.sender, init_supply); + } + + function set_minter(address _minter) external { + assert(msg.sender == minter); + minter = _minter; + } + + function totalSupply() external view returns (uint256) { + return total_supply; + } + + function allowance(address _owner, address _spender) + external + view + returns (uint256) + { + /*** + *@notice Check the amount of tokens that an owner allowed to a spender + *@param _owner The address which owns the funds + *@param _spender The address which will spend the funds + *@return uint256 specifying the amount of tokens still available for the spender + */ + return allowances[_owner][_spender]; + } + + function transfer(address _to, uint256 _value) external returns (bool) { + /*** + *@notice Transfer `_value` tokens from `msg.sender` to `_to` + *@dev Vyper does not allow underflows, so the subtraction in + * this function will revert on an insufficient balance + *@param _to The address to transfer to + *@param _value The amount to be transferred + *@return bool success + */ + assert(_to != address(0)); // dev: transfers to 0x0 are not allowed + balanceOf[msg.sender] = balanceOf[msg.sender].sub(_value); + balanceOf[_to] = balanceOf[_to].add(_value); + emit Transfer(msg.sender, _to, _value); + return true; + } + + function transferFrom( + address _from, + address _to, + uint256 _value + ) external returns (bool) { + /*** + * @notice Transfer `_value` tokens from `_from` to `_to` + * @param _from address The address which you want to send tokens from + * @param _to address The address which you want to transfer to + * @param _value uint256 the amount of tokens to be transferred + * @return bool success + */ + require(_from != address(0), "ERC20: transfer from the zero address"); + require(_to != address(0), "ERC20: transfer to the zero address"); // dev: transfers to 0x0 are not allowed + // NOTE: vyper does not allow underflows + // so the following subtraction would revert on insufficient balance + balanceOf[_from] = balanceOf[_from].sub(_value); + balanceOf[_to] = balanceOf[_to].add(_value); + if (msg.sender != minter) { + allowances[_from][msg.sender] = allowances[_from][msg.sender].sub( + _value + ); + } + emit Transfer(_from, _to, _value); + return true; + } + + function approve(address _spender, uint256 _value) external returns (bool) { + allowances[msg.sender][_spender] = _value; + + emit Approval(msg.sender, _spender, _value); + return true; + } + + function mint(address _to, uint256 _value) external { + assert(msg.sender == minter); + assert(_to != address(0)); + + total_supply = total_supply.add(_value); + balanceOf[_to] = balanceOf[_to].add(_value); + emit Transfer(address(0), _to, _value); + } + + function _burn(address _to, uint256 _value) internal { + assert(_to != address(0)); + + total_supply = total_supply.sub(_value); + balanceOf[_to] = balanceOf[_to].sub(_value); + emit Transfer(_to, address(0), _value); + } + + function burn(uint256 _value) external { + require(msg.sender == minter, "Only minter is allowed to burn"); + _burn(msg.sender, _value); + } + + function burnFrom(address _to, uint256 _value) external { + require(msg.sender == minter, "Only minter is allowed to burn"); + _burn(_to, _value); + } +} \ No newline at end of file diff --git a/contracts/curveFork/LiquidityGauge.sol b/contracts/curveFork/LiquidityGauge.sol new file mode 100644 index 00000000..7a447409 --- /dev/null +++ b/contracts/curveFork/LiquidityGauge.sol @@ -0,0 +1,398 @@ +pragma solidity 0.8.4; + +/*** + *@title Liquidity Gauge + * SPDX-License-Identifier: MIT + *@notice Used for measuring liquidity and insurance + */ + +//dao-contracts +import "./interfaces/IGaugeController.sol"; +import "./interfaces/ICRV.sol"; +import "./interfaces/IMinter.sol"; +import "./interfaces/IVotingEscrow.sol"; + + +//libraries +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +contract LiquidityGauge is ReentrancyGuard { + event Deposit(address indexed provider, uint256 value); + event Withdraw(address indexed provider, uint256 value); + event UpdateLiquidityLimit( + address user, + uint256 original_balance, + uint256 original_supply, + uint256 working_balance, + uint256 working_supply, + uint256 voting_balance, + uint256 voting_total + ); + + uint256 constant TOKENLESS_PRODUCTION = 40; + uint256 constant BOOST_WARMUP = 86400 * 14; + uint256 constant WEEK = 604800; + + //Contracts + IMinter public minter; + ICRV public token; + IERC20 public template; + IGaugeController public controller; + IVotingEscrow public voting_escrow; + + mapping(address => uint256) public balanceOf; + uint256 public totalSupply; + uint256 public future_epoch_time; + + // caller -> recipient -> can deposit? + mapping(address => mapping(address => bool)) public approved_to_deposit; + + mapping(address => uint256) public working_balances; + uint256 public working_supply; + + // The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint + // All values are kept in units of being multiplied by 1e18 + uint256 public period; //modified from "int256 public period" since it never be minus. + + uint256[100000000000000000000000000000] public period_timestamp; + + // 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint + uint256[100000000000000000000000000000] public integrate_inv_supply; // bump epoch when rate() changes. Iis(t)=int(r'(t)/S(t))dt (from CurveDAO whitepaper) + + // 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint + mapping(address => uint256) public integrate_inv_supply_of; + mapping(address => uint256) public integrate_checkpoint_of; + + // ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint + // Units rate * t = already number of coins per address to issue + mapping(address => uint256) public integrateFraction; //Mintable Token amount (include minted amount) + + uint256 public inflation_rate; + bool public is_killed; + + address public admin; + + modifier onlyOwner() { + require( + admin == msg.sender, + "Caller is not allowed to operate" + ); + _; + } + + /*** + *@notice Contract constructor + *@param _lp_addr Liquidity Pool contract address + *@param _minter Minter contract address + */ + constructor( + address _lp_addr, + address _minter + ) { + require(_lp_addr != address(0)); + require(_minter != address(0)); + + template = IERC20(_lp_addr); + minter = IMinter(_minter); + address _token_addr = minter.token(); + token = ICRV(_token_addr); + controller = IGaugeController(minter.controller()); + voting_escrow = IVotingEscrow(controller.votingEscrow()); + period_timestamp[0] = block.timestamp; + inflation_rate = token.rate(); + future_epoch_time = token.futureEpochTimeWrite(); + admin = msg.sender; + } + + /*** + *@notice Calculate limits which depend on the amount of Token per-user. + * Effectively it calculates working balances to apply amplification + * of production by + *@param _addr User address + *@param _l User's amount of liquidity (LP tokens) + *@param _L Total amount of liquidity (LP tokens) + */ + function _update_liquidity_limit( + address _addr, + uint256 _l, + uint256 _L + ) internal { + // To be called after totalSupply is updated + uint256 _voting_balance = voting_escrow.balanceOf( + _addr, + block.timestamp + ); + uint256 _voting_total = voting_escrow.totalSupply(block.timestamp); + + uint256 _lim = (_l * TOKENLESS_PRODUCTION) / 100; + if ( + (_voting_total > 0) && + (block.timestamp > period_timestamp[0] + BOOST_WARMUP) + ) { + _lim += + (_L * _voting_balance * (100 - TOKENLESS_PRODUCTION)) / + _voting_total / + 100; + } + + _lim = min(_l, _lim); + uint256 _old_bal = working_balances[_addr]; + working_balances[_addr] = _lim; + uint256 _working_supply = working_supply + _lim - _old_bal; + working_supply = _working_supply; + + emit UpdateLiquidityLimit( + _addr, + _l, + _L, + _lim, + _working_supply, + _voting_balance, + _voting_total + ); + } + + //to avoid "stack too deep" + struct CheckPointParameters { + uint256 period; + uint256 period_time; + uint256 integrate_inv_supply; + uint256 rate; + uint256 new_rate; + uint256 prev_future_epoch; + uint256 working_balance; + uint256 working_supply; + } + + /*** + *@notice Checkpoint for a user + *@param _addr User address + * + *This function does, + *1. Calculate Iis for All: Calc and add Iis for every week. Iis only increses over time. + *2. Calculate Iu for _addr: Calc by (defferece between Iis(last time) and Iis(this time))* LP deposit amount of _addr(include locking boost) + * + * working_supply & working_balance = total_supply & total_balance with locking boost。 + * Check whitepaper about Iis and Iu. + */ + function _checkpoint(address _addr) internal { + CheckPointParameters memory _st; + + _st.period = period; + _st.period_time = period_timestamp[_st.period]; + _st.integrate_inv_supply = integrate_inv_supply[_st.period]; + _st.rate = inflation_rate; + _st.new_rate = _st.rate; + _st.prev_future_epoch = future_epoch_time; + if (_st.prev_future_epoch >= _st.period_time) { + //update future_epoch_time & inflation_rate + future_epoch_time = token.futureEpochTimeWrite(); + _st.new_rate = token.rate(); + inflation_rate = _st.new_rate; + } + controller.checkpointGauge(address(this)); + + uint256 _working_balance = working_balances[_addr]; + uint256 _working_supply = working_supply; + + if (is_killed) { + _st.rate = 0; // Stop distributing inflation as soon as killed + } + + // Update integral of 1/supply + if (block.timestamp > _st.period_time) { + uint256 _prev_week_time = _st.period_time; + uint256 _week_time; + unchecked { + _week_time = min( + ((_st.period_time + WEEK) / WEEK) * WEEK, + block.timestamp + ); + } + + for (uint256 i; i < 500; ) { + uint256 _dt = _week_time - _prev_week_time; + uint256 _w = controller.gaugeRelativeWeight( + address(this), + (_prev_week_time / WEEK) * WEEK + ); + + if (_working_supply > 0) { + if ( + _st.prev_future_epoch >= _prev_week_time && + _st.prev_future_epoch < _week_time + ) { + // If we went across one or multiple epochs, apply the rate + // of the first epoch until it ends, and then the rate of + // the last epoch. + // If more than one epoch is crossed - the gauge gets less, + // but that'd meen it wasn't called for more than 1 year + _st.integrate_inv_supply += + (_st.rate * + _w * + (_st.prev_future_epoch - _prev_week_time)) / + _working_supply; + _st.rate = _st.new_rate; + _st.integrate_inv_supply += + (_st.rate * + _w * + (_week_time - _st.prev_future_epoch)) / + _working_supply; + } else { + _st.integrate_inv_supply += + (_st.rate * _w * _dt) / + _working_supply; + } + // On precisions of the calculation + // rate ~= 10e18 + // last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + // _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + // The largest loss is at dt = 1 + // Loss is 1e-9 - acceptable + } + if (_week_time == block.timestamp) { + break; + } + _prev_week_time = _week_time; + _week_time = min(_week_time + WEEK, block.timestamp); + unchecked { + ++i; + } + } + } + + _st.period += 1; + period = _st.period; + period_timestamp[_st.period] = block.timestamp; + integrate_inv_supply[_st.period] = _st.integrate_inv_supply; + + // Update user-specific integrals + // Calc the ΔIu of _addr and add it to Iu. + integrateFraction[_addr] += + (_working_balance * + (_st.integrate_inv_supply - integrate_inv_supply_of[_addr])) / + 10**18; + integrate_inv_supply_of[_addr] = _st.integrate_inv_supply; + integrate_checkpoint_of[_addr] = block.timestamp; + } + + /*** + *@notice Record a checkpoint for `_addr` + *@param _addr User address + *@return bool success + */ + function userCheckpoint(address _addr) external returns (bool) { + require( + (msg.sender == _addr) || (msg.sender == address(minter)), + "dev: unauthorized" + ); + _checkpoint(_addr); + _update_liquidity_limit(_addr, balanceOf[_addr], totalSupply); + return true; + } + + /*** + *@notice Get the number of claimable tokens per user + *@dev This function should be manually changed to "view" in the ABI + *@return uint256 number of claimable tokens per user + */ + function claimable_tokens(address _addr) external returns (uint256) { + _checkpoint(_addr); + return (integrateFraction[_addr] - + minter.minted(_addr, address(this))); + } + + /*** + *@notice Kick `_addr` for abusing their boost + *@dev Only if either they had another voting event, or their voting escrow lock expired + *@param _addr Address to kick + */ + function kick(address _addr) external { + uint256 _t_last = integrate_checkpoint_of[_addr]; + uint256 _t_ve = voting_escrow.userPointHistoryTs( + _addr, + voting_escrow.getUserPointEpoch(_addr) + ); + uint256 _balance = balanceOf[_addr]; + + require( + voting_escrow.balanceOf(_addr, block.timestamp) == 0 || + _t_ve > _t_last, + "dev: kick not allowed" + ); + require( + working_balances[_addr] > (_balance * TOKENLESS_PRODUCTION) / 100, + "dev: kick not needed" + ); + + _checkpoint(_addr); + _update_liquidity_limit(_addr, balanceOf[_addr], totalSupply); + } + + /*** + *@notice Set whether `_addr` can deposit tokens for `msg.sender` + *@param _addr Address to set approval on + *@param can_deposit bool - can this account deposit for `msg.sender`? + */ + function set_approve_deposit(address _addr, bool can_deposit) external { + approved_to_deposit[_addr][msg.sender] = can_deposit; + } + + /*** + *@notice Deposit `_value` LP tokens + *@param _value Number of tokens to deposit + *@param _addr Address to deposit for + */ + function deposit(uint256 _value, address _addr) external nonReentrant { + if (_addr != msg.sender) { + require(approved_to_deposit[msg.sender][_addr], "Not approved"); + } + + _checkpoint(_addr); + + if (_value != 0) { + uint256 _balance = balanceOf[_addr] + _value; + uint256 _supply = totalSupply + _value; + balanceOf[_addr] = _balance; + totalSupply = _supply; + + _update_liquidity_limit(_addr, _balance, _supply); + + require(template.transferFrom(msg.sender, address(this), _value)); + } + emit Deposit(_addr, _value); + } + + /*** + *@notice Withdraw `_value` LP tokens + *@param _value Number of tokens to withdraw + */ + function withdraw(uint256 _value) external nonReentrant { + _checkpoint(msg.sender); + + uint256 _balance = balanceOf[msg.sender] - _value; + uint256 _supply = totalSupply - _value; + balanceOf[msg.sender] = _balance; + totalSupply = _supply; + + _update_liquidity_limit(msg.sender, _balance, _supply); + + require(template.transfer(msg.sender, _value)); + + emit Withdraw(msg.sender, _value); + } + + function integrate_checkpoint() external view returns (uint256) { + return period_timestamp[period]; + } + + function kill_me() external onlyOwner { + is_killed = !is_killed; + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} From a4f6fdd9881e0b100576931fe023c159241e0ac4 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 10:34:11 +0900 Subject: [PATCH 011/412] Fix VotingEscrow. --- contracts/curveFork/VotingEscrow.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 1b0c9f19..827a011c 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -223,9 +223,6 @@ contract VotingEscrow is ReentrancyGuard { int128 _newDSlope = 0; uint256 _epoch = epoch; - int128( - int128(uint128(oldLocked_.end)) - int128(uint128(block.timestamp)) - ); if (addr_ != address(0)) { // Calculate slopes and biases // Kept at zero when they have to From 7e37dc6ecb6dc132290b2113522448baa7e95684 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 10:39:38 +0900 Subject: [PATCH 012/412] Add Minter test code. --- test/fork/Minter.test.ts | 332 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 test/fork/Minter.test.ts diff --git a/test/fork/Minter.test.ts b/test/fork/Minter.test.ts new file mode 100644 index 00000000..8a919e03 --- /dev/null +++ b/test/fork/Minter.test.ts @@ -0,0 +1,332 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract, Signer } from "ethers"; + +let snapshotId: string; + +async function snapshot(): Promise { + return await (ethers.provider.send("evm_snapshot", []) as Promise); +} + +async function restore(snapshotId: string): Promise { + return await ethers.provider.send("evm_revert", [snapshotId]); +} + +describe("Minter", function () { + const YEAR: BigNumber = BigNumber.from(86400 * 365); + const MONTH: BigNumber = BigNumber.from(86400 * 30); + const WEEK: BigNumber = BigNumber.from(86400 * 7); + const DAY: BigNumber = BigNumber.from(86400); + + const name = "tokenToken"; + const symbol = "token"; + const decimal = 18; + + const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + const MAX_UINT256 = BigNumber.from("115792089237316195423570985008687907853269984665640564039457584007913129639935"); + const two_to_the_256_minus_1 = BigNumber.from("2").pow(BigNumber.from("256")).sub(BigNumber.from("1")); + const ten_to_the_21 = BigNumber.from("1000000000000000000000"); + const ten_to_the_20 = BigNumber.from("100000000000000000000"); + const ten_to_the_19 = BigNumber.from("10000000000000000000"); + const ten_to_the_18 = BigNumber.from("1000000000000000000"); + const ten_to_the_17 = BigNumber.from("100000000000000000"); + const ten_to_the_9 = BigNumber.from("1000000000"); + + const a = BigNumber.from("5"); + const zero = BigNumber.from("0"); + + const TYPE_WEIGHTS = [zero , ten_to_the_17.mul(a), ten_to_the_19]; + const GAUGE_WEIGHTS = [ten_to_the_19, ten_to_the_18, ten_to_the_17.mul(a)]; + const GAUGE_TYPES = [BigNumber.from("1"), BigNumber.from("1"), BigNumber.from("2")]; + + let creator: Signer; + let alice: Signer; + let bob: Signer; + let charly: Signer; + + let accountsAddress: string[]; + + let token: Contract; + let voting_escrow: Contract; + let gauge_controller: Contract; + let mock_lp_token: Contract; + let minter: Contract; + let lg1: Contract; + let lg2: Contract; + let lg3: Contract; + let three_gauges_contracts: Contract[]; + let three_gauges: any[]; + + before(async () => { + //import + [creator, alice, bob, charly] = await ethers.getSigners(); + + const Token = await ethers.getContractFactory("CRV"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + const GaugeController = await ethers.getContractFactory("GaugeController"); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGauge"); + const TestLP = await ethers.getContractFactory("TestLP"); + const Minter = await ethers.getContractFactory("Minter"); + + //deploy + token = await Token.deploy(); + voting_escrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + gauge_controller = await GaugeController.deploy(token.address, voting_escrow.address); + + mock_lp_token = await TestLP.deploy("tokenDAO LP token", "iToken", decimal, ten_to_the_21); //Not using the actual tokenDAO contract + minter = await Minter.deploy(token.address, gauge_controller.address); + + lg1 = await LiquidityGauge.deploy(mock_lp_token.address, minter.address); + lg2 = await LiquidityGauge.deploy(mock_lp_token.address, minter.address); + lg3 = await LiquidityGauge.deploy(mock_lp_token.address, minter.address); + + three_gauges_contracts = [lg1, lg2, lg3]; + three_gauges = [lg1.address, lg2.address, lg3.address]; + + //--------setup--------// + await token.setMinter(minter.address); + let accounts = [creator, alice, bob, charly]; + accountsAddress = [ + await creator.getAddress(), + await alice.getAddress(), + await bob.getAddress(), + await charly.getAddress(), + ]; + + //add types + for (let i = 0; i < 3; i++) { + await gauge_controller.addType("Liquidity", TYPE_WEIGHTS[i]); + } + + //add gauges + for (let i = 0; i < 3; i++) { + await gauge_controller.addGauge(three_gauges[i], GAUGE_TYPES[i], GAUGE_WEIGHTS[i]); + } + + //token transfer + for (let i = 1; i < 4; i++) { + await mock_lp_token.transfer(accountsAddress[i], ten_to_the_18); + } + + //approve gauge + for (let i = 0; i < 3; i++) { + for (let t = 0; t < 3; t++) { + await mock_lp_token.connect(accounts[i + 1]).approve(three_gauges[t], ten_to_the_18); + } + } + }); + + beforeEach(async () => { + snapshotId = await snapshot(); + }); + + afterEach(async () => { + await restore(snapshotId); + }); + + describe("test_minter", function () { + it("test_mint", async () => { + await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + + await minter.connect(alice).mint(three_gauges[0]); //gauge_address, msg.sender, mint + let expected = await lg1.integrateFraction(accountsAddress[1]); + + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await token.balanceOf(accountsAddress[1])).to.equal(expected); + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(expected); + }); + + it("test_mint_immediate", async () => { + //setup + await three_gauges_contracts[0].connect(alice).deposit(ten_to_the_18, accountsAddress[1]); + + let t0 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + let moment = t0.add(WEEK).div(WEEK).mul(WEEK).add("5"); + await ethers.provider.send("evm_setNextBlockTimestamp", [moment.toNumber()]); + + //mint + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal("0"); + await minter.connect(alice).mint(three_gauges[0]); + + //check + let balance = await token.balanceOf(accountsAddress[1]); + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(balance); + }); + + it("test_mint_multiple_same_gauge", async () => { + await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + await minter.connect(alice).mint(three_gauges[0]); + let balance = await token.balanceOf(accountsAddress[1]); + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + await minter.connect(alice).mint(three_gauges[0]); + let expected = await lg1.integrateFraction(accountsAddress[1]); + let final_balance = await token.balanceOf(accountsAddress[1]); + + expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success + expect(final_balance).to.equal(expected); //2nd mint works fine + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(expected); //tracks fine + }); + + it("test_mint_multiple_gauges", async () => { + //setup + await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + await lg2.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + await lg3.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + + //mint + for (let i = 0; i < 3; i++) { + await minter.connect(alice).mint(three_gauges[i]); + } + + //check + let total_minted = BigNumber.from("0"); + + for (let i = 0; i < 3; i++) { + let gauge = three_gauges_contracts[i]; + let minted = await minter.minted(accountsAddress[1], gauge.address); + expect(minted).to.equal(await gauge.integrateFraction(accountsAddress[1])); + total_minted = total_minted.add(minted); + } + + expect(await token.balanceOf(accountsAddress[1])).to.equal(total_minted); + }); + + it("test_mint_after_withdraw", async () => { + await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [WEEK.mul(BigNumber.from("2")).toNumber()]); + + await lg1.connect(alice).withdraw(ten_to_the_18); + await minter.connect(alice).mint(three_gauges[0]); + + expect((await token.balanceOf(accountsAddress[1])).gt(BigNumber.from("0"))).to.equal(true); + }); + + it("test_mint_multiple_after_withdraw", async () => { + await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [10]); + await lg1.connect(alice).withdraw(ten_to_the_18); + await minter.connect(alice).mint(three_gauges[0]); + + let balance = await token.balanceOf(accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [10]); + await minter.connect(alice).mint(three_gauges[0]); + + expect(await token.balanceOf(accountsAddress[1])).to.equal(balance); + }); + + it("test_no_deposit", async () => { + await minter.connect(alice).mint(three_gauges[0]); + expect(await token.balanceOf(accountsAddress[1])).to.equal(zero); + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(zero); + }); + + it("test_mint_wrong_gauge", async () => { + await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + await minter.connect(alice).mint(three_gauges[1]); + + //check + expect(await token.balanceOf(accountsAddress[1])).to.equal(zero); + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(zero); + expect(await minter.minted(accountsAddress[1], three_gauges[1])).to.equal(zero); + }); + + it("test_mint_not_a_gauge", async () => { + await expect(minter.mint(accountsAddress[1])).to.revertedWith("dev: gauge is not added"); + }); + + it("test_mint_before_inflation_begins", async () => { + await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); + expect(await token.miningEpoch()).to.equal(BigNumber.from("-1")); + + await minter.connect(alice).mint(three_gauges[0]); + expect(await token.balanceOf(accountsAddress[1])).to.equal(BigNumber.from("0")); + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(zero); + }); + + it("test_mint_many_multiple_gauges", async () => { + //setup + await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + await lg2.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + await lg3.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + + let addresses = [ + lg1.address, + lg2.address, + lg3.address, + ZERO_ADDRESS, + ZERO_ADDRESS, + ZERO_ADDRESS, + ZERO_ADDRESS, + ZERO_ADDRESS, + ]; + await minter.connect(alice).mintMany(addresses); + + //check + let total_minted = BigNumber.from("0"); + + for (let i = 0; i < 3; i++) { + let gauge = three_gauges_contracts[i]; + let minted = await minter.minted(accountsAddress[1], gauge.address); + expect(minted).to.equal(await gauge.integrateFraction(accountsAddress[1])); + total_minted = total_minted.add(minted); + } + + expect(await token.balanceOf(accountsAddress[1])).to.equal(total_minted); + }); + + it("test_toggle_approve_mint", async () => { + await minter.connect(alice).toggleApproveMint(accountsAddress[2]); + expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(true); + + await minter.connect(alice).toggleApproveMint(accountsAddress[2]); + expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(false); + }); + + it("test_mint_for", async () => { + await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + + await minter.connect(alice).toggleApproveMint(accountsAddress[2]); + expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(true); + + await minter.connect(bob).mintFor(lg1.address, accountsAddress[1]); + + let expected = await lg1.integrateFraction(accountsAddress[1]); + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await token.balanceOf(accountsAddress[1])).to.equal(expected); + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(expected); + }); + + it("test_mint_for_fail", async () => { + await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); + + expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(false); + + await minter.connect(bob).mintFor(lg1.address, accountsAddress[1]); + + expect(await token.balanceOf(accountsAddress[1])).to.equal(0); + expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(0); + }); + }); +}); From ef2c7ece268f9fc1333231619373ad1dd1699ac1 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 26 Oct 2023 17:30:40 +0900 Subject: [PATCH 013/412] Gauge controller type --- contracts/curveFork/GaugeController.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol index daaa11de..619050c4 100644 --- a/contracts/curveFork/GaugeController.sol +++ b/contracts/curveFork/GaugeController.sol @@ -486,7 +486,7 @@ contract GaugeController { function _changeGaugeWeight(address addr_, uint256 weight_) internal { // Change gauge weight // Only needed when testing in reality - int128 _gaugeType = gaugeTypes_[addr_]; + int128 _gaugeType = gaugeTypes_[addr_] - 1; uint256 _oldGaugeWeight = _getWeight(addr_); uint256 _typeWeight = _getTypeWeight(_gaugeType); uint256 _oldSum = _getSum(_gaugeType); From 5cade9eb7d93e59172c55fd4ee91dfa2d694285f Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 27 Oct 2023 17:03:19 +0900 Subject: [PATCH 014/412] Add Minter test code --- test/fork/Minter.test.ts | 332 -------------------------------- test/fork/Minter/Minter.test.ts | 241 +++++++++++++++++++++++ test/fork/helper.ts | 136 +++++++++++++ 3 files changed, 377 insertions(+), 332 deletions(-) delete mode 100644 test/fork/Minter.test.ts create mode 100644 test/fork/Minter/Minter.test.ts create mode 100644 test/fork/helper.ts diff --git a/test/fork/Minter.test.ts b/test/fork/Minter.test.ts deleted file mode 100644 index 8a919e03..00000000 --- a/test/fork/Minter.test.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract, Signer } from "ethers"; - -let snapshotId: string; - -async function snapshot(): Promise { - return await (ethers.provider.send("evm_snapshot", []) as Promise); -} - -async function restore(snapshotId: string): Promise { - return await ethers.provider.send("evm_revert", [snapshotId]); -} - -describe("Minter", function () { - const YEAR: BigNumber = BigNumber.from(86400 * 365); - const MONTH: BigNumber = BigNumber.from(86400 * 30); - const WEEK: BigNumber = BigNumber.from(86400 * 7); - const DAY: BigNumber = BigNumber.from(86400); - - const name = "tokenToken"; - const symbol = "token"; - const decimal = 18; - - const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; - - const MAX_UINT256 = BigNumber.from("115792089237316195423570985008687907853269984665640564039457584007913129639935"); - const two_to_the_256_minus_1 = BigNumber.from("2").pow(BigNumber.from("256")).sub(BigNumber.from("1")); - const ten_to_the_21 = BigNumber.from("1000000000000000000000"); - const ten_to_the_20 = BigNumber.from("100000000000000000000"); - const ten_to_the_19 = BigNumber.from("10000000000000000000"); - const ten_to_the_18 = BigNumber.from("1000000000000000000"); - const ten_to_the_17 = BigNumber.from("100000000000000000"); - const ten_to_the_9 = BigNumber.from("1000000000"); - - const a = BigNumber.from("5"); - const zero = BigNumber.from("0"); - - const TYPE_WEIGHTS = [zero , ten_to_the_17.mul(a), ten_to_the_19]; - const GAUGE_WEIGHTS = [ten_to_the_19, ten_to_the_18, ten_to_the_17.mul(a)]; - const GAUGE_TYPES = [BigNumber.from("1"), BigNumber.from("1"), BigNumber.from("2")]; - - let creator: Signer; - let alice: Signer; - let bob: Signer; - let charly: Signer; - - let accountsAddress: string[]; - - let token: Contract; - let voting_escrow: Contract; - let gauge_controller: Contract; - let mock_lp_token: Contract; - let minter: Contract; - let lg1: Contract; - let lg2: Contract; - let lg3: Contract; - let three_gauges_contracts: Contract[]; - let three_gauges: any[]; - - before(async () => { - //import - [creator, alice, bob, charly] = await ethers.getSigners(); - - const Token = await ethers.getContractFactory("CRV"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - const GaugeController = await ethers.getContractFactory("GaugeController"); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGauge"); - const TestLP = await ethers.getContractFactory("TestLP"); - const Minter = await ethers.getContractFactory("Minter"); - - //deploy - token = await Token.deploy(); - voting_escrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - gauge_controller = await GaugeController.deploy(token.address, voting_escrow.address); - - mock_lp_token = await TestLP.deploy("tokenDAO LP token", "iToken", decimal, ten_to_the_21); //Not using the actual tokenDAO contract - minter = await Minter.deploy(token.address, gauge_controller.address); - - lg1 = await LiquidityGauge.deploy(mock_lp_token.address, minter.address); - lg2 = await LiquidityGauge.deploy(mock_lp_token.address, minter.address); - lg3 = await LiquidityGauge.deploy(mock_lp_token.address, minter.address); - - three_gauges_contracts = [lg1, lg2, lg3]; - three_gauges = [lg1.address, lg2.address, lg3.address]; - - //--------setup--------// - await token.setMinter(minter.address); - let accounts = [creator, alice, bob, charly]; - accountsAddress = [ - await creator.getAddress(), - await alice.getAddress(), - await bob.getAddress(), - await charly.getAddress(), - ]; - - //add types - for (let i = 0; i < 3; i++) { - await gauge_controller.addType("Liquidity", TYPE_WEIGHTS[i]); - } - - //add gauges - for (let i = 0; i < 3; i++) { - await gauge_controller.addGauge(three_gauges[i], GAUGE_TYPES[i], GAUGE_WEIGHTS[i]); - } - - //token transfer - for (let i = 1; i < 4; i++) { - await mock_lp_token.transfer(accountsAddress[i], ten_to_the_18); - } - - //approve gauge - for (let i = 0; i < 3; i++) { - for (let t = 0; t < 3; t++) { - await mock_lp_token.connect(accounts[i + 1]).approve(three_gauges[t], ten_to_the_18); - } - } - }); - - beforeEach(async () => { - snapshotId = await snapshot(); - }); - - afterEach(async () => { - await restore(snapshotId); - }); - - describe("test_minter", function () { - it("test_mint", async () => { - await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - - await minter.connect(alice).mint(three_gauges[0]); //gauge_address, msg.sender, mint - let expected = await lg1.integrateFraction(accountsAddress[1]); - - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await token.balanceOf(accountsAddress[1])).to.equal(expected); - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(expected); - }); - - it("test_mint_immediate", async () => { - //setup - await three_gauges_contracts[0].connect(alice).deposit(ten_to_the_18, accountsAddress[1]); - - let t0 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - let moment = t0.add(WEEK).div(WEEK).mul(WEEK).add("5"); - await ethers.provider.send("evm_setNextBlockTimestamp", [moment.toNumber()]); - - //mint - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal("0"); - await minter.connect(alice).mint(three_gauges[0]); - - //check - let balance = await token.balanceOf(accountsAddress[1]); - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(balance); - }); - - it("test_mint_multiple_same_gauge", async () => { - await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - await minter.connect(alice).mint(three_gauges[0]); - let balance = await token.balanceOf(accountsAddress[1]); - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - await minter.connect(alice).mint(three_gauges[0]); - let expected = await lg1.integrateFraction(accountsAddress[1]); - let final_balance = await token.balanceOf(accountsAddress[1]); - - expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success - expect(final_balance).to.equal(expected); //2nd mint works fine - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(expected); //tracks fine - }); - - it("test_mint_multiple_gauges", async () => { - //setup - await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - await lg2.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - await lg3.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - - //mint - for (let i = 0; i < 3; i++) { - await minter.connect(alice).mint(three_gauges[i]); - } - - //check - let total_minted = BigNumber.from("0"); - - for (let i = 0; i < 3; i++) { - let gauge = three_gauges_contracts[i]; - let minted = await minter.minted(accountsAddress[1], gauge.address); - expect(minted).to.equal(await gauge.integrateFraction(accountsAddress[1])); - total_minted = total_minted.add(minted); - } - - expect(await token.balanceOf(accountsAddress[1])).to.equal(total_minted); - }); - - it("test_mint_after_withdraw", async () => { - await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [WEEK.mul(BigNumber.from("2")).toNumber()]); - - await lg1.connect(alice).withdraw(ten_to_the_18); - await minter.connect(alice).mint(three_gauges[0]); - - expect((await token.balanceOf(accountsAddress[1])).gt(BigNumber.from("0"))).to.equal(true); - }); - - it("test_mint_multiple_after_withdraw", async () => { - await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [10]); - await lg1.connect(alice).withdraw(ten_to_the_18); - await minter.connect(alice).mint(three_gauges[0]); - - let balance = await token.balanceOf(accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [10]); - await minter.connect(alice).mint(three_gauges[0]); - - expect(await token.balanceOf(accountsAddress[1])).to.equal(balance); - }); - - it("test_no_deposit", async () => { - await minter.connect(alice).mint(three_gauges[0]); - expect(await token.balanceOf(accountsAddress[1])).to.equal(zero); - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(zero); - }); - - it("test_mint_wrong_gauge", async () => { - await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - await minter.connect(alice).mint(three_gauges[1]); - - //check - expect(await token.balanceOf(accountsAddress[1])).to.equal(zero); - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(zero); - expect(await minter.minted(accountsAddress[1], three_gauges[1])).to.equal(zero); - }); - - it("test_mint_not_a_gauge", async () => { - await expect(minter.mint(accountsAddress[1])).to.revertedWith("dev: gauge is not added"); - }); - - it("test_mint_before_inflation_begins", async () => { - await lg1.connect(alice).deposit(ten_to_the_18, accountsAddress[1]); - expect(await token.miningEpoch()).to.equal(BigNumber.from("-1")); - - await minter.connect(alice).mint(three_gauges[0]); - expect(await token.balanceOf(accountsAddress[1])).to.equal(BigNumber.from("0")); - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(zero); - }); - - it("test_mint_many_multiple_gauges", async () => { - //setup - await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - await lg2.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - await lg3.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - - let addresses = [ - lg1.address, - lg2.address, - lg3.address, - ZERO_ADDRESS, - ZERO_ADDRESS, - ZERO_ADDRESS, - ZERO_ADDRESS, - ZERO_ADDRESS, - ]; - await minter.connect(alice).mintMany(addresses); - - //check - let total_minted = BigNumber.from("0"); - - for (let i = 0; i < 3; i++) { - let gauge = three_gauges_contracts[i]; - let minted = await minter.minted(accountsAddress[1], gauge.address); - expect(minted).to.equal(await gauge.integrateFraction(accountsAddress[1])); - total_minted = total_minted.add(minted); - } - - expect(await token.balanceOf(accountsAddress[1])).to.equal(total_minted); - }); - - it("test_toggle_approve_mint", async () => { - await minter.connect(alice).toggleApproveMint(accountsAddress[2]); - expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(true); - - await minter.connect(alice).toggleApproveMint(accountsAddress[2]); - expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(false); - }); - - it("test_mint_for", async () => { - await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - - await minter.connect(alice).toggleApproveMint(accountsAddress[2]); - expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(true); - - await minter.connect(bob).mintFor(lg1.address, accountsAddress[1]); - - let expected = await lg1.integrateFraction(accountsAddress[1]); - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await token.balanceOf(accountsAddress[1])).to.equal(expected); - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(expected); - }); - - it("test_mint_for_fail", async () => { - await lg1.connect(alice).deposit(ten_to_the_17, accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [MONTH.toNumber()]); - - expect(await minter.allowedToMintFor(accountsAddress[2], accountsAddress[1])).to.equal(false); - - await minter.connect(bob).mintFor(lg1.address, accountsAddress[1]); - - expect(await token.balanceOf(accountsAddress[1])).to.equal(0); - expect(await minter.minted(accountsAddress[1], three_gauges[0])).to.equal(0); - }); - }); -}); diff --git a/test/fork/Minter/Minter.test.ts b/test/fork/Minter/Minter.test.ts new file mode 100644 index 00000000..c74a8123 --- /dev/null +++ b/test/fork/Minter/Minter.test.ts @@ -0,0 +1,241 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, GaugeControllerTestSetup } from "../helper"; + +describe("Minter", function () { + let setup: GaugeControllerTestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new GaugeControllerTestSetup(); + await setup.setup(); + await setup.addType(); + await setup.addGauge(); + await setup.createLP(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("Minter Behavior", function () { + + // Test basic mint functionality + it("test mint", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); //gauge_address, msg.sender, mint + let expected = await setup.three_gauges_contracts[0].integrateFraction(setup.accountsAddress[1]); + + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(expected); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(expected); + }); + + // Test minting immediately after setup + it("test mint immediate", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + let t0 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + let moment = t0.add(setup.WEEK).div(setup.WEEK).mul(setup.WEEK).add("5"); + await ethers.provider.send("evm_setNextBlockTimestamp", [moment.toNumber()]); + + //mint + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal("0"); + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + + //check + let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(balance); + }); + + // Test multiple mint operations on the same gauge + it("test mint multiple same gauge", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + let expected = await setup.three_gauges_contracts[0].integrateFraction(setup.accountsAddress[1]); + let final_balance = await setup.token.balanceOf(setup.accountsAddress[1]); + + expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success + expect(final_balance).to.equal(expected); //2nd mint works fine + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(expected); //tracks fine + }); + + // Test minting across multiple gauges + it("test mint multiple gauges", async () => { + //setup + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.three_gauges_contracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.three_gauges_contracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + //mint + for (let i = 0; i < 3; i++) { + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[i]); + } + + //check + let total_minted = BigNumber.from("0"); + + for (let i = 0; i < 3; i++) { + let gauge = setup.three_gauges_contracts[i]; + let minted = await setup.minter.minted(setup.accountsAddress[1], gauge.address); + expect(minted).to.equal(await gauge.integrateFraction(setup.accountsAddress[1])); + total_minted = total_minted.add(minted); + } + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(total_minted); + }); + + // Test minting after withdrawing + it("test mint after withdraw", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.WEEK.mul(BigNumber.from("2")).toNumber()]); + + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + + expect((await setup.token.balanceOf(setup.accountsAddress[1])).gt(BigNumber.from("0"))).to.equal(true); + }); + + // Test multiple mints after withdrawing + it("test mint multiple after withdraw", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [10]); + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + + let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [10]); + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(balance); + }); + + // Test mint without any deposit + it("test no deposit", async () => { + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(setup.zero); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(setup.zero); + }); + + // Test minting with the wrong gauge + it("test mint wrong gauge", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[1]); + + //check + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(setup.zero); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(setup.zero); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[1])).to.equal(setup.zero); + }); + + // Test minting with an invalid gauge address + it("test mint not a gauge", async () => { + await expect(setup.minter.mint(setup.accountsAddress[1])).to.revertedWith("dev: gauge is not added"); + }); + + // Test minting before inflation begins + it("test mint before inflation begins", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + expect(await setup.token.miningEpoch()).to.equal(BigNumber.from("-1")); + + await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(BigNumber.from("0")); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(setup.zero); + }); + + // Test mintMany function with multiple gauges + it("test mintMany function multiple gauges", async () => { + //setup + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.three_gauges_contracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.three_gauges_contracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + let addresses = [ + setup.three_gauges[0], + setup.three_gauges[1], + setup.three_gauges[2], + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + ]; + await setup.minter.connect(setup.accounts[1]).mintMany(addresses); + + //check + let total_minted = BigNumber.from("0"); + + for (let i = 0; i < 3; i++) { + let gauge = setup.three_gauges_contracts[i]; + let minted = await setup.minter.minted(setup.accountsAddress[1], gauge.address); + expect(minted).to.equal(await gauge.integrateFraction(setup.accountsAddress[1])); + total_minted = total_minted.add(minted); + } + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(total_minted); + }); + + // Test toggling of the mint approval function + it("test toggleApproveMint function", async () => { + await setup.minter.connect(setup.accounts[1]).toggleApproveMint(setup.accountsAddress[2]); + expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(true); + + await setup.minter.connect(setup.accounts[1]).toggleApproveMint(setup.accountsAddress[2]); + expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(false); + }); + + // Test minting on behalf of another user + it("test mintFor function", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + await setup.minter.connect(setup.accounts[1]).toggleApproveMint(setup.accountsAddress[2]); + expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(true); + + await setup.minter.connect(setup.accounts[2]).mintFor(setup.three_gauges[0], setup.accountsAddress[1]); + + let expected = await setup.three_gauges_contracts[0].integrateFraction(setup.accountsAddress[1]); + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(expected); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(expected); + }); + + // Test mintFor function when not approved + it("test mintForFail function", async () => { + await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(false); + + await setup.minter.connect(setup.accounts[2]).mintFor(setup.three_gauges[0], setup.accountsAddress[1]); + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(0); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(0); + }); + }); +}); diff --git a/test/fork/helper.ts b/test/fork/helper.ts new file mode 100644 index 00000000..a726fc58 --- /dev/null +++ b/test/fork/helper.ts @@ -0,0 +1,136 @@ +import { ethers } from "hardhat"; +import { BigNumber, Contract, Signer } from "ethers"; + + +class EVMUtils { + async snapshot(): Promise { + return await ethers.provider.send("evm_snapshot", []); + } + + async restore(snapshotId: string): Promise { + return await ethers.provider.send("evm_revert", [snapshotId]); + } +} + +class GaugeControllerTestSetup { + + readonly DAY = BigNumber.from(86400); + readonly WEEK = BigNumber.from(86400 * 7); + readonly MONTH: BigNumber = BigNumber.from(86400 * 30); + readonly YEAR = BigNumber.from(86400 * 365); + + readonly name = "Token"; + readonly symbol = "Token"; + readonly decimal = 18; + readonly INITIAL_SUPPLY = BigNumber.from("1303030303000000000000000000"); + + readonly ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + readonly ten_to_the_24 = BigNumber.from("1000000000000000000000000"); + readonly ten_to_the_21 = BigNumber.from("1000000000000000000000"); + readonly ten_to_the_20 = BigNumber.from("100000000000000000000"); + readonly ten_to_the_19 = BigNumber.from("10000000000000000000"); + readonly ten_to_the_18 = BigNumber.from("1000000000000000000"); + readonly ten_to_the_17 = BigNumber.from("100000000000000000"); + readonly ten_to_the_9 = BigNumber.from("1000000000"); + readonly a = BigNumber.from("2"); + readonly b = BigNumber.from("5"); + readonly zero = BigNumber.from("0"); + + readonly GAUGE_TYPES = [BigNumber.from("1"), BigNumber.from("1"), BigNumber.from("2")]; + + accounts: Signer[]; + accountsAddress: String[]; + token: Contract; + voting_escrow: Contract; + gauge_controller: Contract; + mock_lp_token: Contract; + minter: Contract; + three_gauges_contracts: Contract[]; + three_gauges: string[]; + + TYPE_WEIGHTS: BigNumber[]; + GAUGE_WEIGHTS: BigNumber[]; + + async setup() { + + const [creator, alice, bob, charly] = await ethers.getSigners(); + this.accounts = [creator, alice, bob, charly]; + this.accountsAddress = [ + await creator.getAddress(), + await alice.getAddress(), + await bob.getAddress(), + await charly.getAddress(), + ]; + + this.TYPE_WEIGHTS = [this.ten_to_the_17.mul(this.b), this.ten_to_the_18.mul(this.a)]; + this.GAUGE_WEIGHTS = [this.ten_to_the_18.mul(this.a), this.ten_to_the_18, this.ten_to_the_17.mul(this.b)]; + + const Token = await ethers.getContractFactory("CRV"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + const GaugeController = await ethers.getContractFactory("GaugeController"); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGauge"); + const TestLP = await ethers.getContractFactory("TestLP"); + const Minter = await ethers.getContractFactory("Minter"); + + // deploy + this.token = await Token.deploy(); + this.voting_escrow = await VotingEscrow.deploy( + this.token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + this.gauge_controller = await GaugeController.deploy(this.token.address, this.voting_escrow.address); + this.mock_lp_token = await TestLP.deploy("tokenDAO LP token", "iToken", 18, BigNumber.from("1000000000000000000000")); + this.minter = await Minter.deploy(this.token.address, this.gauge_controller.address); + const lg1 = await LiquidityGauge.deploy(this.mock_lp_token.address, this.minter.address); + const lg2 = await LiquidityGauge.deploy(this.mock_lp_token.address, this.minter.address); + const lg3 = await LiquidityGauge.deploy(this.mock_lp_token.address, this.minter.address); + this.three_gauges_contracts = [lg1, lg2, lg3]; + this.three_gauges = [lg1.address, lg2.address, lg3.address]; + + await this.token.setMinter(this.minter.address); + // setup + await this.gauge_controller.addType("none", 0); + } + + + async addType(){ + await this.gauge_controller.addType("Liquidity", this.TYPE_WEIGHTS[0]); + await this.gauge_controller.addType("Liquidity", this.TYPE_WEIGHTS[1]); + } + + async addGaugeZero(){ + for (let i = 0; i < 2; i++) { + await this.gauge_controller.addGauge(this.three_gauges[i], this.GAUGE_TYPES[i], 0); + } + } + async addGauge(){ + for (let i = 0; i < 3; i++) { + await this.gauge_controller.addGauge(this.three_gauges[i], this.GAUGE_TYPES[i], this.GAUGE_WEIGHTS[i]); + } + } + + async createLock(){ + await this.token.approve(this.voting_escrow.address, BigNumber.from("1000000000000000000000000")); + await this.voting_escrow.createLock( + BigNumber.from("1000000000000000000000000"), + BigNumber.from((await ethers.provider.getBlock("latest")).timestamp).add(this.YEAR) + ); + } + + async createLP(){ + for (let i = 1; i < 4; i++) { + await this.mock_lp_token.transfer(this.accountsAddress[i], this.ten_to_the_18); + } + + for (let i = 0; i < 3; i++) { + for (let t = 0; t < 3; t++) { + await this.mock_lp_token.connect(this.accounts[i + 1]).approve(this.three_gauges[t], this.ten_to_the_18); + } + } + } +} + +export { EVMUtils, GaugeControllerTestSetup }; From 7f576c5a612ba586298aeb6fe1285830e403751f Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 27 Oct 2023 17:03:59 +0900 Subject: [PATCH 015/412] Change fork version. --- contracts/curveFork/CRV.sol | 2 +- contracts/curveFork/GaugeController.sol | 2 +- contracts/curveFork/LiquidityGauge.sol | 2 +- contracts/curveFork/Minter.sol | 2 +- contracts/curveFork/VotingEscrow.sol | 97 +++++++++++-------- contracts/curveFork/interfaces/ICRV.sol | 2 +- .../curveFork/interfaces/IGaugeController.sol | 2 +- .../curveFork/interfaces/ILiquidityGauge.sol | 2 +- contracts/curveFork/interfaces/IMinter.sol | 2 +- .../interfaces/ISmartWalletChecker.sol | 2 +- .../curveFork/interfaces/IVotingEscrow.sol | 10 +- 11 files changed, 71 insertions(+), 54 deletions(-) diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol index f058234e..04027e38 100644 --- a/contracts/curveFork/CRV.sol +++ b/contracts/curveFork/CRV.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.4; +pragma solidity 0.8.18; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol index 619050c4..3916e9f0 100644 --- a/contracts/curveFork/GaugeController.sol +++ b/contracts/curveFork/GaugeController.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.4; +pragma solidity 0.8.18; /*** *@title Gauge Controller diff --git a/contracts/curveFork/LiquidityGauge.sol b/contracts/curveFork/LiquidityGauge.sol index 7a447409..b9dec78c 100644 --- a/contracts/curveFork/LiquidityGauge.sol +++ b/contracts/curveFork/LiquidityGauge.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.4; +pragma solidity 0.8.18; /*** *@title Liquidity Gauge diff --git a/contracts/curveFork/Minter.sol b/contracts/curveFork/Minter.sol index c0da1d64..ef0a59f3 100644 --- a/contracts/curveFork/Minter.sol +++ b/contracts/curveFork/Minter.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.4; +pragma solidity 0.8.18; /*** *@title Token Minter diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 827a011c..95425e7d 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity 0.8.18; // Voting escrow to have time-weighted votes // Votes have a weight depending on time, so that users are committed @@ -235,9 +235,6 @@ contract VotingEscrow is ReentrancyGuard { int128(uint128(oldLocked_.end) - uint128(block.timestamp)); } - // Read values of scheduled changes in the slope - // old_locked.end can be in the past and in the future - // new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros if (newLocked_.end > block.timestamp && newLocked_.amount > 0) { unchecked { _uNew.slope = int128( @@ -249,6 +246,9 @@ contract VotingEscrow is ReentrancyGuard { int128(uint128(newLocked_.end) - uint128(block.timestamp)); } + // Read values of scheduled changes in the slope + // old_locked.end can be in the past and in the future + // new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros _oldDSlope = slopeChanges[oldLocked_.end]; if (newLocked_.end != 0) { if (newLocked_.end == oldLocked_.end) { @@ -402,11 +402,18 @@ contract VotingEscrow is ReentrancyGuard { LockedBalance memory lockedBalance_, int128 type_ ) internal { - LockedBalance memory _locked = lockedBalance_; - uint256 supplyBefore = supply; + LockedBalance memory _locked = LockedBalance( + lockedBalance_.amount, + lockedBalance_.end + ); + LockedBalance memory _oldLocked = LockedBalance( + lockedBalance_.amount, + lockedBalance_.end + ); + + uint256 _supplyBefore = supply; + supply = _supplyBefore + value_; - supply = supplyBefore + value_; - LockedBalance memory oldLocked = _locked; // Adding to existing lock, or if a lock is expired - creating a new one _locked.amount += int128(uint128(value_)); if (unlockTime_ != 0) { @@ -418,7 +425,7 @@ contract VotingEscrow is ReentrancyGuard { // Both old_locked.end could be current or expired (>/< block.timestamp) // value == 0 (extend lock) or value > 0 (add to lock or extend lock) // _locked.end > block.timestamp (always) - _checkpoint(addr_, oldLocked, _locked); + _checkpoint(addr_, _oldLocked, _locked); if (value_ != 0) { require( @@ -428,7 +435,7 @@ contract VotingEscrow is ReentrancyGuard { } emit Deposit(addr_, value_, _locked.end, type_, block.timestamp); - emit Supply(supplyBefore, supplyBefore + value_); + emit Supply(_supplyBefore, _supplyBefore + value_); } /*** @@ -552,11 +559,18 @@ contract VotingEscrow is ReentrancyGuard { * @dev Only possible if the lock has expired */ function withdraw() external nonReentrant { - LockedBalance memory _locked = locked[msg.sender]; + LockedBalance memory _locked = LockedBalance( + locked[msg.sender].amount, + locked[msg.sender].end + ); require(block.timestamp >= _locked.end, "The lock didn't expire"); uint256 _value = uint256(int256(_locked.amount)); - LockedBalance memory oldLocked = _locked; + LockedBalance memory _oldLocked = LockedBalance( + locked[msg.sender].amount, + locked[msg.sender].end + ); + _locked.end = 0; _locked.amount = 0; locked[msg.sender] = _locked; @@ -566,7 +580,7 @@ contract VotingEscrow is ReentrancyGuard { // old_locked can have either expired <= timestamp or zero end // _locked has only 0 end // Both can have >= 0 amount - _checkpoint(msg.sender, oldLocked, _locked); + _checkpoint(msg.sender, _oldLocked, _locked); require(ERC20(token).transfer(msg.sender, _value), "Transfer failed"); @@ -731,33 +745,36 @@ contract VotingEscrow is ReentrancyGuard { Point memory point_, uint256 t_ ) internal view returns (uint256) { - Point memory lastPoint = point_; - uint256 _ti = (lastPoint.ts / WEEK) * WEEK; + Point memory _lastPoint = point_; + uint256 _ti; + unchecked { + _ti = (_lastPoint.ts / WEEK) * WEEK; + } for (uint256 i; i < 255; ) { _ti += WEEK; - int128 d_slope = 0; + int128 _dSlope = 0; if (_ti > t_) { _ti = t_; } else { - d_slope = slopeChanges[_ti]; + _dSlope = slopeChanges[_ti]; } - lastPoint.bias -= - lastPoint.slope * - int128(int256(_ti) - int256(lastPoint.ts)); + _lastPoint.bias -= + _lastPoint.slope * + int128(int256(_ti) - int256(_lastPoint.ts)); if (_ti == t_) { break; } - lastPoint.slope += d_slope; - lastPoint.ts = _ti; + _lastPoint.slope += _dSlope; + _lastPoint.ts = _ti; unchecked { ++i; } } - if (lastPoint.bias < 0) { - lastPoint.bias = 0; + if (_lastPoint.bias < 0) { + _lastPoint.bias = 0; } - return uint256(int256(lastPoint.bias)); + return uint256(int256(_lastPoint.bias)); } /*** @@ -768,8 +785,8 @@ contract VotingEscrow is ReentrancyGuard { */ function totalSupply(uint256 t_) external view returns (uint256) { uint256 _epoch = epoch; - Point memory lastPoint = pointHistory[_epoch]; - return supplyAt(lastPoint, t_); + Point memory _lastPoint = pointHistory[_epoch]; + return supplyAt(_lastPoint, t_); } /*** @@ -779,8 +796,8 @@ contract VotingEscrow is ReentrancyGuard { */ function totalSupply() external view returns (uint256) { uint256 _epoch = epoch; - Point memory lastPoint = pointHistory[_epoch]; - return supplyAt(lastPoint, block.timestamp); + Point memory _lastPoint = pointHistory[_epoch]; + return supplyAt(_lastPoint, block.timestamp); } /*** @@ -791,26 +808,26 @@ contract VotingEscrow is ReentrancyGuard { function totalSupplyAt(uint256 block_) external view returns (uint256) { require(block_ <= block.number, "Invalid block number"); uint256 _epoch = epoch; - uint256 targetEpoch = findBlockEpoch(block_, _epoch); + uint256 _targetEpoch = findBlockEpoch(block_, _epoch); - Point memory point = pointHistory[targetEpoch]; + Point memory _point = pointHistory[_targetEpoch]; uint256 _dt = 0; - if (targetEpoch < _epoch) { - Point memory pointNext = pointHistory[targetEpoch + 1]; - if (point.blk != pointNext.blk) { + if (_targetEpoch < _epoch) { + Point memory _pointNext = pointHistory[_targetEpoch + 1]; + if (_point.blk != _pointNext.blk) { _dt = - ((block_ - point.blk) * (pointNext.ts - point.ts)) / - (pointNext.blk - point.blk); + ((block_ - _point.blk) * (_pointNext.ts - _point.ts)) / + (_pointNext.blk - _point.blk); } } else { - if (point.blk != block.number) { + if (_point.blk != block.number) { _dt = - ((block_ - point.blk) * (block.timestamp - point.ts)) / - (block.number - point.blk); + ((block_ - _point.blk) * (block.timestamp - _point.ts)) / + (block.number - _point.blk); } } // Now _dt contains info on how far are we beyond point - return supplyAt(point, point.ts + _dt); + return supplyAt(_point, _point.ts + _dt); } /*** diff --git a/contracts/curveFork/interfaces/ICRV.sol b/contracts/curveFork/interfaces/ICRV.sol index c8710f56..b07be78e 100644 --- a/contracts/curveFork/interfaces/ICRV.sol +++ b/contracts/curveFork/interfaces/ICRV.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity 0.8.18; interface ICRV { function mint(address to_, uint256 value_) external returns (bool); diff --git a/contracts/curveFork/interfaces/IGaugeController.sol b/contracts/curveFork/interfaces/IGaugeController.sol index 95a8ae41..16e3d9b7 100644 --- a/contracts/curveFork/interfaces/IGaugeController.sol +++ b/contracts/curveFork/interfaces/IGaugeController.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity 0.8.18; interface IGaugeController { function gaugeTypes(address addr_) external view returns (uint256); diff --git a/contracts/curveFork/interfaces/ILiquidityGauge.sol b/contracts/curveFork/interfaces/ILiquidityGauge.sol index b62c362a..75b6e1b9 100644 --- a/contracts/curveFork/interfaces/ILiquidityGauge.sol +++ b/contracts/curveFork/interfaces/ILiquidityGauge.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity 0.8.18; interface ILiquidityGauge { function userCheckpoint(address addr_) external returns (bool); diff --git a/contracts/curveFork/interfaces/IMinter.sol b/contracts/curveFork/interfaces/IMinter.sol index a80dc607..74dc0e60 100644 --- a/contracts/curveFork/interfaces/IMinter.sol +++ b/contracts/curveFork/interfaces/IMinter.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity 0.8.18; interface IMinter { diff --git a/contracts/curveFork/interfaces/ISmartWalletChecker.sol b/contracts/curveFork/interfaces/ISmartWalletChecker.sol index 60bb6422..e98406cb 100644 --- a/contracts/curveFork/interfaces/ISmartWalletChecker.sol +++ b/contracts/curveFork/interfaces/ISmartWalletChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity 0.8.18; // Interface for checking whether address belongs to a whitelisted type of a smart wallet. // When new types are added - the whole contract is changed diff --git a/contracts/curveFork/interfaces/IVotingEscrow.sol b/contracts/curveFork/interfaces/IVotingEscrow.sol index 92a68826..5af927f3 100644 --- a/contracts/curveFork/interfaces/IVotingEscrow.sol +++ b/contracts/curveFork/interfaces/IVotingEscrow.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.4; +pragma solidity 0.8.18; interface IVotingEscrow { function getLastUserSlope(address addr_) external view returns (int128); @@ -20,9 +20,9 @@ interface IVotingEscrow { view returns (uint256); - function userPointHistoryTs(address _addr, uint256 _idx) - external - view - returns (uint256); + function userPointHistoryTs( + address addr, + uint256 epoch + ) external view returns (uint256); } \ No newline at end of file From da17493f77fbb5e72305f4f2edcce60bc449aa7a Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 27 Oct 2023 17:08:58 +0900 Subject: [PATCH 016/412] Add version to config. --- hardhat.config.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 25df0d01..19325989 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -49,14 +49,19 @@ module.exports = { }, }, }, - ], - }, - networks: { - hardhat: { - allowUnlimitedContractSize: true, - accounts: { - count: 200, - accountsBalance: "1000000000000000000000000", + { + version: "0.8.18", + settings: { + outputSelection: { + "*": { + "*": ["storageLayout"], + }, + }, + optimizer: { + enabled: true, + runs: 1000, + }, + }, }, }, localhost: { @@ -138,7 +143,8 @@ module.exports = { alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads? }, paths: { - tests: "./test/unit", + // tests: "./test/unit", + tests: "./test/fork", }, defender: { apiKey: process.env.DEFENDER_TEAM_API_KEY, From 105f06f25f6a52217b67c480771748fe35a11202 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 27 Oct 2023 17:09:19 +0900 Subject: [PATCH 017/412] Add test code for GaugeController --- .../GaugeController/gaugesWeights.test.ts | 161 ++++++++++++++++++ test/fork/GaugeController/timestamps.test.ts | 42 +++++ test/fork/GaugeController/totalWeight.test.ts | 60 +++++++ test/fork/GaugeController/vote.test.ts | 139 +++++++++++++++ .../GaugeController/voteWeightUnitary.test.ts | 98 +++++++++++ 5 files changed, 500 insertions(+) create mode 100644 test/fork/GaugeController/gaugesWeights.test.ts create mode 100644 test/fork/GaugeController/timestamps.test.ts create mode 100644 test/fork/GaugeController/totalWeight.test.ts create mode 100644 test/fork/GaugeController/vote.test.ts create mode 100644 test/fork/GaugeController/voteWeightUnitary.test.ts diff --git a/test/fork/GaugeController/gaugesWeights.test.ts b/test/fork/GaugeController/gaugesWeights.test.ts new file mode 100644 index 00000000..8ac6c50b --- /dev/null +++ b/test/fork/GaugeController/gaugesWeights.test.ts @@ -0,0 +1,161 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, GaugeControllerTestSetup } from "../helper"; + +describe("GaugeController", function () { + let setup: GaugeControllerTestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new GaugeControllerTestSetup(); + await setup.setup(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("test gauges weights", function () { + it("test addGauges", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); + await setup.gauge_controller.addGauge(setup.three_gauges[1], 0, 0); + + expect(await setup.gauge_controller.gauges(0)).to.equal(setup.three_gauges[0]); + expect(await setup.gauge_controller.gauges(1)).to.equal(setup.three_gauges[1]); + }); + + it("test nGauges", async () => { + expect(await setup.gauge_controller.nGauges()).to.equal("0"); + + await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); + await setup.gauge_controller.addGauge(setup.three_gauges[1], 0, 0); + + expect(await setup.gauge_controller.nGauges()).to.equal("2"); + }); + + it("test nGauges same gauge", async () => { + expect(await setup.gauge_controller.nGauges()).to.equal("0"); + + await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); + await expect(setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0)).to.revertedWith( + "cannot add the same gauge twice" + ); + + expect(await setup.gauge_controller.nGauges()).to.equal("1"); + }); + + it("test nGaugeTypes", async () => { + expect(await setup.gauge_controller.nGaugeTypes()).to.equal("1"); // unset & LiquidityGauge + + await setup.gauge_controller.addType("Insurance", 0); + + expect(await setup.gauge_controller.nGaugeTypes()).to.equal(BigNumber.from("2")); //unset, LiquidityGauge, Insurance + }); + + it("test gaugeTypes", async () => { + await setup.gauge_controller.addType("Insurance", 0); + + await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, 0); + await setup.gauge_controller.addGauge(setup.three_gauges[1], 0, 0); + + expect(await setup.gauge_controller.gaugeTypes(setup.three_gauges[0])).to.equal("1"); + expect(await setup.gauge_controller.gaugeTypes(setup.three_gauges[1])).to.equal("0"); + }); + + it("test gauge weight", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, setup.ten_to_the_19); + + expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal(setup.ten_to_the_19); + }); + + it("test gauge weight as zero", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); + + expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal("0"); + }); + + it("test getGaugeWeight", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); + + expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal("0"); + + await setup.gauge_controller.changeGaugeWeight(setup.three_gauges[0], setup.ten_to_the_21); + + expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal(setup.ten_to_the_21); + }); + + it("test type weight", async () => { + await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[0]); + expect(await setup.gauge_controller.getTypeWeight(1)).to.equal(setup.TYPE_WEIGHTS[0]); + expect(await setup.gauge_controller.getTypeWeight(2)).to.equal("0"); + }); + + it("test changeTypeWeight", async () => { + await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[0]); + await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[1]); + await setup.gauge_controller.changeTypeWeight(2, setup.TYPE_WEIGHTS[1]); + await setup.gauge_controller.changeTypeWeight(1, BigNumber.from("31337")); + + expect(await setup.gauge_controller.getTypeWeight(0)).to.equal(0); + expect(await setup.gauge_controller.getTypeWeight(1)).to.equal("31337"); + expect(await setup.gauge_controller.getTypeWeight(2)).to.equal(setup.TYPE_WEIGHTS[1]); + }); + + it("test relative weight write", async () => { + await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[0]); + await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[1]); + + await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.GAUGE_WEIGHTS[0]); + await setup.gauge_controller.addGauge(setup.three_gauges[1], 1, setup.GAUGE_WEIGHTS[1]); + await setup.gauge_controller.addGauge(setup.three_gauges[2], 2, setup.GAUGE_WEIGHTS[2]); + + let gauge_type = [0, 0, 1]; + let total_weight = setup.TYPE_WEIGHTS[0] + .mul(setup.GAUGE_WEIGHTS[0]) + .add(setup.TYPE_WEIGHTS[0].mul(setup.GAUGE_WEIGHTS[1])) + .add(setup.TYPE_WEIGHTS[1].mul(setup.GAUGE_WEIGHTS[2])); + + await ethers.provider.send("evm_increaseTime", [setup.WEEK.mul(2).toNumber()]); + await ethers.provider.send("evm_mine",[]); + + let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + + for (let i = 0; i < 3; i++) { + await setup.gauge_controller.gaugeRelativeWeightWrite(setup.three_gauges[i], t); + let relative_weight = await setup.gauge_controller.gaugeRelativeWeight(setup.three_gauges[i], t); + expect(relative_weight).to.equal( + setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) + ); + } + + await ethers.provider.send("evm_increaseTime", [setup.YEAR.div("2").toNumber()]); + await ethers.provider.send("evm_mine",[]); + + for (let i = 0; i < 3; i++) { + await setup.gauge_controller.gaugeRelativeWeightWrite(setup.three_gauges[i], t); + let relative_weight = await setup.gauge_controller.gaugeRelativeWeight(setup.three_gauges[i], t); + expect(relative_weight).to.equal( + setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) + ); + } + + await ethers.provider.send("evm_increaseTime", [setup.YEAR.div("10").toNumber()]); + await ethers.provider.send("evm_mine",[]); + + for (let i = 0; i < 3; i++) { + await setup.gauge_controller.gaugeRelativeWeightWrite(setup.three_gauges[i], t); + let relative_weight = await setup.gauge_controller.gaugeRelativeWeight(setup.three_gauges[i], t); + expect(relative_weight).to.equal( + setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) + ); + } + }); + }); +}); diff --git a/test/fork/GaugeController/timestamps.test.ts b/test/fork/GaugeController/timestamps.test.ts new file mode 100644 index 00000000..613ce566 --- /dev/null +++ b/test/fork/GaugeController/timestamps.test.ts @@ -0,0 +1,42 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, GaugeControllerTestSetup } from "../helper"; + +describe("GaugeController", function () { + let setup: GaugeControllerTestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new GaugeControllerTestSetup(); + await setup.setup(); + await setup.addType(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("test_timestamp", function () { + it("test_timestamp", async () => { + let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + expect(await setup.gauge_controller.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); + + for (let i = 0; i < 5; i++) { + //await time.increase(YEAR.mul(BigNumber.from('11')).div(BigNumber.from('10'))); + let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + await ethers.provider.send("evm_increaseTime", [setup.YEAR.mul("11").div("10").toNumber()]); + + await setup.gauge_controller.checkpoint(); + now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + expect(await setup.gauge_controller.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); //technically, blocktimestamp for this tx is "now+1", but it works fine for here because of .div(setup.WEEK) rounds down the number. + } + }); + }); +}); diff --git a/test/fork/GaugeController/totalWeight.test.ts b/test/fork/GaugeController/totalWeight.test.ts new file mode 100644 index 00000000..6a121de3 --- /dev/null +++ b/test/fork/GaugeController/totalWeight.test.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; +import { BigNumber } from "ethers"; +import { EVMUtils, GaugeControllerTestSetup } from "../helper"; + +describe("GaugeController", function () { + let setup: GaugeControllerTestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new GaugeControllerTestSetup(); + await setup.setup(); + await setup.addType(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("test_total_weight", function () { + it("test_total_weight", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.GAUGE_WEIGHTS[0]); + expect(await setup.gauge_controller.getTotalWeight()).to.equal(setup.GAUGE_WEIGHTS[0].mul(setup.TYPE_WEIGHTS[0])); + }); + + it("test_changeTypeWeight", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.ten_to_the_18); + + await setup.gauge_controller.changeTypeWeight(1, BigNumber.from("31337")); + + expect(await setup.gauge_controller.getTotalWeight()).to.equal(setup.ten_to_the_18.mul(BigNumber.from("31337"))); + }); + + it("test_changeGaugeWeight", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.ten_to_the_18); + + await setup.gauge_controller.changeGaugeWeight(setup.three_gauges[0], BigNumber.from("31337")); + + expect(await setup.gauge_controller.getTotalWeight()).to.equal(setup.TYPE_WEIGHTS[0].mul(BigNumber.from("31337"))); + }); + + it("test_multiple", async () => { + await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.GAUGE_WEIGHTS[0]); + await setup.gauge_controller.addGauge(setup.three_gauges[1], 1, setup.GAUGE_WEIGHTS[1]); + await setup.gauge_controller.addGauge(setup.three_gauges[2], 2, setup.GAUGE_WEIGHTS[2]); + + let expected = setup.GAUGE_WEIGHTS[0] + .mul(setup.TYPE_WEIGHTS[0]) + .add(setup.GAUGE_WEIGHTS[1].mul(setup.TYPE_WEIGHTS[0])) + .add(setup.GAUGE_WEIGHTS[2].mul(setup.TYPE_WEIGHTS[1])); + + expect(await setup.gauge_controller.getTotalWeight()).to.equal(expected); + }); + }); +}); diff --git a/test/fork/GaugeController/vote.test.ts b/test/fork/GaugeController/vote.test.ts new file mode 100644 index 00000000..b3315a7b --- /dev/null +++ b/test/fork/GaugeController/vote.test.ts @@ -0,0 +1,139 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, GaugeControllerTestSetup } from "../helper"; + +describe("GaugeController", function () { + let setup: GaugeControllerTestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new GaugeControllerTestSetup(); + await setup.setup(); + await setup.addType(); + await setup.addGaugeZero(); + await setup.createLock(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("test_vote", function () { + it("test_vote", async () => { + // Test if voting for a gauge with a weight of 10000 sets the user's vote power to 10000. + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); //Total vote power used by user + }); + + it("test_vote_partial", async () => { + // Test if voting with a weight of 1234 sets the user's vote power to 1234. + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234); + expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("1234")); + }); + + it("test_vote_change", async () => { + // Test if it's possible to change the weight of the same gauge after 10 days. + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234); + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234)).to.revertedWith( + "Cannot vote so often" + ); + + await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); + + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 42); + + expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("42")); + }); + + it("test_vote_remove", async () => { + // Test that removing a vote sets the user's voting power to 0. + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 10000); + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234)).to.revertedWith( + "Cannot vote so often" + ); + + await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 0); + + expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(setup.zero); + }); + + it("test_vote_multiple", async () => { + // Test if voting for multiple gauges sums up the user's voting power. + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 4000); + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 6000); + + expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); + }); + + it("test_vote_no_balance", async () => { + // Test that users without locked tokens can't vote. + await expect(setup.gauge_controller.connect(setup.accounts[1]).voteForGaugeWeights(setup.three_gauges[0], 10000)).to.revertedWith( + "Your token lock expires too soon" + ); + }); + + it("test_vote_expired", async () => { + // Test that users with expired token locks can't vote. + await ethers.provider.send("evm_increaseTime", [setup.YEAR.mul("2").toNumber()]); + + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000)).to.revertedWith( + "Your token lock expires too soon" + ); + }); + + it("test_invalid_gauge_id", async () => { + // Test that voting with an invalid gauge ID throws an error. + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[2], 10000)).to.revertedWith("Gauge not added"); + }); + + it("test_over_user_weight", async () => { + // Test that voting with power exceeding the user's capacity throws an error. + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10001)).to.revertedWith( + "You used all your voting power" + ); + }); + + it("test_over_weight_multiple", async () => { + // Test that voting for multiple gauges with total power exceeding user's capacity throws an error. + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 8000); + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 4000)).to.revertedWith( + "Used too much power" + ); + }); + + it("test_over_weight_adjust_existing", async () => { + // Test that adjusting an existing vote to exceed the user's total voting power throws an error. + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 6000); + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 3000); + + await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); + + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 8000)).to.revertedWith( + "Used too much power" + ); + }); + + it("test_exceeding_voting_power", async () => { + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 6000); + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 5000)).to.revertedWith( + "Used too much power" + ); + }); + + it("test_emit_vote_event", async () => { + let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp + 1); + await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 5000)) + .to.emit(setup.gauge_controller, "VoteForGauge") + .withArgs(now, setup.accountsAddress[0], setup.three_gauges[0], 5000); + }); + + }); +}); diff --git a/test/fork/GaugeController/voteWeightUnitary.test.ts b/test/fork/GaugeController/voteWeightUnitary.test.ts new file mode 100644 index 00000000..1a5d602c --- /dev/null +++ b/test/fork/GaugeController/voteWeightUnitary.test.ts @@ -0,0 +1,98 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, GaugeControllerTestSetup } from "../helper"; + +describe("GaugeController", function () { + let setup: GaugeControllerTestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new GaugeControllerTestSetup(); + await setup.setup(); + await setup.addType(); + await setup.addGaugeZero(); + await setup.createLock(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("test_vote_weight_unitary", function () { + it("test_no_immediate_effect_on_weight", async () => { + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + expect( + await setup.gauge_controller.gaugeRelativeWeight( + setup.three_gauges[0], + BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) + ) + ).to.equal(BigNumber.from("0")); + }); + + it("test_remove_vote_no_immediate_effect", async () => { + // ゲージに対して投票を行った後、時間を進めてそのゲージのチェックポイントを取ります。 + // その後、投票を取り消して、投票の取り消しは即座には影響しないことを確認します。 + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + + await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); + + await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 0); + + expect( + await setup.gauge_controller.gaugeRelativeWeight( + setup.three_gauges[0], + BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) + ) + ).to.equal(setup.ten_to_the_18); + }); + + it("test_effect_on_following_period", async () => { + // ゲージに対して投票を行い、一週間時間を進めた後にそのゲージの重みが変わっていることを確認します。 + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + + await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); + await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); + expect( + await setup.gauge_controller.gaugeRelativeWeight( + setup.three_gauges[0], + BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) + ) + ).to.equal(setup.ten_to_the_18); + }); + + it("test_remove_vote_means_no_weight", async () => { + // ゲージに対して投票を行い、時間を進めてそのゲージのチェックポイントを取ります。 + // 投票を取り消し、さらに一週間時間を進めた後、そのゲージの重みが0になっていることを確認します。 + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); + await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); + + expect( + await setup.gauge_controller.gaugeRelativeWeight( + setup.three_gauges[0], + BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) + ) + ).to.equal(setup.ten_to_the_18); + + await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 0); + await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); + await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); + + expect( + await setup.gauge_controller.gaugeRelativeWeight( + setup.three_gauges[0], + BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) + ) + ).to.equal(setup.zero); + }); + }); +}); + From e7e5ff3dd9e50960955d3bb2832abffa677655cc Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 27 Oct 2023 17:15:46 +0900 Subject: [PATCH 018/412] Rename TestSetup --- test/fork/GaugeController/gaugesWeights.test.ts | 6 +++--- test/fork/GaugeController/timestamps.test.ts | 6 +++--- test/fork/GaugeController/totalWeight.test.ts | 6 +++--- test/fork/GaugeController/vote.test.ts | 6 +++--- test/fork/GaugeController/voteWeightUnitary.test.ts | 6 +++--- test/fork/Minter/Minter.test.ts | 6 +++--- test/fork/helper.ts | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test/fork/GaugeController/gaugesWeights.test.ts b/test/fork/GaugeController/gaugesWeights.test.ts index 8ac6c50b..0abb527f 100644 --- a/test/fork/GaugeController/gaugesWeights.test.ts +++ b/test/fork/GaugeController/gaugesWeights.test.ts @@ -1,15 +1,15 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; -import { EVMUtils, GaugeControllerTestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../helper"; describe("GaugeController", function () { - let setup: GaugeControllerTestSetup; + let setup: TestSetup; let evm: EVMUtils; let snapshotId: string; before(async () => { - setup = new GaugeControllerTestSetup(); + setup = new TestSetup(); await setup.setup(); }); diff --git a/test/fork/GaugeController/timestamps.test.ts b/test/fork/GaugeController/timestamps.test.ts index 613ce566..6b49741a 100644 --- a/test/fork/GaugeController/timestamps.test.ts +++ b/test/fork/GaugeController/timestamps.test.ts @@ -1,15 +1,15 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; -import { EVMUtils, GaugeControllerTestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../helper"; describe("GaugeController", function () { - let setup: GaugeControllerTestSetup; + let setup: TestSetup; let evm: EVMUtils; let snapshotId: string; before(async () => { - setup = new GaugeControllerTestSetup(); + setup = new TestSetup(); await setup.setup(); await setup.addType(); }); diff --git a/test/fork/GaugeController/totalWeight.test.ts b/test/fork/GaugeController/totalWeight.test.ts index 6a121de3..706d06f6 100644 --- a/test/fork/GaugeController/totalWeight.test.ts +++ b/test/fork/GaugeController/totalWeight.test.ts @@ -1,14 +1,14 @@ import { expect } from "chai"; import { BigNumber } from "ethers"; -import { EVMUtils, GaugeControllerTestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../helper"; describe("GaugeController", function () { - let setup: GaugeControllerTestSetup; + let setup: TestSetup; let evm: EVMUtils; let snapshotId: string; before(async () => { - setup = new GaugeControllerTestSetup(); + setup = new TestSetup(); await setup.setup(); await setup.addType(); }); diff --git a/test/fork/GaugeController/vote.test.ts b/test/fork/GaugeController/vote.test.ts index b3315a7b..d2c625e3 100644 --- a/test/fork/GaugeController/vote.test.ts +++ b/test/fork/GaugeController/vote.test.ts @@ -1,15 +1,15 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; -import { EVMUtils, GaugeControllerTestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../helper"; describe("GaugeController", function () { - let setup: GaugeControllerTestSetup; + let setup: TestSetup; let evm: EVMUtils; let snapshotId: string; before(async () => { - setup = new GaugeControllerTestSetup(); + setup = new TestSetup(); await setup.setup(); await setup.addType(); await setup.addGaugeZero(); diff --git a/test/fork/GaugeController/voteWeightUnitary.test.ts b/test/fork/GaugeController/voteWeightUnitary.test.ts index 1a5d602c..963e967c 100644 --- a/test/fork/GaugeController/voteWeightUnitary.test.ts +++ b/test/fork/GaugeController/voteWeightUnitary.test.ts @@ -1,15 +1,15 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; -import { EVMUtils, GaugeControllerTestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../helper"; describe("GaugeController", function () { - let setup: GaugeControllerTestSetup; + let setup: TestSetup; let evm: EVMUtils; let snapshotId: string; before(async () => { - setup = new GaugeControllerTestSetup(); + setup = new TestSetup(); await setup.setup(); await setup.addType(); await setup.addGaugeZero(); diff --git a/test/fork/Minter/Minter.test.ts b/test/fork/Minter/Minter.test.ts index c74a8123..13923c74 100644 --- a/test/fork/Minter/Minter.test.ts +++ b/test/fork/Minter/Minter.test.ts @@ -1,15 +1,15 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; -import { EVMUtils, GaugeControllerTestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../helper"; describe("Minter", function () { - let setup: GaugeControllerTestSetup; + let setup: TestSetup; let evm: EVMUtils; let snapshotId: string; before(async () => { - setup = new GaugeControllerTestSetup(); + setup = new TestSetup(); await setup.setup(); await setup.addType(); await setup.addGauge(); diff --git a/test/fork/helper.ts b/test/fork/helper.ts index a726fc58..4d2d4411 100644 --- a/test/fork/helper.ts +++ b/test/fork/helper.ts @@ -12,7 +12,7 @@ class EVMUtils { } } -class GaugeControllerTestSetup { +class TestSetup { readonly DAY = BigNumber.from(86400); readonly WEEK = BigNumber.from(86400 * 7); @@ -133,4 +133,4 @@ class GaugeControllerTestSetup { } } -export { EVMUtils, GaugeControllerTestSetup }; +export { EVMUtils, TestSetup }; From 3c975e9722cc6c8304ab6b0e42c27a7f271f0258 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 27 Oct 2023 17:35:30 +0900 Subject: [PATCH 019/412] Change variables to CamelCase --- .../GaugeController/gaugesWeights.test.ts | 106 +++++++++--------- test/fork/GaugeController/timestamps.test.ts | 6 +- test/fork/GaugeController/totalWeight.test.ts | 24 ++-- test/fork/GaugeController/vote.test.ts | 58 +++++----- .../GaugeController/voteWeightUnitary.test.ts | 40 +++---- test/fork/Minter/Minter.test.ts | 96 ++++++++-------- test/fork/helper.ts | 46 ++++---- 7 files changed, 188 insertions(+), 188 deletions(-) diff --git a/test/fork/GaugeController/gaugesWeights.test.ts b/test/fork/GaugeController/gaugesWeights.test.ts index 0abb527f..38e47719 100644 --- a/test/fork/GaugeController/gaugesWeights.test.ts +++ b/test/fork/GaugeController/gaugesWeights.test.ts @@ -24,97 +24,97 @@ describe("GaugeController", function () { describe("test gauges weights", function () { it("test addGauges", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); - await setup.gauge_controller.addGauge(setup.three_gauges[1], 0, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); - expect(await setup.gauge_controller.gauges(0)).to.equal(setup.three_gauges[0]); - expect(await setup.gauge_controller.gauges(1)).to.equal(setup.three_gauges[1]); + expect(await setup.gaugeController.gauges(0)).to.equal(setup.gaugesAddress[0]); + expect(await setup.gaugeController.gauges(1)).to.equal(setup.gaugesAddress[1]); }); it("test nGauges", async () => { - expect(await setup.gauge_controller.nGauges()).to.equal("0"); + expect(await setup.gaugeController.nGauges()).to.equal("0"); - await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); - await setup.gauge_controller.addGauge(setup.three_gauges[1], 0, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); - expect(await setup.gauge_controller.nGauges()).to.equal("2"); + expect(await setup.gaugeController.nGauges()).to.equal("2"); }); it("test nGauges same gauge", async () => { - expect(await setup.gauge_controller.nGauges()).to.equal("0"); + expect(await setup.gaugeController.nGauges()).to.equal("0"); - await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); - await expect(setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0)).to.revertedWith( + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); + await expect(setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0)).to.revertedWith( "cannot add the same gauge twice" ); - expect(await setup.gauge_controller.nGauges()).to.equal("1"); + expect(await setup.gaugeController.nGauges()).to.equal("1"); }); it("test nGaugeTypes", async () => { - expect(await setup.gauge_controller.nGaugeTypes()).to.equal("1"); // unset & LiquidityGauge + expect(await setup.gaugeController.nGaugeTypes()).to.equal("1"); // unset & LiquidityGauge - await setup.gauge_controller.addType("Insurance", 0); + await setup.gaugeController.addType("Liquidity", 0); - expect(await setup.gauge_controller.nGaugeTypes()).to.equal(BigNumber.from("2")); //unset, LiquidityGauge, Insurance + expect(await setup.gaugeController.nGaugeTypes()).to.equal(BigNumber.from("2")); //unset, LiquidityGauge, Liquidity }); it("test gaugeTypes", async () => { - await setup.gauge_controller.addType("Insurance", 0); + await setup.gaugeController.addType("Liquidity", 0); - await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, 0); - await setup.gauge_controller.addGauge(setup.three_gauges[1], 0, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); - expect(await setup.gauge_controller.gaugeTypes(setup.three_gauges[0])).to.equal("1"); - expect(await setup.gauge_controller.gaugeTypes(setup.three_gauges[1])).to.equal("0"); + expect(await setup.gaugeController.gaugeTypes(setup.gaugesAddress[0])).to.equal("1"); + expect(await setup.gaugeController.gaugeTypes(setup.gaugesAddress[1])).to.equal("0"); }); it("test gauge weight", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, setup.ten_to_the_19); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, setup.ten_to_the_19); - expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal(setup.ten_to_the_19); + expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal(setup.ten_to_the_19); }); it("test gauge weight as zero", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); - expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal("0"); + expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal("0"); }); it("test getGaugeWeight", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 0, 0); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); - expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal("0"); + expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal("0"); - await setup.gauge_controller.changeGaugeWeight(setup.three_gauges[0], setup.ten_to_the_21); + await setup.gaugeController.changeGaugeWeight(setup.gaugesAddress[0], setup.ten_to_the_21); - expect(await setup.gauge_controller.getGaugeWeight(setup.three_gauges[0])).to.equal(setup.ten_to_the_21); + expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal(setup.ten_to_the_21); }); it("test type weight", async () => { - await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[0]); - expect(await setup.gauge_controller.getTypeWeight(1)).to.equal(setup.TYPE_WEIGHTS[0]); - expect(await setup.gauge_controller.getTypeWeight(2)).to.equal("0"); + await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); + expect(await setup.gaugeController.getTypeWeight(1)).to.equal(setup.TYPE_WEIGHTS[0]); + expect(await setup.gaugeController.getTypeWeight(2)).to.equal("0"); }); it("test changeTypeWeight", async () => { - await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[0]); - await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[1]); - await setup.gauge_controller.changeTypeWeight(2, setup.TYPE_WEIGHTS[1]); - await setup.gauge_controller.changeTypeWeight(1, BigNumber.from("31337")); - - expect(await setup.gauge_controller.getTypeWeight(0)).to.equal(0); - expect(await setup.gauge_controller.getTypeWeight(1)).to.equal("31337"); - expect(await setup.gauge_controller.getTypeWeight(2)).to.equal(setup.TYPE_WEIGHTS[1]); + await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); + await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[1]); + await setup.gaugeController.changeTypeWeight(2, setup.TYPE_WEIGHTS[1]); + await setup.gaugeController.changeTypeWeight(1, BigNumber.from("31337")); + + expect(await setup.gaugeController.getTypeWeight(0)).to.equal(0); + expect(await setup.gaugeController.getTypeWeight(1)).to.equal("31337"); + expect(await setup.gaugeController.getTypeWeight(2)).to.equal(setup.TYPE_WEIGHTS[1]); }); it("test relative weight write", async () => { - await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[0]); - await setup.gauge_controller.addType("Insurance", setup.TYPE_WEIGHTS[1]); + await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); + await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[1]); - await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.GAUGE_WEIGHTS[0]); - await setup.gauge_controller.addGauge(setup.three_gauges[1], 1, setup.GAUGE_WEIGHTS[1]); - await setup.gauge_controller.addGauge(setup.three_gauges[2], 2, setup.GAUGE_WEIGHTS[2]); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.GAUGE_WEIGHTS[0]); + await setup.gaugeController.addGauge(setup.gaugesAddress[1], 1, setup.GAUGE_WEIGHTS[1]); + await setup.gaugeController.addGauge(setup.gaugesAddress[2], 2, setup.GAUGE_WEIGHTS[2]); let gauge_type = [0, 0, 1]; let total_weight = setup.TYPE_WEIGHTS[0] @@ -128,9 +128,9 @@ describe("GaugeController", function () { let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); for (let i = 0; i < 3; i++) { - await setup.gauge_controller.gaugeRelativeWeightWrite(setup.three_gauges[i], t); - let relative_weight = await setup.gauge_controller.gaugeRelativeWeight(setup.three_gauges[i], t); - expect(relative_weight).to.equal( + await setup.gaugeController.gaugeRelativeWeightWrite(setup.gaugesAddress[i], t); + let relativeWeight = await setup.gaugeController.gaugeRelativeWeight(setup.gaugesAddress[i], t); + expect(relativeWeight).to.equal( setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) ); } @@ -139,9 +139,9 @@ describe("GaugeController", function () { await ethers.provider.send("evm_mine",[]); for (let i = 0; i < 3; i++) { - await setup.gauge_controller.gaugeRelativeWeightWrite(setup.three_gauges[i], t); - let relative_weight = await setup.gauge_controller.gaugeRelativeWeight(setup.three_gauges[i], t); - expect(relative_weight).to.equal( + await setup.gaugeController.gaugeRelativeWeightWrite(setup.gaugesAddress[i], t); + let relativeWeight = await setup.gaugeController.gaugeRelativeWeight(setup.gaugesAddress[i], t); + expect(relativeWeight).to.equal( setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) ); } @@ -150,9 +150,9 @@ describe("GaugeController", function () { await ethers.provider.send("evm_mine",[]); for (let i = 0; i < 3; i++) { - await setup.gauge_controller.gaugeRelativeWeightWrite(setup.three_gauges[i], t); - let relative_weight = await setup.gauge_controller.gaugeRelativeWeight(setup.three_gauges[i], t); - expect(relative_weight).to.equal( + await setup.gaugeController.gaugeRelativeWeightWrite(setup.gaugesAddress[i], t); + let relativeWeight = await setup.gaugeController.gaugeRelativeWeight(setup.gaugesAddress[i], t); + expect(relativeWeight).to.equal( setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) ); } diff --git a/test/fork/GaugeController/timestamps.test.ts b/test/fork/GaugeController/timestamps.test.ts index 6b49741a..8b5b161f 100644 --- a/test/fork/GaugeController/timestamps.test.ts +++ b/test/fork/GaugeController/timestamps.test.ts @@ -26,16 +26,16 @@ describe("GaugeController", function () { describe("test_timestamp", function () { it("test_timestamp", async () => { let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - expect(await setup.gauge_controller.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); + expect(await setup.gaugeController.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); for (let i = 0; i < 5; i++) { //await time.increase(YEAR.mul(BigNumber.from('11')).div(BigNumber.from('10'))); let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); await ethers.provider.send("evm_increaseTime", [setup.YEAR.mul("11").div("10").toNumber()]); - await setup.gauge_controller.checkpoint(); + await setup.gaugeController.checkpoint(); now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - expect(await setup.gauge_controller.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); //technically, blocktimestamp for this tx is "now+1", but it works fine for here because of .div(setup.WEEK) rounds down the number. + expect(await setup.gaugeController.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); //technically, blocktimestamp for this tx is "now+1", but it works fine for here because of .div(setup.WEEK) rounds down the number. } }); }); diff --git a/test/fork/GaugeController/totalWeight.test.ts b/test/fork/GaugeController/totalWeight.test.ts index 706d06f6..a63832c4 100644 --- a/test/fork/GaugeController/totalWeight.test.ts +++ b/test/fork/GaugeController/totalWeight.test.ts @@ -24,37 +24,37 @@ describe("GaugeController", function () { describe("test_total_weight", function () { it("test_total_weight", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.GAUGE_WEIGHTS[0]); - expect(await setup.gauge_controller.getTotalWeight()).to.equal(setup.GAUGE_WEIGHTS[0].mul(setup.TYPE_WEIGHTS[0])); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.GAUGE_WEIGHTS[0]); + expect(await setup.gaugeController.getTotalWeight()).to.equal(setup.GAUGE_WEIGHTS[0].mul(setup.TYPE_WEIGHTS[0])); }); it("test_changeTypeWeight", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.ten_to_the_18); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.ten_to_the_18); - await setup.gauge_controller.changeTypeWeight(1, BigNumber.from("31337")); + await setup.gaugeController.changeTypeWeight(1, BigNumber.from("31337")); - expect(await setup.gauge_controller.getTotalWeight()).to.equal(setup.ten_to_the_18.mul(BigNumber.from("31337"))); + expect(await setup.gaugeController.getTotalWeight()).to.equal(setup.ten_to_the_18.mul(BigNumber.from("31337"))); }); it("test_changeGaugeWeight", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.ten_to_the_18); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.ten_to_the_18); - await setup.gauge_controller.changeGaugeWeight(setup.three_gauges[0], BigNumber.from("31337")); + await setup.gaugeController.changeGaugeWeight(setup.gaugesAddress[0], BigNumber.from("31337")); - expect(await setup.gauge_controller.getTotalWeight()).to.equal(setup.TYPE_WEIGHTS[0].mul(BigNumber.from("31337"))); + expect(await setup.gaugeController.getTotalWeight()).to.equal(setup.TYPE_WEIGHTS[0].mul(BigNumber.from("31337"))); }); it("test_multiple", async () => { - await setup.gauge_controller.addGauge(setup.three_gauges[0], 1, setup.GAUGE_WEIGHTS[0]); - await setup.gauge_controller.addGauge(setup.three_gauges[1], 1, setup.GAUGE_WEIGHTS[1]); - await setup.gauge_controller.addGauge(setup.three_gauges[2], 2, setup.GAUGE_WEIGHTS[2]); + await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.GAUGE_WEIGHTS[0]); + await setup.gaugeController.addGauge(setup.gaugesAddress[1], 1, setup.GAUGE_WEIGHTS[1]); + await setup.gaugeController.addGauge(setup.gaugesAddress[2], 2, setup.GAUGE_WEIGHTS[2]); let expected = setup.GAUGE_WEIGHTS[0] .mul(setup.TYPE_WEIGHTS[0]) .add(setup.GAUGE_WEIGHTS[1].mul(setup.TYPE_WEIGHTS[0])) .add(setup.GAUGE_WEIGHTS[2].mul(setup.TYPE_WEIGHTS[1])); - expect(await setup.gauge_controller.getTotalWeight()).to.equal(expected); + expect(await setup.gaugeController.getTotalWeight()).to.equal(expected); }); }); }); diff --git a/test/fork/GaugeController/vote.test.ts b/test/fork/GaugeController/vote.test.ts index d2c625e3..88a05996 100644 --- a/test/fork/GaugeController/vote.test.ts +++ b/test/fork/GaugeController/vote.test.ts @@ -28,54 +28,54 @@ describe("GaugeController", function () { describe("test_vote", function () { it("test_vote", async () => { // Test if voting for a gauge with a weight of 10000 sets the user's vote power to 10000. - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); - expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); //Total vote power used by user + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); + expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); //Total vote power used by user }); it("test_vote_partial", async () => { // Test if voting with a weight of 1234 sets the user's vote power to 1234. - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234); - expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("1234")); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234); + expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("1234")); }); it("test_vote_change", async () => { // Test if it's possible to change the weight of the same gauge after 10 days. - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234); - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234)).to.revertedWith( + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234); + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234)).to.revertedWith( "Cannot vote so often" ); await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 42); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 42); - expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("42")); + expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("42")); }); it("test_vote_remove", async () => { // Test that removing a vote sets the user's voting power to 0. - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 10000); - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 1234)).to.revertedWith( + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 10000); + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234)).to.revertedWith( "Cannot vote so often" ); await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 0); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 0); - expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(setup.zero); + expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(setup.zero); }); it("test_vote_multiple", async () => { // Test if voting for multiple gauges sums up the user's voting power. - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 4000); - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 6000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 4000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 6000); - expect(await setup.gauge_controller.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); + expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); }); it("test_vote_no_balance", async () => { // Test that users without locked tokens can't vote. - await expect(setup.gauge_controller.connect(setup.accounts[1]).voteForGaugeWeights(setup.three_gauges[0], 10000)).to.revertedWith( + await expect(setup.gaugeController.connect(setup.accounts[1]).voteForGaugeWeights(setup.gaugesAddress[0], 10000)).to.revertedWith( "Your token lock expires too soon" ); }); @@ -84,55 +84,55 @@ describe("GaugeController", function () { // Test that users with expired token locks can't vote. await ethers.provider.send("evm_increaseTime", [setup.YEAR.mul("2").toNumber()]); - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000)).to.revertedWith( + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000)).to.revertedWith( "Your token lock expires too soon" ); }); it("test_invalid_gauge_id", async () => { // Test that voting with an invalid gauge ID throws an error. - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[2], 10000)).to.revertedWith("Gauge not added"); + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[2], 10000)).to.revertedWith("Gauge not added"); }); it("test_over_user_weight", async () => { // Test that voting with power exceeding the user's capacity throws an error. - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10001)).to.revertedWith( + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10001)).to.revertedWith( "You used all your voting power" ); }); it("test_over_weight_multiple", async () => { // Test that voting for multiple gauges with total power exceeding user's capacity throws an error. - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 8000); - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 4000)).to.revertedWith( + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 8000); + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 4000)).to.revertedWith( "Used too much power" ); }); it("test_over_weight_adjust_existing", async () => { // Test that adjusting an existing vote to exceed the user's total voting power throws an error. - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 6000); - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 3000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 6000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 3000); await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 8000)).to.revertedWith( + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 8000)).to.revertedWith( "Used too much power" ); }); it("test_exceeding_voting_power", async () => { - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 6000); - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[1], 5000)).to.revertedWith( + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 6000); + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 5000)).to.revertedWith( "Used too much power" ); }); it("test_emit_vote_event", async () => { let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp + 1); - await expect(setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 5000)) - .to.emit(setup.gauge_controller, "VoteForGauge") - .withArgs(now, setup.accountsAddress[0], setup.three_gauges[0], 5000); + await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 5000)) + .to.emit(setup.gaugeController, "VoteForGauge") + .withArgs(now, setup.accountsAddress[0], setup.gaugesAddress[0], 5000); }); }); diff --git a/test/fork/GaugeController/voteWeightUnitary.test.ts b/test/fork/GaugeController/voteWeightUnitary.test.ts index 963e967c..effd4a49 100644 --- a/test/fork/GaugeController/voteWeightUnitary.test.ts +++ b/test/fork/GaugeController/voteWeightUnitary.test.ts @@ -27,10 +27,10 @@ describe("GaugeController", function () { describe("test_vote_weight_unitary", function () { it("test_no_immediate_effect_on_weight", async () => { - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); expect( - await setup.gauge_controller.gaugeRelativeWeight( - setup.three_gauges[0], + await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[0], BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) ) ).to.equal(BigNumber.from("0")); @@ -39,16 +39,16 @@ describe("GaugeController", function () { it("test_remove_vote_no_immediate_effect", async () => { // ゲージに対して投票を行った後、時間を進めてそのゲージのチェックポイントを取ります。 // その後、投票を取り消して、投票の取り消しは即座には影響しないことを確認します。 - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 0); + await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 0); expect( - await setup.gauge_controller.gaugeRelativeWeight( - setup.three_gauges[0], + await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[0], BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) ) ).to.equal(setup.ten_to_the_18); @@ -56,13 +56,13 @@ describe("GaugeController", function () { it("test_effect_on_following_period", async () => { // ゲージに対して投票を行い、一週間時間を進めた後にそのゲージの重みが変わっていることを確認します。 - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); - await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); + await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); expect( - await setup.gauge_controller.gaugeRelativeWeight( - setup.three_gauges[0], + await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[0], BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) ) ).to.equal(setup.ten_to_the_18); @@ -71,24 +71,24 @@ describe("GaugeController", function () { it("test_remove_vote_means_no_weight", async () => { // ゲージに対して投票を行い、時間を進めてそのゲージのチェックポイントを取ります。 // 投票を取り消し、さらに一週間時間を進めた後、そのゲージの重みが0になっていることを確認します。 - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 10000); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); + await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); expect( - await setup.gauge_controller.gaugeRelativeWeight( - setup.three_gauges[0], + await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[0], BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) ) ).to.equal(setup.ten_to_the_18); - await setup.gauge_controller.voteForGaugeWeights(setup.three_gauges[0], 0); + await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 0); await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); - await setup.gauge_controller.checkpointGauge(setup.three_gauges[0]); + await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); expect( - await setup.gauge_controller.gaugeRelativeWeight( - setup.three_gauges[0], + await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[0], BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) ) ).to.equal(setup.zero); diff --git a/test/fork/Minter/Minter.test.ts b/test/fork/Minter/Minter.test.ts index 13923c74..923bfb22 100644 --- a/test/fork/Minter/Minter.test.ts +++ b/test/fork/Minter/Minter.test.ts @@ -29,70 +29,70 @@ describe("Minter", function () { // Test basic mint functionality it("test mint", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); //gauge_address, msg.sender, mint - let expected = await setup.three_gauges_contracts[0].integrateFraction(setup.accountsAddress[1]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); //gauge_address, msg.sender, mint + let expected = await setup.gaugesContracts[0].integrateFraction(setup.accountsAddress[1]); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(expected); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(expected); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(expected); }); // Test minting immediately after setup it("test mint immediate", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); let t0 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); let moment = t0.add(setup.WEEK).div(setup.WEEK).mul(setup.WEEK).add("5"); await ethers.provider.send("evm_setNextBlockTimestamp", [moment.toNumber()]); //mint - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal("0"); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal("0"); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); //check let balance = await setup.token.balanceOf(setup.accountsAddress[1]); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(balance); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(balance); }); // Test multiple mint operations on the same gauge it("test mint multiple same gauge", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); let balance = await setup.token.balanceOf(setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); - let expected = await setup.three_gauges_contracts[0].integrateFraction(setup.accountsAddress[1]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); + let expected = await setup.gaugesContracts[0].integrateFraction(setup.accountsAddress[1]); let final_balance = await setup.token.balanceOf(setup.accountsAddress[1]); expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success expect(final_balance).to.equal(expected); //2nd mint works fine - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(expected); //tracks fine + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(expected); //tracks fine }); // Test minting across multiple gauges it("test mint multiple gauges", async () => { //setup - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.three_gauges_contracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.three_gauges_contracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); //mint for (let i = 0; i < 3; i++) { - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[i]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[i]); } //check let total_minted = BigNumber.from("0"); for (let i = 0; i < 3; i++) { - let gauge = setup.three_gauges_contracts[i]; + let gauge = setup.gaugesContracts[i]; let minted = await setup.minter.minted(setup.accountsAddress[1], gauge.address); expect(minted).to.equal(await gauge.integrateFraction(setup.accountsAddress[1])); total_minted = total_minted.add(minted); @@ -103,50 +103,50 @@ describe("Minter", function () { // Test minting after withdrawing it("test mint after withdraw", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.WEEK.mul(BigNumber.from("2")).toNumber()]); - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); expect((await setup.token.balanceOf(setup.accountsAddress[1])).gt(BigNumber.from("0"))).to.equal(true); }); // Test multiple mints after withdrawing it("test mint multiple after withdraw", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [10]); - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); let balance = await setup.token.balanceOf(setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [10]); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(balance); }); // Test mint without any deposit it("test no deposit", async () => { - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(setup.zero); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(setup.zero); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(setup.zero); }); // Test minting with the wrong gauge it("test mint wrong gauge", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[1]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[1]); //check expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(setup.zero); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(setup.zero); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[1])).to.equal(setup.zero); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(setup.zero); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[1])).to.equal(setup.zero); }); // Test minting with an invalid gauge address @@ -156,27 +156,27 @@ describe("Minter", function () { // Test minting before inflation begins it("test mint before inflation begins", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); expect(await setup.token.miningEpoch()).to.equal(BigNumber.from("-1")); - await setup.minter.connect(setup.accounts[1]).mint(setup.three_gauges[0]); + await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(BigNumber.from("0")); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(setup.zero); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(setup.zero); }); // Test mintMany function with multiple gauges it("test mintMany function multiple gauges", async () => { //setup - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.three_gauges_contracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.three_gauges_contracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); let addresses = [ - setup.three_gauges[0], - setup.three_gauges[1], - setup.three_gauges[2], + setup.gaugesAddress[0], + setup.gaugesAddress[1], + setup.gaugesAddress[2], setup.ZERO_ADDRESS, setup.ZERO_ADDRESS, setup.ZERO_ADDRESS, @@ -189,7 +189,7 @@ describe("Minter", function () { let total_minted = BigNumber.from("0"); for (let i = 0; i < 3; i++) { - let gauge = setup.three_gauges_contracts[i]; + let gauge = setup.gaugesContracts[i]; let minted = await setup.minter.minted(setup.accountsAddress[1], gauge.address); expect(minted).to.equal(await gauge.integrateFraction(setup.accountsAddress[1])); total_minted = total_minted.add(minted); @@ -209,33 +209,33 @@ describe("Minter", function () { // Test minting on behalf of another user it("test mintFor function", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); await setup.minter.connect(setup.accounts[1]).toggleApproveMint(setup.accountsAddress[2]); expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(true); - await setup.minter.connect(setup.accounts[2]).mintFor(setup.three_gauges[0], setup.accountsAddress[1]); + await setup.minter.connect(setup.accounts[2]).mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); - let expected = await setup.three_gauges_contracts[0].integrateFraction(setup.accountsAddress[1]); + let expected = await setup.gaugesContracts[0].integrateFraction(setup.accountsAddress[1]); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(expected); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(expected); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(expected); }); // Test mintFor function when not approved it("test mintForFail function", async () => { - await setup.three_gauges_contracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(false); - await setup.minter.connect(setup.accounts[2]).mintFor(setup.three_gauges[0], setup.accountsAddress[1]); + await setup.minter.connect(setup.accounts[2]).mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(0); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.three_gauges[0])).to.equal(0); + expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(0); }); }); }); diff --git a/test/fork/helper.ts b/test/fork/helper.ts index 4d2d4411..02f926f0 100644 --- a/test/fork/helper.ts +++ b/test/fork/helper.ts @@ -42,12 +42,12 @@ class TestSetup { accounts: Signer[]; accountsAddress: String[]; token: Contract; - voting_escrow: Contract; - gauge_controller: Contract; - mock_lp_token: Contract; + votingEscrow: Contract; + gaugeController: Contract; + mockLpToken: Contract; minter: Contract; - three_gauges_contracts: Contract[]; - three_gauges: string[]; + gaugesContracts: Contract[]; + gaugesAddress: string[]; TYPE_WEIGHTS: BigNumber[]; GAUGE_WEIGHTS: BigNumber[]; @@ -75,46 +75,46 @@ class TestSetup { // deploy this.token = await Token.deploy(); - this.voting_escrow = await VotingEscrow.deploy( + this.votingEscrow = await VotingEscrow.deploy( this.token.address, "Voting-escrowed token", "vetoken", "v1" ); - this.gauge_controller = await GaugeController.deploy(this.token.address, this.voting_escrow.address); - this.mock_lp_token = await TestLP.deploy("tokenDAO LP token", "iToken", 18, BigNumber.from("1000000000000000000000")); - this.minter = await Minter.deploy(this.token.address, this.gauge_controller.address); - const lg1 = await LiquidityGauge.deploy(this.mock_lp_token.address, this.minter.address); - const lg2 = await LiquidityGauge.deploy(this.mock_lp_token.address, this.minter.address); - const lg3 = await LiquidityGauge.deploy(this.mock_lp_token.address, this.minter.address); - this.three_gauges_contracts = [lg1, lg2, lg3]; - this.three_gauges = [lg1.address, lg2.address, lg3.address]; + this.gaugeController = await GaugeController.deploy(this.token.address, this.votingEscrow.address); + this.mockLpToken = await TestLP.deploy("tokenDAO LP token", "iToken", 18, BigNumber.from("1000000000000000000000")); + this.minter = await Minter.deploy(this.token.address, this.gaugeController.address); + const lg1 = await LiquidityGauge.deploy(this.mockLpToken.address, this.minter.address); + const lg2 = await LiquidityGauge.deploy(this.mockLpToken.address, this.minter.address); + const lg3 = await LiquidityGauge.deploy(this.mockLpToken.address, this.minter.address); + this.gaugesContracts = [lg1, lg2, lg3]; + this.gaugesAddress = [lg1.address, lg2.address, lg3.address]; await this.token.setMinter(this.minter.address); // setup - await this.gauge_controller.addType("none", 0); + await this.gaugeController.addType("none", 0); } async addType(){ - await this.gauge_controller.addType("Liquidity", this.TYPE_WEIGHTS[0]); - await this.gauge_controller.addType("Liquidity", this.TYPE_WEIGHTS[1]); + await this.gaugeController.addType("Liquidity", this.TYPE_WEIGHTS[0]); + await this.gaugeController.addType("Liquidity", this.TYPE_WEIGHTS[1]); } async addGaugeZero(){ for (let i = 0; i < 2; i++) { - await this.gauge_controller.addGauge(this.three_gauges[i], this.GAUGE_TYPES[i], 0); + await this.gaugeController.addGauge(this.gaugesAddress[i], this.GAUGE_TYPES[i], 0); } } async addGauge(){ for (let i = 0; i < 3; i++) { - await this.gauge_controller.addGauge(this.three_gauges[i], this.GAUGE_TYPES[i], this.GAUGE_WEIGHTS[i]); + await this.gaugeController.addGauge(this.gaugesAddress[i], this.GAUGE_TYPES[i], this.GAUGE_WEIGHTS[i]); } } async createLock(){ - await this.token.approve(this.voting_escrow.address, BigNumber.from("1000000000000000000000000")); - await this.voting_escrow.createLock( + await this.token.approve(this.votingEscrow.address, BigNumber.from("1000000000000000000000000")); + await this.votingEscrow.createLock( BigNumber.from("1000000000000000000000000"), BigNumber.from((await ethers.provider.getBlock("latest")).timestamp).add(this.YEAR) ); @@ -122,12 +122,12 @@ class TestSetup { async createLP(){ for (let i = 1; i < 4; i++) { - await this.mock_lp_token.transfer(this.accountsAddress[i], this.ten_to_the_18); + await this.mockLpToken.transfer(this.accountsAddress[i], this.ten_to_the_18); } for (let i = 0; i < 3; i++) { for (let t = 0; t < 3; t++) { - await this.mock_lp_token.connect(this.accounts[i + 1]).approve(this.three_gauges[t], this.ten_to_the_18); + await this.mockLpToken.connect(this.accounts[i + 1]).approve(this.gaugesAddress[t], this.ten_to_the_18); } } } From f90a6cc77a1f6a89ad991174fc022e2de53f44dc Mon Sep 17 00:00:00 2001 From: takadr Date: Sat, 28 Oct 2023 22:48:05 +0900 Subject: [PATCH 020/412] Added FeeDistributor and its tests to curveFork --- contracts/Tester/MockToken.sol | 65 ++ contracts/curveFork/FeeDistributor.sol | 623 ++++++++++++++++++ test/fork/FeeDistributor/checkpoints.test.ts | 127 ++++ test/fork/FeeDistributor/claimMany.test.ts | 146 ++++ .../FeeDistributor/feeDistribution.test.ts | 273 ++++++++ .../fork/FeeDistributor/killFeeDistro.test.ts | 116 ++++ 6 files changed, 1350 insertions(+) create mode 100644 contracts/Tester/MockToken.sol create mode 100644 contracts/curveFork/FeeDistributor.sol create mode 100644 test/fork/FeeDistributor/checkpoints.test.ts create mode 100644 test/fork/FeeDistributor/claimMany.test.ts create mode 100644 test/fork/FeeDistributor/feeDistribution.test.ts create mode 100644 test/fork/FeeDistributor/killFeeDistro.test.ts diff --git a/contracts/Tester/MockToken.sol b/contracts/Tester/MockToken.sol new file mode 100644 index 00000000..4e3342c9 --- /dev/null +++ b/contracts/Tester/MockToken.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +contract MockToken { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + string public name; + string public symbol; + uint256 public decimals; + uint256 public totalSupply; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); + + constructor(string memory _name, string memory _symbol, uint256 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + } + + function transfer( + address _to, + uint256 _value + ) external returns (bool success) { + require(balanceOf[msg.sender] >= _value, "Insufficient balance"); + balanceOf[msg.sender] -= _value; + balanceOf[_to] += _value; + emit Transfer(msg.sender, _to, _value); + return true; + } + + function transferFrom( + address _from, + address _to, + uint256 _value + ) external returns (bool success) { + require(balanceOf[_from] >= _value, "Insufficient balance"); + require(allowance[_from][msg.sender] >= _value, "Allowance exceeded"); + balanceOf[_from] -= _value; + balanceOf[_to] += _value; + allowance[_from][msg.sender] -= _value; + emit Transfer(_from, _to, _value); + return true; + } + + function approve( + address _spender, + uint256 _value + ) external returns (bool success) { + allowance[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + function _mintForTesting(address _target, uint256 _value) external { + totalSupply += _value; + balanceOf[_target] += _value; + emit Transfer(address(0), _target, _value); + } +} diff --git a/contracts/curveFork/FeeDistributor.sol b/contracts/curveFork/FeeDistributor.sol new file mode 100644 index 00000000..3d906af4 --- /dev/null +++ b/contracts/curveFork/FeeDistributor.sol @@ -0,0 +1,623 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +struct Point { + int128 bias; + int128 slope; + uint256 ts; + uint256 blk; +} + +interface IVotingEscrow { + function userPointEpoch(address addr) external view returns (uint256); + + function epoch() external view returns (uint256); + + function userPointHistory( + address addr, + uint256 loc + ) external view returns (Point memory); + + function pointHistory(uint256 loc) external view returns (Point memory); + + function checkpoint() external; +} + +contract FeeDistributor is ReentrancyGuard { + uint256 public constant WEEK = 7 * 86400; + uint256 public constant TOKEN_CHECKPOINT_DEADLINE = 86400; + + uint256 public startTime; + uint256 public timeCursor; + mapping(address => uint256) public timeCursorOf; + mapping(address => uint256) public userEpochOf; + + uint256 public lastTokenTime; + mapping(uint256 => uint256) public tokensPerWeek; + + address public votingEscrow; + address public token; + uint256 public totalReceived; + uint256 public tokenLastBalance; + + mapping(uint256 => uint256) public veSupply; // VE total supply at week bounds + + address public admin; + address public futureAdmin; + bool public canCheckpointToken; + address public emergencyReturn; + bool public isKilled; + + event CommitAdmin(address indexed admin); + event ApplyAdmin(address indexed admin); + event ToggleAllowCheckpointToken(bool toggleFlag); + event CheckpointToken(uint256 time, uint256 tokens); + event Claimed( + address indexed recipient, + uint256 amount, + uint256 claimEpoch, + uint256 maxEpoch + ); + + /*** + * @notice Contract constructor + * @param votingEscrow_ VotingEscrow contract address + * @param startTime_ Epoch time for fee distribution to start + * @param token_ Fee token address (3CRV) + * @param admin_ Admin address + * @param emergencyReturn_ Address to transfer `_token` balance to if this contract is killed + */ + constructor( + address votingEscrow_, + uint256 startTime_, + address token_, + address admin_, + address emergencyReturn_ + ) { + uint256 t = (startTime_ / WEEK) * WEEK; + startTime = t; + lastTokenTime = t; + timeCursor = t; + token = token_; + votingEscrow = votingEscrow_; + admin = admin_; + emergencyReturn = emergencyReturn_; + } + + function _checkpointToken() internal { + uint256 _tokenBalance = IERC20(token).balanceOf(address(this)); + uint256 _toDistribute = _tokenBalance - tokenLastBalance; + tokenLastBalance = _tokenBalance; + + uint256 _t = lastTokenTime; + uint256 _sinceLast = block.timestamp - _t; + lastTokenTime = block.timestamp; + uint256 _thisWeek = (_t / WEEK) * WEEK; + uint256 _nextWeek = 0; + + for (uint256 i; i < 20; ) { + _nextWeek = _thisWeek + WEEK; + if (block.timestamp < _nextWeek) { + if (_sinceLast == 0 && block.timestamp == _t) { + tokensPerWeek[_thisWeek] += _toDistribute; + } else { + tokensPerWeek[_thisWeek] += + (_toDistribute * (block.timestamp - _t)) / + _sinceLast; + } + break; + } else { + if (_sinceLast == 0 && _nextWeek == _t) { + tokensPerWeek[_thisWeek] += _toDistribute; + } else { + tokensPerWeek[_thisWeek] += + (_toDistribute * (_nextWeek - _t)) / + _sinceLast; + } + } + _t = _nextWeek; + _thisWeek = _nextWeek; + unchecked { + ++i; + } + } + + emit CheckpointToken(block.timestamp, _toDistribute); + } + + /*** + * @notice Update the token checkpoint + * @dev Calculates the total number of tokens to be distributed in a given week. + During setup for the initial distribution this function is only callable + by the contract owner. Beyond initial distro, it can be enabled for anyone + to call. + */ + function checkpointToken() external { + require( + msg.sender == admin || + (canCheckpointToken && + block.timestamp > lastTokenTime + 1 hours), + "Unauthorized" + ); + _checkpointToken(); + } + + function _findTimestampEpoch( + address ve_, + uint256 timestamp_ + ) internal view returns (uint256) { + uint256 _min = 0; + uint256 _max = IVotingEscrow(ve_).epoch(); + + unchecked { + for (uint256 i; i < 128; i++) { + if (_min >= _max) { + break; + } + uint256 _mid = (_min + _max + 2) / 2; + Point memory pt = IVotingEscrow(ve_).pointHistory(_mid); + if (pt.ts <= timestamp_) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + } + return _min; + } + + function _findTimestampUserEpoch( + address ve_, + address user_, + uint256 timestamp_, + uint256 maxUserEpoch_ + ) internal view returns (uint256) { + uint256 _min = 0; + uint256 _max = maxUserEpoch_; + + unchecked { + for (uint256 i; i < 128; i++) { + if (_min >= _max) { + break; + } + uint256 _mid = (_min + _max + 2) / 2; + Point memory pt = IVotingEscrow(ve_).userPointHistory( + user_, + _mid + ); + if (pt.ts <= timestamp_) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + } + return _min; + } + + /*** + * @notice Get the veYNWK balance for `user_` at `timestamp_` + * @param user_ Address to query balance for + * @param timestamp_ Epoch time + * @return uint256 veYNWK balance + */ + function veForAt( + address user_, + uint256 timestamp_ + ) external view returns (uint256) { + address _ve = votingEscrow; + uint256 _maxUserEpoch = IVotingEscrow(_ve).userPointEpoch(user_); + uint256 _epoch = _findTimestampUserEpoch( + _ve, + user_, + timestamp_, + _maxUserEpoch + ); + Point memory _pt = IVotingEscrow(_ve).userPointHistory(user_, _epoch); + return + uint256( + int256( + _pt.bias - + _pt.slope * + int128(int256(timestamp_) - int256(_pt.ts)) + ) + ); + } + + function _checkpointTotalSupply() internal { + address _ve = votingEscrow; + uint256 _t = timeCursor; + uint256 _roundedTimestamp = (block.timestamp / WEEK) * WEEK; + IVotingEscrow(_ve).checkpoint(); + + for (uint256 i; i < 20; ) { + if (_t > _roundedTimestamp) { + break; + } else { + uint256 _epoch = _findTimestampEpoch(_ve, _t); + Point memory _pt = IVotingEscrow(_ve).pointHistory(_epoch); + int128 _dt = 0; + if (_t > _pt.ts) { + _dt = int128(int256(_t) - int256(_pt.ts)); + } + veSupply[_t] = uint256(int256(_pt.bias - _pt.slope * _dt)); + _t += WEEK; + } + unchecked { + ++i; + } + } + + timeCursor = _t; + } + + /*** + * @notice Update the veCRV total supply checkpoint + * @dev The checkpoint is also updated by the first claimant each new epoch week. This function may be called independently of a claim, to reduce claiming gas costs. + */ + function checkpointTotalSupply() external { + address _ve = votingEscrow; + uint256 _t = timeCursor; + uint256 _roundedTimestamp = (block.timestamp / WEEK) * WEEK; + IVotingEscrow(_ve).checkpoint(); + + for (uint256 i; i < 20; ) { + if (_t > _roundedTimestamp) { + break; + } else { + uint256 _epoch = _findTimestampEpoch(_ve, _t); + Point memory _pt = IVotingEscrow(_ve).pointHistory(_epoch); + uint256 _dt = 0; + if (_t > _pt.ts) { + _dt = uint256(int256(_t) - int256(_pt.ts)); + } + veSupply[_t] = uint256( + int256( + Math.max( + uint256(int256(_pt.bias)) - + uint256(int256(_pt.slope)) * + _dt, + 0 + ) + ) + ); + } + _t += WEEK; + unchecked { + ++i; + } + } + + timeCursor = _t; + } + + function _claim( + address addr_, + address ve_, + uint256 lastTokenTime_ + ) internal returns (uint256) { + // Minimal user_epoch is 0 (if user had no point) + uint256 _userEpoch = 0; + uint256 _toDistribute = 0; + + uint256 _maxUserEpoch = IVotingEscrow(ve_).userPointEpoch(addr_); + uint256 _startTime = startTime; + + if (_maxUserEpoch == 0) { + // No lock = no fees + return 0; + } + + uint256 _weekCursor = timeCursorOf[addr_]; + if (_weekCursor == 0) { + // Need to do the initial binary search + _userEpoch = _findTimestampUserEpoch( + ve_, + addr_, + _startTime, + _maxUserEpoch + ); + } else { + _userEpoch = userEpochOf[addr_]; + } + + if (_userEpoch == 0) { + _userEpoch = 1; + } + + Point memory _userPoint = IVotingEscrow(ve_).userPointHistory( + addr_, + _userEpoch + ); + + if (_weekCursor == 0) { + _weekCursor = ((_userPoint.ts + WEEK - 1) / WEEK) * WEEK; + } + + if (_weekCursor >= lastTokenTime_) { + return 0; + } + + if (_weekCursor < _startTime) { + _weekCursor = _startTime; + } + + Point memory _oldUserPoint = Point({bias: 0, slope: 0, ts: 0, blk: 0}); + + // Iterate over weeks + for (uint256 i; i < 50; ) { + if (_weekCursor >= lastTokenTime_) { + break; + } else if ( + _weekCursor >= _userPoint.ts && _userEpoch <= _maxUserEpoch + ) { + _userEpoch += 1; + _oldUserPoint = _userPoint; + if (_userEpoch > _maxUserEpoch) { + _userPoint = Point({bias: 0, slope: 0, ts: 0, blk: 0}); + } else { + _userPoint = IVotingEscrow(ve_).userPointHistory( + addr_, + _userEpoch + ); + } + } else { + uint256 dt = uint256( + int256(_weekCursor) - int256(_oldUserPoint.ts) + ); + uint256 balanceOf = uint256( + int256( + Math.max( + uint256(int256(_oldUserPoint.bias)) - + dt * + uint256(int256(_oldUserPoint.slope)), + 0 + ) + ) + ); + if (balanceOf == 0 && _userEpoch > _maxUserEpoch) { + break; + } + if (balanceOf > 0) { + _toDistribute += + (balanceOf * tokensPerWeek[_weekCursor]) / + veSupply[_weekCursor]; + } + _weekCursor += WEEK; + } + unchecked { + ++i; + } + } + + _userEpoch = Math.min(_maxUserEpoch, _userEpoch - 1); + userEpochOf[addr_] = _userEpoch; + timeCursorOf[addr_] = _weekCursor; + + emit Claimed(addr_, _toDistribute, _userEpoch, _maxUserEpoch); + + return _toDistribute; + } + + /*** + * @notice Claim fees for `msg.sender` + * @dev Each call to claim look at a maximum of 50 user veCRV points. + For accounts with many veCRV related actions, this function + may need to be called more than once to claim all available + fees. In the `Claimed` event that fires, if `claim_epoch` is + less than `max_epoch`, the account may claim again. + * @return uint256 Amount of fees claimed in the call + */ + function claim() external nonReentrant returns (uint256) { + require(!isKilled, "Contract is killed"); + address _addr = msg.sender; + if (block.timestamp >= timeCursor) { + _checkpointTotalSupply(); + } + + uint256 _lastTokenTime = lastTokenTime; + + if ( + canCheckpointToken && (block.timestamp > _lastTokenTime + 1 hours) + ) { + _checkpointToken(); + _lastTokenTime = block.timestamp; + } + + unchecked { + _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; + } + + uint256 amount = _claim(_addr, votingEscrow, _lastTokenTime); + if (amount != 0) { + require(IERC20(token).transfer(_addr, amount), "Transfer failed"); + tokenLastBalance -= amount; + } + + return amount; + } + + /*** + * @notice Claim fees for `addr_` + * @dev Each call to claim look at a maximum of 50 user veCRV points. + For accounts with many veCRV related actions, this function + may need to be called more than once to claim all available + fees. In the `Claimed` event that fires, if `claim_epoch` is + less than `max_epoch`, the account may claim again. + * @param addr_ Address to claim fees for + * @return uint256 Amount of fees claimed in the call + */ + function claim(address addr_) external nonReentrant returns (uint256) { + require(!isKilled, "Contract is killed"); + + if (block.timestamp >= timeCursor) { + _checkpointTotalSupply(); + } + + uint256 _lastTokenTime = lastTokenTime; + + if ( + canCheckpointToken && (block.timestamp > _lastTokenTime + 1 hours) + ) { + _checkpointToken(); + _lastTokenTime = block.timestamp; + } + + unchecked { + _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; + } + + uint256 amount = _claim(addr_, votingEscrow, _lastTokenTime); + if (amount != 0) { + require(IERC20(token).transfer(addr_, amount), "Transfer failed"); + tokenLastBalance -= amount; + } + + return amount; + } + + /*** + * @notice Make multiple fee claims in a single call + * @dev Used to claim for many accounts at once, or to make + multiple claims for the same address when that address + has significant veCRV history + * @param receivers_ List of addresses to claim for. Claiming + terminates at the first `ZERO_ADDRESS`. + * @return bool success + */ + function claimMany( + address[] memory receivers_ + ) external nonReentrant returns (bool) { + require(!isKilled, "Contract is killed"); + + if (block.timestamp >= timeCursor) { + _checkpointTotalSupply(); + } + + uint256 _lastTokenTime = lastTokenTime; + + if ( + canCheckpointToken && (block.timestamp > _lastTokenTime + 1 hours) + ) { + _checkpointToken(); + _lastTokenTime = block.timestamp; + } + + _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; + uint256 _total = 0; + uint256 _l = receivers_.length; + for (uint256 i; i < _l; ) { + address addr = receivers_[i]; + if (addr == address(0)) { + break; + } + + uint256 amount = _claim(addr, votingEscrow, _lastTokenTime); + if (amount != 0) { + require( + IERC20(token).transfer(addr, amount), + "Transfer failed" + ); + _total += amount; + } + unchecked { + ++i; + } + } + + if (_total != 0) { + tokenLastBalance -= _total; + } + + return true; + } + + /*** + * @notice Receive 3CRV into the contract and trigger a token checkpoint + * @param coin_ Address of the coin being received (must be 3CRV) + * @return bool success + */ + function burn(address coin_) external returns (bool) { + require(coin_ == address(token), "Invalid token"); + require(!isKilled, "Contract is killed"); + + uint256 _amount = IERC20(token).balanceOf(msg.sender); + if (_amount > 0) { + IERC20(token).transferFrom(msg.sender, address(this), _amount); + if ( + canCheckpointToken && block.timestamp > lastTokenTime + 1 hours + ) { + _checkpointToken(); + } + } + return true; + } + + /*** + * @notice Commit transfer of ownership + * @param addr_ New admin address + */ + function commitAdmin(address addr_) external onlyAdmin { + futureAdmin = addr_; + emit CommitAdmin(addr_); + } + + /*** + * @notice Apply transfer of ownership + */ + function applyAdmin() external onlyAdmin { + require(futureAdmin != address(0), "No admin set"); + admin = futureAdmin; + emit ApplyAdmin(futureAdmin); + } + + /*** + * @notice Toggle permission for checkpointing by any account + */ + function toggleAllowCheckpointToken() external onlyAdmin { + canCheckpointToken = !canCheckpointToken; + emit ToggleAllowCheckpointToken(canCheckpointToken); + } + + /*** + * @notice Kill the contract + * @dev Killing transfers the entire 3CRV balance to the emergency return address + and blocks the ability to claim or burn. The contract cannot be unkilled. + */ + function killMe() external onlyAdmin { + isKilled = true; + require( + IERC20(token).transfer( + emergencyReturn, + IERC20(token).balanceOf(address(this)) + ), + "Transfer failed" + ); + } + + /*** + * @notice Recover ERC20 tokens from this contract + * @dev Tokens are sent to the emergency return address. + * @param coin_ Token address + * @return bool success + */ + function recoverBalance(address coin_) external onlyAdmin returns (bool) { + require(coin_ != address(token), "Cannot recover this token"); + + uint256 amount = IERC20(coin_).balanceOf(address(this)); + require( + IERC20(coin_).transfer(emergencyReturn, amount), + "Transfer failed" + ); + return true; + } + + modifier onlyAdmin() { + require(admin == msg.sender, "Access denied"); + _; + } +} diff --git a/test/fork/FeeDistributor/checkpoints.test.ts b/test/fork/FeeDistributor/checkpoints.test.ts new file mode 100644 index 00000000..eaa48daa --- /dev/null +++ b/test/fork/FeeDistributor/checkpoints.test.ts @@ -0,0 +1,127 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +const DAY = 86400; +const WEEK = DAY * 7; +const YEAR = DAY * 365; + +describe("FeeDistributor", () => { + let alice, bob, charlie: SignerWithAddress; + + let distributor: Contract; + let votingEscrow: Contract; + let token: Contract; + let coinA: Contract; + + beforeEach(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + const CRV = await ethers.getContractFactory("CRV"); + const Token = await ethers.getContractFactory("MockToken"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + coinA = await Token.deploy("Coin A", "USDA", 18); + await coinA.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + distributor = await Distributor.deploy( + votingEscrow.address, + await time.latest(), + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + }); + + // TODO + // 下記の観点のテストを追加 + // https://discord.com/channels/729808684359876718/729812922649542758/1162241240360816730 + + describe("test_checkpoints", () => { + beforeEach(async function () { + await token.approve(votingEscrow.address, ethers.constants.MaxUint256); + await votingEscrow.createLock( + ethers.utils.parseEther("1000"), + (await time.latest()) + WEEK * 52 + ); + }); + it("test_checkpoint_total_supply", async function () { + const startTime = await distributor.timeCursor(); + const weekEpoch = + Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; + + await time.increaseTo(weekEpoch); + + const weekBlock = await time.latestBlock(); + + await distributor.checkpointTotalSupply(); + + expect(await distributor.veSupply(startTime)).to.equal(0); + expect(await distributor.veSupply(weekEpoch)).to.equal( + await votingEscrow.totalSupplyAt(weekBlock) + ); + }); + + it("test_advance_time_cursor", async function () { + const startTime = (await distributor.timeCursor()).toNumber(); + await time.increase(YEAR); + await distributor.checkpointTotalSupply(); + const newTimeCursor = (await distributor.timeCursor()).toNumber(); + expect(newTimeCursor).to.equal(startTime + WEEK * 20); + expect(await distributor.veSupply(startTime + WEEK * 19)).to.be.above(0); + expect(await distributor.veSupply(startTime + WEEK * 20)).to.equal(0); + + await distributor.checkpointTotalSupply(); + + expect(await distributor.timeCursor()).to.equal(startTime + WEEK * 40); + expect(await distributor.veSupply(startTime + WEEK * 20)).to.be.above(0); + expect(await distributor.veSupply(startTime + WEEK * 39)).to.be.above(0); + expect(await distributor.veSupply(startTime + WEEK * 40)).to.equal(0); + }); + + it("test_claim_checkpoints_total_supply", async function () { + const start_time = (await distributor.timeCursor()).toNumber(); + + await distributor.connect(alice)["claim()"](); + + expect((await distributor.timeCursor()).toNumber()).to.equal( + start_time + WEEK + ); + }); + + it("test_toggle_allow_checkpoint", async function () { + const lastTokenTime = (await distributor.lastTokenTime()).toNumber(); + + await time.increase(WEEK); + + await distributor.connect(alice)["claim()"](); + expect((await distributor.lastTokenTime()).toNumber()).to.equal( + lastTokenTime + ); + + await distributor.toggleAllowCheckpointToken(); + const tx = await distributor.connect(alice)["claim()"](); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockNumber); + + expect((await distributor.lastTokenTime()).toNumber()).to.equal( + block.timestamp + ); + }); + }); +}); diff --git a/test/fork/FeeDistributor/claimMany.test.ts b/test/fork/FeeDistributor/claimMany.test.ts new file mode 100644 index 00000000..24cbaa38 --- /dev/null +++ b/test/fork/FeeDistributor/claimMany.test.ts @@ -0,0 +1,146 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { time, takeSnapshot } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +const DAY = 86400; +const WEEK = DAY * 7; + +describe("FeeDistributor", () => { + let alice, bob, charlie: SignerWithAddress; + + let distributor: Contract; + let votingEscrow: Contract; + let token: Contract; + let coinA: Contract; + + beforeEach(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + const CRV = await ethers.getContractFactory("CRV"); + const Token = await ethers.getContractFactory("MockToken"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + coinA = await Token.deploy("Coin A", "USDA", 18); + await coinA.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + distributor = await Distributor.deploy( + votingEscrow.address, + await time.latest(), + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + }); + + describe("test_claim_many", () => { + const amount = ethers.utils.parseEther("1000"); + it("test_claim_many", async function () { + for (let acct of [alice, bob, charlie]) { + await token.connect(acct).approve(votingEscrow.address, amount.mul(10)); + await token.connect(alice).transfer(acct.address, amount); + await votingEscrow + .connect(acct) + .createLock(amount, (await time.latest()) + 8 * WEEK); + } + await time.increase(WEEK); + let startTime = await time.latest(); + await time.increase(WEEK * 5); + const Distributor = await ethers.getContractFactory("FeeDistributor"); + distributor = await Distributor.deploy( + votingEscrow.address, + startTime, + coinA.address, + alice.address, + alice.address + ); + await coinA._mintForTesting( + distributor.address, + ethers.utils.parseEther("10") + ); + await distributor.checkpointToken(); + await time.increase(WEEK); + await distributor.checkpointToken(); + + const snapshot = await takeSnapshot(); + + await distributor + .connect(alice) + .claimMany( + [alice.address, bob.address, charlie.address].concat( + Array(17).fill(ethers.constants.AddressZero) + ) + ); + let balances = [ + await coinA.balanceOf(alice.address), + await coinA.balanceOf(bob.address), + await coinA.balanceOf(charlie.address), + ]; + + await snapshot.restore(); + + await distributor.connect(alice)["claim()"](); + await distributor.connect(bob)["claim()"](); + await distributor.connect(charlie)["claim()"](); + expect(balances).to.deep.equal([ + await coinA.balanceOf(alice.address), + await coinA.balanceOf(bob.address), + await coinA.balanceOf(charlie.address), + ]); + }); + it("test_claim_many_same_account", async function () { + for (let acct of [alice, bob, charlie]) { + await token.connect(acct).approve(votingEscrow.address, amount.mul(10)); + await token.connect(alice).transfer(acct.address, amount); + await votingEscrow + .connect(acct) + .createLock(amount, (await time.latest()) + 8 * WEEK); + } + await time.increase(WEEK); + let startTime = await time.latest(); + await time.increase(WEEK * 5); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + distributor = await Distributor.deploy( + votingEscrow.address, + startTime, + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + + await coinA._mintForTesting( + distributor.address, + ethers.utils.parseEther("10") + ); + await distributor.checkpointToken(); + await time.increase(WEEK); + await distributor.checkpointToken(); + + const expected = await distributor.connect(alice).callStatic["claim()"](); + + expect(expected).to.above(0); + expect( + await distributor + .connect(alice) + .claimMany(Array(20).fill(alice.address)) + .toString() + ).to.changeTokenBalance(coinA, alice, expected); + }); + }); +}); diff --git a/test/fork/FeeDistributor/feeDistribution.test.ts b/test/fork/FeeDistributor/feeDistribution.test.ts new file mode 100644 index 00000000..bd92b26c --- /dev/null +++ b/test/fork/FeeDistributor/feeDistribution.test.ts @@ -0,0 +1,273 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +const DAY = 86400; +const WEEK = DAY * 7; + +describe("FeeDistributor", () => { + let alice, bob, charlie: SignerWithAddress; + + let distributor: Contract; + let votingEscrow: Contract; + let token: Contract; + let coinA: Contract; + + beforeEach(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + const CRV = await ethers.getContractFactory("CRV"); + const Token = await ethers.getContractFactory("MockToken"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + coinA = await Token.deploy("Coin A", "USDA", 18); + await coinA.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + distributor = await Distributor.deploy( + votingEscrow.address, + await time.latest(), + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + }); + + describe("test_fee_distribution", () => { + it("test_deposited_after", async function () { + const amount = ethers.utils.parseEther("1000"); + await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); + await coinA._mintForTesting(bob.address, ethers.utils.parseEther("100")); + + for (let i = 0; i < 5; i++) { + for (let j = 0; j < 7; j++) { + await coinA + .connect(bob) + .transfer(distributor.address, ethers.utils.parseEther("1")); + await distributor.checkpointToken(); + await distributor.checkpointTotalSupply(); + await time.increase(DAY); + } + } + + await time.increase(WEEK); + await votingEscrow + .connect(alice) + .createLock(amount, (await time.latest()) + 3 * WEEK); + await time.increase(2 * WEEK); + + await distributor.connect(alice)["claim()"](); + expect( + await distributor.connect(alice)["claim()"]() + ).to.changeTokenBalance(coinA, alice, 0); + }); + + it("test_deposited_during", async function () { + const amount = ethers.utils.parseEther("1000"); + await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); + await coinA._mintForTesting(bob.address, ethers.utils.parseEther("100")); + + await time.increase(WEEK); + await votingEscrow + .connect(alice) + .createLock(amount, (await time.latest()) + 8 * WEEK); + await time.increase(WEEK); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + distributor = await Distributor.deploy( + votingEscrow.address, + await time.latest(), + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 7; j++) { + await coinA + .connect(bob) + .transfer(distributor.address, ethers.utils.parseEther("1")); + await distributor.checkpointToken(); + await distributor.checkpointTotalSupply(); + await time.increase(DAY); + } + } + + await time.increase(WEEK); + await distributor.checkpointToken(); + await distributor.connect(alice)["claim()"](); + + const balanceAlice = await coinA.balanceOf(alice.address); + const diff = Math.abs( + balanceAlice.sub(ethers.utils.parseEther("21")).toNumber() + ); + expect(diff).to.be.lessThan(10); + }); + + it("test_deposited_before", async function () { + const [alice, bob] = await ethers.getSigners(); + const amount = ethers.utils.parseEther("1000"); + + await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); + await coinA + .connect(bob) + ._mintForTesting(bob.address, ethers.utils.parseEther("100")); + + await votingEscrow + .connect(alice) + .createLock(amount, (await time.latest()) + 8 * WEEK); + await time.increase(WEEK); + const startTime = await time.latest(); + await time.increase(WEEK * 5); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + distributor = await Distributor.deploy( + votingEscrow.address, + startTime, + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + + await coinA + .connect(bob) + .transfer(distributor.address, ethers.utils.parseEther("10")); + await distributor.checkpointToken(); + await time.increase(WEEK); + await distributor.checkpointToken(); + await distributor.connect(alice)["claim()"](); + + const balanceAlice = await coinA.balanceOf(alice.address); + expect( + Math.abs(balanceAlice.sub(ethers.utils.parseEther("10")).toNumber()) + ).to.be.lessThan(10); + }); + + it("test_deposited_twice", async function () { + const amount = ethers.utils.parseEther("1000"); + + await token.approve(votingEscrow.address, amount.mul(10)); + await coinA._mintForTesting(bob.address, ethers.utils.parseEther("100")); + + const currentTimestamp = await time.latest(); + await votingEscrow.createLock(amount, currentTimestamp + 4 * WEEK); + + await time.increase(WEEK); + + const startTime = await time.latest(); + + await time.increase(3 * WEEK); + + await votingEscrow.connect(alice).withdraw(); + const excludeTime = Math.floor((await time.latest()) / WEEK) * WEEK; + await votingEscrow + .connect(alice) + .createLock(amount, (await time.latest()) + 4 * WEEK); + + await time.increase(2 * WEEK); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + distributor = await Distributor.deploy( + votingEscrow.address, + startTime, + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + + await coinA + .connect(bob) + .transfer(distributor.address, ethers.utils.parseEther("10")); + await distributor.checkpointToken(); + + await time.increase(WEEK); + + await distributor.checkpointToken(); + + await distributor.connect(alice)["claim()"](); + + const tokensToExclude = await distributor.tokensPerWeek(excludeTime); + + expect( + ethers.utils + .parseEther("10") + .sub(await coinA.balanceOf(alice.address)) + .sub(tokensToExclude) + ).to.be.lt(10); + }); + + it("test_deposited_parallel", async function () { + const amount = ethers.utils.parseEther("1000"); + + await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); + await token.connect(bob).approve(votingEscrow.address, amount.mul(10)); + await token.connect(alice).transfer(bob.address, amount); + await coinA._mintForTesting( + charlie.address, + ethers.utils.parseEther("100") + ); + + const currentTimestamp = await time.latest(); + await votingEscrow + .connect(alice) + .createLock(amount, currentTimestamp + 8 * WEEK); + await votingEscrow + .connect(bob) + .createLock(amount, currentTimestamp + 8 * WEEK); + + await time.increase(WEEK); + + const startTime = await time.latest(); + + await time.increase(5 * WEEK); + + const Distributor = await ethers.getContractFactory("FeeDistributor"); + distributor = await Distributor.deploy( + votingEscrow.address, + startTime, + coinA.address, + alice.address, + alice.address + ); + await distributor.deployed(); + + await coinA + .connect(charlie) + .transfer(distributor.address, ethers.utils.parseEther("10")); + await distributor.checkpointToken(); + + await time.increase(WEEK); + + await distributor.checkpointToken(); + + await distributor.connect(alice)["claim()"](); + await distributor.connect(bob)["claim()"](); + + const balanceAlice = await coinA.balanceOf(alice.address); + const balanceBob = await coinA.balanceOf(bob.address); + + expect(balanceAlice).to.equal(balanceBob); + expect(balanceAlice.add(balanceBob)).to.be.closeTo( + ethers.utils.parseEther("10"), + 20 + ); + }); + }); +}); diff --git a/test/fork/FeeDistributor/killFeeDistro.test.ts b/test/fork/FeeDistributor/killFeeDistro.test.ts new file mode 100644 index 00000000..2008675f --- /dev/null +++ b/test/fork/FeeDistributor/killFeeDistro.test.ts @@ -0,0 +1,116 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +describe("FeeDistributor", () => { + let alice, bob, charlie: SignerWithAddress; + + let distributor: Contract; + let votingEscrow: Contract; + let token: Contract; + let coinA: Contract; + + beforeEach(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const CRV = await ethers.getContractFactory("CRV"); + const Token = await ethers.getContractFactory("MockToken"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + coinA = await Token.deploy("Coin A", "USDA", 18); + await coinA.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + }); + + describe("test_kill_fee_distro", () => { + let accounts: SignerWithAddress[]; + beforeEach(async () => { + const Distributor = await ethers.getContractFactory("FeeDistributor"); + distributor = await Distributor.deploy( + votingEscrow.address, + await time.latest(), + coinA.address, + alice.address, + bob.address + ); + await distributor.deployed(); + accounts = await ethers.getSigners(); + }); + + it("test_assumptions", async function () { + expect(await distributor.isKilled()).to.be.false; + expect(await distributor.emergencyReturn()).to.equal(bob.address); + }); + + it("test_kill", async function () { + await distributor.connect(alice).killMe(); + expect(await distributor.isKilled()).to.be.true; + }); + + it("test_multi_kill", async function () { + await distributor.connect(alice).killMe(); + await distributor.connect(alice).killMe(); + expect(await distributor.isKilled()).to.be.true; + }); + + it("test_killing_transfers_tokens", async function () { + await coinA._mintForTesting(distributor.address, 31337); + await distributor.connect(alice).killMe(); + + expect(await distributor.emergencyReturn()).to.equal(bob.address); + expect(await coinA.balanceOf(bob.address)).to.equal(31337); + }); + + it("test_multi_kill_token_transfer", async function () { + await coinA._mintForTesting(distributor.address, 10000); + await distributor.connect(alice).killMe(); + + await coinA._mintForTesting(distributor.address, 30000); + await distributor.connect(alice).killMe(); + + expect(await distributor.emergencyReturn()).to.equal(bob.address); + expect(await coinA.balanceOf(bob.address)).to.equal(40000); + }); + + for (let idx = 1; idx <= 2; idx++) { + it(`test_only_admin_for_account_index_${idx}`, async function () { + await expect(distributor.connect(accounts[idx]).killMe()).to.be + .reverted; + }); + + it(`test_cannot_claim_after_killed_for_account_index_${idx}`, async function () { + await distributor.connect(alice).killMe(); + await expect(distributor.connect(accounts[idx])["claim()"]()).to.be + .reverted; + }); + + it(`test_cannot_claim_for_after_killed_for_account_index_${idx}`, async function () { + await distributor.connect(alice).killMe(); + await expect( + distributor.connect(accounts[idx])["claim(address)"](alice.address) + ).to.be.reverted; + }); + + it(`test_cannot_claim_many_after_killed_for_account_index_${idx}`, async function () { + await distributor.connect(alice).killMe(); + await expect( + distributor + .connect(accounts[idx]) + .claimMany(new Array(20).fill(alice.address)) + ).to.be.reverted; + }); + } + }); +}); From 15158a566096e08daa6c6344b610e74a23648d83 Mon Sep 17 00:00:00 2001 From: takadr Date: Tue, 31 Oct 2023 15:34:35 +0900 Subject: [PATCH 021/412] Restructured test/fork --- test/fork/GaugeController/timestamps.test.ts | 42 -- test/fork/GaugeController/vote.test.ts | 139 ------ test/fork/Minter/Minter.test.ts | 241 ---------- .../FeeDistributor/checkpoints.test.ts | 12 +- .../FeeDistributor/claimMany.test.ts | 12 +- .../FeeDistributor/feeDistribution.test.ts | 12 +- .../FeeDistributor/killFeeDistro.test.ts | 11 +- .../GaugeController/gaugesWeights.test.ts | 148 ++++-- .../GaugeController/timestamps.test.ts | 62 +++ .../GaugeController/totalWeight.test.ts | 55 ++- .../fork/unitary/GaugeController/vote.test.ts | 200 +++++++++ .../GaugeController/voteWeightUnitary.test.ts | 41 +- test/fork/unitary/Minter/Minter.test.ts | 420 ++++++++++++++++++ 13 files changed, 911 insertions(+), 484 deletions(-) delete mode 100644 test/fork/GaugeController/timestamps.test.ts delete mode 100644 test/fork/GaugeController/vote.test.ts delete mode 100644 test/fork/Minter/Minter.test.ts rename test/fork/{ => unitary}/FeeDistributor/checkpoints.test.ts (94%) rename test/fork/{ => unitary}/FeeDistributor/claimMany.test.ts (94%) rename test/fork/{ => unitary}/FeeDistributor/feeDistribution.test.ts (97%) rename test/fork/{ => unitary}/FeeDistributor/killFeeDistro.test.ts (94%) rename test/fork/{ => unitary}/GaugeController/gaugesWeights.test.ts (53%) create mode 100644 test/fork/unitary/GaugeController/timestamps.test.ts rename test/fork/{ => unitary}/GaugeController/totalWeight.test.ts (52%) create mode 100644 test/fork/unitary/GaugeController/vote.test.ts rename test/fork/{ => unitary}/GaugeController/voteWeightUnitary.test.ts (77%) create mode 100644 test/fork/unitary/Minter/Minter.test.ts diff --git a/test/fork/GaugeController/timestamps.test.ts b/test/fork/GaugeController/timestamps.test.ts deleted file mode 100644 index 8b5b161f..00000000 --- a/test/fork/GaugeController/timestamps.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../helper"; - -describe("GaugeController", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - before(async () => { - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - }); - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - }); - - afterEach(async () => { - await evm.restore(snapshotId); - }); - - describe("test_timestamp", function () { - it("test_timestamp", async () => { - let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - expect(await setup.gaugeController.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); - - for (let i = 0; i < 5; i++) { - //await time.increase(YEAR.mul(BigNumber.from('11')).div(BigNumber.from('10'))); - let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - await ethers.provider.send("evm_increaseTime", [setup.YEAR.mul("11").div("10").toNumber()]); - - await setup.gaugeController.checkpoint(); - now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - expect(await setup.gaugeController.timeTotal()).to.equal(now.add(setup.WEEK).div(setup.WEEK).or(BigNumber.from("0")).mul(setup.WEEK)); //technically, blocktimestamp for this tx is "now+1", but it works fine for here because of .div(setup.WEEK) rounds down the number. - } - }); - }); -}); diff --git a/test/fork/GaugeController/vote.test.ts b/test/fork/GaugeController/vote.test.ts deleted file mode 100644 index 88a05996..00000000 --- a/test/fork/GaugeController/vote.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../helper"; - -describe("GaugeController", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - before(async () => { - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - await setup.addGaugeZero(); - await setup.createLock(); - }); - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - }); - - afterEach(async () => { - await evm.restore(snapshotId); - }); - - describe("test_vote", function () { - it("test_vote", async () => { - // Test if voting for a gauge with a weight of 10000 sets the user's vote power to 10000. - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); - expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); //Total vote power used by user - }); - - it("test_vote_partial", async () => { - // Test if voting with a weight of 1234 sets the user's vote power to 1234. - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234); - expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("1234")); - }); - - it("test_vote_change", async () => { - // Test if it's possible to change the weight of the same gauge after 10 days. - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234); - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234)).to.revertedWith( - "Cannot vote so often" - ); - - await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 42); - - expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("42")); - }); - - it("test_vote_remove", async () => { - // Test that removing a vote sets the user's voting power to 0. - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 10000); - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234)).to.revertedWith( - "Cannot vote so often" - ); - - await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 0); - - expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(setup.zero); - }); - - it("test_vote_multiple", async () => { - // Test if voting for multiple gauges sums up the user's voting power. - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 4000); - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 6000); - - expect(await setup.gaugeController.voteUserPower(setup.accountsAddress[0])).to.equal(BigNumber.from("10000")); - }); - - it("test_vote_no_balance", async () => { - // Test that users without locked tokens can't vote. - await expect(setup.gaugeController.connect(setup.accounts[1]).voteForGaugeWeights(setup.gaugesAddress[0], 10000)).to.revertedWith( - "Your token lock expires too soon" - ); - }); - - it("test_vote_expired", async () => { - // Test that users with expired token locks can't vote. - await ethers.provider.send("evm_increaseTime", [setup.YEAR.mul("2").toNumber()]); - - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000)).to.revertedWith( - "Your token lock expires too soon" - ); - }); - - it("test_invalid_gauge_id", async () => { - // Test that voting with an invalid gauge ID throws an error. - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[2], 10000)).to.revertedWith("Gauge not added"); - }); - - it("test_over_user_weight", async () => { - // Test that voting with power exceeding the user's capacity throws an error. - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10001)).to.revertedWith( - "You used all your voting power" - ); - }); - - it("test_over_weight_multiple", async () => { - // Test that voting for multiple gauges with total power exceeding user's capacity throws an error. - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 8000); - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 4000)).to.revertedWith( - "Used too much power" - ); - }); - - it("test_over_weight_adjust_existing", async () => { - // Test that adjusting an existing vote to exceed the user's total voting power throws an error. - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 6000); - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 3000); - - await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); - - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 8000)).to.revertedWith( - "Used too much power" - ); - }); - - it("test_exceeding_voting_power", async () => { - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 6000); - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 5000)).to.revertedWith( - "Used too much power" - ); - }); - - it("test_emit_vote_event", async () => { - let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp + 1); - await expect(setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 5000)) - .to.emit(setup.gaugeController, "VoteForGauge") - .withArgs(now, setup.accountsAddress[0], setup.gaugesAddress[0], 5000); - }); - - }); -}); diff --git a/test/fork/Minter/Minter.test.ts b/test/fork/Minter/Minter.test.ts deleted file mode 100644 index 923bfb22..00000000 --- a/test/fork/Minter/Minter.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../helper"; - -describe("Minter", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - before(async () => { - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - await setup.addGauge(); - await setup.createLP(); - }); - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - }); - - afterEach(async () => { - await evm.restore(snapshotId); - }); - - describe("Minter Behavior", function () { - - // Test basic mint functionality - it("test mint", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); //gauge_address, msg.sender, mint - let expected = await setup.gaugesContracts[0].integrateFraction(setup.accountsAddress[1]); - - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(expected); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(expected); - }); - - // Test minting immediately after setup - it("test mint immediate", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); - - let t0 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - let moment = t0.add(setup.WEEK).div(setup.WEEK).mul(setup.WEEK).add("5"); - await ethers.provider.send("evm_setNextBlockTimestamp", [moment.toNumber()]); - - //mint - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal("0"); - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - - //check - let balance = await setup.token.balanceOf(setup.accountsAddress[1]); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(balance); - }); - - // Test multiple mint operations on the same gauge - it("test mint multiple same gauge", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - let balance = await setup.token.balanceOf(setup.accountsAddress[1]); - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - let expected = await setup.gaugesContracts[0].integrateFraction(setup.accountsAddress[1]); - let final_balance = await setup.token.balanceOf(setup.accountsAddress[1]); - - expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success - expect(final_balance).to.equal(expected); //2nd mint works fine - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(expected); //tracks fine - }); - - // Test minting across multiple gauges - it("test mint multiple gauges", async () => { - //setup - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.gaugesContracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.gaugesContracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - - //mint - for (let i = 0; i < 3; i++) { - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[i]); - } - - //check - let total_minted = BigNumber.from("0"); - - for (let i = 0; i < 3; i++) { - let gauge = setup.gaugesContracts[i]; - let minted = await setup.minter.minted(setup.accountsAddress[1], gauge.address); - expect(minted).to.equal(await gauge.integrateFraction(setup.accountsAddress[1])); - total_minted = total_minted.add(minted); - } - - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(total_minted); - }); - - // Test minting after withdrawing - it("test mint after withdraw", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [setup.WEEK.mul(BigNumber.from("2")).toNumber()]); - - await setup.gaugesContracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - - expect((await setup.token.balanceOf(setup.accountsAddress[1])).gt(BigNumber.from("0"))).to.equal(true); - }); - - // Test multiple mints after withdrawing - it("test mint multiple after withdraw", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [10]); - await setup.gaugesContracts[0].connect(setup.accounts[1]).withdraw(setup.ten_to_the_18); - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - - let balance = await setup.token.balanceOf(setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [10]); - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(balance); - }); - - // Test mint without any deposit - it("test no deposit", async () => { - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(setup.zero); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(setup.zero); - }); - - // Test minting with the wrong gauge - it("test mint wrong gauge", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[1]); - - //check - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(setup.zero); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(setup.zero); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[1])).to.equal(setup.zero); - }); - - // Test minting with an invalid gauge address - it("test mint not a gauge", async () => { - await expect(setup.minter.mint(setup.accountsAddress[1])).to.revertedWith("dev: gauge is not added"); - }); - - // Test minting before inflation begins - it("test mint before inflation begins", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_18, setup.accountsAddress[1]); - expect(await setup.token.miningEpoch()).to.equal(BigNumber.from("-1")); - - await setup.minter.connect(setup.accounts[1]).mint(setup.gaugesAddress[0]); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(BigNumber.from("0")); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(setup.zero); - }); - - // Test mintMany function with multiple gauges - it("test mintMany function multiple gauges", async () => { - //setup - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.gaugesContracts[1].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - await setup.gaugesContracts[2].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - - let addresses = [ - setup.gaugesAddress[0], - setup.gaugesAddress[1], - setup.gaugesAddress[2], - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, - ]; - await setup.minter.connect(setup.accounts[1]).mintMany(addresses); - - //check - let total_minted = BigNumber.from("0"); - - for (let i = 0; i < 3; i++) { - let gauge = setup.gaugesContracts[i]; - let minted = await setup.minter.minted(setup.accountsAddress[1], gauge.address); - expect(minted).to.equal(await gauge.integrateFraction(setup.accountsAddress[1])); - total_minted = total_minted.add(minted); - } - - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(total_minted); - }); - - // Test toggling of the mint approval function - it("test toggleApproveMint function", async () => { - await setup.minter.connect(setup.accounts[1]).toggleApproveMint(setup.accountsAddress[2]); - expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(true); - - await setup.minter.connect(setup.accounts[1]).toggleApproveMint(setup.accountsAddress[2]); - expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(false); - }); - - // Test minting on behalf of another user - it("test mintFor function", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - - await setup.minter.connect(setup.accounts[1]).toggleApproveMint(setup.accountsAddress[2]); - expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(true); - - await setup.minter.connect(setup.accounts[2]).mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); - - let expected = await setup.gaugesContracts[0].integrateFraction(setup.accountsAddress[1]); - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(expected); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(expected); - }); - - // Test mintFor function when not approved - it("test mintForFail function", async () => { - await setup.gaugesContracts[0].connect(setup.accounts[1]).deposit(setup.ten_to_the_17, setup.accountsAddress[1]); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - - expect(await setup.minter.allowedToMintFor(setup.accountsAddress[2], setup.accountsAddress[1])).to.equal(false); - - await setup.minter.connect(setup.accounts[2]).mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); - - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(0); - expect(await setup.minter.minted(setup.accountsAddress[1], setup.gaugesAddress[0])).to.equal(0); - }); - }); -}); diff --git a/test/fork/FeeDistributor/checkpoints.test.ts b/test/fork/unitary/FeeDistributor/checkpoints.test.ts similarity index 94% rename from test/fork/FeeDistributor/checkpoints.test.ts rename to test/fork/unitary/FeeDistributor/checkpoints.test.ts index eaa48daa..4f56b82a 100644 --- a/test/fork/FeeDistributor/checkpoints.test.ts +++ b/test/fork/unitary/FeeDistributor/checkpoints.test.ts @@ -1,7 +1,11 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { Contract } from "ethers"; -import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; const DAY = 86400; @@ -15,8 +19,10 @@ describe("FeeDistributor", () => { let votingEscrow: Contract; let token: Contract; let coinA: Contract; + let snapshot: SnapshotRestorer; beforeEach(async function () { + snapshot = await takeSnapshot(); [alice, bob, charlie] = await ethers.getSigners(); const Distributor = await ethers.getContractFactory("FeeDistributor"); @@ -48,6 +54,10 @@ describe("FeeDistributor", () => { await distributor.deployed(); }); + afterEach(async () => { + await snapshot.restore(); + }); + // TODO // 下記の観点のテストを追加 // https://discord.com/channels/729808684359876718/729812922649542758/1162241240360816730 diff --git a/test/fork/FeeDistributor/claimMany.test.ts b/test/fork/unitary/FeeDistributor/claimMany.test.ts similarity index 94% rename from test/fork/FeeDistributor/claimMany.test.ts rename to test/fork/unitary/FeeDistributor/claimMany.test.ts index 24cbaa38..e131ad11 100644 --- a/test/fork/FeeDistributor/claimMany.test.ts +++ b/test/fork/unitary/FeeDistributor/claimMany.test.ts @@ -1,13 +1,18 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { Contract } from "ethers"; -import { time, takeSnapshot } from "@nomicfoundation/hardhat-network-helpers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; const DAY = 86400; const WEEK = DAY * 7; describe("FeeDistributor", () => { + let snapshot: SnapshotRestorer; let alice, bob, charlie: SignerWithAddress; let distributor: Contract; @@ -16,6 +21,7 @@ describe("FeeDistributor", () => { let coinA: Contract; beforeEach(async function () { + snapshot = await takeSnapshot(); [alice, bob, charlie] = await ethers.getSigners(); const Distributor = await ethers.getContractFactory("FeeDistributor"); @@ -47,6 +53,10 @@ describe("FeeDistributor", () => { await distributor.deployed(); }); + afterEach(async () => { + await snapshot.restore(); + }); + describe("test_claim_many", () => { const amount = ethers.utils.parseEther("1000"); it("test_claim_many", async function () { diff --git a/test/fork/FeeDistributor/feeDistribution.test.ts b/test/fork/unitary/FeeDistributor/feeDistribution.test.ts similarity index 97% rename from test/fork/FeeDistributor/feeDistribution.test.ts rename to test/fork/unitary/FeeDistributor/feeDistribution.test.ts index bd92b26c..5388f881 100644 --- a/test/fork/FeeDistributor/feeDistribution.test.ts +++ b/test/fork/unitary/FeeDistributor/feeDistribution.test.ts @@ -1,7 +1,11 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { Contract } from "ethers"; -import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; const DAY = 86400; @@ -9,13 +13,14 @@ const WEEK = DAY * 7; describe("FeeDistributor", () => { let alice, bob, charlie: SignerWithAddress; - let distributor: Contract; let votingEscrow: Contract; let token: Contract; let coinA: Contract; + let snapshot: SnapshotRestorer; beforeEach(async function () { + snapshot = await takeSnapshot(); [alice, bob, charlie] = await ethers.getSigners(); const Distributor = await ethers.getContractFactory("FeeDistributor"); @@ -46,6 +51,9 @@ describe("FeeDistributor", () => { ); await distributor.deployed(); }); + afterEach(async () => { + await snapshot.restore(); + }); describe("test_fee_distribution", () => { it("test_deposited_after", async function () { diff --git a/test/fork/FeeDistributor/killFeeDistro.test.ts b/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts similarity index 94% rename from test/fork/FeeDistributor/killFeeDistro.test.ts rename to test/fork/unitary/FeeDistributor/killFeeDistro.test.ts index 2008675f..8e374f1c 100644 --- a/test/fork/FeeDistributor/killFeeDistro.test.ts +++ b/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts @@ -1,7 +1,11 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { Contract } from "ethers"; -import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; describe("FeeDistributor", () => { @@ -11,8 +15,10 @@ describe("FeeDistributor", () => { let votingEscrow: Contract; let token: Contract; let coinA: Contract; + let snapshot: SnapshotRestorer; beforeEach(async function () { + snapshot = await takeSnapshot(); [alice, bob, charlie] = await ethers.getSigners(); const CRV = await ethers.getContractFactory("CRV"); @@ -33,6 +39,9 @@ describe("FeeDistributor", () => { ); await votingEscrow.deployed(); }); + afterEach(async () => { + await snapshot.restore(); + }); describe("test_kill_fee_distro", () => { let accounts: SignerWithAddress[]; diff --git a/test/fork/GaugeController/gaugesWeights.test.ts b/test/fork/unitary/GaugeController/gaugesWeights.test.ts similarity index 53% rename from test/fork/GaugeController/gaugesWeights.test.ts rename to test/fork/unitary/GaugeController/gaugesWeights.test.ts index 38e47719..f5529c41 100644 --- a/test/fork/GaugeController/gaugesWeights.test.ts +++ b/test/fork/unitary/GaugeController/gaugesWeights.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../../helper"; describe("GaugeController", function () { let setup: TestSetup; @@ -27,8 +27,12 @@ describe("GaugeController", function () { await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); - expect(await setup.gaugeController.gauges(0)).to.equal(setup.gaugesAddress[0]); - expect(await setup.gaugeController.gauges(1)).to.equal(setup.gaugesAddress[1]); + expect(await setup.gaugeController.gauges(0)).to.equal( + setup.gaugesAddress[0] + ); + expect(await setup.gaugeController.gauges(1)).to.equal( + setup.gaugesAddress[1] + ); }); it("test nGauges", async () => { @@ -44,9 +48,9 @@ describe("GaugeController", function () { expect(await setup.gaugeController.nGauges()).to.equal("0"); await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); - await expect(setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0)).to.revertedWith( - "cannot add the same gauge twice" - ); + await expect( + setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0) + ).to.revertedWith("cannot add the same gauge twice"); expect(await setup.gaugeController.nGauges()).to.equal("1"); }); @@ -56,7 +60,9 @@ describe("GaugeController", function () { await setup.gaugeController.addType("Liquidity", 0); - expect(await setup.gaugeController.nGaugeTypes()).to.equal(BigNumber.from("2")); //unset, LiquidityGauge, Liquidity + expect(await setup.gaugeController.nGaugeTypes()).to.equal( + BigNumber.from("2") + ); //unset, LiquidityGauge, Liquidity }); it("test gaugeTypes", async () => { @@ -65,35 +71,56 @@ describe("GaugeController", function () { await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, 0); await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); - expect(await setup.gaugeController.gaugeTypes(setup.gaugesAddress[0])).to.equal("1"); - expect(await setup.gaugeController.gaugeTypes(setup.gaugesAddress[1])).to.equal("0"); + expect( + await setup.gaugeController.gaugeTypes(setup.gaugesAddress[0]) + ).to.equal("1"); + expect( + await setup.gaugeController.gaugeTypes(setup.gaugesAddress[1]) + ).to.equal("0"); }); it("test gauge weight", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, setup.ten_to_the_19); + await setup.gaugeController.addGauge( + setup.gaugesAddress[0], + 0, + setup.ten_to_the_19 + ); - expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal(setup.ten_to_the_19); + expect( + await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) + ).to.equal(setup.ten_to_the_19); }); it("test gauge weight as zero", async () => { await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); - expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal("0"); + expect( + await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) + ).to.equal("0"); }); it("test getGaugeWeight", async () => { await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); - expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal("0"); + expect( + await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) + ).to.equal("0"); - await setup.gaugeController.changeGaugeWeight(setup.gaugesAddress[0], setup.ten_to_the_21); + await setup.gaugeController.changeGaugeWeight( + setup.gaugesAddress[0], + setup.ten_to_the_21 + ); - expect(await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0])).to.equal(setup.ten_to_the_21); + expect( + await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) + ).to.equal(setup.ten_to_the_21); }); it("test type weight", async () => { await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); - expect(await setup.gaugeController.getTypeWeight(1)).to.equal(setup.TYPE_WEIGHTS[0]); + expect(await setup.gaugeController.getTypeWeight(1)).to.equal( + setup.TYPE_WEIGHTS[0] + ); expect(await setup.gaugeController.getTypeWeight(2)).to.equal("0"); }); @@ -105,16 +132,30 @@ describe("GaugeController", function () { expect(await setup.gaugeController.getTypeWeight(0)).to.equal(0); expect(await setup.gaugeController.getTypeWeight(1)).to.equal("31337"); - expect(await setup.gaugeController.getTypeWeight(2)).to.equal(setup.TYPE_WEIGHTS[1]); + expect(await setup.gaugeController.getTypeWeight(2)).to.equal( + setup.TYPE_WEIGHTS[1] + ); }); it("test relative weight write", async () => { await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[1]); - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.GAUGE_WEIGHTS[0]); - await setup.gaugeController.addGauge(setup.gaugesAddress[1], 1, setup.GAUGE_WEIGHTS[1]); - await setup.gaugeController.addGauge(setup.gaugesAddress[2], 2, setup.GAUGE_WEIGHTS[2]); + await setup.gaugeController.addGauge( + setup.gaugesAddress[0], + 1, + setup.GAUGE_WEIGHTS[0] + ); + await setup.gaugeController.addGauge( + setup.gaugesAddress[1], + 1, + setup.GAUGE_WEIGHTS[1] + ); + await setup.gaugeController.addGauge( + setup.gaugesAddress[2], + 2, + setup.GAUGE_WEIGHTS[2] + ); let gauge_type = [0, 0, 1]; let total_weight = setup.TYPE_WEIGHTS[0] @@ -122,38 +163,73 @@ describe("GaugeController", function () { .add(setup.TYPE_WEIGHTS[0].mul(setup.GAUGE_WEIGHTS[1])) .add(setup.TYPE_WEIGHTS[1].mul(setup.GAUGE_WEIGHTS[2])); - await ethers.provider.send("evm_increaseTime", [setup.WEEK.mul(2).toNumber()]); - await ethers.provider.send("evm_mine",[]); + await ethers.provider.send("evm_increaseTime", [ + setup.WEEK.mul(2).toNumber(), + ]); + await ethers.provider.send("evm_mine", []); - let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + let t = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); for (let i = 0; i < 3; i++) { - await setup.gaugeController.gaugeRelativeWeightWrite(setup.gaugesAddress[i], t); - let relativeWeight = await setup.gaugeController.gaugeRelativeWeight(setup.gaugesAddress[i], t); + await setup.gaugeController.gaugeRelativeWeightWrite( + setup.gaugesAddress[i], + t + ); + let relativeWeight = await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[i], + t + ); expect(relativeWeight).to.equal( - setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) + setup.ten_to_the_18 + .mul(setup.GAUGE_WEIGHTS[i]) + .mul(setup.TYPE_WEIGHTS[gauge_type[i]]) + .div(total_weight) ); } - await ethers.provider.send("evm_increaseTime", [setup.YEAR.div("2").toNumber()]); - await ethers.provider.send("evm_mine",[]); + await ethers.provider.send("evm_increaseTime", [ + setup.YEAR.div("2").toNumber(), + ]); + await ethers.provider.send("evm_mine", []); for (let i = 0; i < 3; i++) { - await setup.gaugeController.gaugeRelativeWeightWrite(setup.gaugesAddress[i], t); - let relativeWeight = await setup.gaugeController.gaugeRelativeWeight(setup.gaugesAddress[i], t); + await setup.gaugeController.gaugeRelativeWeightWrite( + setup.gaugesAddress[i], + t + ); + let relativeWeight = await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[i], + t + ); expect(relativeWeight).to.equal( - setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) + setup.ten_to_the_18 + .mul(setup.GAUGE_WEIGHTS[i]) + .mul(setup.TYPE_WEIGHTS[gauge_type[i]]) + .div(total_weight) ); } - await ethers.provider.send("evm_increaseTime", [setup.YEAR.div("10").toNumber()]); - await ethers.provider.send("evm_mine",[]); + await ethers.provider.send("evm_increaseTime", [ + setup.YEAR.div("10").toNumber(), + ]); + await ethers.provider.send("evm_mine", []); for (let i = 0; i < 3; i++) { - await setup.gaugeController.gaugeRelativeWeightWrite(setup.gaugesAddress[i], t); - let relativeWeight = await setup.gaugeController.gaugeRelativeWeight(setup.gaugesAddress[i], t); + await setup.gaugeController.gaugeRelativeWeightWrite( + setup.gaugesAddress[i], + t + ); + let relativeWeight = await setup.gaugeController.gaugeRelativeWeight( + setup.gaugesAddress[i], + t + ); expect(relativeWeight).to.equal( - setup.ten_to_the_18.mul(setup.GAUGE_WEIGHTS[i]).mul(setup.TYPE_WEIGHTS[gauge_type[i]]).div(total_weight) + setup.ten_to_the_18 + .mul(setup.GAUGE_WEIGHTS[i]) + .mul(setup.TYPE_WEIGHTS[gauge_type[i]]) + .div(total_weight) ); } }); diff --git a/test/fork/unitary/GaugeController/timestamps.test.ts b/test/fork/unitary/GaugeController/timestamps.test.ts new file mode 100644 index 00000000..a97842f9 --- /dev/null +++ b/test/fork/unitary/GaugeController/timestamps.test.ts @@ -0,0 +1,62 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, TestSetup } from "../../helper"; + +describe("GaugeController", function () { + let setup: TestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new TestSetup(); + await setup.setup(); + await setup.addType(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("test_timestamp", function () { + it("test_timestamp", async () => { + let now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + expect(await setup.gaugeController.timeTotal()).to.equal( + now + .add(setup.WEEK) + .div(setup.WEEK) + .or(BigNumber.from("0")) + .mul(setup.WEEK) + ); + + for (let i = 0; i < 5; i++) { + //await time.increase(YEAR.mul(BigNumber.from('11')).div(BigNumber.from('10'))); + let t = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + await ethers.provider.send("evm_increaseTime", [ + setup.YEAR.mul("11").div("10").toNumber(), + ]); + + await setup.gaugeController.checkpoint(); + now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + expect(await setup.gaugeController.timeTotal()).to.equal( + now + .add(setup.WEEK) + .div(setup.WEEK) + .or(BigNumber.from("0")) + .mul(setup.WEEK) + ); //technically, blocktimestamp for this tx is "now+1", but it works fine for here because of .div(setup.WEEK) rounds down the number. + } + }); + }); +}); diff --git a/test/fork/GaugeController/totalWeight.test.ts b/test/fork/unitary/GaugeController/totalWeight.test.ts similarity index 52% rename from test/fork/GaugeController/totalWeight.test.ts rename to test/fork/unitary/GaugeController/totalWeight.test.ts index a63832c4..a746265f 100644 --- a/test/fork/GaugeController/totalWeight.test.ts +++ b/test/fork/unitary/GaugeController/totalWeight.test.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../../helper"; describe("GaugeController", function () { let setup: TestSetup; @@ -24,30 +24,63 @@ describe("GaugeController", function () { describe("test_total_weight", function () { it("test_total_weight", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.GAUGE_WEIGHTS[0]); - expect(await setup.gaugeController.getTotalWeight()).to.equal(setup.GAUGE_WEIGHTS[0].mul(setup.TYPE_WEIGHTS[0])); + await setup.gaugeController.addGauge( + setup.gaugesAddress[0], + 1, + setup.GAUGE_WEIGHTS[0] + ); + expect(await setup.gaugeController.getTotalWeight()).to.equal( + setup.GAUGE_WEIGHTS[0].mul(setup.TYPE_WEIGHTS[0]) + ); }); it("test_changeTypeWeight", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.ten_to_the_18); + await setup.gaugeController.addGauge( + setup.gaugesAddress[0], + 1, + setup.ten_to_the_18 + ); await setup.gaugeController.changeTypeWeight(1, BigNumber.from("31337")); - expect(await setup.gaugeController.getTotalWeight()).to.equal(setup.ten_to_the_18.mul(BigNumber.from("31337"))); + expect(await setup.gaugeController.getTotalWeight()).to.equal( + setup.ten_to_the_18.mul(BigNumber.from("31337")) + ); }); it("test_changeGaugeWeight", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.ten_to_the_18); + await setup.gaugeController.addGauge( + setup.gaugesAddress[0], + 1, + setup.ten_to_the_18 + ); - await setup.gaugeController.changeGaugeWeight(setup.gaugesAddress[0], BigNumber.from("31337")); + await setup.gaugeController.changeGaugeWeight( + setup.gaugesAddress[0], + BigNumber.from("31337") + ); - expect(await setup.gaugeController.getTotalWeight()).to.equal(setup.TYPE_WEIGHTS[0].mul(BigNumber.from("31337"))); + expect(await setup.gaugeController.getTotalWeight()).to.equal( + setup.TYPE_WEIGHTS[0].mul(BigNumber.from("31337")) + ); }); it("test_multiple", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, setup.GAUGE_WEIGHTS[0]); - await setup.gaugeController.addGauge(setup.gaugesAddress[1], 1, setup.GAUGE_WEIGHTS[1]); - await setup.gaugeController.addGauge(setup.gaugesAddress[2], 2, setup.GAUGE_WEIGHTS[2]); + await setup.gaugeController.addGauge( + setup.gaugesAddress[0], + 1, + setup.GAUGE_WEIGHTS[0] + ); + await setup.gaugeController.addGauge( + setup.gaugesAddress[1], + 1, + setup.GAUGE_WEIGHTS[1] + ); + await setup.gaugeController.addGauge( + setup.gaugesAddress[2], + 2, + setup.GAUGE_WEIGHTS[2] + ); let expected = setup.GAUGE_WEIGHTS[0] .mul(setup.TYPE_WEIGHTS[0]) diff --git a/test/fork/unitary/GaugeController/vote.test.ts b/test/fork/unitary/GaugeController/vote.test.ts new file mode 100644 index 00000000..ada6e518 --- /dev/null +++ b/test/fork/unitary/GaugeController/vote.test.ts @@ -0,0 +1,200 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, TestSetup } from "../../helper"; + +describe("GaugeController", function () { + let setup: TestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new TestSetup(); + await setup.setup(); + await setup.addType(); + await setup.addGaugeZero(); + await setup.createLock(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("test_vote", function () { + it("test_vote", async () => { + // Test if voting for a gauge with a weight of 10000 sets the user's vote power to 10000. + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 10000 + ); + expect( + await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) + ).to.equal(BigNumber.from("10000")); //Total vote power used by user + }); + + it("test_vote_partial", async () => { + // Test if voting with a weight of 1234 sets the user's vote power to 1234. + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[1], + 1234 + ); + expect( + await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) + ).to.equal(BigNumber.from("1234")); + }); + + it("test_vote_change", async () => { + // Test if it's possible to change the weight of the same gauge after 10 days. + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[1], + 1234 + ); + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234) + ).to.revertedWith("Cannot vote so often"); + + await ethers.provider.send("evm_increaseTime", [ + setup.DAY.mul("10").toNumber(), + ]); + + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[1], + 42 + ); + + expect( + await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) + ).to.equal(BigNumber.from("42")); + }); + + it("test_vote_remove", async () => { + // Test that removing a vote sets the user's voting power to 0. + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[1], + 10000 + ); + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234) + ).to.revertedWith("Cannot vote so often"); + + await ethers.provider.send("evm_increaseTime", [ + setup.DAY.mul("10").toNumber(), + ]); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[1], + 0 + ); + + expect( + await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) + ).to.equal(setup.zero); + }); + + it("test_vote_multiple", async () => { + // Test if voting for multiple gauges sums up the user's voting power. + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 4000 + ); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[1], + 6000 + ); + + expect( + await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) + ).to.equal(BigNumber.from("10000")); + }); + + it("test_vote_no_balance", async () => { + // Test that users without locked tokens can't vote. + await expect( + setup.gaugeController + .connect(setup.accounts[1]) + .voteForGaugeWeights(setup.gaugesAddress[0], 10000) + ).to.revertedWith("Your token lock expires too soon"); + }); + + it("test_vote_expired", async () => { + // Test that users with expired token locks can't vote. + await ethers.provider.send("evm_increaseTime", [ + setup.YEAR.mul("2").toNumber(), + ]); + + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000) + ).to.revertedWith("Your token lock expires too soon"); + }); + + it("test_invalid_gauge_id", async () => { + // Test that voting with an invalid gauge ID throws an error. + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[2], 10000) + ).to.revertedWith("Gauge not added"); + }); + + it("test_over_user_weight", async () => { + // Test that voting with power exceeding the user's capacity throws an error. + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10001) + ).to.revertedWith("You used all your voting power"); + }); + + it("test_over_weight_multiple", async () => { + // Test that voting for multiple gauges with total power exceeding user's capacity throws an error. + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 8000 + ); + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 4000) + ).to.revertedWith("Used too much power"); + }); + + it("test_over_weight_adjust_existing", async () => { + // Test that adjusting an existing vote to exceed the user's total voting power throws an error. + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 6000 + ); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[1], + 3000 + ); + + await ethers.provider.send("evm_increaseTime", [ + setup.DAY.mul("10").toNumber(), + ]); + + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 8000) + ).to.revertedWith("Used too much power"); + }); + + it("test_exceeding_voting_power", async () => { + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 6000 + ); + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 5000) + ).to.revertedWith("Used too much power"); + }); + + it("test_emit_vote_event", async () => { + let now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + 1 + ); + await expect( + setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 5000) + ) + .to.emit(setup.gaugeController, "VoteForGauge") + .withArgs(now, setup.accountsAddress[0], setup.gaugesAddress[0], 5000); + }); + }); +}); diff --git a/test/fork/GaugeController/voteWeightUnitary.test.ts b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts similarity index 77% rename from test/fork/GaugeController/voteWeightUnitary.test.ts rename to test/fork/unitary/GaugeController/voteWeightUnitary.test.ts index effd4a49..5b13887c 100644 --- a/test/fork/GaugeController/voteWeightUnitary.test.ts +++ b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../helper"; +import { EVMUtils, TestSetup } from "../../helper"; describe("GaugeController", function () { let setup: TestSetup; @@ -27,7 +27,10 @@ describe("GaugeController", function () { describe("test_vote_weight_unitary", function () { it("test_no_immediate_effect_on_weight", async () => { - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 10000 + ); expect( await setup.gaugeController.gaugeRelativeWeight( setup.gaugesAddress[0], @@ -39,12 +42,20 @@ describe("GaugeController", function () { it("test_remove_vote_no_immediate_effect", async () => { // ゲージに対して投票を行った後、時間を進めてそのゲージのチェックポイントを取ります。 // その後、投票を取り消して、投票の取り消しは即座には影響しないことを確認します。 - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 10000 + ); - await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); + await ethers.provider.send("evm_increaseTime", [ + setup.DAY.mul("10").toNumber(), + ]); await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 0); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 0 + ); expect( await setup.gaugeController.gaugeRelativeWeight( @@ -56,7 +67,10 @@ describe("GaugeController", function () { it("test_effect_on_following_period", async () => { // ゲージに対して投票を行い、一週間時間を進めた後にそのゲージの重みが変わっていることを確認します。 - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 10000 + ); await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); @@ -71,8 +85,13 @@ describe("GaugeController", function () { it("test_remove_vote_means_no_weight", async () => { // ゲージに対して投票を行い、時間を進めてそのゲージのチェックポイントを取ります。 // 投票を取り消し、さらに一週間時間を進めた後、そのゲージの重みが0になっていることを確認します。 - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000); - await ethers.provider.send("evm_increaseTime", [setup.DAY.mul("10").toNumber()]); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 10000 + ); + await ethers.provider.send("evm_increaseTime", [ + setup.DAY.mul("10").toNumber(), + ]); await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); expect( @@ -82,7 +101,10 @@ describe("GaugeController", function () { ) ).to.equal(setup.ten_to_the_18); - await setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 0); + await setup.gaugeController.voteForGaugeWeights( + setup.gaugesAddress[0], + 0 + ); await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); @@ -95,4 +117,3 @@ describe("GaugeController", function () { }); }); }); - diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts new file mode 100644 index 00000000..13a92223 --- /dev/null +++ b/test/fork/unitary/Minter/Minter.test.ts @@ -0,0 +1,420 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, TestSetup } from "../../helper"; + +describe("Minter", function () { + let setup: TestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new TestSetup(); + await setup.setup(); + await setup.addType(); + await setup.addGauge(); + await setup.createLP(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("Minter Behavior", function () { + // Test basic mint functionality + it("test mint", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); //gauge_address, msg.sender, mint + let expected = await setup.gaugesContracts[0].integrateFraction( + setup.accountsAddress[1] + ); + + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expected + ); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(expected); + }); + + // Test minting immediately after setup + it("test mint immediate", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + let t0 = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + let moment = t0.add(setup.WEEK).div(setup.WEEK).mul(setup.WEEK).add("5"); + await ethers.provider.send("evm_setNextBlockTimestamp", [ + moment.toNumber(), + ]); + + //mint + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal("0"); + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + + //check + let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(balance); + }); + + // Test multiple mint operations on the same gauge + it("test mint multiple same gauge", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + let expected = await setup.gaugesContracts[0].integrateFraction( + setup.accountsAddress[1] + ); + let final_balance = await setup.token.balanceOf(setup.accountsAddress[1]); + + expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success + expect(final_balance).to.equal(expected); //2nd mint works fine + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(expected); //tracks fine + }); + + // Test minting across multiple gauges + it("test mint multiple gauges", async () => { + //setup + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[1] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[2] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + //mint + for (let i = 0; i < 3; i++) { + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[i]); + } + + //check + let total_minted = BigNumber.from("0"); + + for (let i = 0; i < 3; i++) { + let gauge = setup.gaugesContracts[i]; + let minted = await setup.minter.minted( + setup.accountsAddress[1], + gauge.address + ); + expect(minted).to.equal( + await gauge.integrateFraction(setup.accountsAddress[1]) + ); + total_minted = total_minted.add(minted); + } + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + total_minted + ); + }); + + // Test minting after withdrawing + it("test mint after withdraw", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [ + setup.WEEK.mul(BigNumber.from("2")).toNumber(), + ]); + + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .withdraw(setup.ten_to_the_18); + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + + expect( + (await setup.token.balanceOf(setup.accountsAddress[1])).gt( + BigNumber.from("0") + ) + ).to.equal(true); + }); + + // Test multiple mints after withdrawing + it("test mint multiple after withdraw", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [10]); + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .withdraw(setup.ten_to_the_18); + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + + let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [10]); + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + balance + ); + }); + + // Test mint without any deposit + it("test no deposit", async () => { + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + setup.zero + ); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(setup.zero); + }); + + // Test minting with the wrong gauge + it("test mint wrong gauge", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[1]); + + //check + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + setup.zero + ); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(setup.zero); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[1] + ) + ).to.equal(setup.zero); + }); + + // Test minting with an invalid gauge address + it("test mint not a gauge", async () => { + await expect(setup.minter.mint(setup.accountsAddress[1])).to.revertedWith( + "dev: gauge is not added" + ); + }); + + // Test minting before inflation begins + it("test mint before inflation begins", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + expect(await setup.token.miningEpoch()).to.equal(BigNumber.from("-1")); + + await setup.minter + .connect(setup.accounts[1]) + .mint(setup.gaugesAddress[0]); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + BigNumber.from("0") + ); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(setup.zero); + }); + + // Test mintMany function with multiple gauges + it("test mintMany function multiple gauges", async () => { + //setup + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[1] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.gaugesContracts[2] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + let addresses = [ + setup.gaugesAddress[0], + setup.gaugesAddress[1], + setup.gaugesAddress[2], + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + setup.ZERO_ADDRESS, + ]; + await setup.minter.connect(setup.accounts[1]).mintMany(addresses); + + //check + let total_minted = BigNumber.from("0"); + + for (let i = 0; i < 3; i++) { + let gauge = setup.gaugesContracts[i]; + let minted = await setup.minter.minted( + setup.accountsAddress[1], + gauge.address + ); + expect(minted).to.equal( + await gauge.integrateFraction(setup.accountsAddress[1]) + ); + total_minted = total_minted.add(minted); + } + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + total_minted + ); + }); + + // Test toggling of the mint approval function + it("test toggleApproveMint function", async () => { + await setup.minter + .connect(setup.accounts[1]) + .toggleApproveMint(setup.accountsAddress[2]); + expect( + await setup.minter.allowedToMintFor( + setup.accountsAddress[2], + setup.accountsAddress[1] + ) + ).to.equal(true); + + await setup.minter + .connect(setup.accounts[1]) + .toggleApproveMint(setup.accountsAddress[2]); + expect( + await setup.minter.allowedToMintFor( + setup.accountsAddress[2], + setup.accountsAddress[1] + ) + ).to.equal(false); + }); + + // Test minting on behalf of another user + it("test mintFor function", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + await setup.minter + .connect(setup.accounts[1]) + .toggleApproveMint(setup.accountsAddress[2]); + expect( + await setup.minter.allowedToMintFor( + setup.accountsAddress[2], + setup.accountsAddress[1] + ) + ).to.equal(true); + + await setup.minter + .connect(setup.accounts[2]) + .mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); + + let expected = await setup.gaugesContracts[0].integrateFraction( + setup.accountsAddress[1] + ); + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expected + ); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(expected); + }); + + // Test mintFor function when not approved + it("test mintForFail function", async () => { + await setup.gaugesContracts[0] + .connect(setup.accounts[1]) + .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + + await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + + expect( + await setup.minter.allowedToMintFor( + setup.accountsAddress[2], + setup.accountsAddress[1] + ) + ).to.equal(false); + + await setup.minter + .connect(setup.accounts[2]) + .mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); + + expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(0); + expect( + await setup.minter.minted( + setup.accountsAddress[1], + setup.gaugesAddress[0] + ) + ).to.equal(0); + }); + }); +}); From 59c4664bd597ca5c8de8b90680fc960843e32dc0 Mon Sep 17 00:00:00 2001 From: takadr Date: Tue, 31 Oct 2023 15:35:04 +0900 Subject: [PATCH 022/412] Added tests for VotingEscrow --- contracts/curveFork/VotingEscrow.sol | 65 +- .../depositWithdrawVoting.test.ts | 336 +++++++++++ .../VotingEscrow/votingEscrow.test.ts | 567 ++++++++++++++++++ .../zeroBalanceAtUnlockTime.test.ts | 116 ++++ .../VotingEscrow/votingescrowAdmin.test.ts | 76 +++ 5 files changed, 1136 insertions(+), 24 deletions(-) create mode 100644 test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts create mode 100644 test/fork/integration/VotingEscrow/votingEscrow.test.ts create mode 100644 test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts create mode 100644 test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 95425e7d..2e59a6c9 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -266,14 +266,25 @@ contract VotingEscrow is ReentrancyGuard { blk: block.number }); if (_epoch > 0) { - _lastPoint = pointHistory[_epoch]; + _lastPoint = Point({ + bias: pointHistory[_epoch].bias, + slope: pointHistory[_epoch].slope, + ts: pointHistory[_epoch].ts, + blk: pointHistory[_epoch].blk + }); } uint256 _lastCheckpoint = _lastPoint.ts; // initial_last_point is used for extrapolation to calculate block number // (approximately, for *At methods) and save them // as we cannot figure that out exactly from inside the contract - Point memory _initialLastPoint = _lastPoint; + // Point memory _initialLastPoint = _lastPoint; + Point memory _initialLastPoint = Point({ + bias: _lastPoint.bias, + slope: _lastPoint.slope, + ts: _lastPoint.ts, + blk: _lastPoint.blk + }); uint256 _blockSlope = 0; if (block.timestamp > _lastPoint.ts) { _blockSlope = @@ -326,7 +337,12 @@ contract VotingEscrow is ReentrancyGuard { _lastPoint.blk = block.number; break; } else { - pointHistory[_epoch] = _lastPoint; + pointHistory[_epoch] = Point({ + bias: _lastPoint.bias, + slope: _lastPoint.slope, + ts: _lastPoint.ts, + blk: _lastPoint.blk + }); } unchecked { ++i; @@ -704,32 +720,33 @@ contract VotingEscrow is ReentrancyGuard { } } - Point memory upoint = userPointHistory[addr_][min_]; - uint256 maxEpoch = epoch; - uint256 epoch_ = findBlockEpoch(block_, maxEpoch); - Point memory point0 = pointHistory[epoch_]; - uint256 dBlock = 0; - uint256 dt = 0; - - if (epoch_ < maxEpoch) { - Point memory point1 = pointHistory[epoch_ + 1]; - dBlock = point1.blk - point0.blk; - dt = point1.ts - point0.ts; + Point memory _upoint = userPointHistory[addr_][min_]; + uint256 _maxEpoch = epoch; + uint256 _epoch = findBlockEpoch(block_, _maxEpoch); + Point memory _point0 = pointHistory[_epoch]; + // revert(_epoch.uintToString()); + uint256 _dBlock = 0; + uint256 _dt = 0; + + if (_epoch < _maxEpoch) { + Point memory _point1 = pointHistory[_epoch + 1]; + _dBlock = _point1.blk - _point0.blk; + _dt = _point1.ts - _point0.ts; } else { - dBlock = block.number - point0.blk; - dt = block.timestamp - point0.ts; + _dBlock = block.number - _point0.blk; + _dt = block.timestamp - _point0.ts; } - uint256 blockTime = point0.ts; - if (dBlock != 0) { - blockTime += (dt * (block_ - point0.blk)) / dBlock; + uint256 _blockTime = _point0.ts; + if (_dBlock != 0) { + _blockTime += _dt * (block_ - _point0.blk) / _dBlock; } - upoint.bias -= - upoint.slope * - int128(int256(blockTime) - int256(upoint.ts)); - if (upoint.bias >= 0) { - return uint256(int256(upoint.bias)); + _upoint.bias -= + _upoint.slope * + int128(int256(_blockTime) - int256(_upoint.ts)); + if (_upoint.bias >= 0) { + return uint256(int256(_upoint.bias)); } else { return 0; } diff --git a/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts b/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts new file mode 100644 index 00000000..b98d2654 --- /dev/null +++ b/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts @@ -0,0 +1,336 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +const ACCOUNT_NUM = 10; +const NUMBER_OF_ATTEMPTS = 30; // テストの試行回数 +const YEAR = BigNumber.from(86400 * 365); +const WEEK = BigNumber.from(86400 * 7); +const two_to_the_256_minus_1 = BigNumber.from("2") + .pow(BigNumber.from("256")) + .sub(BigNumber.from("1")); +const ten_to_the_40 = BigNumber.from( + "10000000000000000000000000000000000000000" +); + +describe("VotingEscrow", function () { + let accounts: SignerWithAddress[]; + let votingEscrow: Contract; + let token: Contract; + + let stAccountN: number; + let stAccount: SignerWithAddress; + let stValue: BigNumber = BigNumber.from("0"); + let stLockDuration: BigNumber; + let votingBalances: { [key: string]: BigNumber }[]; + let unlockTime: BigNumber; + + let snapshot: SnapshotRestorer; + + before(async () => { + snapshot = await takeSnapshot(); + accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); + + const Token = await ethers.getContractFactory("MockToken"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await Token.deploy("Test Token", "TST", 18); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + //init + for (let i = 0; i < ACCOUNT_NUM; i++) { + await token + .connect(accounts[i]) + ._mintForTesting(accounts[i].address, ten_to_the_40); + await token + .connect(accounts[i]) + .approve(votingEscrow.address, two_to_the_256_minus_1); + } + + //setup + votingBalances = new Array(ACCOUNT_NUM).fill({ + value: BigNumber.from("0"), + unlockTime: BigNumber.from("0"), + }); + }); + + after(async () => { + await snapshot.restore(); + }); + + //--------------------------------------------- functions -----------------------------------------------------------// + + function rdmValue(a): BigNumber { + let rdm = BigNumber.from(Math.floor(Math.random() * a).toString()); + return rdm; + } + + //--------------------------------------------- randomly excuted functions -----------------------------------------------------------// + async function ruleCreateLock() { + console.log("ruleCreateLock"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + //stValue + stValue = rdmValue(9007199254740991); + + //number of weeks to lock a deposit + stLockDuration = rdmValue(255); //uint8.max + + let timestamp = BigNumber.from(await time.latest()); + unlockTime = timestamp.add(WEEK.mul(stLockDuration)).div(WEEK).mul(WEEK); + + if (stValue.eq(0)) { + console.log("--revert: 1"); + await expect( + votingEscrow.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("need non-zero value"); + } else if (votingBalances[stAccountN]["value"].gt("0")) { + console.log("--revert: 2"); + await expect( + votingEscrow.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("Withdraw old tokens first"); + } else if (unlockTime.lte(timestamp)) { + console.log("--revert: 3"); + await expect( + votingEscrow.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("Can only lock until time in the future"); + } else if (unlockTime.gte(timestamp.add(YEAR.mul("4")))) { + console.log("--revert: 4"); + await expect( + votingEscrow.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("Voting lock can be 4 years max"); + } else { + console.log("--success, account:", stAccountN); + const tx = await votingEscrow + .connect(stAccount) + .createLock(stValue, unlockTime); + const receipt = await tx.wait(); + votingBalances[stAccountN] = { + value: stValue, + unlockTime: receipt.events[1]["args"]["locktime"], + }; + } + } + + async function ruleIncreaseAmount() { + console.log("ruleIncreaseAmount"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + //stValue + stValue = rdmValue(9007199254740991); + + let timestamp = BigNumber.from(await time.latest()); + + if (stValue.eq(0)) { + console.log("--revert: 1"); + await expect( + votingEscrow.connect(stAccount).increaseAmount(stValue) + ).to.revertedWith("dev: need non-zero value"); + } else if (votingBalances[stAccountN]["value"].eq("0")) { + console.log("--revert: 2"); + await expect( + votingEscrow.connect(stAccount).increaseAmount(stValue) + ).to.revertedWith("No existing lock found"); + } else if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { + console.log("--revert: 3"); + await expect( + votingEscrow.connect(stAccount).increaseAmount(stValue) + ).to.revertedWith("Cannot add to expired lock. Withdraw"); + } else { + await votingEscrow.connect(stAccount).increaseAmount(stValue); + votingBalances[stAccountN]["value"] = + votingBalances[stAccountN]["value"].add(stValue); + + console.log( + "--success, account:", + stAccountN, + "new balance:", + votingBalances[stAccountN]["value"].toString() + ); + } + } + + async function ruleIncreaseUnlockTime() { + console.log("ruleIncreaseUnlockTime"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + //unlockTime + let timestamp = BigNumber.from(await time.latest()); + stLockDuration = rdmValue(255); //number of weeks + let unlockTime = timestamp + .add(stLockDuration.mul(WEEK)) + .div(WEEK) + .mul(WEEK); + + if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { + console.log("--revert: 1"); + await expect( + votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Lock expired"); + } else if (votingBalances[stAccountN]["value"].eq("0")) { + console.log("--revert: 2"); + await expect( + votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Nothing is locked"); + } else if (votingBalances[stAccountN]["unlockTime"].gte(unlockTime)) { + console.log("--revert: 3"); + await expect( + votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Can only increase lock duration"); + } else if (unlockTime.gt(timestamp.add(YEAR.mul("4")))) { + console.log("--revert: 4"); + await expect( + votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Voting lock can be 4 years max"); + } else { + console.log("--success, account:", stAccountN); + const tx = await votingEscrow + .connect(stAccount) + .increaseUnlockTime(unlockTime); + const receipt = await tx.wait(); + votingBalances[stAccountN]["unlockTime"] = + receipt.events[0]["args"]["locktime"]; + } + } + + async function ruleWithdraw() { + console.log("ruleWithdraw"); + // Withdraw tokens from the voting escrow. + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + let timestamp = BigNumber.from(await time.latest()); + + if (votingBalances[stAccountN]["unlockTime"].gt(timestamp)) { + console.log("--reverted"); + await expect(votingEscrow.connect(stAccount).withdraw()).to.revertedWith( + "The lock didn't expire" + ); + } else { + console.log("--success, account:", stAccountN); + await votingEscrow.connect(stAccount).withdraw(); + votingBalances[stAccountN]["value"] = BigNumber.from("0"); + } + } + + async function ruleCheckpoint() { + console.log("ruleCheckpoint"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + await votingEscrow.connect(stAccount).checkpoint(); + } + + async function ruleAdvanceTime() { + console.log("ruleAdvanceTime"); + + let stSleepDuration = Math.floor(Math.random() * 3) + 1; //1~4 + + await time.increase(WEEK.mul(stSleepDuration).toNumber()); + + if (stSleepDuration == 1) { + console.log("Time advanced"); + } else { + console.log("Time advanced"); + } + } + + let func = [ + "ruleCreateLock", + "ruleIncreaseAmount", + "ruleIncreaseUnlockTime", + "ruleWithdraw", + "ruleCheckpoint", + "ruleAdvanceTime", + ]; + + describe("test_deposit_withdraw_voting", function () { + //set arbitral number of repeats + for (let x = 0; x < NUMBER_OF_ATTEMPTS; x++) { + it("try " + eval("x+1"), async () => { + for (let i = 0; i < 100; i++) { + let n = (await rdmValue(func.length)).toNumber(); + await eval(func[n])(); + } + }); + } + }); + + afterEach(async () => { + console.log("=====Invariant checks====="); + + console.log("invariant_token_balances"); + for (let i = 0; i < ACCOUNT_NUM; i++) { + expect(await token.balanceOf(accounts[i].address)).to.equal( + ten_to_the_40.sub(votingBalances[i]["value"]) + ); + } + + console.log("invariant_escrow_current_balances"); + let total_supply = BigNumber.from("0"); + let timestamp = BigNumber.from(await time.latest()); + + for (let i = 0; i < ACCOUNT_NUM; i++) { + let data = votingBalances[i]; + + let balance = await votingEscrow["balanceOf(address)"]( + accounts[i].address + ); + total_supply = total_supply.add(balance); + + if (data["unlockTime"].gt(timestamp) && data["value"].div(YEAR).gt("0")) { + expect(balance.isZero()).to.equal(false); + } else if (data["value"].isZero() || data["unlockTime"].lte(timestamp)) { + expect(balance.isZero()).to.equal(true); + } + } + expect(await votingEscrow["totalSupply()"]()).to.equal(total_supply); + + console.log("invariant_historic_balances"); + total_supply = BigNumber.from("0"); + let blocknumber = (await time.latestBlock()) - 4; + console.log(blocknumber); + + for (let i = 0; i < ACCOUNT_NUM; i++) { + total_supply = total_supply.add( + await votingEscrow.balanceOfAt(accounts[i].address, blocknumber) + ); + } + + expect(await votingEscrow.totalSupplyAt(blocknumber)).to.equal( + total_supply + ); + }); +}); diff --git a/test/fork/integration/VotingEscrow/votingEscrow.test.ts b/test/fork/integration/VotingEscrow/votingEscrow.test.ts new file mode 100644 index 00000000..468075f2 --- /dev/null +++ b/test/fork/integration/VotingEscrow/votingEscrow.test.ts @@ -0,0 +1,567 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +type Stage = { + blockNumber: number; + timestamp: number; + bias?: string; +}; + +const H = 3600; +const DAY = 86400; +const WEEK = 7 * DAY; +const MAXTIME = DAY * 365 * 4; // 126144000 +const SCALE = 1e20; +const TOL = (120 / WEEK) * SCALE; + +describe("Voting Powers Test", function () { + // Test voting power in the following scenario. + // Alice: + // ~~~~~~~ + // ^ + // | * * + // | | \ | \ + // | | \ | \ + // +-+---+---+------+---> t + + // Bob: + // ~~~~~~~ + // ^ + // | * + // | | \ + // | | \ + // +-+---+---+---+--+---> t + + // Alice has 100% of voting power in the first period. + // She has 2/3 power at the start of 2nd period, with Bob having 1/2 power + // (due to smaller locktime). + // Alice's power grows to 100% by Bob's unlock. + + // Checking that totalSupply is appropriate. + + // After the test is done, check all over again with balanceOfAt / totalSupplyAt + let alice, bob: SignerWithAddress; + let votingEscrow: Contract; + let token: Contract; + let t0: number; + let wTotal, wAlice, wBob: BigNumber; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + const CRV = await ethers.getContractFactory("CRV"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + [alice, bob] = await ethers.getSigners(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + it("test voting powers", async function () { + const amount: BigNumber = ethers.utils.parseEther("1000"); + await token.connect(alice).transfer(bob.address, amount); + const stages: { [key: string]: Stage | Stage[] } = {}; + + await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); + await token.connect(bob).approve(votingEscrow.address, amount.mul(10)); + + expect(await votingEscrow["totalSupply()"]()).to.equal(0); + expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + + const timeToNextWeek = + (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - + (await time.latest()); + + // Move to timing which is good for testing - beginning of a UTC week + await time.increase(timeToNextWeek); + + await ethers.provider.send("evm_increaseTime", [H]); + + stages["before_deposits"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + await votingEscrow + .connect(alice) + .createLock(amount, (await time.latest()) + WEEK); + + stages["alice_deposit"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("alice_deposit", stages["alice_deposit"]); + + await time.increase(H); + + const totalSupply = await votingEscrow["totalSupply()"](); + const aliceBalance = await votingEscrow["balanceOf(address)"]( + alice.address + ); + + expect(approx(totalSupply, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to + .be.true; + expect(approx(aliceBalance, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to + .be.true; + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + + t0 = await time.latest(); + + stages["alice_in_0"] = []; + stages["alice_in_0"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }); + + // console.log("alice_in_0", stages["alice_in_0"]); + + // Simulating the passage of time with 7 days and 24 hours per day + for (let i = 0; i < 7; i++) { + for (let _ = 0; _ < 24; _++) { + await time.increase(H); + } + + const dt = (await time.latest()) - t0; + const totalSupply = await votingEscrow["totalSupply()"](); + const aliceBalance = await votingEscrow["balanceOf(address)"]( + alice.address + ); + + expect( + approx( + totalSupply, + amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), + TOL + ) + ).to.be.true; + expect( + approx( + aliceBalance, + amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), + TOL + ) + ).to.be.true; + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + + stages["alice_in_0"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + bias: aliceBalance.toString(), + }); + } + // console.log("alice_in_0", stages["alice_in_0"]); + await ethers.provider.send("evm_increaseTime", [H]); + + expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); + await votingEscrow.connect(alice).withdraw(); + stages["alice_withdraw"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("alice_withdraw", stages["alice_withdraw"]); + + expect(await votingEscrow["totalSupply()"]()).to.equal(0); + expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + + await time.increase(H); + + // Calculate the next week for round counting + const nextWeek = + (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - + (await time.latest()); + + await time.increase(nextWeek); + + await votingEscrow + .connect(alice) + .createLock(amount, (await time.latest()) + 2 * WEEK); + + stages["alice_deposit_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("alice_deposit_2", stages["alice_deposit_2"]); + + expect( + approx( + await votingEscrow["totalSupply()"](), + amount.div(MAXTIME).mul(2 * WEEK), + TOL + ) + ).to.be.true; + expect( + approx( + await votingEscrow["balanceOf(address)"](alice.address), + amount.div(MAXTIME).mul(2 * WEEK), + TOL + ) + ).to.be.true; + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + + await votingEscrow + .connect(bob) + .createLock(amount, (await time.latest()) + WEEK); + + stages["bob_deposit_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("bob_deposit_2", stages["bob_deposit_2"]); + + expect( + approx( + await votingEscrow["totalSupply()"](), + amount.div(MAXTIME).mul(3 * WEEK), + TOL + ) + ).to.be.true; + expect( + approx( + await votingEscrow["balanceOf(address)"](alice.address), + amount.div(MAXTIME).mul(2 * WEEK), + TOL + ) + ).to.be.true; + expect( + approx( + await votingEscrow["balanceOf(address)"](bob.address), + amount.div(MAXTIME).mul(WEEK), + TOL + ) + ).to.be.true; + + t0 = await time.latest(); + await time.increase(H); + + stages["alice_bob_in_2"] = []; + // Beginning of week: weight 3 + // End of week: weight 1 + for (let i = 0; i < 7; i++) { + for (let _ = 0; _ < 24; _++) { + await time.increase(H); + } + const dt = (await time.latest()) - t0; + wTotal = await votingEscrow["totalSupply()"](); + wAlice = await votingEscrow["balanceOf(address)"](alice.address); + wBob = await votingEscrow["balanceOf(address)"](bob.address); + expect(wTotal).to.equal(wAlice.add(wBob)); + expect( + approx(wAlice, amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), TOL) + ).to.be.true; + expect(approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), TOL)) + .to.be.true; + + stages["alice_bob_in_2"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + bias: `${wAlice.toString()}, ${wBob.toString()}`, + }); + } + // console.log("alice_bob_in_2", stages["alice_bob_in_2"]); + + await time.increase(H); + + await votingEscrow.connect(bob).withdraw(); + t0 = await time.latest(); + stages["bob_withdraw_1"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + const wTotal1 = await votingEscrow["totalSupply()"](); + const wAlice1 = await votingEscrow["balanceOf(address)"](alice.address); + expect(wAlice1).to.equal(wTotal1); + expect(approx(wTotal1, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be + .true; + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + + await time.increase(H); + + stages["alice_in_2"] = []; + for (let i = 0; i < 7; i++) { + for (let _ = 0; _ < 24; _++) { + await time.increase(H); + } + const dt = (await time.latest()) - t0; + wTotal = await votingEscrow["totalSupply()"](); + wAlice = await votingEscrow["balanceOf(address)"](alice.address); + expect(wTotal).to.equal(wAlice); + expect( + approx( + wTotal, + amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), + TOL + ) + ).to.be.true; + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + stages["alice_in_2"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }); + } + // console.log("alice_in_2", stages["alice_in_2"]); + + await votingEscrow.connect(alice).withdraw(); + stages["alice_withdraw_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + await time.increase(H); + + await votingEscrow.connect(bob).withdraw(); + stages["bob_withdraw_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + // console.log("bob_withdraw_2", stages["bob_withdraw_2"]); + + expect(await votingEscrow["totalSupply()"]()).to.equal(0); + expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); + expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); + + // Now test historical balanceOfAt and others + expect( + await votingEscrow.balanceOfAt( + alice.address, + stages["before_deposits"].blockNumber + ) + ).to.equal(0); + expect( + await votingEscrow.balanceOfAt( + bob.address, + stages["before_deposits"].blockNumber + ) + ).to.equal(0); + expect( + await votingEscrow.totalSupplyAt(stages["before_deposits"].blockNumber) + ).to.equal(0); + + wAlice = await votingEscrow.balanceOfAt( + alice.address, + stages["alice_deposit"].blockNumber + ); + + expect(approx(wAlice, amount.div(MAXTIME).mul(WEEK - H), TOL)).to.be.true; + expect( + await votingEscrow.balanceOfAt( + bob.address, + stages["alice_deposit"].blockNumber + ) + ).to.equal(0); + wTotal = await votingEscrow.totalSupplyAt( + stages["alice_deposit"].blockNumber + ); + expect(wAlice).to.equal(wTotal); + + for (let i = 0; i < stages["alice_in_0"].length; i++) { + const block = stages["alice_in_0"][i].blockNumber; + wAlice = await votingEscrow.balanceOfAt(alice.address, block); + wBob = await votingEscrow.balanceOfAt(bob.address, block); + wTotal = await votingEscrow.totalSupplyAt(block); + expect(wBob).to.equal(0); + expect(wAlice).to.equal(wTotal); + const timeLeft = Math.floor((WEEK * (7 - i)) / 7) - 2 * H; + const error1h = (H / timeLeft) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + expect(approx(wAlice, amount.div(MAXTIME).mul(timeLeft), error1h)).to.be + .true; + } + + wTotal = await votingEscrow.totalSupplyAt( + stages["alice_withdraw"].blockNumber + ); + wAlice = await votingEscrow.balanceOfAt( + alice.address, + stages["alice_withdraw"].blockNumber + ); + wBob = await votingEscrow.balanceOfAt( + bob.address, + stages["alice_withdraw"].blockNumber + ); + expect(wAlice).to.equal(wBob); + expect(wAlice).to.equal(wTotal); + expect(wTotal).to.equal(0); + + wTotal = await votingEscrow.totalSupplyAt( + stages["alice_deposit_2"].blockNumber + ); + wAlice = await votingEscrow.balanceOfAt( + alice.address, + stages["alice_deposit_2"].blockNumber + ); + expect(approx(wTotal, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; + expect(wTotal).to.equal(wAlice); + expect( + await votingEscrow.balanceOfAt( + bob.address, + stages["alice_deposit_2"].blockNumber + ) + ).to.equal(0); + + wTotal = await votingEscrow.totalSupplyAt( + stages["bob_deposit_2"].blockNumber + ); + wAlice = await votingEscrow.balanceOfAt( + alice.address, + stages["bob_deposit_2"].blockNumber + ); + wBob = await votingEscrow.balanceOfAt( + bob.address, + stages["bob_deposit_2"].blockNumber + ); + expect(wTotal).to.equal(wAlice.add(wBob)); + expect(approx(wTotal, amount.div(MAXTIME).mul(3 * WEEK), TOL)).to.be.true; + expect(approx(wAlice, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; + + t0 = stages["bob_deposit_2"].timestamp; + for (let i = 0; i < stages["alice_bob_in_2"].length; i++) { + const block = stages["alice_bob_in_2"][i].blockNumber; + wAlice = await votingEscrow.balanceOfAt(alice.address, block); + wBob = await votingEscrow.balanceOfAt(bob.address, block); + wTotal = await votingEscrow.totalSupplyAt(block); + expect(wTotal).to.equal(wAlice.add(wBob)); + const dt = stages["alice_bob_in_2"][i].timestamp - t0; + const error1h = (H / (2 * WEEK - i * DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + expect( + approx( + wAlice, + amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), + error1h + ) + ).to.be.true; + expect( + approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), error1h) + ).to.be.true; + } + + wTotal = await votingEscrow.totalSupplyAt( + stages["bob_withdraw_1"].blockNumber + ); + wAlice = await votingEscrow.balanceOfAt( + alice.address, + stages["bob_withdraw_1"].blockNumber + ); + wBob = await votingEscrow.balanceOfAt( + bob.address, + stages["bob_withdraw_1"].blockNumber + ); + expect(wTotal).to.equal(wAlice); + expect(approx(wTotal, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be + .true; + expect(wBob).to.equal(0); + + t0 = stages["bob_withdraw_1"].timestamp; + for (let i = 0; i < stages["alice_in_2"].length; i++) { + const block = stages["alice_in_2"][i].blockNumber; + wAlice = await votingEscrow.balanceOfAt(alice.address, block); + const wBob = await votingEscrow.balanceOfAt(bob.address, block); + wTotal = await votingEscrow.totalSupplyAt(block); + expect(wTotal).to.equal(wAlice); + expect(wBob).to.equal(0); + const dt = stages["alice_in_2"][i].timestamp - t0; + const error1h = (H / (WEEK - i * DAY + DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + + expect( + approx( + wTotal, + amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), + error1h + ) + ).to.be.true; + } + + wTotal = await votingEscrow.totalSupplyAt( + stages["bob_withdraw_2"].blockNumber + ); + wAlice = await votingEscrow.balanceOfAt( + alice.address, + stages["bob_withdraw_2"].blockNumber + ); + const wBob4 = await votingEscrow.balanceOfAt( + bob.address, + stages["bob_withdraw_2"].blockNumber + ); + expect(wTotal).to.equal(0); + expect(wAlice).to.equal(0); + expect(wBob4).to.equal(0); + + await showStats(); + }); + + function approx(value: BigNumber, target: BigNumber, tol: number) { + if (value.isZero() && target.isZero()) { + return true; + } + + const diff = value.sub(target).abs(); + const sum = value.add(target); + const ratio = diff.mul(2).mul(BigNumber.from(SCALE.toString())).div(sum); + + return ratio.lte(BigNumber.from(tol.toString())); + } + + async function showStats() { + const latestBlock = await time.latestBlock(); + for (let i = 2; i < latestBlock; i++) { + let a = await votingEscrow.balanceOfAt(alice.address, `${i}`); + let b = await votingEscrow.balanceOfAt(bob.address, `${i}`); + console.log(`Block ${i}: a: ${a.toString()} b: ${b.toString()}`); + } + const epoch = (await votingEscrow.epoch()).toNumber(); + const aliceEpoch = ( + await votingEscrow.userPointEpoch(alice.address) + ).toNumber(); + const bobEpoch = ( + await votingEscrow.userPointEpoch(bob.address) + ).toNumber(); + for (let i = 0; i <= epoch; i++) { + let p = await votingEscrow.pointHistory(i); + console.log( + `epoch: ${i}: bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + ); + } + for (let i = 0; i <= aliceEpoch; i++) { + let p = await votingEscrow.userPointHistory(alice.address, i); + let balanceAt = p.blk.isZero() + ? 0 + : await votingEscrow.balanceOfAt(alice.address, p.blk.toNumber()); + console.log( + `alice epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + ); + } + for (let i = 0; i < bobEpoch; i++) { + let p = await votingEscrow.userPointHistory(bob.address, i); + let balanceAt = p.blk.isZero() + ? 0 + : await votingEscrow.balanceOfAt(bob.address, p.blk.toNumber()); + console.log( + `bob epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + ); + } + } +}); diff --git a/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts b/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts new file mode 100644 index 00000000..f2a77509 --- /dev/null +++ b/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts @@ -0,0 +1,116 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +const WEEK = 86400 * 7; + +describe("Voting Escrow tests", function () { + let accounts: SignerWithAddress[]; + let votingEscrow: Contract; + let token: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async () => { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + + const CRV = await ethers.getContractFactory("CRV"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + await token + .connect(accounts[0]) + .approve(votingEscrow.address, ethers.utils.parseEther("1")); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + for ( + let st_initial = WEEK * 2; + st_initial <= WEEK * 52; + st_initial += WEEK * 2 + ) { + it(`create lock with zero balance: Lock ${Math.floor( + st_initial / WEEK + )} Week`, async () => { + const expectedUnlock = (await time.latest()) + st_initial; + await votingEscrow + .connect(accounts[0]) + .createLock(ethers.utils.parseEther("1"), expectedUnlock); + + const actualUnlock = (await votingEscrow.locked(accounts[0].address))[1]; + + await time.increase(actualUnlock - (await time.latest()) - 5); + + expect( + await votingEscrow["balanceOf(address)"](accounts[0].address) + ).to.not.equal(0); + + await time.increase(10); + + expect( + await votingEscrow["balanceOf(address)"](accounts[0].address) + ).to.equal(0); + }); + } + + for ( + let st_initial = WEEK * 2; + st_initial <= WEEK * 52; + st_initial += WEEK * 2 + ) { + for (let st_extend = WEEK; st_extend <= WEEK * 2; st_extend += WEEK) { + it(`increase unlock with zero balance: Lock ${Math.floor( + st_initial / WEEK + )} Week, extend: ${Math.floor(st_extend / WEEK)} Week`, async () => { + await votingEscrow + .connect(accounts[0]) + .createLock( + ethers.utils.parseEther("1"), + (await time.latest()) + st_initial + ); + + const initialUnlock = ( + await votingEscrow.locked(accounts[0].address) + )[1].toNumber(); + const extendedExpectedUnlock = initialUnlock + st_extend; + + await votingEscrow.increaseUnlockTime(extendedExpectedUnlock); + + const extendedActualUnlock = ( + await votingEscrow.locked(accounts[0].address) + )[1]; + + await time.increase(extendedActualUnlock - (await time.latest()) - 5); + + expect( + await votingEscrow["balanceOf(address)"](accounts[0].address) + ).to.not.equal(0); + + await time.increase(10); + + expect( + await votingEscrow["balanceOf(address)"](accounts[0].address) + ).to.equal(0); + }); + } + } +}); diff --git a/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts b/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts new file mode 100644 index 00000000..f0cfbab0 --- /dev/null +++ b/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts @@ -0,0 +1,76 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +describe("VotingEscrow", () => { + let votingEscrow: Contract; + let token: Contract; + let accounts: SignerWithAddress[]; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + const CRV = await ethers.getContractFactory("CRV"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + accounts = await ethers.getSigners(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + it("test_commit_admin_only", async function () { + await expect( + votingEscrow + .connect(accounts[1]) + .commitTransferOwnership(accounts[1].address) + ).to.be.revertedWith("admin only"); + }); + + it("test_apply_admin_only", async function () { + await expect( + votingEscrow.connect(accounts[1]).applyTransferOwnership() + ).to.be.revertedWith("admin only"); + }); + + it("test_commit_transfer_ownership", async function () { + await votingEscrow + .connect(accounts[0]) + .commitTransferOwnership(accounts[1].address); + + expect(await votingEscrow.admin()).to.equal(accounts[0].address); + expect(await votingEscrow.futureAdmin()).to.equal(accounts[1].address); + }); + + it("test_apply_transfer_ownership", async function () { + await votingEscrow + .connect(accounts[0]) + .commitTransferOwnership(accounts[1].address); + await votingEscrow.connect(accounts[0]).applyTransferOwnership(); + + expect(await votingEscrow.admin()).to.equal(accounts[1].address); + }); + + it("test_apply_without_commit", async function () { + await expect( + votingEscrow.connect(accounts[0]).applyTransferOwnership() + ).to.be.revertedWith("admin not set"); + }); +}); From 7d794f52a0855e52d443feda9af575af31cf81a8 Mon Sep 17 00:00:00 2001 From: takadr Date: Tue, 31 Oct 2023 15:38:08 +0900 Subject: [PATCH 023/412] Modified setting up order in curve fork tests --- test/fork/unitary/GaugeController/gaugesWeights.test.ts | 7 ++----- test/fork/unitary/GaugeController/timestamps.test.ts | 9 +++------ test/fork/unitary/GaugeController/totalWeight.test.ts | 9 +++------ test/fork/unitary/GaugeController/vote.test.ts | 9 +++------ .../unitary/GaugeController/voteWeightUnitary.test.ts | 9 +++------ test/fork/unitary/Minter/Minter.test.ts | 9 +++------ 6 files changed, 17 insertions(+), 35 deletions(-) diff --git a/test/fork/unitary/GaugeController/gaugesWeights.test.ts b/test/fork/unitary/GaugeController/gaugesWeights.test.ts index f5529c41..1f2b7eb0 100644 --- a/test/fork/unitary/GaugeController/gaugesWeights.test.ts +++ b/test/fork/unitary/GaugeController/gaugesWeights.test.ts @@ -8,14 +8,11 @@ describe("GaugeController", function () { let evm: EVMUtils; let snapshotId: string; - before(async () => { - setup = new TestSetup(); - await setup.setup(); - }); - beforeEach(async () => { evm = new EVMUtils(); snapshotId = await evm.snapshot(); + setup = new TestSetup(); + await setup.setup(); }); afterEach(async () => { diff --git a/test/fork/unitary/GaugeController/timestamps.test.ts b/test/fork/unitary/GaugeController/timestamps.test.ts index a97842f9..20529d03 100644 --- a/test/fork/unitary/GaugeController/timestamps.test.ts +++ b/test/fork/unitary/GaugeController/timestamps.test.ts @@ -8,15 +8,12 @@ describe("GaugeController", function () { let evm: EVMUtils; let snapshotId: string; - before(async () => { - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - }); - beforeEach(async () => { evm = new EVMUtils(); snapshotId = await evm.snapshot(); + setup = new TestSetup(); + await setup.setup(); + await setup.addType(); }); afterEach(async () => { diff --git a/test/fork/unitary/GaugeController/totalWeight.test.ts b/test/fork/unitary/GaugeController/totalWeight.test.ts index a746265f..4e33dbcf 100644 --- a/test/fork/unitary/GaugeController/totalWeight.test.ts +++ b/test/fork/unitary/GaugeController/totalWeight.test.ts @@ -7,15 +7,12 @@ describe("GaugeController", function () { let evm: EVMUtils; let snapshotId: string; - before(async () => { - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - }); - beforeEach(async () => { evm = new EVMUtils(); snapshotId = await evm.snapshot(); + setup = new TestSetup(); + await setup.setup(); + await setup.addType(); }); afterEach(async () => { diff --git a/test/fork/unitary/GaugeController/vote.test.ts b/test/fork/unitary/GaugeController/vote.test.ts index ada6e518..654146a0 100644 --- a/test/fork/unitary/GaugeController/vote.test.ts +++ b/test/fork/unitary/GaugeController/vote.test.ts @@ -8,7 +8,9 @@ describe("GaugeController", function () { let evm: EVMUtils; let snapshotId: string; - before(async () => { + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); setup = new TestSetup(); await setup.setup(); await setup.addType(); @@ -16,11 +18,6 @@ describe("GaugeController", function () { await setup.createLock(); }); - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - }); - afterEach(async () => { await evm.restore(snapshotId); }); diff --git a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts index 5b13887c..fa5ded6e 100644 --- a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts +++ b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts @@ -8,7 +8,9 @@ describe("GaugeController", function () { let evm: EVMUtils; let snapshotId: string; - before(async () => { + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); setup = new TestSetup(); await setup.setup(); await setup.addType(); @@ -16,11 +18,6 @@ describe("GaugeController", function () { await setup.createLock(); }); - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - }); - afterEach(async () => { await evm.restore(snapshotId); }); diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts index 13a92223..185f9916 100644 --- a/test/fork/unitary/Minter/Minter.test.ts +++ b/test/fork/unitary/Minter/Minter.test.ts @@ -8,7 +8,9 @@ describe("Minter", function () { let evm: EVMUtils; let snapshotId: string; - before(async () => { + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); setup = new TestSetup(); await setup.setup(); await setup.addType(); @@ -16,11 +18,6 @@ describe("Minter", function () { await setup.createLP(); }); - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - }); - afterEach(async () => { await evm.restore(snapshotId); }); From 6593c3304fcdab1caef4493097d1eee5cc91bb38 Mon Sep 17 00:00:00 2001 From: takadr Date: Tue, 31 Oct 2023 15:38:51 +0900 Subject: [PATCH 024/412] Added LiquidityGaugeV5 to curve fork --- contracts/curveFork/LiquidityGaugeV6.sol | 805 +++++++++++++++++++++++ 1 file changed, 805 insertions(+) create mode 100644 contracts/curveFork/LiquidityGaugeV6.sol diff --git a/contracts/curveFork/LiquidityGaugeV6.sol b/contracts/curveFork/LiquidityGaugeV6.sol new file mode 100644 index 00000000..b107a022 --- /dev/null +++ b/contracts/curveFork/LiquidityGaugeV6.sol @@ -0,0 +1,805 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +interface ICRV20 { + function futureEpochTimeWrite() external returns (uint256); + + function rate() external view returns (uint256); +} + +interface IController { + function checkpointGauge(address addr) external; + + function gaugeRelativeWeight( + address addr, + uint256 time + ) external view returns (uint256); +} + +interface IERC20Extended { + function symbol() external view returns (string memory); +} + +interface IERC1271 { + function isValidSignature( + bytes32 _hash, + bytes calldata _signature + ) external view returns (bytes32); +} + +interface IFactory { + function admin() external view returns (address); +} + +interface IMinter { + function minted( + address user, + address gauge + ) external view returns (uint256); +} + +interface IVotingEscrow { + function userPointEpoch(address addr) external view returns (uint256); + + function userPointHistoryTs( + address addr, + uint256 epoch + ) external view returns (uint256); +} + +interface IVotingEscrowBoost { + function adjustedBalanceOf( + address _account + ) external view returns (uint256); +} + +contract LiquidityGaugeV6 is ReentrancyGuard { + event Deposit(address indexed provider, uint256 value); + event Withdraw(address indexed provider, uint256 value); + event UpdateLiquidityLimit( + address user, + uint256 originalBalance, + uint256 originalSupply, + uint256 workingBalance, + uint256 workingSupply + ); + event CommitOwnership(address indexed admin); + event ApplyOwnership(address indexed admin); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + struct Reward { + address token; + address distributor; + uint256 periodFinish; + uint256 rate; + uint256 lastUpdate; + uint256 integral; + } + + // to avoid "stack too deep" + struct CheckPointParameters { + int128 period; + uint256 periodTime; + uint256 integrateInvSupply; + uint256 inflationParams; + uint256 rate; + uint256 newRate; + uint256 prevFutureEpoch; + uint256 workingBalance; + uint256 workingSupply; + } + + // to avoid "stack too deep" + struct RewardParameters { + uint256 userBalance; + address receiver; + uint256 rewardCount; + address token; + uint256 integral; + uint256 lastUpdate; + uint256 duration; + uint256 integralFor; + uint256 newClaimable; + uint256 claimData; + uint256 totalClaimable; + uint256 totalClaimed; + } + + // Constants + uint256 public constant MAX_REWARDS = 8; + uint256 public constant TOKENLESS_PRODUCTION = 40; + uint256 public constant WEEK = 604800; + string public constant VERSION = "v6.0.0"; // <- updated from v5.0.0 (adds `create_from_blueprint` pattern) + + bytes32 public constant EIP712_TYPEHASH = + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 public constant EIP2612_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + bytes32 public constant VERSION_HASH = keccak256(abi.encodePacked(VERSION)); + + // Immutable Variables + bytes32 public immutable NAME_HASH; + uint256 public immutable CACHED_CHAIN_ID; + bytes32 public immutable salt; + bytes32 public immutable CACHED_DOMAIN_SEPARATOR; + + // Address Constants + address public constant CRV = 0xD533a949740bb3306d119CC777fa900bA034cd52; + address public constant GAUGE_CONTROLLER = + 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB; + address public constant MINTER = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0; + address public constant VEBOOST_PROXY = + 0x8E0c00ed546602fD9927DF742bbAbF726D5B0d16; + address public constant VOTING_ESCROW = + 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2; + + // ERC20 + mapping(address => uint256) public balanceOf; + uint256 public totalSupply; + mapping(address => mapping(address => uint256)) public allowance; + + string public name; + string public symbol; + + // ERC2612 + mapping(address => uint256) public nonces; + + // Gauge + address public factory; + address public lpToken; + + bool public isKilled; + + // [future_epoch_time uint40][inflation_rate uint216] + uint256 public inflationParams; + + // For tracking external rewards + uint256 public rewardCount; + mapping(address => Reward) public rewardData; // Assuming you've already defined a 'Reward' struct + + // claimant -> default reward receiver + mapping(address => address) public rewardsReceiver; + + // reward token -> claiming address -> integral + mapping(address => mapping(address => uint256)) public rewardIntegralFor; + + // user -> [uint128 claimable amount][uint128 claimed amount] + mapping(address => mapping(address => uint256)) public claimData; + + mapping(address => uint256) public workingBalances; + uint256 public workingSupply; + + // 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint + mapping(address => uint256) public integrateInvSupplyOf; + mapping(address => uint256) public integrateCheckpointOf; + + // ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint + mapping(address => uint256) public integrateFraction; + + // The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint + int128 public period; + + // array of reward tokens + address[MAX_REWARDS] public rewardTokens; // Assuming MAX_REWARDS is a constant already defined + + // Using dynamic array instead of fixed 100000000000000000000000000000 array to avoid warning about collisions + uint256[] public periodTimestamp; + uint256[] public integrateInvSupply; + + constructor(address lpToken_) { + require(lpToken == address(0)); + + lpToken = lpToken_; + factory = msg.sender; + + string memory _symbol = IERC20Extended(lpToken_).symbol(); + name = string(abi.encodePacked("Curve.fi ", _symbol, " Gauge Deposit")); + symbol = string(abi.encodePacked(_symbol, "-gauge")); + + periodTimestamp[0] = block.timestamp; + + // Assuming you have the CRV20 interface defined somewhere for the following line + inflationParams = + (ICRV20(CRV).futureEpochTimeWrite() << 216) + + ICRV20(CRV).rate(); + + NAME_HASH = keccak256(abi.encodePacked(name)); + salt = blockhash(block.number - 1); + CACHED_CHAIN_ID = block.chainid; + CACHED_DOMAIN_SEPARATOR = keccak256( + abi.encodePacked( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + CACHED_CHAIN_ID, + address(this), + salt + ) + ); + } + + function _domainSeparator() internal view returns (bytes32) { + if (block.chainid != CACHED_CHAIN_ID) { + return + keccak256( + abi.encode( + EIP712_TYPEHASH, + NAME_HASH, + VERSION_HASH, + block.chainid, + address(this), + salt + ) + ); + } + return CACHED_DOMAIN_SEPARATOR; + } + + function _checkpoint(address addr) internal { + CheckPointParameters memory _st; + + _st.period = period; + _st.periodTime = periodTimestamp[uint256(uint128(_st.period))]; + _st.integrateInvSupply = integrateInvSupply[ + uint256(uint128(_st.period)) + ]; + + _st.inflationParams = inflationParams; + _st.rate = _st.inflationParams & ((1 << 216) - 1); + _st.prevFutureEpoch = _st.inflationParams >> 216; + _st.newRate = _st.rate; + + if (_st.prevFutureEpoch >= _st.periodTime) { + _st.newRate = ICRV20(CRV).rate(); + inflationParams = + (ICRV20(CRV).futureEpochTimeWrite() << 216) + + _st.newRate; + } + + if (isKilled) { + _st.rate = 0; + _st.newRate = 0; + } + + if (block.timestamp > _st.periodTime) { + uint256 _workingSupply = workingSupply; + IController(GAUGE_CONTROLLER).checkpointGauge(address(this)); + uint256 prevWeekTime = _st.periodTime; + uint256 weekTime = ((_st.periodTime + WEEK) / WEEK) * WEEK < + block.timestamp + ? ((_st.periodTime + WEEK) / WEEK) * WEEK + : block.timestamp; + + for (uint256 i = 0; i < 500; i++) { + uint256 dt = weekTime - prevWeekTime; + uint256 w = IController(GAUGE_CONTROLLER).gaugeRelativeWeight( + address(this), + (prevWeekTime / WEEK) * WEEK + ); + + if (_workingSupply > 0) { + if ( + _st.prevFutureEpoch >= prevWeekTime && + _st.prevFutureEpoch < weekTime + ) { + _st.integrateInvSupply += + (_st.rate * + w * + (_st.prevFutureEpoch - prevWeekTime)) / + _workingSupply; + _st.rate = _st.newRate; + _st.integrateInvSupply += + (_st.rate * w * (weekTime - _st.prevFutureEpoch)) / + _workingSupply; + } else { + _st.integrateInvSupply += + (_st.rate * w * dt) / + _workingSupply; + } + } + + if (weekTime == block.timestamp) { + break; + } + prevWeekTime = weekTime; + weekTime = weekTime + WEEK < block.timestamp + ? weekTime + WEEK + : block.timestamp; + } + } + + _st.period += 1; + period = _st.period; + periodTimestamp[uint256(uint128(_st.period))] = block.timestamp; + integrateInvSupply[uint256(uint128(_st.period))] = _st + .integrateInvSupply; + + uint256 _workingBalance = workingBalances[addr]; + integrateFraction[addr] += + (_workingBalance * + (_st.integrateInvSupply - integrateInvSupplyOf[addr])) / + 10 ** 18; + integrateInvSupplyOf[addr] = _st.integrateInvSupply; + integrateCheckpointOf[addr] = block.timestamp; + } + + function _checkpointRewards( + address user_, + uint256 totalSupply_, + bool claim_, + address receiver_ + ) internal { + RewardParameters memory _rp; + _rp.userBalance = 0; + _rp.receiver = receiver_; + if (user_ != address(0)) { + _rp.userBalance = balanceOf[user_]; + if (claim_ && receiver_ == address(0)) { + _rp.receiver = rewardsReceiver[user_]; + if (_rp.receiver == address(0)) { + _rp.receiver = user_; + } + } + } + + _rp.rewardCount = rewardCount; + for (uint256 i = 0; i < MAX_REWARDS; i++) { + if (i == _rp.rewardCount) { + break; + } + _rp.token = rewardTokens[i]; + + _rp.integral = rewardData[_rp.token].integral; + _rp.lastUpdate = block.timestamp < + rewardData[_rp.token].periodFinish + ? block.timestamp + : rewardData[_rp.token].periodFinish; + _rp.duration = _rp.lastUpdate - rewardData[_rp.token].lastUpdate; + if (_rp.duration != 0) { + rewardData[_rp.token].lastUpdate = _rp.lastUpdate; + if (totalSupply_ != 0) { + _rp.integral += + (_rp.duration * rewardData[_rp.token].rate * 10 ** 18) / + totalSupply_; + rewardData[_rp.token].integral = _rp.integral; + } + } + + if (user_ != address(0)) { + _rp.integralFor = rewardIntegralFor[_rp.token][user_]; + _rp.newClaimable = 0; + + if (_rp.integralFor < _rp.integral) { + rewardIntegralFor[_rp.token][user_] = _rp.integral; + _rp.newClaimable = + (_rp.userBalance * (_rp.integral - _rp.integralFor)) / + 10 ** 18; + } + + _rp.claimData = claimData[user_][_rp.token]; + _rp.totalClaimable = (_rp.claimData >> 128) + _rp.newClaimable; + if (_rp.totalClaimable > 0) { + _rp.totalClaimed = _rp.claimData & ((1 << 128) - 1); + if (claim_) { + bytes memory data = abi.encodeWithSignature( + "transfer(address,uint256)", + _rp.receiver, + _rp.totalClaimable + ); + (bool success, bytes memory response) = _rp.token.call( + data + ); + require( + success && + (response.length == 0 || + abi.decode(response, (bool))), + "Transfer failed" + ); + claimData[user_][_rp.token] = + _rp.totalClaimed + + _rp.totalClaimable; + } else if (_rp.newClaimable > 0) { + claimData[user_][_rp.token] = + _rp.totalClaimed + + (_rp.totalClaimable << 128); + } + } + } + } + } + + function _updateLiquidityLimit( + address addr_, + uint256 l_, + uint256 L_ + ) internal { + uint256 _votingBalance = IVotingEscrowBoost(VEBOOST_PROXY) + .adjustedBalanceOf(addr_); + uint256 _votingTotal = IERC20(VOTING_ESCROW).totalSupply(); + + uint256 _lim = (l_ * TOKENLESS_PRODUCTION) / 100; + if (_votingTotal > 0) { + _lim += + (((L_ * _votingBalance) / _votingTotal) * + (100 - TOKENLESS_PRODUCTION)) / + 100; + } + + _lim = l_ < _lim ? l_ : _lim; + uint256 _oldBal = workingBalances[addr_]; + workingBalances[addr_] = _lim; + uint256 _workingSupply = workingSupply + _lim - _oldBal; + workingSupply = _workingSupply; + + emit UpdateLiquidityLimit(addr_, l_, L_, _lim, _workingSupply); + } + + function _transfer(address from_, address to_, uint256 value_) internal { + _checkpoint(from_); + _checkpoint(to_); + + if (value_ != 0) { + uint256 _totalSupply = totalSupply; + bool _isRewards = rewardCount != 0; + if (_isRewards) { + _checkpointRewards(from_, _totalSupply, false, address(0)); + } + uint256 _newBalance = balanceOf[from_] - value_; + balanceOf[from_] = _newBalance; + _updateLiquidityLimit(from_, _newBalance, _totalSupply); + + if (_isRewards) { + _checkpointRewards(to_, _totalSupply, false, address(0)); + } + _newBalance = balanceOf[to_] + value_; + balanceOf[to_] = _newBalance; + _updateLiquidityLimit(to_, _newBalance, _totalSupply); + } + + emit Transfer(from_, to_, value_); + } + + // TODO initial value + // addr_ = msg.sender + // claimRewards_ = false + function deposit( + uint256 value_, + address addr_, + bool claimRewards_ + ) external nonReentrant { + _checkpoint(addr_); + + if (value_ != 0) { + bool _isRewards = rewardCount != 0; + uint256 _totalSupply = totalSupply; + if (_isRewards) { + _checkpointRewards( + addr_, + _totalSupply, + claimRewards_, + address(0) + ); + } + + _totalSupply += value_; + uint256 _newBalance = balanceOf[addr_] + value_; + balanceOf[addr_] = _newBalance; + totalSupply = _totalSupply; + + _updateLiquidityLimit(addr_, _newBalance, _totalSupply); + + IERC20(lpToken).transferFrom(msg.sender, address(this), value_); + } + + emit Deposit(addr_, value_); + emit Transfer(address(0), addr_, value_); + } + + // TODO initial value + // claimRewards_ = false + function withdraw( + uint256 value_, + bool claimRewards_ + ) external nonReentrant { + _checkpoint(msg.sender); + + if (value_ != 0) { + bool _isRewards = rewardCount != 0; + uint256 _totalSupply = totalSupply; + if (_isRewards) { + _checkpointRewards( + msg.sender, + _totalSupply, + claimRewards_, + address(0) + ); + } + + _totalSupply -= value_; + uint256 _newBalance = balanceOf[msg.sender] - value_; + balanceOf[msg.sender] = _newBalance; + totalSupply = _totalSupply; + + _updateLiquidityLimit(msg.sender, _newBalance, _totalSupply); + + IERC20(lpToken).transfer(msg.sender, value_); + } + + emit Withdraw(msg.sender, value_); + emit Transfer(msg.sender, address(0), value_); + } + + // TODO + // addr_ = msg.sender, address receiver_ = address(0) + function claimRewards( + address addr_, + address receiver_ + ) external nonReentrant { + if (receiver_ != address(0)) { + require(addr_ == msg.sender); + } + _checkpointRewards(addr_, totalSupply, true, receiver_); + } + + function transferFrom( + address from_, + address to_, + uint256 value_ + ) external nonReentrant returns (bool) { + uint256 _allowance = allowance[from_][msg.sender]; + if (_allowance != type(uint256).max) { + allowance[from_][msg.sender] = _allowance - value_; + } + _transfer(from_, to_, value_); + return true; + } + + function transfer( + address to_, + uint256 value_ + ) external nonReentrant returns (bool) { + _transfer(msg.sender, to_, value_); + return true; + } + + function approve(address spender_, uint256 value_) external returns (bool) { + allowance[msg.sender][spender_] = value_; + emit Approval(msg.sender, spender_, value_); + return true; + } + + function permit( + address owner_, + address spender_, + uint256 value_, + uint256 deadline_, + uint8 v_, + bytes32 r_, + bytes32 s_ + ) external returns (bool) { + require(owner_ != address(0)); + require(block.timestamp <= deadline_, "Expired"); + + uint256 _nonce = nonces[owner_]; + bytes32 _digest = keccak256( + abi.encodePacked( + "\x19\x01", + _domainSeparator(), + keccak256( + abi.encode( + EIP2612_TYPEHASH, + owner_, + spender_, + value_, + _nonce, + deadline_ + ) + ) + ) + ); + require(ecrecover(_digest, v_, r_, s_) == owner_, "Invalid sig"); + + allowance[owner_][spender_] = value_; + nonces[owner_] = _nonce + 1; + + emit Approval(owner_, spender_, value_); + return true; + } + + function increaseAllowance( + address spender_, + uint256 addedValue_ + ) external returns (bool) { + uint256 _currentAllowance = allowance[msg.sender][spender_]; + uint256 _newAllowance = _currentAllowance + addedValue_; + allowance[msg.sender][spender_] = _newAllowance; + emit Approval(msg.sender, spender_, _newAllowance); + return true; + } + + function decreaseAllowance( + address spender_, + uint256 subtractedValue_ + ) external returns (bool) { + uint256 _currentAllowance = allowance[msg.sender][spender_]; + uint256 _newAllowance = _currentAllowance - subtractedValue_; + allowance[msg.sender][spender_] = _newAllowance; + emit Approval(msg.sender, spender_, _newAllowance); + return true; + } + + function userCheckpoint(address addr_) external returns (bool) { + require(msg.sender == addr_ || msg.sender == MINTER); + _checkpoint(addr_); + _updateLiquidityLimit(addr_, balanceOf[addr_], totalSupply); + return true; + } + + function setRewardsReceiver(address receiver_) external { + rewardsReceiver[msg.sender] = receiver_; + } + + function kick(address addr_) external { + uint256 _tLast = integrateCheckpointOf[addr_]; + uint256 _tVe = IVotingEscrow(VOTING_ESCROW).userPointHistoryTs( + addr_, + IVotingEscrow(VOTING_ESCROW).userPointEpoch(addr_) + ); + uint256 _balance = balanceOf[addr_]; + + require( + ERC20(VOTING_ESCROW).balanceOf(addr_) == 0 || _tVe > _tLast, + "Not allowed" + ); + require( + workingBalances[addr_] > (_balance * TOKENLESS_PRODUCTION) / 100, + "Not needed" + ); + + _checkpoint(addr_); + _updateLiquidityLimit(addr_, balanceOf[addr_], totalSupply); + } + + function depositRewardToken( + address rewardToken_, + uint256 amount_ + ) external nonReentrant { + require( + msg.sender == rewardData[rewardToken_].distributor, + "Invalid dist" + ); + + _checkpointRewards(address(0), totalSupply, false, address(0)); + + bytes memory _data = abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + msg.sender, + address(this), + amount_ + ); + (bool success, bytes memory response) = rewardToken_.call(_data); + require(success); + require(response.length == 0 || abi.decode(response, (bool))); + + uint256 _periodFinish = rewardData[rewardToken_].periodFinish; + if (block.timestamp >= _periodFinish) { + rewardData[rewardToken_].rate = amount_ / WEEK; + } else { + uint256 remaining = _periodFinish - block.timestamp; + uint256 leftover = remaining * rewardData[rewardToken_].rate; + rewardData[rewardToken_].rate = (amount_ + leftover) / WEEK; + } + + rewardData[rewardToken_].lastUpdate = block.timestamp; + rewardData[rewardToken_].periodFinish = block.timestamp + WEEK; + } + + function addReward( + address rewardToken_, + address distributor_ + ) external onlyAdmin { + require(rewardCount < MAX_REWARDS, "Max"); + require(rewardData[rewardToken_].distributor == address(0), "Has dist"); + + rewardData[rewardToken_].distributor = distributor_; + rewardTokens[rewardCount] = rewardToken_; + ++rewardCount; + } + + function setRewardDistributor( + address rewardToken_, + address distributor_ + ) external { + address currentDistributor = rewardData[rewardToken_].distributor; + require( + msg.sender == currentDistributor || + msg.sender == IFactory(factory).admin() + ); + require(currentDistributor != address(0)); + require(distributor_ != address(0)); + + rewardData[rewardToken_].distributor = distributor_; + } + + function setKilled(bool isKilled_) external onlyAdmin { + isKilled = isKilled_; + } + + function claimedReward( + address addr_, + address token_ + ) external view returns (uint256) { + return claimData[addr_][token_] % 2 ** 128; + } + + function claimableReward( + address user_, + address rewardToken_ + ) external view returns (uint256) { + uint256 integral = rewardData[rewardToken_].integral; + uint256 totalSupply_ = totalSupply; + if (totalSupply_ != 0) { + uint256 lastUpdate = block.timestamp < + rewardData[rewardToken_].periodFinish + ? block.timestamp + : rewardData[rewardToken_].periodFinish; + uint256 duration = lastUpdate - rewardData[rewardToken_].lastUpdate; + integral += + (duration * rewardData[rewardToken_].rate * 10 ** 18) / + totalSupply_; + } + uint256 integralFor = rewardIntegralFor[rewardToken_][user_]; + uint256 newClaimable = (balanceOf[user_] * (integral - integralFor)) / + 10 ** 18; + + return (claimData[user_][rewardToken_] >> 128) + newClaimable; + } + + function claimableTokens(address addr_) external returns (uint256) { + _checkpoint(addr_); + return + integrateFraction[addr_] - + IMinter(MINTER).minted(addr_, address(this)); + } + + function integrateCheckpoint() external view returns (uint256) { + return periodTimestamp[uint256(uint128(period))]; + } + + function futureEpochTime() external view returns (uint256) { + return inflationParams >> 216; + } + + function inflationRate() external view returns (uint256) { + return inflationParams % 2 ** 216; + } + + function decimals() external pure returns (uint256) { + return 18; + } + + function version() external pure returns (string memory) { + return VERSION; + } + + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparator(); + } + + modifier onlyAdmin() { + require(msg.sender == IFactory(factory).admin()); + _; + } +} From 2463ea13d3c3adcaf32e330b37c797829dcb1776 Mon Sep 17 00:00:00 2001 From: takadr Date: Tue, 31 Oct 2023 15:39:23 +0900 Subject: [PATCH 025/412] fixed typo in hardhat config --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 19325989..59a34dbf 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -63,7 +63,7 @@ module.exports = { }, }, }, - }, + ], localhost: { url: "http://127.0.0.1:8545", }, From 22179cb01f952b83a5a05f418d4c1fc9f9fa20e0 Mon Sep 17 00:00:00 2001 From: takadr Date: Tue, 31 Oct 2023 15:50:16 +0900 Subject: [PATCH 026/412] Deleted comments --- contracts/curveFork/VotingEscrow.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 2e59a6c9..65879c28 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -278,7 +278,6 @@ contract VotingEscrow is ReentrancyGuard { // initial_last_point is used for extrapolation to calculate block number // (approximately, for *At methods) and save them // as we cannot figure that out exactly from inside the contract - // Point memory _initialLastPoint = _lastPoint; Point memory _initialLastPoint = Point({ bias: _lastPoint.bias, slope: _lastPoint.slope, From 5277000755b839a0ccb10d6d29f533ea4cbbb8c3 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 1 Nov 2023 11:54:44 +0900 Subject: [PATCH 027/412] Add test code for LiquidityGauge --- contracts/curveFork/LiquidityGauge.sol | 2 +- .../curveFork/interfaces/IVotingEscrow.sol | 4 +- test/fork/helper.ts | 53 +++- .../LiquidityGauge/liquidityGauge.test.ts | 286 ++++++++++++++++++ 4 files changed, 327 insertions(+), 18 deletions(-) create mode 100644 test/fork/integration/LiquidityGauge/liquidityGauge.test.ts diff --git a/contracts/curveFork/LiquidityGauge.sol b/contracts/curveFork/LiquidityGauge.sol index b9dec78c..4c64a6a3 100644 --- a/contracts/curveFork/LiquidityGauge.sol +++ b/contracts/curveFork/LiquidityGauge.sol @@ -313,7 +313,7 @@ contract LiquidityGauge is ReentrancyGuard { uint256 _t_last = integrate_checkpoint_of[_addr]; uint256 _t_ve = voting_escrow.userPointHistoryTs( _addr, - voting_escrow.getUserPointEpoch(_addr) + voting_escrow.userPointEpoch(_addr) ); uint256 _balance = balanceOf[_addr]; diff --git a/contracts/curveFork/interfaces/IVotingEscrow.sol b/contracts/curveFork/interfaces/IVotingEscrow.sol index 5af927f3..000d8068 100644 --- a/contracts/curveFork/interfaces/IVotingEscrow.sol +++ b/contracts/curveFork/interfaces/IVotingEscrow.sol @@ -12,10 +12,10 @@ interface IVotingEscrow { view returns (uint256); - //function balanceOf(address addr)external view returns (uint256); + function balanceOf(address addr)external view returns (uint256); function totalSupply(uint256 t_) external view returns (uint256); - function getUserPointEpoch(address _user) + function userPointEpoch(address _user) external view returns (uint256); diff --git a/test/fork/helper.ts b/test/fork/helper.ts index 02f926f0..fe761033 100644 --- a/test/fork/helper.ts +++ b/test/fork/helper.ts @@ -16,7 +16,7 @@ class TestSetup { readonly DAY = BigNumber.from(86400); readonly WEEK = BigNumber.from(86400 * 7); - readonly MONTH: BigNumber = BigNumber.from(86400 * 30); + readonly MONTH = BigNumber.from(86400 * 30); readonly YEAR = BigNumber.from(86400 * 365); readonly name = "Token"; @@ -36,9 +36,19 @@ class TestSetup { readonly a = BigNumber.from("2"); readonly b = BigNumber.from("5"); readonly zero = BigNumber.from("0"); + readonly MAX_UINT256 = BigNumber.from("115792089237316195423570985008687907853269984665640564039457584007913129639935"); readonly GAUGE_TYPES = [BigNumber.from("1"), BigNumber.from("1"), BigNumber.from("2")]; + + creator: Signer; + alice: Signer; + bob: Signer; + charly: Signer; + creatorAddress: String; + aliceAddress: String; + bobAddress: String; + charlyAddress: String; accounts: Signer[]; accountsAddress: String[]; token: Contract; @@ -46,6 +56,7 @@ class TestSetup { gaugeController: Contract; mockLpToken: Contract; minter: Contract; + lg: Contract; gaugesContracts: Contract[]; gaugesAddress: string[]; @@ -54,13 +65,17 @@ class TestSetup { async setup() { - const [creator, alice, bob, charly] = await ethers.getSigners(); - this.accounts = [creator, alice, bob, charly]; + [this.creator, this.alice, this.bob, this.charly] = await ethers.getSigners(); + this.accounts = [this.creator, this.alice, this.bob, this.charly]; + this.creatorAddress = await this.creator.getAddress(); + this.aliceAddress = await this.alice.getAddress(); + this.bobAddress = await this.bob.getAddress(); + this.charlyAddress = await this.charly.getAddress(); this.accountsAddress = [ - await creator.getAddress(), - await alice.getAddress(), - await bob.getAddress(), - await charly.getAddress(), + this.creatorAddress, + this.aliceAddress, + this.bobAddress, + this.charlyAddress, ]; this.TYPE_WEIGHTS = [this.ten_to_the_17.mul(this.b), this.ten_to_the_18.mul(this.a)]; @@ -80,16 +95,24 @@ class TestSetup { "Voting-escrowed token", "vetoken", "v1" - ); + ); this.gaugeController = await GaugeController.deploy(this.token.address, this.votingEscrow.address); - this.mockLpToken = await TestLP.deploy("tokenDAO LP token", "iToken", 18, BigNumber.from("1000000000000000000000")); + this.mockLpToken = await TestLP.deploy("tokenDAO LP token", "iToken", 18, this.ten_to_the_21.mul("2")); this.minter = await Minter.deploy(this.token.address, this.gaugeController.address); - const lg1 = await LiquidityGauge.deploy(this.mockLpToken.address, this.minter.address); - const lg2 = await LiquidityGauge.deploy(this.mockLpToken.address, this.minter.address); - const lg3 = await LiquidityGauge.deploy(this.mockLpToken.address, this.minter.address); - this.gaugesContracts = [lg1, lg2, lg3]; - this.gaugesAddress = [lg1.address, lg2.address, lg3.address]; - + this.lg = await LiquidityGauge.deploy( + this.mockLpToken.address, + this.minter.address, + ); + const lg2 = await LiquidityGauge.deploy( + this.mockLpToken.address, + this.minter.address, + ); + const lg3 = await LiquidityGauge.deploy( + this.mockLpToken.address, + this.minter.address, + ); + this.gaugesContracts = [this.lg, lg2, lg3]; + this.gaugesAddress = [this.lg.address, lg2.address, lg3.address]; await this.token.setMinter(this.minter.address); // setup await this.gaugeController.addType("none", 0); diff --git a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts new file mode 100644 index 00000000..6be27d3b --- /dev/null +++ b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts @@ -0,0 +1,286 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, TestSetup } from "../../helper"; + +describe("LiquidityGauge", function () { + let setup: TestSetup; + let evm: EVMUtils; + let snapshotId: string; + + before(async () => { + setup = new TestSetup(); + await setup.setup(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + describe("Gauge Integral Calculations", function () { + + /** + * テスト: test_gauge_integral + * 1. AliceとBobの保有量と積分の初期化を行う。 + * 2. 現在のブロックのタイムスタンプと初期レートを取得する。 + * 3. タイプを追加し、その重みを変更する。 + * 4. LPトークンをAliceとBobに均等に送信する。 + * 5. 積分の更新処理を行うための補助関数「update_integral」を定義する。 + * 6. Bobが預金または引き出しを繰り返し行い、Aliceがそれをランダムに行う10回のループを開始する。 + * a. ランダムな時間を経過させるシミュレーションを行う。 + * b. Bobがランダムに預金または引き出しを行う。 + * c. 20%の確率でAliceも預金または引き出しを行う。 + * d. 同じ秒数でのユーザーチェックポイントの更新が影響しないことを確認する。 + * e. AliceとBobの保有量が正しいことを確認する。 + * f. もう一度、ランダムな時間を経過させるシミュレーションを行う。 + * g. Aliceのユーザーチェックポイントを更新し、積分を更新する。 + * 7. テストが終了する。 + */ + it("should correctly calculate user integrals over randomized actions", async () => { + // AliceとBobの保有量と積分を初期化 + let alice_staked = BigNumber.from("0"); + let bob_staked = BigNumber.from("0"); + let integral = BigNumber.from("0"); + + // 最新のブロックのタイムスタンプを取得 + let checkpoint = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + // 初期レートの取得 + let checkpoint_rate = await setup.token.rate(); + let checkpoint_supply = BigNumber.from("0"); + let checkpoint_balance = BigNumber.from("0"); + + // タイプの追加とその重みの変更 + await setup.gaugeController.addType("Liquidity", BigNumber.from("0")); + await setup.gaugeController.changeTypeWeight(1, setup.ten_to_the_18); + await setup.gaugeController.addGauge(setup.lg.address, 1, setup.ten_to_the_18); + + // LPトークンを送信 + const creatorBalance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + await setup.mockLpToken.transfer( + setup.aliceAddress, + (creatorBalance).div(BigNumber.from("2")) + ); + await setup.mockLpToken.transfer( + setup.bobAddress, + (creatorBalance).div(BigNumber.from("2")) + ); + + // 積分を更新する関数 + async function update_integral() { + let t1 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + let rate1 = await setup.token.rate(); + let t_epoch = await setup.token.startEpochTime(); + let rate_x_time = BigNumber.from("0"); + + // checkpoint >= t_epoch + if (checkpoint.gte(t_epoch)) { + // t1 - checkpoint * rate1 + rate_x_time = t1.sub(checkpoint).mul(rate1); + } else { + // t_epoch - checkpoint * checkpoint_rate ( t1 - t_epoch * rate1) + rate_x_time = t_epoch.sub(checkpoint).mul(checkpoint_rate).add(t1.sub(t_epoch).mul(rate1)); + } + + // checkpoint_supply > 0 + if (checkpoint_supply.gt(BigNumber.from("0"))) { + // integral + rate_x_time * checkpoint_balance / checkpoint_supply + integral = integral.add(rate_x_time.mul(checkpoint_balance).div(checkpoint_supply)); + } + + checkpoint_rate = rate1; + checkpoint = t1; + checkpoint_supply = await setup.lg.totalSupply(); + checkpoint_balance = await setup.lg.balanceOf(setup.aliceAddress); + } + + // Bobは常に預金または引き出しを行い、Aliceはそれをあまり行わない + for (let i = 0; i < 10; i++) { + let is_alice = Math.random() < 0.2; + + // ランダムな時間経過をシミュレート + let dt = BigNumber.from(Math.floor(Math.random() * 86400 * 73).toString()).add(BigNumber.from("1")); + await ethers.provider.send("evm_increaseTime", [dt.toNumber()]); + + // Bobの処理 + let is_withdraw = i > 0 && Math.random() < 0.5; + + if (is_withdraw) { + // 引き出し処理 + let amount = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + .mul(await setup.lg.balanceOf(setup.bobAddress)) + .div(BigNumber.from("10000")); + await setup.lg.connect(setup.bob).withdraw(amount); + await update_integral(); + bob_staked = bob_staked.sub(amount); + } else { + // 預金処理 + let amount = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + .mul(await setup.mockLpToken.balanceOf(setup.bobAddress)) + .div(BigNumber.from("10")) + .div(BigNumber.from("10000")); + await setup.mockLpToken.connect(setup.bob).approve(setup.lg.address, amount); + await setup.lg.connect(setup.bob).deposit(amount, setup.bobAddress); + await update_integral(); + bob_staked = bob_staked.add(amount); + } + + // Aliceの処理 + if (is_alice) { + let is_withdraw_alice = (await setup.lg.balanceOf(setup.aliceAddress)) > 0 && Math.random() > 0.5; + if (is_withdraw_alice) { + // 引き出し処理 + let amount_alice = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + .mul(await setup.lg.balanceOf(setup.aliceAddress)) + .div(BigNumber.from("10")) + .div(BigNumber.from("10000")); + await setup.lg.connect(setup.alice).withdraw(amount_alice); + await update_integral(); + alice_staked = alice_staked.sub(amount_alice); + } else { + // 預金処理 + let amount_alice = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + .mul(await setup.mockLpToken.balanceOf(setup.aliceAddress)) + .div(BigNumber.from("10000")); + await setup.mockLpToken.connect(setup.alice).approve(setup.lg.address, amount_alice); + await setup.lg.connect(setup.alice).deposit(amount_alice, setup.aliceAddress); + await update_integral(); + alice_staked = alice_staked.add(amount_alice); + } + } + + // 同じ秒数でのチェックポイントの更新は影響しないことの確認 + if (Math.random() < 0.5) { + await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + } + if (Math.random() < 0.5) { + await setup.lg.connect(setup.bob).userCheckpoint(setup.bobAddress); + } + + // 保有量の確認 + expect(await setup.lg.balanceOf(setup.aliceAddress)).to.equal(alice_staked); + expect(await setup.lg.balanceOf(setup.bobAddress)).to.equal(bob_staked); + expect(await setup.lg.totalSupply()).to.equal(alice_staked.add(bob_staked)); + + // ランダムな時間経過をさらにシミュレート + dt = BigNumber.from(Math.floor(Math.random() * 86400 * 19).toString()).add(BigNumber.from("1")); + await ethers.provider.send("evm_increaseTime", [dt.toNumber()]); + + await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await update_integral(); + } + }); + }); + + + + /** + * test_mining_with_votelock の全体的な流れ: + * + * 1. 2週間と5秒の時間を進める。 + * 2. ゲージとコントローラーをセットアップし、適切なレートを設定する。 + * 3. AliceとBobにトークンを転送し、それぞれのアドレスに関連する承認を設定する。 + * 4. Aliceは投票のエスクローにトークンをロックすることで、BOOSTを取得する。 + * 5. AliceとBobはそれぞれ流動性をデポジットする。 + * 6. Aliceの投票ロックの存在とBobの投票ロックの不在を確認する。 + * 7. 時間をさらに進め、両方のユーザーのチェックポイントを更新する。 + * 8. 4週間後、AliceとBobの投票エスクローのバランスが0であることを確認する。 + * 9. Aliceが投票ロックでTokenを獲得したため、彼女はBobの2.5倍のリワードを獲得することを確認する。 + * 10. さらに時間を進め、両方のユーザーのチェックポイントを更新する。 + * 11. 最終的に、AliceとBobが同じ量のリワードを獲得していることを確認する。 + */ + describe("Mining with Vote Locking", function () { + it("should distribute rewards according to vote lock status", async () => { + // 2週間と5秒時間を進める + await ethers.provider.send("evm_increaseTime", [ + setup.WEEK.mul(BigNumber.from("2")).add(BigNumber.from("5")).toNumber(), + ]); + + // ゲージをコントローラーに接続して適切なレートなどを設定する + await setup.gaugeController.addType("Liquidity", 0); + await setup.gaugeController.changeTypeWeight(1, setup.ten_to_the_18); + await setup.gaugeController.addGauge(setup.lg.address, 1, setup.ten_to_the_18); + + // トークンの準備 + await setup.token.transfer(setup.aliceAddress, setup.ten_to_the_20); + await setup.token.transfer(setup.bobAddress, setup.ten_to_the_20); + + await setup.token.connect(setup.alice).approve(setup.votingEscrow.address, setup.MAX_UINT256); + await setup.token.connect(setup.bob).approve(setup.votingEscrow.address, setup.MAX_UINT256); + const creatorBalance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + await setup.mockLpToken.transfer( + setup.aliceAddress, + (creatorBalance).div(BigNumber.from("2")) + ); + await setup.mockLpToken.transfer( + setup.bobAddress, + (creatorBalance).div(BigNumber.from("2")) + ); + + await setup.mockLpToken.connect(setup.alice).approve(setup.lg.address, setup.MAX_UINT256); + await setup.mockLpToken.connect(setup.bob).approve(setup.lg.address, setup.MAX_UINT256); + + // Aliceがescrowにデポジットする。AliceはBOOSTを持っていることになる + let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + + await setup.votingEscrow.connect(setup.alice).createLock(setup.ten_to_the_20, t.add(setup.WEEK.mul(BigNumber.from("2")))); + + // AliceとBobが一部の流動性をデポジットする + await setup.lg.connect(setup.alice).deposit(setup.ten_to_the_21, setup.aliceAddress); + await setup.lg.connect(setup.bob).deposit(setup.ten_to_the_21, setup.bobAddress); + let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + + // 現在、Aliceは投票ロックを持っているが、Bobは持っていないことを確認する + expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.aliceAddress, now)).to.not.equal(setup.zero); + expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.bobAddress, now)).to.equal(setup.zero); + + // 時間を進めてチェックポイントを更新する + now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(setup.WEEK.mul(BigNumber.from("4"))).toNumber()]); + + // チェックポイント更新 + await ethers.provider.send("evm_setAutomine", [false]); + await setup.lg.connect(setup.bob).userCheckpoint(setup.bobAddress); + await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await ethers.provider.send("evm_mine", []); + await ethers.provider.send("evm_setAutomine", [true]); + + // 4週間後、balanceOfは0であるべき + now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.aliceAddress, now)).to.equal(setup.zero); + expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.bobAddress, now)).to.equal(setup.zero); + + // AliceはTokenを投票ロックしたので、2.5倍のTokenを獲得 + let rewards_alice = await setup.lg.integrateFraction(setup.aliceAddress); + let rewards_bob = await setup.lg.integrateFraction(setup.bobAddress); + expect(rewards_alice.mul(BigNumber.from("10000000000000000")).div(rewards_bob)).to.equal( + BigNumber.from("25000000000000000") + ); + + // 時間を進めてチェックポイントを更新: 今は誰もがTokenを投票ロックしていない + now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(setup.WEEK.mul(BigNumber.from("4"))).toNumber()]); + + await ethers.provider.send("evm_setAutomine", [false]); + await setup.lg.connect(setup.bob).userCheckpoint(setup.bobAddress); + await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await ethers.provider.send("evm_mine", []); + await ethers.provider.send("evm_setAutomine", [true]); + let old_rewards_alice = rewards_alice; + let old_rewards_bob = rewards_bob; + // 今、AliceはBobと同じ量を獲得した + rewards_alice = await setup.lg.integrateFraction(setup.aliceAddress); + rewards_bob = await setup.lg.integrateFraction(setup.bobAddress); + console.log(rewards_alice) + console.log(rewards_bob) + let d_alice = rewards_alice.sub(old_rewards_alice); + let d_bob = rewards_bob.sub(old_rewards_bob); + + expect(d_alice.sub(d_bob)).to.equal(setup.zero); + }); + }); +}); From d2abef4477a0101d91b88eb755510ee84c5aca07 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 1 Nov 2023 14:23:23 +0900 Subject: [PATCH 028/412] Matching the interface to LiquidityGaugeV6 --- contracts/curveFork/LiquidityGauge.sol | 4 +- .../LiquidityGauge/liquidityGauge.test.ts | 12 +- test/fork/unitary/Minter/Minter.test.ts | 244 +++++++++--------- 3 files changed, 130 insertions(+), 130 deletions(-) diff --git a/contracts/curveFork/LiquidityGauge.sol b/contracts/curveFork/LiquidityGauge.sol index 4c64a6a3..35406b1b 100644 --- a/contracts/curveFork/LiquidityGauge.sol +++ b/contracts/curveFork/LiquidityGauge.sol @@ -345,7 +345,7 @@ contract LiquidityGauge is ReentrancyGuard { *@param _value Number of tokens to deposit *@param _addr Address to deposit for */ - function deposit(uint256 _value, address _addr) external nonReentrant { + function deposit(uint256 _value, address _addr, bool claimRewards_) external nonReentrant { if (_addr != msg.sender) { require(approved_to_deposit[msg.sender][_addr], "Not approved"); } @@ -369,7 +369,7 @@ contract LiquidityGauge is ReentrancyGuard { *@notice Withdraw `_value` LP tokens *@param _value Number of tokens to withdraw */ - function withdraw(uint256 _value) external nonReentrant { + function withdraw(uint256 _value, bool claimRewards_) external nonReentrant { _checkpoint(msg.sender); uint256 _balance = balanceOf[msg.sender] - _value; diff --git a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts index 6be27d3b..b2df5451 100644 --- a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts +++ b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts @@ -114,7 +114,7 @@ describe("LiquidityGauge", function () { let amount = BigNumber.from(Math.floor(Math.random() * 10000).toString()) .mul(await setup.lg.balanceOf(setup.bobAddress)) .div(BigNumber.from("10000")); - await setup.lg.connect(setup.bob).withdraw(amount); + await setup.lg.connect(setup.bob).withdraw(amount, false); await update_integral(); bob_staked = bob_staked.sub(amount); } else { @@ -124,7 +124,7 @@ describe("LiquidityGauge", function () { .div(BigNumber.from("10")) .div(BigNumber.from("10000")); await setup.mockLpToken.connect(setup.bob).approve(setup.lg.address, amount); - await setup.lg.connect(setup.bob).deposit(amount, setup.bobAddress); + await setup.lg.connect(setup.bob).deposit(amount, setup.bobAddress, false); await update_integral(); bob_staked = bob_staked.add(amount); } @@ -138,7 +138,7 @@ describe("LiquidityGauge", function () { .mul(await setup.lg.balanceOf(setup.aliceAddress)) .div(BigNumber.from("10")) .div(BigNumber.from("10000")); - await setup.lg.connect(setup.alice).withdraw(amount_alice); + await setup.lg.connect(setup.alice).withdraw(amount_alice, false); await update_integral(); alice_staked = alice_staked.sub(amount_alice); } else { @@ -147,7 +147,7 @@ describe("LiquidityGauge", function () { .mul(await setup.mockLpToken.balanceOf(setup.aliceAddress)) .div(BigNumber.from("10000")); await setup.mockLpToken.connect(setup.alice).approve(setup.lg.address, amount_alice); - await setup.lg.connect(setup.alice).deposit(amount_alice, setup.aliceAddress); + await setup.lg.connect(setup.alice).deposit(amount_alice, setup.aliceAddress, false); await update_integral(); alice_staked = alice_staked.add(amount_alice); } @@ -230,8 +230,8 @@ describe("LiquidityGauge", function () { await setup.votingEscrow.connect(setup.alice).createLock(setup.ten_to_the_20, t.add(setup.WEEK.mul(BigNumber.from("2")))); // AliceとBobが一部の流動性をデポジットする - await setup.lg.connect(setup.alice).deposit(setup.ten_to_the_21, setup.aliceAddress); - await setup.lg.connect(setup.bob).deposit(setup.ten_to_the_21, setup.bobAddress); + await setup.lg.connect(setup.alice).deposit(setup.ten_to_the_21, setup.aliceAddress, false); + await setup.lg.connect(setup.bob).deposit(setup.ten_to_the_21, setup.bobAddress, false); let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); // 現在、Aliceは投票ロックを持っているが、Bobは持っていないことを確認する diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts index 185f9916..66da5f58 100644 --- a/test/fork/unitary/Minter/Minter.test.ts +++ b/test/fork/unitary/Minter/Minter.test.ts @@ -25,36 +25,36 @@ describe("Minter", function () { describe("Minter Behavior", function () { // Test basic mint functionality it("test mint", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); //gauge_address, msg.sender, mint - let expected = await setup.gaugesContracts[0].integrateFraction( - setup.accountsAddress[1] + .connect(setup.alice) + .mint(setup.lg.address); //gauge_address, msg.sender, mint + let expected = await setup.lg.integrateFraction( + setup.aliceAddress ); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( expected ); expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(expected); }); // Test minting immediately after setup it("test mint immediate", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_18, setup.aliceAddress, false); let t0 = BigNumber.from( (await ethers.provider.getBlock("latest")).timestamp @@ -67,49 +67,49 @@ describe("Minter", function () { //mint expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal("0"); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); + .connect(setup.alice) + .mint(setup.lg.address); //check - let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + let balance = await setup.token.balanceOf(setup.aliceAddress); expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(balance); }); // Test multiple mint operations on the same gauge it("test mint multiple same gauge", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_18, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); - let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + .connect(setup.alice) + .mint(setup.lg.address); + let balance = await setup.token.balanceOf(setup.aliceAddress); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); - let expected = await setup.gaugesContracts[0].integrateFraction( - setup.accountsAddress[1] + .connect(setup.alice) + .mint(setup.lg.address); + let expected = await setup.lg.integrateFraction( + setup.aliceAddress ); - let final_balance = await setup.token.balanceOf(setup.accountsAddress[1]); + let final_balance = await setup.token.balanceOf(setup.aliceAddress); expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success expect(final_balance).to.equal(expected); //2nd mint works fine expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(expected); //tracks fine }); @@ -117,22 +117,22 @@ describe("Minter", function () { // Test minting across multiple gauges it("test mint multiple gauges", async () => { //setup - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await setup.gaugesContracts[1] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await setup.gaugesContracts[2] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); //mint for (let i = 0; i < 3; i++) { await setup.minter - .connect(setup.accounts[1]) + .connect(setup.alice) .mint(setup.gaugesAddress[i]); } @@ -142,39 +142,39 @@ describe("Minter", function () { for (let i = 0; i < 3; i++) { let gauge = setup.gaugesContracts[i]; let minted = await setup.minter.minted( - setup.accountsAddress[1], + setup.aliceAddress, gauge.address ); expect(minted).to.equal( - await gauge.integrateFraction(setup.accountsAddress[1]) + await gauge.integrateFraction(setup.aliceAddress) ); total_minted = total_minted.add(minted); } - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( total_minted ); }); // Test minting after withdrawing it("test mint after withdraw", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_18, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [ setup.WEEK.mul(BigNumber.from("2")).toNumber(), ]); - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .withdraw(setup.ten_to_the_18); + await setup.lg + .connect(setup.alice) + .withdraw(setup.ten_to_the_18, false); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); + .connect(setup.alice) + .mint(setup.lg.address); expect( - (await setup.token.balanceOf(setup.accountsAddress[1])).gt( + (await setup.token.balanceOf(setup.aliceAddress)).gt( BigNumber.from("0") ) ).to.equal(true); @@ -182,26 +182,26 @@ describe("Minter", function () { // Test multiple mints after withdrawing it("test mint multiple after withdraw", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_18, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [10]); - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .withdraw(setup.ten_to_the_18); + await setup.lg + .connect(setup.alice) + .withdraw(setup.ten_to_the_18, false); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); + .connect(setup.alice) + .mint(setup.lg.address); - let balance = await setup.token.balanceOf(setup.accountsAddress[1]); + let balance = await setup.token.balanceOf(setup.aliceAddress); await ethers.provider.send("evm_increaseTime", [10]); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); + .connect(setup.alice) + .mint(setup.lg.address); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( balance ); }); @@ -209,43 +209,43 @@ describe("Minter", function () { // Test mint without any deposit it("test no deposit", async () => { await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + .connect(setup.alice) + .mint(setup.lg.address); + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( setup.zero ); expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(setup.zero); }); // Test minting with the wrong gauge it("test mint wrong gauge", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_18, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); await setup.minter - .connect(setup.accounts[1]) + .connect(setup.alice) .mint(setup.gaugesAddress[1]); //check - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( setup.zero ); expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(setup.zero); expect( await setup.minter.minted( - setup.accountsAddress[1], + setup.aliceAddress, setup.gaugesAddress[1] ) ).to.equal(setup.zero); @@ -253,28 +253,28 @@ describe("Minter", function () { // Test minting with an invalid gauge address it("test mint not a gauge", async () => { - await expect(setup.minter.mint(setup.accountsAddress[1])).to.revertedWith( + await expect(setup.minter.mint(setup.aliceAddress)).to.revertedWith( "dev: gauge is not added" ); }); // Test minting before inflation begins it("test mint before inflation begins", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_18, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_18, setup.aliceAddress, false); expect(await setup.token.miningEpoch()).to.equal(BigNumber.from("-1")); await setup.minter - .connect(setup.accounts[1]) - .mint(setup.gaugesAddress[0]); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + .connect(setup.alice) + .mint(setup.lg.address); + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( BigNumber.from("0") ); expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(setup.zero); }); @@ -282,20 +282,20 @@ describe("Minter", function () { // Test mintMany function with multiple gauges it("test mintMany function multiple gauges", async () => { //setup - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await setup.gaugesContracts[1] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await setup.gaugesContracts[2] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); let addresses = [ - setup.gaugesAddress[0], + setup.lg.address, setup.gaugesAddress[1], setup.gaugesAddress[2], setup.ZERO_ADDRESS, @@ -304,7 +304,7 @@ describe("Minter", function () { setup.ZERO_ADDRESS, setup.ZERO_ADDRESS, ]; - await setup.minter.connect(setup.accounts[1]).mintMany(addresses); + await setup.minter.connect(setup.alice).mintMany(addresses); //check let total_minted = BigNumber.from("0"); @@ -312,16 +312,16 @@ describe("Minter", function () { for (let i = 0; i < 3; i++) { let gauge = setup.gaugesContracts[i]; let minted = await setup.minter.minted( - setup.accountsAddress[1], + setup.aliceAddress, gauge.address ); expect(minted).to.equal( - await gauge.integrateFraction(setup.accountsAddress[1]) + await gauge.integrateFraction(setup.aliceAddress) ); total_minted = total_minted.add(minted); } - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( total_minted ); }); @@ -329,87 +329,87 @@ describe("Minter", function () { // Test toggling of the mint approval function it("test toggleApproveMint function", async () => { await setup.minter - .connect(setup.accounts[1]) + .connect(setup.alice) .toggleApproveMint(setup.accountsAddress[2]); expect( await setup.minter.allowedToMintFor( setup.accountsAddress[2], - setup.accountsAddress[1] + setup.aliceAddress ) ).to.equal(true); await setup.minter - .connect(setup.accounts[1]) + .connect(setup.alice) .toggleApproveMint(setup.accountsAddress[2]); expect( await setup.minter.allowedToMintFor( setup.accountsAddress[2], - setup.accountsAddress[1] + setup.aliceAddress ) ).to.equal(false); }); // Test minting on behalf of another user it("test mintFor function", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); await setup.minter - .connect(setup.accounts[1]) + .connect(setup.alice) .toggleApproveMint(setup.accountsAddress[2]); expect( await setup.minter.allowedToMintFor( setup.accountsAddress[2], - setup.accountsAddress[1] + setup.aliceAddress ) ).to.equal(true); await setup.minter .connect(setup.accounts[2]) - .mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); + .mintFor(setup.lg.address, setup.aliceAddress); - let expected = await setup.gaugesContracts[0].integrateFraction( - setup.accountsAddress[1] + let expected = await setup.lg.integrateFraction( + setup.aliceAddress ); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal( + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( expected ); expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(expected); }); // Test mintFor function when not approved it("test mintForFail function", async () => { - await setup.gaugesContracts[0] - .connect(setup.accounts[1]) - .deposit(setup.ten_to_the_17, setup.accountsAddress[1]); + await setup.lg + .connect(setup.alice) + .deposit(setup.ten_to_the_17, setup.aliceAddress, false); await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); expect( await setup.minter.allowedToMintFor( setup.accountsAddress[2], - setup.accountsAddress[1] + setup.aliceAddress ) ).to.equal(false); await setup.minter .connect(setup.accounts[2]) - .mintFor(setup.gaugesAddress[0], setup.accountsAddress[1]); + .mintFor(setup.lg.address, setup.aliceAddress); - expect(await setup.token.balanceOf(setup.accountsAddress[1])).to.equal(0); + expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal(0); expect( await setup.minter.minted( - setup.accountsAddress[1], - setup.gaugesAddress[0] + setup.aliceAddress, + setup.lg.address ) ).to.equal(0); }); From 0b3c7d380c9e0a60fbd1a5d612f5f417954c07a3 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 1 Nov 2023 14:25:59 +0900 Subject: [PATCH 029/412] LiquidityGaugeV6 modified for testing --- contracts/curveFork/LiquidityGaugeV6.sol | 134 ++++++++--------------- 1 file changed, 44 insertions(+), 90 deletions(-) diff --git a/contracts/curveFork/LiquidityGaugeV6.sol b/contracts/curveFork/LiquidityGaugeV6.sol index b107a022..2f780a30 100644 --- a/contracts/curveFork/LiquidityGaugeV6.sol +++ b/contracts/curveFork/LiquidityGaugeV6.sol @@ -4,58 +4,15 @@ pragma solidity 0.8.18; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -interface ICRV20 { - function futureEpochTimeWrite() external returns (uint256); - - function rate() external view returns (uint256); -} - -interface IController { - function checkpointGauge(address addr) external; - - function gaugeRelativeWeight( - address addr, - uint256 time - ) external view returns (uint256); -} +import "./interfaces/IGaugeController.sol"; +import "./interfaces/ICRV.sol"; +import "./interfaces/IMinter.sol"; +import "./interfaces/IVotingEscrow.sol"; interface IERC20Extended { function symbol() external view returns (string memory); } -interface IERC1271 { - function isValidSignature( - bytes32 _hash, - bytes calldata _signature - ) external view returns (bytes32); -} - -interface IFactory { - function admin() external view returns (address); -} - -interface IMinter { - function minted( - address user, - address gauge - ) external view returns (uint256); -} - -interface IVotingEscrow { - function userPointEpoch(address addr) external view returns (uint256); - - function userPointHistoryTs( - address addr, - uint256 epoch - ) external view returns (uint256); -} - -interface IVotingEscrowBoost { - function adjustedBalanceOf( - address _account - ) external view returns (uint256); -} - contract LiquidityGaugeV6 is ReentrancyGuard { event Deposit(address indexed provider, uint256 value); event Withdraw(address indexed provider, uint256 value); @@ -135,16 +92,6 @@ contract LiquidityGaugeV6 is ReentrancyGuard { bytes32 public immutable salt; bytes32 public immutable CACHED_DOMAIN_SEPARATOR; - // Address Constants - address public constant CRV = 0xD533a949740bb3306d119CC777fa900bA034cd52; - address public constant GAUGE_CONTROLLER = - 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB; - address public constant MINTER = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0; - address public constant VEBOOST_PROXY = - 0x8E0c00ed546602fD9927DF742bbAbF726D5B0d16; - address public constant VOTING_ESCROW = - 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2; - // ERC20 mapping(address => uint256) public balanceOf; uint256 public totalSupply; @@ -157,9 +104,14 @@ contract LiquidityGaugeV6 is ReentrancyGuard { mapping(address => uint256) public nonces; // Gauge - address public factory; + address public admin; address public lpToken; + address public token; + address public votingEscrow; + address public minter; + address public gaugeController; + bool public isKilled; // [future_epoch_time uint40][inflation_rate uint216] @@ -195,25 +147,32 @@ contract LiquidityGaugeV6 is ReentrancyGuard { address[MAX_REWARDS] public rewardTokens; // Assuming MAX_REWARDS is a constant already defined // Using dynamic array instead of fixed 100000000000000000000000000000 array to avoid warning about collisions - uint256[] public periodTimestamp; - uint256[] public integrateInvSupply; + uint256[100000000000000000000000000000] public periodTimestamp; + uint256[100000000000000000000000000000] public integrateInvSupply; - constructor(address lpToken_) { + constructor( + address lpToken_, + address minter_ + ) { require(lpToken == address(0)); lpToken = lpToken_; - factory = msg.sender; + minter = minter_; + token = IMinter(minter).token(); + gaugeController = IMinter(minter).controller(); + votingEscrow = IGaugeController(gaugeController).votingEscrow(); string memory _symbol = IERC20Extended(lpToken_).symbol(); name = string(abi.encodePacked("Curve.fi ", _symbol, " Gauge Deposit")); symbol = string(abi.encodePacked(_symbol, "-gauge")); periodTimestamp[0] = block.timestamp; + admin = msg.sender; // Assuming you have the CRV20 interface defined somewhere for the following line inflationParams = - (ICRV20(CRV).futureEpochTimeWrite() << 216) + - ICRV20(CRV).rate(); + (ICRV(token).futureEpochTimeWrite() << 216) + + ICRV(token).rate(); NAME_HASH = keccak256(abi.encodePacked(name)); salt = blockhash(block.number - 1); @@ -262,9 +221,9 @@ contract LiquidityGaugeV6 is ReentrancyGuard { _st.newRate = _st.rate; if (_st.prevFutureEpoch >= _st.periodTime) { - _st.newRate = ICRV20(CRV).rate(); + _st.newRate = ICRV(token).rate(); inflationParams = - (ICRV20(CRV).futureEpochTimeWrite() << 216) + + (ICRV(token).futureEpochTimeWrite() << 216) + _st.newRate; } @@ -275,16 +234,13 @@ contract LiquidityGaugeV6 is ReentrancyGuard { if (block.timestamp > _st.periodTime) { uint256 _workingSupply = workingSupply; - IController(GAUGE_CONTROLLER).checkpointGauge(address(this)); + IGaugeController(gaugeController).checkpointGauge(address(this)); uint256 prevWeekTime = _st.periodTime; - uint256 weekTime = ((_st.periodTime + WEEK) / WEEK) * WEEK < - block.timestamp - ? ((_st.periodTime + WEEK) / WEEK) * WEEK - : block.timestamp; + uint256 weekTime = min((_st.periodTime + WEEK) / WEEK * WEEK, block.timestamp); for (uint256 i = 0; i < 500; i++) { uint256 dt = weekTime - prevWeekTime; - uint256 w = IController(GAUGE_CONTROLLER).gaugeRelativeWeight( + uint256 w = IGaugeController(gaugeController).gaugeRelativeWeight( address(this), (prevWeekTime / WEEK) * WEEK ); @@ -314,9 +270,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { break; } prevWeekTime = weekTime; - weekTime = weekTime + WEEK < block.timestamp - ? weekTime + WEEK - : block.timestamp; + weekTime = min(weekTime + WEEK, block.timestamp); } } @@ -362,10 +316,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { _rp.token = rewardTokens[i]; _rp.integral = rewardData[_rp.token].integral; - _rp.lastUpdate = block.timestamp < - rewardData[_rp.token].periodFinish - ? block.timestamp - : rewardData[_rp.token].periodFinish; + _rp.lastUpdate = min(block.timestamp, rewardData[_rp.token].periodFinish); _rp.duration = _rp.lastUpdate - rewardData[_rp.token].lastUpdate; if (_rp.duration != 0) { rewardData[_rp.token].lastUpdate = _rp.lastUpdate; @@ -425,9 +376,8 @@ contract LiquidityGaugeV6 is ReentrancyGuard { uint256 l_, uint256 L_ ) internal { - uint256 _votingBalance = IVotingEscrowBoost(VEBOOST_PROXY) - .adjustedBalanceOf(addr_); - uint256 _votingTotal = IERC20(VOTING_ESCROW).totalSupply(); + uint256 _votingBalance = IVotingEscrow(votingEscrow).balanceOf(addr_); + uint256 _votingTotal = IERC20(votingEscrow).totalSupply(); uint256 _lim = (l_ * TOKENLESS_PRODUCTION) / 100; if (_votingTotal > 0) { @@ -437,7 +387,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { 100; } - _lim = l_ < _lim ? l_ : _lim; + _lim = min(l_, _lim); uint256 _oldBal = workingBalances[addr_]; workingBalances[addr_] = _lim; uint256 _workingSupply = workingSupply + _lim - _oldBal; @@ -641,7 +591,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { } function userCheckpoint(address addr_) external returns (bool) { - require(msg.sender == addr_ || msg.sender == MINTER); + require(msg.sender == addr_ || msg.sender == minter); _checkpoint(addr_); _updateLiquidityLimit(addr_, balanceOf[addr_], totalSupply); return true; @@ -653,14 +603,14 @@ contract LiquidityGaugeV6 is ReentrancyGuard { function kick(address addr_) external { uint256 _tLast = integrateCheckpointOf[addr_]; - uint256 _tVe = IVotingEscrow(VOTING_ESCROW).userPointHistoryTs( + uint256 _tVe = IVotingEscrow(votingEscrow).userPointHistoryTs( addr_, - IVotingEscrow(VOTING_ESCROW).userPointEpoch(addr_) + IVotingEscrow(votingEscrow).userPointEpoch(addr_) ); uint256 _balance = balanceOf[addr_]; require( - ERC20(VOTING_ESCROW).balanceOf(addr_) == 0 || _tVe > _tLast, + ERC20(votingEscrow).balanceOf(addr_) == 0 || _tVe > _tLast, "Not allowed" ); require( @@ -725,7 +675,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { address currentDistributor = rewardData[rewardToken_].distributor; require( msg.sender == currentDistributor || - msg.sender == IFactory(factory).admin() + msg.sender == admin ); require(currentDistributor != address(0)); require(distributor_ != address(0)); @@ -771,7 +721,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { _checkpoint(addr_); return integrateFraction[addr_] - - IMinter(MINTER).minted(addr_, address(this)); + IMinter(minter).minted(addr_, address(this)); } function integrateCheckpoint() external view returns (uint256) { @@ -798,8 +748,12 @@ contract LiquidityGaugeV6 is ReentrancyGuard { return _domainSeparator(); } + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + modifier onlyAdmin() { - require(msg.sender == IFactory(factory).admin()); + require(msg.sender == admin); _; } } From fc449265839d2baa861f399937ee393357ce3306 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 3 Nov 2023 11:04:37 +0900 Subject: [PATCH 030/412] Fixed LiquidityGaugeV6 test to pass --- contracts/curveFork/LiquidityGaugeV6.sol | 53 ++++++++++-------------- test/fork/helper.ts | 2 +- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/contracts/curveFork/LiquidityGaugeV6.sol b/contracts/curveFork/LiquidityGaugeV6.sol index 2f780a30..469ee417 100644 --- a/contracts/curveFork/LiquidityGaugeV6.sol +++ b/contracts/curveFork/LiquidityGaugeV6.sol @@ -115,7 +115,8 @@ contract LiquidityGaugeV6 is ReentrancyGuard { bool public isKilled; // [future_epoch_time uint40][inflation_rate uint216] - uint256 public inflationParams; + uint256 public futureEpochTime; + uint256 public inflationRate; // For tracking external rewards uint256 public rewardCount; @@ -170,9 +171,8 @@ contract LiquidityGaugeV6 is ReentrancyGuard { admin = msg.sender; // Assuming you have the CRV20 interface defined somewhere for the following line - inflationParams = - (ICRV(token).futureEpochTimeWrite() << 216) + - ICRV(token).rate(); + inflationRate = ICRV(token).rate(); + futureEpochTime = ICRV(token).futureEpochTimeWrite(); NAME_HASH = keccak256(abi.encodePacked(name)); salt = blockhash(block.number - 1); @@ -215,16 +215,14 @@ contract LiquidityGaugeV6 is ReentrancyGuard { uint256(uint128(_st.period)) ]; - _st.inflationParams = inflationParams; - _st.rate = _st.inflationParams & ((1 << 216) - 1); - _st.prevFutureEpoch = _st.inflationParams >> 216; + _st.rate = inflationRate; + _st.prevFutureEpoch = futureEpochTime; _st.newRate = _st.rate; if (_st.prevFutureEpoch >= _st.periodTime) { + futureEpochTime = ICRV(token).futureEpochTimeWrite(); _st.newRate = ICRV(token).rate(); - inflationParams = - (ICRV(token).futureEpochTimeWrite() << 216) + - _st.newRate; + inflationRate = _st.newRate; } if (isKilled) { @@ -235,29 +233,29 @@ contract LiquidityGaugeV6 is ReentrancyGuard { if (block.timestamp > _st.periodTime) { uint256 _workingSupply = workingSupply; IGaugeController(gaugeController).checkpointGauge(address(this)); - uint256 prevWeekTime = _st.periodTime; - uint256 weekTime = min((_st.periodTime + WEEK) / WEEK * WEEK, block.timestamp); + uint256 _prevWeekTime = _st.periodTime; + uint256 _weekTime = min((_st.periodTime + WEEK) / WEEK * WEEK, block.timestamp); - for (uint256 i = 0; i < 500; i++) { - uint256 dt = weekTime - prevWeekTime; + for (uint256 i = 0; i < 500;) { + uint256 dt = _weekTime - _prevWeekTime; uint256 w = IGaugeController(gaugeController).gaugeRelativeWeight( address(this), - (prevWeekTime / WEEK) * WEEK + (_prevWeekTime / WEEK) * WEEK ); if (_workingSupply > 0) { if ( - _st.prevFutureEpoch >= prevWeekTime && - _st.prevFutureEpoch < weekTime + _st.prevFutureEpoch >= _prevWeekTime && + _st.prevFutureEpoch < _weekTime ) { _st.integrateInvSupply += (_st.rate * w * - (_st.prevFutureEpoch - prevWeekTime)) / + (_st.prevFutureEpoch - _prevWeekTime)) / _workingSupply; _st.rate = _st.newRate; _st.integrateInvSupply += - (_st.rate * w * (weekTime - _st.prevFutureEpoch)) / + (_st.rate * w * (_weekTime - _st.prevFutureEpoch)) / _workingSupply; } else { _st.integrateInvSupply += @@ -266,11 +264,14 @@ contract LiquidityGaugeV6 is ReentrancyGuard { } } - if (weekTime == block.timestamp) { + if (_weekTime == block.timestamp) { break; } - prevWeekTime = weekTime; - weekTime = min(weekTime + WEEK, block.timestamp); + _prevWeekTime = _weekTime; + _weekTime = min(_weekTime + WEEK, block.timestamp); + unchecked { + ++i; + } } } @@ -728,14 +729,6 @@ contract LiquidityGaugeV6 is ReentrancyGuard { return periodTimestamp[uint256(uint128(period))]; } - function futureEpochTime() external view returns (uint256) { - return inflationParams >> 216; - } - - function inflationRate() external view returns (uint256) { - return inflationParams % 2 ** 216; - } - function decimals() external pure returns (uint256) { return 18; } diff --git a/test/fork/helper.ts b/test/fork/helper.ts index fe761033..71dc7f4a 100644 --- a/test/fork/helper.ts +++ b/test/fork/helper.ts @@ -84,7 +84,7 @@ class TestSetup { const Token = await ethers.getContractFactory("CRV"); const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); const GaugeController = await ethers.getContractFactory("GaugeController"); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGauge"); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); const TestLP = await ethers.getContractFactory("TestLP"); const Minter = await ethers.getContractFactory("Minter"); From 1309abaf5b8e1fe8d1885008b09ca95466c69363 Mon Sep 17 00:00:00 2001 From: takadr Date: Fri, 3 Nov 2023 13:44:16 +0900 Subject: [PATCH 031/412] Added integration tests for FeeDistributor --- contracts/curveFork/FeeDistributor.sol | 106 ++-- contracts/curveFork/VotingEscrow.sol | 6 +- .../FeeDistributor/checkpointTs.test.ts | 122 +++++ .../distributefFeesStateful.test.ts | 506 ++++++++++++++++++ 4 files changed, 686 insertions(+), 54 deletions(-) create mode 100644 test/fork/integration/FeeDistributor/checkpointTs.test.ts create mode 100644 test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts diff --git a/contracts/curveFork/FeeDistributor.sol b/contracts/curveFork/FeeDistributor.sol index 3d906af4..391f9b21 100644 --- a/contracts/curveFork/FeeDistributor.sol +++ b/contracts/curveFork/FeeDistributor.sol @@ -159,8 +159,8 @@ contract FeeDistributor is ReentrancyGuard { break; } uint256 _mid = (_min + _max + 2) / 2; - Point memory pt = IVotingEscrow(ve_).pointHistory(_mid); - if (pt.ts <= timestamp_) { + Point memory _pt = IVotingEscrow(ve_).pointHistory(_mid); + if (_pt.ts <= timestamp_) { _min = _mid; } else { _max = _mid - 1; @@ -185,11 +185,11 @@ contract FeeDistributor is ReentrancyGuard { break; } uint256 _mid = (_min + _max + 2) / 2; - Point memory pt = IVotingEscrow(ve_).userPointHistory( + Point memory _pt = IVotingEscrow(ve_).userPointHistory( user_, _mid ); - if (pt.ts <= timestamp_) { + if (_pt.ts <= timestamp_) { _min = _mid; } else { _max = _mid - 1; @@ -218,14 +218,14 @@ contract FeeDistributor is ReentrancyGuard { _maxUserEpoch ); Point memory _pt = IVotingEscrow(_ve).userPointHistory(user_, _epoch); - return - uint256( - int256( - _pt.bias - - _pt.slope * - int128(int256(timestamp_) - int256(_pt.ts)) - ) - ); + int128 _balance = _pt.bias - + _pt.slope * + int128(int256(timestamp_ - _pt.ts)); + if (_balance < 0) { + return 0; + } else { + return uint256(uint128(_balance)); + } } function _checkpointTotalSupply() internal { @@ -275,16 +275,13 @@ contract FeeDistributor is ReentrancyGuard { if (_t > _pt.ts) { _dt = uint256(int256(_t) - int256(_pt.ts)); } - veSupply[_t] = uint256( - int256( - Math.max( - uint256(int256(_pt.bias)) - - uint256(int256(_pt.slope)) * - _dt, - 0 - ) - ) - ); + + int128 _balance = _pt.bias - _pt.slope * int128(int256(_dt)); + if (_balance < 0) { + veSupply[_t] = 0; + } else { + veSupply[_t] = uint256(uint128(_balance)); + } } _t += WEEK; unchecked { @@ -356,7 +353,12 @@ contract FeeDistributor is ReentrancyGuard { _weekCursor >= _userPoint.ts && _userEpoch <= _maxUserEpoch ) { _userEpoch += 1; - _oldUserPoint = _userPoint; + _oldUserPoint = Point({ + bias: _userPoint.bias, + slope: _userPoint.slope, + ts: _userPoint.ts, + blk: _userPoint.blk + }); if (_userEpoch > _maxUserEpoch) { _userPoint = Point({bias: 0, slope: 0, ts: 0, blk: 0}); } else { @@ -366,25 +368,25 @@ contract FeeDistributor is ReentrancyGuard { ); } } else { - uint256 dt = uint256( - int256(_weekCursor) - int256(_oldUserPoint.ts) - ); - uint256 balanceOf = uint256( - int256( - Math.max( - uint256(int256(_oldUserPoint.bias)) - - dt * - uint256(int256(_oldUserPoint.slope)), - 0 - ) - ) - ); - if (balanceOf == 0 && _userEpoch > _maxUserEpoch) { + int256 _dt = int256(_weekCursor) - int256(_oldUserPoint.ts); + int256 _balanceOf = int256(_oldUserPoint.bias) - + _dt * + int256(_oldUserPoint.slope); + if ( + int256(_oldUserPoint.bias) - + _dt * + int256(_oldUserPoint.slope) < + 0 + ) { + _balanceOf = 0; + } + + if (_balanceOf == 0 && _userEpoch > _maxUserEpoch) { break; } - if (balanceOf > 0) { + if (_balanceOf > 0) { _toDistribute += - (balanceOf * tokensPerWeek[_weekCursor]) / + (uint256(_balanceOf) * tokensPerWeek[_weekCursor]) / veSupply[_weekCursor]; } _weekCursor += WEEK; @@ -432,13 +434,13 @@ contract FeeDistributor is ReentrancyGuard { _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; } - uint256 amount = _claim(_addr, votingEscrow, _lastTokenTime); - if (amount != 0) { - require(IERC20(token).transfer(_addr, amount), "Transfer failed"); - tokenLastBalance -= amount; + uint256 _amount = _claim(_addr, votingEscrow, _lastTokenTime); + if (_amount != 0) { + require(IERC20(token).transfer(_addr, _amount), "Transfer failed"); + tokenLastBalance -= _amount; } - return amount; + return _amount; } /*** @@ -511,18 +513,18 @@ contract FeeDistributor is ReentrancyGuard { uint256 _total = 0; uint256 _l = receivers_.length; for (uint256 i; i < _l; ) { - address addr = receivers_[i]; - if (addr == address(0)) { + address _addr = receivers_[i]; + if (_addr == address(0)) { break; } - uint256 amount = _claim(addr, votingEscrow, _lastTokenTime); - if (amount != 0) { + uint256 _amount = _claim(_addr, votingEscrow, _lastTokenTime); + if (_amount != 0) { require( - IERC20(token).transfer(addr, amount), + IERC20(token).transfer(_addr, _amount), "Transfer failed" ); - _total += amount; + _total += _amount; } unchecked { ++i; @@ -608,9 +610,9 @@ contract FeeDistributor is ReentrancyGuard { function recoverBalance(address coin_) external onlyAdmin returns (bool) { require(coin_ != address(token), "Cannot recover this token"); - uint256 amount = IERC20(coin_).balanceOf(address(this)); + uint256 _amount = IERC20(coin_).balanceOf(address(this)); require( - IERC20(coin_).transfer(emergencyReturn, amount), + IERC20(coin_).transfer(emergencyReturn, _amount), "Transfer failed" ); return true; diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 65879c28..94ec1bc6 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -588,7 +588,10 @@ contract VotingEscrow is ReentrancyGuard { _locked.end = 0; _locked.amount = 0; - locked[msg.sender] = _locked; + locked[msg.sender] = LockedBalance( + _locked.amount, + _locked.end + ); uint256 _supplyBefore = supply; supply = _supplyBefore - _value; @@ -723,7 +726,6 @@ contract VotingEscrow is ReentrancyGuard { uint256 _maxEpoch = epoch; uint256 _epoch = findBlockEpoch(block_, _maxEpoch); Point memory _point0 = pointHistory[_epoch]; - // revert(_epoch.uintToString()); uint256 _dBlock = 0; uint256 _dt = 0; diff --git a/test/fork/integration/FeeDistributor/checkpointTs.test.ts b/test/fork/integration/FeeDistributor/checkpointTs.test.ts new file mode 100644 index 00000000..a411094e --- /dev/null +++ b/test/fork/integration/FeeDistributor/checkpointTs.test.ts @@ -0,0 +1,122 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +const DAY = 86400; +const WEEK = DAY * 7; + +describe("FeeDistributor", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let coinA: Contract; + let votingEscrow: Contract; + let feeDistributor: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + + const FeeDistributor = await ethers.getContractFactory("FeeDistributor"); + const CRV = await ethers.getContractFactory("CRV"); + const Token = await ethers.getContractFactory("MockToken"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await CRV.deploy(); + await token.deployed(); + + coinA = await Token.deploy("Coin A", "USDA", 18); + await coinA.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + feeDistributor = await FeeDistributor.deploy( + votingEscrow.address, + await time.latest(), + coinA.address, + accounts[0].address, + accounts[0].address + ); + await feeDistributor.deployed(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + function generateUniqueRandomNumbers( + count: number, + min: number, + max: number + ): number[] { + const set = new Set(); + while (set.size < count) { + const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; + set.add(randomValue); + } + return Array.from(set); + } + + it("test checkpoint total supply", async function () { + const stAmount = generateUniqueRandomNumbers(10, 1 * 1e4, 100 * 1e4); + const stLocktime = generateUniqueRandomNumbers(10, 1, 52); + const stSleep = generateUniqueRandomNumbers(10, 1, 30); + for (let i = 0; i < 10; i++) { + await token + .connect(accounts[i]) + .approve(votingEscrow.address, ethers.constants.MaxUint256); + await token + .connect(accounts[0]) + .transfer(await accounts[i].address, ethers.utils.parseEther("1000")); + } + + let finalLock = 0; + for (let i = 0; i < 10; i++) { + const sleepTime = Math.floor(stSleep[i] * 86400); + await ethers.provider.send("evm_increaseTime", [sleepTime]); + const lockTime = (await time.latest()) + sleepTime + WEEK * stLocktime[i]; + finalLock = Math.max(finalLock, lockTime); + + await votingEscrow + .connect(accounts[i]) + .createLock( + BigNumber.from(stAmount[i].toFixed(0)).mul((1e14).toString()), + Math.floor(lockTime) + ); + } + + while ((await time.latest()) < finalLock) { + const weekEpoch = + Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; // WEEK * WEEK; + + await time.increaseTo(weekEpoch); + + const weekBlock = await time.latestBlock(); + + await ethers.provider.send("evm_increaseTime", [1]); + + // Max: 42 weeks + // doing checkpoint 3 times is enough + for (let i = 0; i < 3; i++) { + await feeDistributor.connect(accounts[0]).checkpointTotalSupply(); + } + + const expected = await votingEscrow.totalSupplyAt(weekBlock); + const actual = await feeDistributor.veSupply(weekEpoch); + console.log(`expected: ${expected} actual: ${actual}`); + expect(actual).to.equal(expected); + } + }); +}); diff --git a/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts b/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts new file mode 100644 index 00000000..1c4c8f5e --- /dev/null +++ b/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts @@ -0,0 +1,506 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +// 参考) brownie Stateful Tests +// https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html + +const ACCOUNT_NUM = 5; +const NUMBER_OF_ATTEMPTS = 30; +const WEEK = 86400 * 7; +const YEAR = 86400 * 365; +const two_to_the_256_minus_1 = BigNumber.from("2") + .pow(BigNumber.from("256")) + .sub(BigNumber.from("1")); +const MOUNT_DECIMALS = 3; + +// Helper functions to generate random variables -----> +function randomValue(min: number, max: number): BigNumber { + return BigNumber.from( + Math.floor(Math.random() * (max - min) + min).toString() + ); +} +function getRandomAccountNum(): number { + // Corresponds strategy("address", length=5) + let rdm = Math.floor(Math.random() * ACCOUNT_NUM); //0~9 integer + return rdm; +} +function getRandomWeeks(): BigNumber { + // Corresponds strategy("uint256", min_value=1, max_value=12) + return randomValue(1, 12); +} +function getRandomAmounts(): BigNumber { + // Corresponds strategy("decimal", min_value=1, max_value=100, places=3) + return randomValue(1 * 10 ** MOUNT_DECIMALS, 100 * 10 ** MOUNT_DECIMALS).mul( + BigNumber.from(10).pow(18 - MOUNT_DECIMALS) + ); +} +function getRandomsTime(): BigNumber { + // Corresponds strategy("uint256", min_value=0, max_value=86400 * 3) + return randomValue(0, 86400 * 3); +} +// ------------------------------------------------ + +describe("FeeDistributor", function () { + let accounts: SignerWithAddress[]; + let votingEscrow: Contract; + let distributor: Contract; + let feeCoin: Contract; + let token: Contract; + + let lockedUntil: { [key: string]: number } = {}; + let fees: { [key: number]: BigNumber } = {}; // timestamp -> amount + let userClaims: { [key: string]: { [key: number]: BigNumber[] } } = {}; // address -> timestamp -> [claimed, timeCursor] + let totalFees: BigNumber = ethers.utils.parseEther("1"); + + let snapshot: SnapshotRestorer; + + beforeEach(async () => { + snapshot = await takeSnapshot(); + accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); + + const CRV = await ethers.getContractFactory("CRV"); + const Token = await ethers.getContractFactory("MockToken"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + const FeeDistributor = await ethers.getContractFactory("FeeDistributor"); + + token = await CRV.deploy(); + await token.deployed(); + + feeCoin = await Token.deploy("Test Token", "TST", 18); + await feeCoin.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + for (let i = 0; i < ACCOUNT_NUM; i++) { + // ensure accounts[:5] all have tokens that may be locked + await token + .connect(accounts[0]) + .transfer(accounts[i].address, ethers.utils.parseEther("10000000")); + await token + .connect(accounts[i]) + .approve(votingEscrow.address, two_to_the_256_minus_1); + + userClaims[accounts[i].address] = []; + } + + // accounts[0] locks 10,000,000 tokens for 2 years - longer than the maximum duration of the test + await votingEscrow + .connect(accounts[0]) + .createLock( + ethers.utils.parseEther("10000000"), + (await time.latest()) + YEAR * 2 + ); + + lockedUntil[accounts[0].address] = ( + await votingEscrow.lockedEnd(accounts[0].address) + ).toNumber(); + + // a week later we deploy the fee distributor + // await ethers.provider.send("evm_increaseTime", [WEEK]); + await time.increase(WEEK); + + distributor = await FeeDistributor.deploy( + votingEscrow.address, + await time.latest(), + feeCoin.address, + accounts[0].address, + accounts[0].address + ); + await distributor.deployed(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + //--------------------------------------------- functions -----------------------------------------------------------// + async function _checkActiveLock(stAcct: SignerWithAddress) { + // check if `st_acct` has an active lock + if (!lockedUntil[stAcct.address]) { + return false; + } + + const currentTime = await time.latest(); + + if (lockedUntil[stAcct.address] < currentTime) { + await votingEscrow.connect(stAcct).withdraw(); + delete lockedUntil[stAcct.address]; + return false; + } + + return true; + } + //--------------------------------------------- randomly excuted functions -----------------------------------------------------------// + async function ruleNewLock( + stAcct?: SignerWithAddress, + stAmount?: BigNumber, + stWeeks?: BigNumber, + stTime?: BigNumber + ) { + /* + Add a new user lock. + + Arguments + --------- + stAcct : SignerWithAddress + Account to lock tokens for. If this account already has an active + lock, the rule is skipped. + stAmount: BigNumber + Amount of tokens to lock. + stWeeks: BigNumber + Duration of lock, given in weeks. + stTime: BigNumber + Duration to sleep before action, in seconds. + */ + stAcct = stAcct || accounts[getRandomAccountNum()]; + stAmount = stAmount || getRandomAmounts(); + stWeeks = stWeeks || getRandomWeeks(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleNewLock --- + stAcct: ${ + stAcct.address + }, stAmount: ${stAmount.toString()}, stWeeks: ${stWeeks.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + if (!(await _checkActiveLock(stAcct))) { + const until = + (Math.floor((await time.latest()) / WEEK) + stWeeks.toNumber()) * WEEK; + await votingEscrow.connect(stAcct).createLock(stAmount, until); + lockedUntil[stAcct.address] = until; + } + } + + async function ruleExtendLock( + stAcct?: SignerWithAddress, + stWeeks?: BigNumber, + stTime?: BigNumber + ) { + /* + Extend an existing user lock. + + Arguments + --------- + stAcct: SignerWithAddress + Account to extend lock for. If this account does not have an active + lock, the rule is skipped. + stWeeks: BigNumber + Duration to extend the lock, given in weeks. + stTime: BigNumber + Duration to sleep before action, in seconds. + */ + stAcct = stAcct || accounts[getRandomAccountNum()]; + stWeeks = stWeeks || getRandomWeeks(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleExtendLock --- stAmount ${ + stAcct.address + }, stAmount: ${stWeeks.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + if (await _checkActiveLock(stAcct)) { + const until = + (Math.floor( + (await votingEscrow.lockedEnd(stAcct.address)).toNumber() / WEEK + ) + + stWeeks.toNumber()) * + WEEK; + const newUntil = Math.min( + until, + Math.floor(((await time.latest()) + YEAR * 4) / WEEK) * WEEK + ); + await votingEscrow.connect(stAcct).increaseUnlockTime(newUntil); + lockedUntil[stAcct.address] = newUntil; + } + } + + async function ruleIncreaseLockAmount( + stAcct?: SignerWithAddress, + stAmount?: BigNumber, + stTime?: BigNumber + ) { + /* + Increase the amount of an existing user lock. + + Arguments + --------- + stAcct : SignerWithAddress + Account to increase lock amount for. If this account does not have an + active lock, the rule is skipped. + stAmount : BigNumber + Amount of tokens to add to lock. + stTime : number + Duration to sleep before action, in seconds. + */ + stAcct = accounts[getRandomAccountNum()]; + stAmount = getRandomAmounts(); + stTime = getRandomsTime(); + + console.log(` + ruleIncreaseLockAmount --- stAmount ${ + stAcct.address + }, stAmount: ${stAmount.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + if (await _checkActiveLock(stAcct)) { + await votingEscrow.connect(stAcct).increaseAmount(stAmount); + } + } + + async function ruleClaimFees(stAcct?: SignerWithAddress, stTime?: BigNumber) { + /* + Claim fees for a user. + + Arguments + --------- + stAcct : SignerWithAddress + Account to claim fees for. + stTime : number + Duration to sleep before action, in seconds. + */ + stAcct = accounts[getRandomAccountNum()]; + stTime = getRandomsTime(); + + console.log(` + ruleClaimFees --- stAmount ${stAcct.address}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + // For debug --- + // const t0: number = (await distributor.startTime()).toNumber(); + // const ue = await distributor.userEpochOf(stAcct.address); + // const up = await votingEscrow.userPointHistory(stAcct.address, ue); + // console.log(`Week: + // ${Math.floor(((await time.latest()) - t0) / WEEK)} + + // Point: ${up} + // `); + // --- + + const claimed = await feeCoin.balanceOf(stAcct.address); + const tx = await distributor.connect(stAcct)["claim()"](); + const newClaimed = (await feeCoin.balanceOf(stAcct.address)).sub(claimed); + userClaims[stAcct.address][tx.blockNumber] = [ + newClaimed, + await distributor.timeCursorOf(stAcct.address), + ]; + } + + async function ruleTransferFees(stAmount?: BigNumber, stTime?: BigNumber) { + /* + Transfer fees into the distributor and make a checkpoint. + + If this is the first checkpoint, `can_checkpoint_token` is also + enabled. + + Arguments + --------- + stAmount : BigNumber + Amount of fee tokens to add to the distributor. + stTime : number + Duration to sleep before action, in seconds. + */ + stAmount = stAmount || getRandomAmounts(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleTransferFees --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + const tx = await feeCoin._mintForTesting(distributor.address, stAmount); + + if (!(await distributor.canCheckpointToken())) { + await distributor.toggleAllowCheckpointToken(); + await distributor.checkpointToken(); + } + + fees[tx.blockNumber] = stAmount; + totalFees = totalFees.add(stAmount); + } + + async function ruleTransferFeesWithoutCheckpoint( + stAmount?: BigNumber, + stTime?: BigNumber + ) { + /* + Transfer fees into the distributor without checkpointing. + + Arguments + --------- + stAmount : BigNumber + Amount of fee tokens to add to the distributor. + stTime : number + Duration to sleep before action, in seconds. + */ + stAmount = stAmount || getRandomAmounts(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleTransferFeesWithoutCheckpoint --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + const tx = await feeCoin._mintForTesting(distributor.address, stAmount); + + fees[tx.blockNumber] = stAmount; + totalFees = totalFees.add(stAmount); + } + + async function teardown() { + /* + Claim fees for all accounts and verify that only dust remains. + */ + console.log("teardown----"); + if (!(await distributor.canCheckpointToken())) { + //if no token checkpoint occured, add 100,000 tokens prior to teardown + await ruleTransferFees( + ethers.utils.parseEther("100000"), + BigNumber.from("0") + ); + } + + // Need two checkpoints to get tokens fully distributed + // Because tokens for current week are obtained in the next week + // And that is by design + await distributor.checkpointToken(); + await ethers.provider.send("evm_increaseTime", [WEEK * 2]); + await distributor.checkpointToken(); + + for (const acct of accounts) { + // For debug ---> + // const t0: number = (await distributor.startTime()).toNumber(); + // const ue = await distributor.userEpochOf(acct.address); + // const up = await votingEscrow.userPointHistory(acct.address, ue); + // console.log(`Week: + // ${Math.floor(((await time.latest()) - t0) / WEEK)} + + // Point: ${up} + // `); + // <---- + + await distributor.connect(acct)["claim()"](); + } + + const t0: number = (await distributor.startTime()).toNumber(); + const t1: number = Math.floor((await time.latest()) / WEEK) * WEEK; + + const tokensPerUserPerWeek = []; + for (const acct of accounts) { + tokensPerUserPerWeek[acct.address] = []; + for (let w = t0; w < t1 + WEEK; w += WEEK) { + const tokens = (await distributor.tokensPerWeek(w)) + .mul(await distributor.veForAt(acct.address, w)) + .div(await distributor.veSupply(w)); + tokensPerUserPerWeek[acct.address].push(tokens); + } + } + + // Display results-------------------------------------------------- + console.log(`Results: ${tokensPerUserPerWeek}`); + console.log(`TokensPerUserPerWeek------------`); + Object.entries(tokensPerUserPerWeek).forEach(([key, val]) => { + console.log(`${key}: ${val}`); + }); + console.log(``); + console.log(`Fees-----------`); + Object.entries(fees).forEach(([key, val]) => { + console.log(`${key}: ${val}`); + }); + console.log(``); + console.log(`Total Fee-----------`); + console.log(totalFees.toString()); + console.log(``); + console.log(`User claims---------`); + Object.entries(userClaims).forEach(([key, val]) => { + console.log(`${key}:`); + Object.entries(val).forEach(([k, v]) => { + console.log(`${k}: ${v}`); + }); + }); + console.log(``); + console.log(`User balances---------`); + for (const acct of accounts) { + console.log( + acct.address, + (await feeCoin.balanceOf(acct.address)).toString() + ); + } + console.log(`feeCoin balance of Distributor--------`); + console.log((await feeCoin.balanceOf(distributor.address)).toString()); + // ------------------------------------------- + + for (const acct of accounts) { + expect(await feeCoin.balanceOf(acct.address)).to.equal( + tokensPerUserPerWeek[acct.address].reduce( + (a: BigNumber, b: BigNumber) => a.add(b), + BigNumber.from("0") + ) + ); + } + + // Check if all fees are distributed + expect(await feeCoin.balanceOf(distributor.address)).to.be.lt(100); + } + + let func = [ + "ruleNewLock", + "ruleExtendLock", + "ruleIncreaseLockAmount", + "ruleClaimFees", + "ruleTransferFees", + "ruleTransferFeesWithoutCheckpoint", + ]; + + describe("test_deposit_withdraw_voting", function () { + it(`should distributes fee`, async () => { + // Corresponds initializer initialize_new_lock and initialize_transfer_fees + // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html#initializers + // initialize_new_lock: This is equivalent to `rule_new_lock` to make it more likely we have at least 2 accounts locked at the start of the test run. + // initialize_transfer_fees: This is equivalent to `rule_transfer_fees` to make it more likely that claimable fees are available from the start of the test. + const initializerSeed = Math.random(); + if (initializerSeed < 0.2) { + ruleNewLock(); + ruleTransferFees(); + } else if (initializerSeed < 0.4) { + ruleTransferFees(); + ruleNewLock(); + } else if (initializerSeed < 0.6) { + ruleNewLock(); + } else if (initializerSeed < 0.8) { + ruleTransferFees(); + } + + for (let x = 0; x < NUMBER_OF_ATTEMPTS; x++) { + let n = (await randomValue(0, func.length)).toNumber(); + await eval(func[n])(); + } + + await teardown(); + }); + }); +}); From 5ef623e55ddcae3e1bf79a7f0c59909423ac5c43 Mon Sep 17 00:00:00 2001 From: takadr Date: Tue, 7 Nov 2023 14:58:35 +0900 Subject: [PATCH 032/412] Added Minter integration tests and fixed loop conditions on GaugeController --- contracts/curveFork/GaugeController.sol | 4 +- .../LiquidityGauge/liquidityGauge.test.ts | 7 +- .../integration/Minter/components.test.ts | 351 ++++++++++++++++++ .../Minter/minterIntegration.test.ts | 195 ++++++++++ .../depositWithdrawVoting.test.ts | 8 +- 5 files changed, 553 insertions(+), 12 deletions(-) create mode 100644 test/fork/integration/Minter/components.test.ts create mode 100644 test/fork/integration/Minter/minterIntegration.test.ts diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol index 3916e9f0..26352052 100644 --- a/contracts/curveFork/GaugeController.sol +++ b/contracts/curveFork/GaugeController.sol @@ -236,7 +236,7 @@ contract GaugeController { } uint256 _pt = pointsTotal[_t]; - for (int128 _gaugeType = 1; _gaugeType < 100; ) { + for (int128 _gaugeType; _gaugeType < 100; ) { if (_gaugeType == _nGaugeTypes) { break; } @@ -251,7 +251,7 @@ contract GaugeController { _t += WEEK; _pt = 0; // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) - for (int128 _gaugeType = 1; _gaugeType < 100; ) { + for (int128 _gaugeType; _gaugeType < 100; ) { if (_gaugeType == _nGaugeTypes) { break; } diff --git a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts index b2df5451..6d5a99e3 100644 --- a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts +++ b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts @@ -8,14 +8,11 @@ describe("LiquidityGauge", function () { let evm: EVMUtils; let snapshotId: string; - before(async () => { - setup = new TestSetup(); - await setup.setup(); - }); - beforeEach(async () => { evm = new EVMUtils(); snapshotId = await evm.snapshot(); + setup = new TestSetup(); + await setup.setup(); }); afterEach(async () => { diff --git a/test/fork/integration/Minter/components.test.ts b/test/fork/integration/Minter/components.test.ts new file mode 100644 index 00000000..bc733040 --- /dev/null +++ b/test/fork/integration/Minter/components.test.ts @@ -0,0 +1,351 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +const ACCOUNT_NUM = 4; +const NUMBER_OF_ATTEMPTS = 30; +const SCALE = BigNumber.from((1e20).toString()); +const WEEK = 86400 * 7; +const MONTH = 86400 * 30; + +describe("Liquidity Gauge Tests", function () { + let accounts: SignerWithAddress[]; + let gaugeController: Contract; + let liquidityGauge: Contract; + let mockLpToken: Contract; + let minter: Contract; + let token: Contract; + let votingEscrow: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const MockLpToken = await ethers.getContractFactory("TestLP"); + const Token = await ethers.getContractFactory("CRV"); + const Minter = await ethers.getContractFactory("Minter"); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); + const GaugeController = await ethers.getContractFactory("GaugeController"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await Token.deploy(); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + gaugeController = await GaugeController.deploy( + token.address, + votingEscrow.address + ); + await gaugeController.deployed(); + + minter = await Minter.deploy(token.address, gaugeController.address); + await minter.deployed(); + + mockLpToken = await MockLpToken.deploy( + "Curve LP token", + "usdCrv", + 18, + ethers.utils.parseEther("10") + ); + await mockLpToken.deployed(); + + liquidityGauge = await LiquidityGauge.deploy( + mockLpToken.address, + minter.address + ); + await liquidityGauge.deployed(); + + await token.setMinter(minter.address); + await gaugeController.addType("Liquidity", ethers.utils.parseEther("1")); + await gaugeController.addGauge( + liquidityGauge.address, + 0, + ethers.utils.parseEther("10") + ); + + for (let i = 0; i < ACCOUNT_NUM; i++) { + await mockLpToken + .connect(accounts[0]) + .transfer(accounts[i].address, ethers.utils.parseEther("1")); + await mockLpToken + .connect(accounts[i]) + .approve(liquidityGauge.address, ethers.utils.parseEther("1")); + } + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + function generateUniqueRandomNumbers( + count: number, + min: number, + max: number + ): number[] { + const set = new Set(); + while (set.size < count) { + const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; + set.add(randomValue); + } + return Array.from(set); + } + + function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { + if (value.isZero() && target.isZero()) { + return true; + } + + const diff = value.sub(target).abs(); + const sum = value.add(target); + const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); + + // console.log( + // `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` + // ); + // console.log( + // `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` + // ); + + return ratio.lte(tol); + } + + async function showGaugeInfo() { + console.log("Gauge info----"); + console.log( + "GaugeWeight: ", + (await gaugeController.getGaugeWeight(liquidityGauge.address)).toString() + ); + console.log( + "TypeWeight: ", + (await gaugeController.getTypeWeight(0)).toString() + ); + console.log( + "TotalWeight: ", + (await gaugeController.getTotalWeight()).toString() + ); + console.log( + "TypeWeightSum: ", + (await gaugeController.getWeightsSumPerType(0)).toString() + ); + console.log("TimeTotal: ", (await gaugeController.timeTotal()).toString()); + console.log( + "pointsTotal: ", + ( + await gaugeController.pointsTotal( + (await gaugeController.timeTotal()).toString() + ) + ).toString() + ); + console.log( + "gaugeRelativeWeight: ", + ( + await gaugeController.gaugeRelativeWeight( + liquidityGauge.address, + await time.latest() + ) + ).toString() + ); + console.log( + "TotalSupply: ", + (await liquidityGauge.totalSupply()).toString() + ); + console.log("----"); + } + + for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { + it(`tests duration ${i}`, async function () { + const stDuration = generateUniqueRandomNumbers(3, WEEK, MONTH); + const depositTime: number[] = []; + + await time.increase(WEEK); + + for (let i = 0; i < 3; i++) { + await liquidityGauge + .connect(accounts[i + 1]) + .deposit( + ethers.utils.parseEther("1"), + accounts[i + 1].address, + false + ); + depositTime.push(await time.latest()); + + // await showGaugeInfo(); + } + + const durations: number[] = []; + const balances: BigNumber[] = []; + for (let i = 0; i < 3; i++) { + await time.increase(stDuration[i]); + await liquidityGauge + .connect(accounts[i + 1]) + .withdraw(ethers.utils.parseEther("1"), false); + + const duration = (await time.latest()) - depositTime[i]; + durations.push(duration); + await minter.connect(accounts[i + 1]).mint(liquidityGauge.address); + const balance = await token.balanceOf(accounts[i + 1].address); + balances.push(balance); + + // await showGaugeInfo(); + } + + const totalMinted: BigNumber = balances.reduce( + (a: BigNumber, b: BigNumber) => a.add(b), + ethers.BigNumber.from(0) + ); + const weight1 = Math.floor(durations[0]); + const weight2 = Math.floor(weight1 + (durations[1] - durations[0]) * 1.5); + const weight3 = Math.floor(weight2 + (durations[2] - durations[1]) * 3); + const totalWeight = weight1 + weight2 + weight3; + + console.log( + `Total minted: ${totalMinted.toString()}, Total Weight: ${totalWeight.toString()}` + ); + console.log( + `Balance 1: ${balances[0]} (${balances[0] + .mul(SCALE) + .div(totalMinted)}) Weight 1: ${weight1.toString()} (${ + (100 * weight1) / totalWeight + }%)` + ); + console.log( + `Balance 2: ${balances[1]} (${balances[1] + .mul(SCALE) + .div(totalMinted)}) Weight 2: ${weight2.toString()} (${ + (100 * weight2) / totalWeight + }%)` + ); + console.log( + `Balance 3: ${balances[2]} (${balances[2] + .mul(SCALE) + .div(totalMinted)}) Weight 3: ${weight3.toString()} (${ + (100 * weight3) / totalWeight + }%)` + ); + + expect( + approx( + balances[0].mul(SCALE).div(totalMinted), + BigNumber.from(weight1).mul(SCALE).div(totalWeight), + BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[1].mul(SCALE).div(totalMinted), + BigNumber.from(weight2).mul(SCALE).div(totalWeight), + BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[2].mul(SCALE).div(totalMinted), + BigNumber.from(weight3).mul(SCALE).div(totalWeight), + BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE + ) + ).to.be.true; + }); + } + + for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { + it(`tests amounts ${i}`, async function () { + const stAmounts = generateUniqueRandomNumbers(3, 1e17, 1e18); + const depositTime: number[] = []; + + for (let i = 0; i < 3; i++) { + await liquidityGauge + .connect(accounts[i + 1]) + .deposit(stAmounts[i].toString(), accounts[i + 1].address, false); + depositTime.push(await time.latest()); + } + + await time.increase(MONTH); + + const balances: BigNumber[] = []; + for (let i = 0; i < 3; i++) { + liquidityGauge + .connect(accounts[i + 1]) + .withdraw(stAmounts[i].toString(), false); + } + + for (let i = 0; i < 3; i++) { + await minter.connect(accounts[i + 1]).mint(liquidityGauge.address); + balances.push(await token.balanceOf(accounts[i + 1].address)); + } + const totalDeposited: number = stAmounts.reduce( + (a: number, b: number) => a + b, + 0 + ); + const totalMinted: BigNumber = balances.reduce( + (a: BigNumber, b: BigNumber) => a.add(b), + ethers.BigNumber.from(0) + ); + + console.log( + `Total deposited: ${totalDeposited.toString()}, Total minted: ${totalMinted.toString()}` + ); + console.log( + `Balance 1: ${balances[0]} (${balances[0] + .mul(SCALE) + .div(totalMinted)}) Deposited 1: ${stAmounts[0].toString()} (${ + (100 * stAmounts[0]) / totalDeposited + }%)` + ); + console.log( + `Balance 2: ${balances[1]} (${balances[1] + .mul(SCALE) + .div(totalMinted)}) Deposited 2: ${stAmounts[1].toString()} (${ + (100 * stAmounts[1]) / totalDeposited + }%)` + ); + console.log( + `Balance 3: ${balances[2]} (${balances[2] + .mul(SCALE) + .div(totalMinted)}) Deposited 3: ${stAmounts[2].toString()} (${ + (100 * stAmounts[2]) / totalDeposited + }%)` + ); + + expect( + approx( + balances[0].mul(SCALE).div(totalMinted), + BigNumber.from(stAmounts[0].toString()) + .mul(SCALE) + .div(totalDeposited.toString()), + BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[1].mul(SCALE).div(totalMinted), + BigNumber.from(stAmounts[1].toString()) + .mul(SCALE) + .div(totalDeposited.toString()), + BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[2].mul(SCALE).div(totalMinted), + BigNumber.from(stAmounts[2].toString()) + .mul(SCALE) + .div(totalDeposited.toString()), + BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE + ) + ).to.be.true; + }); + } +}); diff --git a/test/fork/integration/Minter/minterIntegration.test.ts b/test/fork/integration/Minter/minterIntegration.test.ts new file mode 100644 index 00000000..33136395 --- /dev/null +++ b/test/fork/integration/Minter/minterIntegration.test.ts @@ -0,0 +1,195 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +const SCALE = BigNumber.from(10).pow(20); +const WEEK = 86400 * 7; +const MONTH = 86400 * 30; +const W = BigNumber.from(10).pow(18); + +describe("Liquidity Gauge Tests", function () { + let admin, bob, charlie, dan: SignerWithAddress; + let gaugeController: Contract; + let threeGauges: Contract[3] = []; + let mockLpToken: Contract; + let minter: Contract; + let token: Contract; + let votingEscrow: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + [admin, bob, charlie, dan] = await ethers.getSigners(); + const MockLpToken = await ethers.getContractFactory("TestLP"); + const Token = await ethers.getContractFactory("CRV"); + const Minter = await ethers.getContractFactory("Minter"); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); + const GaugeController = await ethers.getContractFactory("GaugeController"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await Token.deploy(); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + gaugeController = await GaugeController.deploy( + token.address, + votingEscrow.address + ); + await gaugeController.deployed(); + + minter = await Minter.deploy(token.address, gaugeController.address); + await minter.deployed(); + + mockLpToken = await MockLpToken.deploy( + "Curve LP token", + "usdCrv", + 18, + ethers.utils.parseEther("10") + ); + await mockLpToken.deployed(); + + for (let i = 0; i < 3; i++) { + threeGauges.push( + await LiquidityGauge.deploy(mockLpToken.address, minter.address) + ); + } + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { + if (value.isZero() && target.isZero()) { + return true; + } + + const diff = value.sub(target).abs(); + const sum = value.add(target); + const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); + + console.log( + `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` + ); + console.log( + `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` + ); + + return ratio.lte(tol); + } + + it(`tests mint`, async function () { + const amount = ethers.utils.parseEther("1"); + await token.connect(admin).setMinter(minter.address); + + const typeWeights = [W.div(2), W.mul(2)]; + const gaugeWeights = [W.mul(2), W.mul(1), W.div(2)]; + const gaugeTypes = [0, 0, 1]; + + // Set up types + for (let i = 0; i < typeWeights.length; i++) { + await gaugeController.connect(admin).addType("Liquidity", 0); + await gaugeController.connect(admin).changeTypeWeight(i, typeWeights[i]); + } + + // Set up gauges + for (let i = 0; i < threeGauges.length; i++) { + await gaugeController + .connect(admin) + .addGauge(threeGauges[i].address, gaugeTypes[i], gaugeWeights[i]); + } + + // Transfer tokens to Bob, Charlie and Dan + await mockLpToken.transfer(bob.address, amount); + await mockLpToken.transfer(charlie.address, amount); + await mockLpToken.transfer(dan.address, amount); + + // For weights to activate + await time.increase(WEEK); + + // Bob and Charlie deposit to gauges with different weights + await mockLpToken.connect(bob).approve(threeGauges[1].address, amount); + await threeGauges[1].connect(bob).deposit(amount, bob.address, false); + await mockLpToken.connect(charlie).approve(threeGauges[2].address, amount); + await threeGauges[2] + .connect(charlie) + .deposit(amount, charlie.address, false); + + await time.increase(MONTH); + + await mockLpToken.connect(dan).approve(threeGauges[1].address, amount); + await threeGauges[1].connect(dan).deposit(amount, dan.address, false); + + await time.increase(MONTH); + + // cannot withdraw too much + await expect(threeGauges[1].connect(bob).withdraw(amount.add(1), false)).to + .be.reverted; + + // Withdraw + await threeGauges[1].connect(bob).withdraw(amount, false); + await threeGauges[2].connect(charlie).withdraw(amount, false); + await threeGauges[1].connect(dan).withdraw(amount, false); + + // Balances after withdrawal + expect(await mockLpToken.balanceOf(bob.address)).to.equal(amount); + expect(await mockLpToken.balanceOf(charlie.address)).to.equal(amount); + expect(await mockLpToken.balanceOf(dan.address)).to.equal(amount); + + // Claim for Bob now + await minter.connect(bob).mint(threeGauges[1].address); + const bobTokens = await token.balanceOf(bob.address); + + await time.increase(MONTH); + + // This won't give anything + await minter.connect(bob).mint(threeGauges[1].address); + expect(await token.balanceOf(bob.address)).to.equal(bobTokens); + + await minter.connect(charlie).mint(threeGauges[2].address); + const charlieTokens = await token.balanceOf(charlie.address); + await minter.connect(dan).mint(threeGauges[1].address); + const danTokens = await token.balanceOf(dan.address); + + const S = bobTokens.add(charlieTokens).add(danTokens); + const ww = gaugeWeights.map((w, i) => w.mul(typeWeights[gaugeTypes[i]])); + const Sw = ww[1].add(ww[2]); // Gauge 0 not used + + // Bob and Charlie were there for full time, gauges 1 and 2 + // Dan was in gauge 1 for half the time + expect( + approx( + bobTokens.mul(SCALE).div(S), + ww[1].mul(SCALE).mul(3).div(4).div(Sw), // 0.75 * ww[1] / Sw + BigNumber.from(10).pow(14).mul(2) // = 2e-6 * SCALE + ) + ).to.be.true; + expect( + approx( + charlieTokens.mul(SCALE).div(S), + ww[2].mul(SCALE).div(Sw), // ww[2] / Sw + BigNumber.from(10).pow(14).mul(2) // = 2e-6 * SCALE + ) + ).to.be.true; + expect( + approx( + danTokens.mul(SCALE).div(S), + ww[1].mul(SCALE).div(4).div(Sw), // 0.25 * ww[1] / Sw + BigNumber.from(10).pow(14).mul(2) // = 2e-6 * SCALE + ) + ).to.be.true; + }); +}); diff --git a/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts b/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts index b98d2654..60db4538 100644 --- a/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts +++ b/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts @@ -33,7 +33,7 @@ describe("VotingEscrow", function () { let snapshot: SnapshotRestorer; - before(async () => { + beforeEach(async () => { snapshot = await takeSnapshot(); accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); @@ -68,10 +68,6 @@ describe("VotingEscrow", function () { }); }); - after(async () => { - await snapshot.restore(); - }); - //--------------------------------------------- functions -----------------------------------------------------------// function rdmValue(a): BigNumber { @@ -332,5 +328,7 @@ describe("VotingEscrow", function () { expect(await votingEscrow.totalSupplyAt(blocknumber)).to.equal( total_supply ); + + await snapshot.restore(); }); }); From 72607d55da3caed8e586d6acad74caa413a4b61c Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 7 Nov 2023 23:04:29 +0900 Subject: [PATCH 033/412] Add test code for LiquidityGauge --- contracts/curveFork/LiquidityGaugeV6.sol | 2 +- .../unitary/LiquidityGauge/checkpoint.test.ts | 45 +++++++++ .../LiquidityGauge/depositWithdraw.test.ts | 92 +++++++++++++++++++ test/fork/unitary/LiquidityGauge/kick.test.ts | 65 +++++++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 test/fork/unitary/LiquidityGauge/checkpoint.test.ts create mode 100644 test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts create mode 100644 test/fork/unitary/LiquidityGauge/kick.test.ts diff --git a/contracts/curveFork/LiquidityGaugeV6.sol b/contracts/curveFork/LiquidityGaugeV6.sol index 469ee417..65a85714 100644 --- a/contracts/curveFork/LiquidityGaugeV6.sol +++ b/contracts/curveFork/LiquidityGaugeV6.sol @@ -592,7 +592,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { } function userCheckpoint(address addr_) external returns (bool) { - require(msg.sender == addr_ || msg.sender == minter); + require(msg.sender == addr_ || msg.sender == minter, "dev: unauthorized"); _checkpoint(addr_); _updateLiquidityLimit(addr_, balanceOf[addr_], totalSupply); return true; diff --git a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts new file mode 100644 index 00000000..86314270 --- /dev/null +++ b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts @@ -0,0 +1,45 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, TestSetup } from "../../helper"; + + +const YEAR = 86400 * 365; + +describe("LiquidityGauge checkpoint", function () { + let setup: TestSetup; + let evm: EVMUtils; + let snapshotId: string; + + beforeEach(async () => { + setup = new TestSetup(); + await setup.setup(); + }); + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + }); + + it("test_user_checkpoint", async function () { + // Assuming `userCheckpoint` is a function on your contract + await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + }); + + it("test_user_checkpoint_new_period", async function () { + await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + + // Increase the time on the blockchain + await ethers.provider.send('evm_increaseTime', [(YEAR * 1.1)]); + await ethers.provider.send('evm_mine'); // this one will actually mine a new block + + await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + }); + + it("test_user_checkpoint_wrong_account", async function () { + // Expect the transaction to be reverted with the specified error message + await expect( + setup.lg.connect(setup.alice).userCheckpoint(setup.bobAddress) + ).to.be.revertedWith("dev: unauthorized"); + }); +}); diff --git a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts new file mode 100644 index 00000000..b379e624 --- /dev/null +++ b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts @@ -0,0 +1,92 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, TestSetup } from "../../helper"; + +describe("LiquidityGauge depositWithdraw", function () { + let setup: TestSetup; + let evm: EVMUtils; + let snapshotId: string; + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + setup = new TestSetup(); + await setup.setup(); + await setup.mockLpToken.approve(setup.lg.address, setup.ten_to_the_21.mul("2")); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + it("should deposit tokens", async function () { + const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const depositAmount = BigNumber.from(100000); + await setup.lg.connect(setup.creator).deposit(100000, setup.creatorAddress, false); + + expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(depositAmount); + expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance.sub(depositAmount)); + expect(await setup.lg.totalSupply()).to.equal(depositAmount); + expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(depositAmount); + }); + + it("should handle zero deposit", async function () { + const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + await setup.lg.deposit(0, setup.creatorAddress, false); + + expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(BigNumber.from(0)); + expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance); + expect(await setup.lg.totalSupply()).to.equal(BigNumber.from(0)); + expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(BigNumber.from(0)); + }); + + it("should revert on deposit with insufficient balance", async function () { + await expect(setup.lg.connect(setup.alice).deposit(100000, setup.creatorAddress, false)).to.be.reverted; + }); + + it("should withdraw tokens", async function () { + const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const depositAmount = BigNumber.from(100000); + + await setup.lg.deposit(depositAmount, setup.creatorAddress, false); + await setup.lg.withdraw(depositAmount, false); + + expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(0); + expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance); + expect(await setup.lg.totalSupply()).to.equal(0); + expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(0); + }); + + it("should handle zero withdrawal", async function () { + const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const depositAmount = BigNumber.from(100000); + + await setup.lg.deposit(depositAmount, setup.creatorAddress, false); + await setup.lg.withdraw(0, false); + + expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(depositAmount); + expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance.sub(depositAmount)); + expect(await setup.lg.totalSupply()).to.equal(depositAmount); + expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(depositAmount); + }); + + it("should withdraw tokens after a new epoch", async function () { + const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const depositAmount = BigNumber.from(100000); + + await setup.lg.deposit(depositAmount, setup.creatorAddress, false); + + await ethers.provider.send("evm_increaseTime", [86400 * 400]); + await ethers.provider.send("evm_mine"); + + await setup.lg.withdraw(depositAmount, false); + + expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(0); + expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance); + expect(await setup.lg.totalSupply()).to.equal(0); + expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(0); + }); + + // Add additional tests as needed... +}); diff --git a/test/fork/unitary/LiquidityGauge/kick.test.ts b/test/fork/unitary/LiquidityGauge/kick.test.ts new file mode 100644 index 00000000..f70c9bba --- /dev/null +++ b/test/fork/unitary/LiquidityGauge/kick.test.ts @@ -0,0 +1,65 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { EVMUtils, TestSetup } from "../../helper"; + +const MAX_UINT256 = ethers.constants.MaxUint256; +const WEEK = 7 * 86400; +const DEPOSIT_AMOUNT = BigNumber.from("10").pow("21"); // 10^21 +const LOCK_AMOUNT = BigNumber.from("10").pow("20"); // 10^20 + +describe("LiquidityGauge kick", function () { + let setup: TestSetup; + let evm: EVMUtils; + let snapshotId: string; + + beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); + setup = new TestSetup(); + await setup.setup(); + await setup.token.transfer(setup.aliceAddress, LOCK_AMOUNT); + await setup.mockLpToken.transfer(setup.aliceAddress, DEPOSIT_AMOUNT); + }); + + afterEach(async () => { + await evm.restore(snapshotId); + }); + + it("test kick functionality", async function () { + // Forward time by 2 weeks + 5 seconds + await ethers.provider.send("evm_increaseTime", [2 * WEEK + 5]); + await ethers.provider.send("evm_mine"); + + // Alice approves tokens to voting escrow and creates a lock + await setup.token.connect(setup.alice).approve(setup.votingEscrow.address, MAX_UINT256); + await setup.votingEscrow.connect(setup.alice).createLock(LOCK_AMOUNT, (await ethers.provider.getBlock('latest')).timestamp + 4 * WEEK); + + // Alice approves LP tokens to Gauge and deposits + await setup.mockLpToken.connect(setup.alice).approve(setup.lg.address, MAX_UINT256); + await setup.lg.connect(setup.alice).deposit(DEPOSIT_AMOUNT, setup.aliceAddress, false); + + // Check working balance of Alice in Gauge + expect(await setup.lg.workingBalances(setup.aliceAddress)).to.equal(DEPOSIT_AMOUNT); + + // Forward time by 1 week + await ethers.provider.send("evm_increaseTime", [WEEK]); + await ethers.provider.send("evm_mine"); + + // Bob tries to kick Alice but should fail because it's not allowed yet + await expect(setup.lg.connect(setup.bob).kick(setup.aliceAddress)).to.be.revertedWith("Not allowed"); + + // Forward time by 4 weeks + await ethers.provider.send("evm_increaseTime", [4 * WEEK]); + await ethers.provider.send("evm_mine"); + + // Now Bob kicks Alice + await setup.lg.connect(setup.bob).kick(setup.aliceAddress); + + // Check the working balance of Alice after kick + expect(await setup.lg.workingBalances(setup.aliceAddress)).to.equal(LOCK_AMOUNT.mul(4)); + + // Trying to kick again should fail as it's not needed + await expect(setup.lg.connect(setup.bob).kick(setup.aliceAddress)).to.be.revertedWith("Not needed"); + }); +}); From 86934dc0491c80b2a8330fb44630c6d940fcbe18 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 8 Nov 2023 09:37:37 +0900 Subject: [PATCH 034/412] Remove old versions of LiquidityGauge --- contracts/curveFork/LiquidityGauge.sol | 398 ------------------------- 1 file changed, 398 deletions(-) delete mode 100644 contracts/curveFork/LiquidityGauge.sol diff --git a/contracts/curveFork/LiquidityGauge.sol b/contracts/curveFork/LiquidityGauge.sol deleted file mode 100644 index 35406b1b..00000000 --- a/contracts/curveFork/LiquidityGauge.sol +++ /dev/null @@ -1,398 +0,0 @@ -pragma solidity 0.8.18; - -/*** - *@title Liquidity Gauge - * SPDX-License-Identifier: MIT - *@notice Used for measuring liquidity and insurance - */ - -//dao-contracts -import "./interfaces/IGaugeController.sol"; -import "./interfaces/ICRV.sol"; -import "./interfaces/IMinter.sol"; -import "./interfaces/IVotingEscrow.sol"; - - -//libraries -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/utils/math/Math.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; - -contract LiquidityGauge is ReentrancyGuard { - event Deposit(address indexed provider, uint256 value); - event Withdraw(address indexed provider, uint256 value); - event UpdateLiquidityLimit( - address user, - uint256 original_balance, - uint256 original_supply, - uint256 working_balance, - uint256 working_supply, - uint256 voting_balance, - uint256 voting_total - ); - - uint256 constant TOKENLESS_PRODUCTION = 40; - uint256 constant BOOST_WARMUP = 86400 * 14; - uint256 constant WEEK = 604800; - - //Contracts - IMinter public minter; - ICRV public token; - IERC20 public template; - IGaugeController public controller; - IVotingEscrow public voting_escrow; - - mapping(address => uint256) public balanceOf; - uint256 public totalSupply; - uint256 public future_epoch_time; - - // caller -> recipient -> can deposit? - mapping(address => mapping(address => bool)) public approved_to_deposit; - - mapping(address => uint256) public working_balances; - uint256 public working_supply; - - // The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint - // All values are kept in units of being multiplied by 1e18 - uint256 public period; //modified from "int256 public period" since it never be minus. - - uint256[100000000000000000000000000000] public period_timestamp; - - // 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint - uint256[100000000000000000000000000000] public integrate_inv_supply; // bump epoch when rate() changes. Iis(t)=int(r'(t)/S(t))dt (from CurveDAO whitepaper) - - // 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint - mapping(address => uint256) public integrate_inv_supply_of; - mapping(address => uint256) public integrate_checkpoint_of; - - // ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint - // Units rate * t = already number of coins per address to issue - mapping(address => uint256) public integrateFraction; //Mintable Token amount (include minted amount) - - uint256 public inflation_rate; - bool public is_killed; - - address public admin; - - modifier onlyOwner() { - require( - admin == msg.sender, - "Caller is not allowed to operate" - ); - _; - } - - /*** - *@notice Contract constructor - *@param _lp_addr Liquidity Pool contract address - *@param _minter Minter contract address - */ - constructor( - address _lp_addr, - address _minter - ) { - require(_lp_addr != address(0)); - require(_minter != address(0)); - - template = IERC20(_lp_addr); - minter = IMinter(_minter); - address _token_addr = minter.token(); - token = ICRV(_token_addr); - controller = IGaugeController(minter.controller()); - voting_escrow = IVotingEscrow(controller.votingEscrow()); - period_timestamp[0] = block.timestamp; - inflation_rate = token.rate(); - future_epoch_time = token.futureEpochTimeWrite(); - admin = msg.sender; - } - - /*** - *@notice Calculate limits which depend on the amount of Token per-user. - * Effectively it calculates working balances to apply amplification - * of production by - *@param _addr User address - *@param _l User's amount of liquidity (LP tokens) - *@param _L Total amount of liquidity (LP tokens) - */ - function _update_liquidity_limit( - address _addr, - uint256 _l, - uint256 _L - ) internal { - // To be called after totalSupply is updated - uint256 _voting_balance = voting_escrow.balanceOf( - _addr, - block.timestamp - ); - uint256 _voting_total = voting_escrow.totalSupply(block.timestamp); - - uint256 _lim = (_l * TOKENLESS_PRODUCTION) / 100; - if ( - (_voting_total > 0) && - (block.timestamp > period_timestamp[0] + BOOST_WARMUP) - ) { - _lim += - (_L * _voting_balance * (100 - TOKENLESS_PRODUCTION)) / - _voting_total / - 100; - } - - _lim = min(_l, _lim); - uint256 _old_bal = working_balances[_addr]; - working_balances[_addr] = _lim; - uint256 _working_supply = working_supply + _lim - _old_bal; - working_supply = _working_supply; - - emit UpdateLiquidityLimit( - _addr, - _l, - _L, - _lim, - _working_supply, - _voting_balance, - _voting_total - ); - } - - //to avoid "stack too deep" - struct CheckPointParameters { - uint256 period; - uint256 period_time; - uint256 integrate_inv_supply; - uint256 rate; - uint256 new_rate; - uint256 prev_future_epoch; - uint256 working_balance; - uint256 working_supply; - } - - /*** - *@notice Checkpoint for a user - *@param _addr User address - * - *This function does, - *1. Calculate Iis for All: Calc and add Iis for every week. Iis only increses over time. - *2. Calculate Iu for _addr: Calc by (defferece between Iis(last time) and Iis(this time))* LP deposit amount of _addr(include locking boost) - * - * working_supply & working_balance = total_supply & total_balance with locking boost。 - * Check whitepaper about Iis and Iu. - */ - function _checkpoint(address _addr) internal { - CheckPointParameters memory _st; - - _st.period = period; - _st.period_time = period_timestamp[_st.period]; - _st.integrate_inv_supply = integrate_inv_supply[_st.period]; - _st.rate = inflation_rate; - _st.new_rate = _st.rate; - _st.prev_future_epoch = future_epoch_time; - if (_st.prev_future_epoch >= _st.period_time) { - //update future_epoch_time & inflation_rate - future_epoch_time = token.futureEpochTimeWrite(); - _st.new_rate = token.rate(); - inflation_rate = _st.new_rate; - } - controller.checkpointGauge(address(this)); - - uint256 _working_balance = working_balances[_addr]; - uint256 _working_supply = working_supply; - - if (is_killed) { - _st.rate = 0; // Stop distributing inflation as soon as killed - } - - // Update integral of 1/supply - if (block.timestamp > _st.period_time) { - uint256 _prev_week_time = _st.period_time; - uint256 _week_time; - unchecked { - _week_time = min( - ((_st.period_time + WEEK) / WEEK) * WEEK, - block.timestamp - ); - } - - for (uint256 i; i < 500; ) { - uint256 _dt = _week_time - _prev_week_time; - uint256 _w = controller.gaugeRelativeWeight( - address(this), - (_prev_week_time / WEEK) * WEEK - ); - - if (_working_supply > 0) { - if ( - _st.prev_future_epoch >= _prev_week_time && - _st.prev_future_epoch < _week_time - ) { - // If we went across one or multiple epochs, apply the rate - // of the first epoch until it ends, and then the rate of - // the last epoch. - // If more than one epoch is crossed - the gauge gets less, - // but that'd meen it wasn't called for more than 1 year - _st.integrate_inv_supply += - (_st.rate * - _w * - (_st.prev_future_epoch - _prev_week_time)) / - _working_supply; - _st.rate = _st.new_rate; - _st.integrate_inv_supply += - (_st.rate * - _w * - (_week_time - _st.prev_future_epoch)) / - _working_supply; - } else { - _st.integrate_inv_supply += - (_st.rate * _w * _dt) / - _working_supply; - } - // On precisions of the calculation - // rate ~= 10e18 - // last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) - // _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) - // The largest loss is at dt = 1 - // Loss is 1e-9 - acceptable - } - if (_week_time == block.timestamp) { - break; - } - _prev_week_time = _week_time; - _week_time = min(_week_time + WEEK, block.timestamp); - unchecked { - ++i; - } - } - } - - _st.period += 1; - period = _st.period; - period_timestamp[_st.period] = block.timestamp; - integrate_inv_supply[_st.period] = _st.integrate_inv_supply; - - // Update user-specific integrals - // Calc the ΔIu of _addr and add it to Iu. - integrateFraction[_addr] += - (_working_balance * - (_st.integrate_inv_supply - integrate_inv_supply_of[_addr])) / - 10**18; - integrate_inv_supply_of[_addr] = _st.integrate_inv_supply; - integrate_checkpoint_of[_addr] = block.timestamp; - } - - /*** - *@notice Record a checkpoint for `_addr` - *@param _addr User address - *@return bool success - */ - function userCheckpoint(address _addr) external returns (bool) { - require( - (msg.sender == _addr) || (msg.sender == address(minter)), - "dev: unauthorized" - ); - _checkpoint(_addr); - _update_liquidity_limit(_addr, balanceOf[_addr], totalSupply); - return true; - } - - /*** - *@notice Get the number of claimable tokens per user - *@dev This function should be manually changed to "view" in the ABI - *@return uint256 number of claimable tokens per user - */ - function claimable_tokens(address _addr) external returns (uint256) { - _checkpoint(_addr); - return (integrateFraction[_addr] - - minter.minted(_addr, address(this))); - } - - /*** - *@notice Kick `_addr` for abusing their boost - *@dev Only if either they had another voting event, or their voting escrow lock expired - *@param _addr Address to kick - */ - function kick(address _addr) external { - uint256 _t_last = integrate_checkpoint_of[_addr]; - uint256 _t_ve = voting_escrow.userPointHistoryTs( - _addr, - voting_escrow.userPointEpoch(_addr) - ); - uint256 _balance = balanceOf[_addr]; - - require( - voting_escrow.balanceOf(_addr, block.timestamp) == 0 || - _t_ve > _t_last, - "dev: kick not allowed" - ); - require( - working_balances[_addr] > (_balance * TOKENLESS_PRODUCTION) / 100, - "dev: kick not needed" - ); - - _checkpoint(_addr); - _update_liquidity_limit(_addr, balanceOf[_addr], totalSupply); - } - - /*** - *@notice Set whether `_addr` can deposit tokens for `msg.sender` - *@param _addr Address to set approval on - *@param can_deposit bool - can this account deposit for `msg.sender`? - */ - function set_approve_deposit(address _addr, bool can_deposit) external { - approved_to_deposit[_addr][msg.sender] = can_deposit; - } - - /*** - *@notice Deposit `_value` LP tokens - *@param _value Number of tokens to deposit - *@param _addr Address to deposit for - */ - function deposit(uint256 _value, address _addr, bool claimRewards_) external nonReentrant { - if (_addr != msg.sender) { - require(approved_to_deposit[msg.sender][_addr], "Not approved"); - } - - _checkpoint(_addr); - - if (_value != 0) { - uint256 _balance = balanceOf[_addr] + _value; - uint256 _supply = totalSupply + _value; - balanceOf[_addr] = _balance; - totalSupply = _supply; - - _update_liquidity_limit(_addr, _balance, _supply); - - require(template.transferFrom(msg.sender, address(this), _value)); - } - emit Deposit(_addr, _value); - } - - /*** - *@notice Withdraw `_value` LP tokens - *@param _value Number of tokens to withdraw - */ - function withdraw(uint256 _value, bool claimRewards_) external nonReentrant { - _checkpoint(msg.sender); - - uint256 _balance = balanceOf[msg.sender] - _value; - uint256 _supply = totalSupply - _value; - balanceOf[msg.sender] = _balance; - totalSupply = _supply; - - _update_liquidity_limit(msg.sender, _balance, _supply); - - require(template.transfer(msg.sender, _value)); - - emit Withdraw(msg.sender, _value); - } - - function integrate_checkpoint() external view returns (uint256) { - return period_timestamp[period]; - } - - function kill_me() external onlyOwner { - is_killed = !is_killed; - } - - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } -} From ffb51cf357907d884bdb3ff33f161fefc31f5f9e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 8 Nov 2023 11:54:21 +0900 Subject: [PATCH 035/412] beforeEach was duplicated --- test/fork/unitary/LiquidityGauge/checkpoint.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts index 86314270..50bb92aa 100644 --- a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts +++ b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts @@ -12,13 +12,14 @@ describe("LiquidityGauge checkpoint", function () { let snapshotId: string; beforeEach(async () => { + evm = new EVMUtils(); + snapshotId = await evm.snapshot(); setup = new TestSetup(); await setup.setup(); }); - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); + afterEach(async () => { + await evm.restore(snapshotId); }); it("test_user_checkpoint", async function () { From 37600ed1ba041555051df317d02a3bda9fe19315 Mon Sep 17 00:00:00 2001 From: takadr Date: Wed, 8 Nov 2023 16:59:18 +0900 Subject: [PATCH 036/412] Add integration tests for GaugeController --- .../GaugeController/typesAndWeights.test.ts | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 test/fork/integration/GaugeController/typesAndWeights.test.ts diff --git a/test/fork/integration/GaugeController/typesAndWeights.test.ts b/test/fork/integration/GaugeController/typesAndWeights.test.ts new file mode 100644 index 00000000..9218028d --- /dev/null +++ b/test/fork/integration/GaugeController/typesAndWeights.test.ts @@ -0,0 +1,272 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +// 参考) brownie Stateful Tests +// https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html + +type GaugeInfo = { + contract: Contract; + type: number; + weight: BigNumber; +}; +const ACCOUNT_NUM = 5; +const MAX_EXAMPLES = 50; +const STATEFUL_STEP_COUNT = 10; +const WEEK = 86400 * 7; + +// Validate gauge weights and gauge weight sum. + +// Strategies +// ---------- +// st_type : Decimal +// Gauge type, multiplied by `len(self.gauges)` to choose a value +// st_gauge_weight : int +// Gauge weight +// st_type_wiehgt : int +// Type weight + +// Helper functions to generate random variables -----> +function randomBigValue(min: number, max: number): BigNumber { + return BigNumber.from( + Math.floor(Math.random() * (max - min) + min).toString() + ); +} +function randomValue(min: number, max: number): number { + return Math.floor(Math.random() * (max - min) + min); +} +function getRandomType(): number { + // Corresponds strategy("decimal", min_value=0, max_value="0.99999999") + return randomValue(0, 99999999) / 100000000; +} +function getRandomWeight(): BigNumber { + // Corresponds strategy("uint", min_value=10 ** 17, max_value=10 ** 19) + return randomBigValue(10 ** 17, 10 ** 19); +} +// ------------------------------------------------ + +describe("GaugeController", function () { + let accounts: SignerWithAddress[]; + let votingEscrow: Contract; + let gaugeController: Contract; + let mockLpToken: Contract; + let token: Contract; + let minter: Contract; + + let typeWeights: BigNumber[] = []; + let gauges: GaugeInfo[] = []; + + let snapshot: SnapshotRestorer; + + beforeEach(async () => { + snapshot = await takeSnapshot(); + accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); + + const MockLpToken = await ethers.getContractFactory("TestLP"); + const Token = await ethers.getContractFactory("CRV"); + const Minter = await ethers.getContractFactory("Minter"); + const GaugeController = await ethers.getContractFactory("GaugeController"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await Token.deploy(); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + gaugeController = await GaugeController.deploy( + token.address, + votingEscrow.address + ); + await gaugeController.deployed(); + + minter = await Minter.deploy(token.address, gaugeController.address); + await minter.deployed(); + + mockLpToken = await MockLpToken.deploy( + "Curve LP token", + "usdCrv", + 18, + ethers.utils.parseEther("10") + ); + await mockLpToken.deployed(); + + typeWeights = []; + gauges = []; + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + //--------------------------------------------- helper functions -----------------------------------------------------------// + + function _gaugeWeight(idx: number): BigNumber { + return gauges.reduce((sum, gauge) => { + return gauge.type === idx ? sum.add(gauge.weight) : sum; + }, BigNumber.from("0")); + } + + //--------------------------------------------- Initializer functions -----------------------------------------------------------// + async function initializeAddType() { + await ruleAddType(); + + // Check + await invariants(); + } + //--------------------------------------------- randomly exceuted functions -----------------------------------------------------------// + async function ruleAddType(stTypeWeight?: BigNumber) { + /* + Add a new gauge type. + */ + stTypeWeight = stTypeWeight || getRandomWeight(); + await gaugeController.connect(accounts[0]).addType("Type!", stTypeWeight); + typeWeights.push(stTypeWeight); + + console.log(`ruleAddType --- stTypeWeight: ${stTypeWeight.toString()}`); + + // Check + await invariants(); + } + + async function ruleAddGauge(gaugeType?: number, stGaugeWeight?: BigNumber) { + /* + Add a new gauge. + + If no types have been added, this rule has not effect. + */ + const stType = getRandomType(); + stGaugeWeight = stGaugeWeight || getRandomWeight(); + + if (typeWeights.length === 0) return; + + gaugeType = gaugeType || Math.floor(stType * typeWeights.length); + console.log( + `ruleAddGauge --- gaugeType: ${gaugeType}, stGaugeWeight: ${stGaugeWeight.toString()}` + ); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); + const gauge = await LiquidityGauge.deploy( + mockLpToken.address, + minter.address + ); + await gauge.deployed(); + + await gaugeController + .connect(accounts[0]) + .addGauge(gauge.address, gaugeType, stGaugeWeight); + + gauges.push({ contract: gauge, type: gaugeType, weight: stGaugeWeight }); + + // Check + await invariants(); + } + + async function invariants() { + await invariantGaugeWeightSums(); + await invariantTotalTypeWeight(); + await invariantRelativeGaugeWeight(); + } + + async function invariantGaugeWeightSums() { + // Validate the gauge weight sums per type. + for (let i = 0; i < typeWeights.length; i++) { + const gaugeWeightSum = _gaugeWeight(i); + expect(await gaugeController.getWeightsSumPerType(i)).to.be.eq( + gaugeWeightSum + ); + } + } + + async function invariantTotalTypeWeight() { + // Validate the total weight. + const totalWeight = typeWeights.reduce((total, weight, idx) => { + return total.add(_gaugeWeight(idx).mul(weight)); + }, BigNumber.from("0")); + + expect(await gaugeController.getTotalWeight()).to.be.eq(totalWeight); + } + + async function invariantRelativeGaugeWeight() { + // Validate the relative gauge weights. + await ethers.provider.send("evm_increaseTime", [WEEK]); + + const totalWeight = typeWeights.reduce((total, weight, idx) => { + return total.add(_gaugeWeight(idx).mul(weight)); + }, BigNumber.from("0")); + + for (let i = 0; i < gauges.length; i++) { + await gaugeController + .connect(accounts[0]) + .checkpointGauge(gauges[i].contract.address); + const expected = BigNumber.from("10") + .pow(18) + .mul(typeWeights[gauges[i].type]) + .mul(gauges[i].weight) + .div(totalWeight); + expect( + await gaugeController.gaugeRelativeWeight( + gauges[i].contract.address, + await time.latest() + ) + ).to.be.eq(expected); + } + } + + async function showStates() { + console.log("States ------------------------"); + console.log("typeWeights.length: ", typeWeights.length); + console.log("gauges.length: ", gauges.length); + for (let i = 0; i < typeWeights.length; i++) { + console.log( + `getWeightsSumPerType(${i}): ${( + await gaugeController.getWeightsSumPerType(i) + ).toString()}` + ); + } + const totalWeight = typeWeights.reduce((total, weight, idx) => { + return total.add(_gaugeWeight(idx).mul(weight)); + }, BigNumber.from("0")); + console.log(`totalWeight: ${totalWeight.toString()}`); + + for (let i = 0; i < gauges.length; i++) { + console.log( + `gaugeRelativeWeight(${gauges[i].contract.address}): ${( + await gaugeController.gaugeRelativeWeight( + gauges[i].contract.address, + await time.latest() + ) + ).toString()}` + ); + } + console.log("------------------------"); + } + + let func = ["ruleAddType", "ruleAddGauge"]; + + describe("gauge weights and gauge weight sum", function () { + for (let i = 0; i < MAX_EXAMPLES; i++) { + it(`tests gauge weights and gauge weight sum ${i}`, async () => { + await initializeAddType(); + + const steps = randomValue(1, STATEFUL_STEP_COUNT); + for (let x = 0; x < steps; x++) { + let n = randomValue(0, func.length); + await eval(func[n])(); + } + + // await showStates(); + }); + } + }); +}); From a7f73d1f8d04152bb7cc644788ff819784be8a7d Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 8 Nov 2023 23:23:49 +0900 Subject: [PATCH 037/412] Fix GaugeController and rewrite test code. --- contracts/curveFork/GaugeController.sol | 4 +- test/fork/Constants.ts | 61 ++++ test/fork/helper.ts | 162 ++++++++-- .../gaugecontrollerAdmin.test.ts | 65 ++++ .../GaugeController/gaugesWeights.test.ts | 278 ++++++------------ .../GaugeController/timestamps.test.ts | 77 +++-- .../GaugeController/totalWeight.test.ts | 115 ++++---- .../fork/unitary/GaugeController/vote.test.ts | 238 ++++++--------- .../GaugeController/voteWeightUnitary.test.ts | 164 +++++------ 9 files changed, 608 insertions(+), 556 deletions(-) create mode 100644 test/fork/Constants.ts create mode 100644 test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol index 3916e9f0..26352052 100644 --- a/contracts/curveFork/GaugeController.sol +++ b/contracts/curveFork/GaugeController.sol @@ -236,7 +236,7 @@ contract GaugeController { } uint256 _pt = pointsTotal[_t]; - for (int128 _gaugeType = 1; _gaugeType < 100; ) { + for (int128 _gaugeType; _gaugeType < 100; ) { if (_gaugeType == _nGaugeTypes) { break; } @@ -251,7 +251,7 @@ contract GaugeController { _t += WEEK; _pt = 0; // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) - for (int128 _gaugeType = 1; _gaugeType < 100; ) { + for (int128 _gaugeType; _gaugeType < 100; ) { if (_gaugeType == _nGaugeTypes) { break; } diff --git a/test/fork/Constants.ts b/test/fork/Constants.ts new file mode 100644 index 00000000..0b03ee47 --- /dev/null +++ b/test/fork/Constants.ts @@ -0,0 +1,61 @@ +import { BigNumber } from "ethers"; + +class Constants { + static DAY = BigNumber.from(86400); + static WEEK = BigNumber.from(86400 * 7); + static MONTH = BigNumber.from(86400 * 30); + static YEAR = BigNumber.from(86400 * 365); + + static day = 86400; + static week = 86400 * 7; + static month = 86400 * 30; + static year = 86400 * 265; + + static TYPE_WEIGHTS: BigNumber[] = [ + BigNumber.from(5).mul(BigNumber.from(10).pow(17)), + BigNumber.from(2).mul(BigNumber.from(10).pow(18)), + ]; + static GAUGE_WEIGHTS: BigNumber[] = [ + BigNumber.from(2).mul(BigNumber.from(10).pow(18)), + BigNumber.from(10).pow(18), + BigNumber.from(5).mul(BigNumber.from(10).pow(17)), + ]; + + static symbol = "Token"; + static decimal = 18; + static INITIAL_SUPPLY = BigNumber.from("1303030303000000000000000000"); + + static ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + static ten_to_the_24 = BigNumber.from("1000000000000000000000000"); + static ten_to_the_21 = BigNumber.from("1000000000000000000000"); + static ten_to_the_20 = BigNumber.from("100000000000000000000"); + static ten_to_the_19 = BigNumber.from("10000000000000000000"); + static ten_to_the_18 = BigNumber.from("1000000000000000000"); + static ten_to_the_17 = BigNumber.from("100000000000000000"); + static ten_to_the_9 = BigNumber.from("1000000000"); + static a = BigNumber.from("2"); + static b = BigNumber.from("5"); + static zero = BigNumber.from("0"); + static MAX_UINT256 = BigNumber.from( + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + ); + + static GAUGE_TYPES = [ + BigNumber.from("1"), + BigNumber.from("1"), + BigNumber.from("2"), + ]; + + static TYPE_WEIGHTS = [ + Constants.ten_to_the_17.mul(Constants.b), + Constants.ten_to_the_18.mul(Constants.a), + ]; + static GAUGE_WEIGHTS = [ + Constants.ten_to_the_18.mul(Constants.a), + Constants.ten_to_the_18, + Constants.ten_to_the_17.mul(Constants.b), + ]; +} + +export default Constants; diff --git a/test/fork/helper.ts b/test/fork/helper.ts index 71dc7f4a..da03d1b1 100644 --- a/test/fork/helper.ts +++ b/test/fork/helper.ts @@ -1,7 +1,6 @@ import { ethers } from "hardhat"; import { BigNumber, Contract, Signer } from "ethers"; - class EVMUtils { async snapshot(): Promise { return await ethers.provider.send("evm_snapshot", []); @@ -12,8 +11,71 @@ class EVMUtils { } } -class TestSetup { +async function deployContracts() { + const [creator, alice, bob, charly] = await ethers.getSigners(); + + // Constants + const TYPE_WEIGHTS = [ + ethers.utils.parseUnits("1", 17), + ethers.utils.parseUnits("1", 18), + ]; + const GAUGE_WEIGHTS = [ + ethers.utils.parseUnits("1", 18), + ethers.utils.parseUnits("1", 18), + ethers.utils.parseUnits("1", 17), + ]; + const TEN_TO_THE_21 = ethers.utils.parseUnits("1", 21); + + // Contract factories + const Token = await ethers.getContractFactory("CRV"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + const GaugeController = await ethers.getContractFactory("GaugeController"); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); + const TestLP = await ethers.getContractFactory("TestLP"); + const Minter = await ethers.getContractFactory("Minter"); + + // Contract deployments + const token = await Token.deploy(); + const votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + const gaugeController = await GaugeController.deploy( + token.address, + votingEscrow.address + ); + const mockLpToken = await TestLP.deploy( + "tokenDAO LP token", + "iToken", + 18, + TEN_TO_THE_21.mul(2) + ); + const minter = await Minter.deploy(token.address, gaugeController.address); + + const lg1 = await LiquidityGauge.deploy(mockLpToken.address, minter.address); + const lg2 = await LiquidityGauge.deploy(mockLpToken.address, minter.address); + const lg3 = await LiquidityGauge.deploy(mockLpToken.address, minter.address); + + await token.setMinter(minter.address); + return { + creator, + alice, + bob, + charly, + token, + votingEscrow, + gaugeController, + mockLpToken, + minter, + threeGauges: [lg1.address, lg2.address, lg3.address], + gauges: [lg1, lg2, lg3], + }; +} + +class TestSetup { readonly DAY = BigNumber.from(86400); readonly WEEK = BigNumber.from(86400 * 7); readonly MONTH = BigNumber.from(86400 * 30); @@ -36,10 +98,15 @@ class TestSetup { readonly a = BigNumber.from("2"); readonly b = BigNumber.from("5"); readonly zero = BigNumber.from("0"); - readonly MAX_UINT256 = BigNumber.from("115792089237316195423570985008687907853269984665640564039457584007913129639935"); - - readonly GAUGE_TYPES = [BigNumber.from("1"), BigNumber.from("1"), BigNumber.from("2")]; + readonly MAX_UINT256 = BigNumber.from( + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + ); + readonly GAUGE_TYPES = [ + BigNumber.from("1"), + BigNumber.from("1"), + BigNumber.from("2"), + ]; creator: Signer; alice: Signer; @@ -64,8 +131,8 @@ class TestSetup { GAUGE_WEIGHTS: BigNumber[]; async setup() { - - [this.creator, this.alice, this.bob, this.charly] = await ethers.getSigners(); + [this.creator, this.alice, this.bob, this.charly] = + await ethers.getSigners(); this.accounts = [this.creator, this.alice, this.bob, this.charly]; this.creatorAddress = await this.creator.getAddress(); this.aliceAddress = await this.alice.getAddress(); @@ -78,8 +145,15 @@ class TestSetup { this.charlyAddress, ]; - this.TYPE_WEIGHTS = [this.ten_to_the_17.mul(this.b), this.ten_to_the_18.mul(this.a)]; - this.GAUGE_WEIGHTS = [this.ten_to_the_18.mul(this.a), this.ten_to_the_18, this.ten_to_the_17.mul(this.b)]; + this.TYPE_WEIGHTS = [ + this.ten_to_the_17.mul(this.b), + this.ten_to_the_18.mul(this.a), + ]; + this.GAUGE_WEIGHTS = [ + this.ten_to_the_18.mul(this.a), + this.ten_to_the_18, + this.ten_to_the_17.mul(this.b), + ]; const Token = await ethers.getContractFactory("CRV"); const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); @@ -95,21 +169,32 @@ class TestSetup { "Voting-escrowed token", "vetoken", "v1" - ); - this.gaugeController = await GaugeController.deploy(this.token.address, this.votingEscrow.address); - this.mockLpToken = await TestLP.deploy("tokenDAO LP token", "iToken", 18, this.ten_to_the_21.mul("2")); - this.minter = await Minter.deploy(this.token.address, this.gaugeController.address); + ); + this.gaugeController = await GaugeController.deploy( + this.token.address, + this.votingEscrow.address + ); + this.mockLpToken = await TestLP.deploy( + "tokenDAO LP token", + "iToken", + 18, + this.ten_to_the_21.mul("2") + ); + this.minter = await Minter.deploy( + this.token.address, + this.gaugeController.address + ); this.lg = await LiquidityGauge.deploy( this.mockLpToken.address, - this.minter.address, - ); + this.minter.address + ); const lg2 = await LiquidityGauge.deploy( this.mockLpToken.address, - this.minter.address, + this.minter.address ); const lg3 = await LiquidityGauge.deploy( this.mockLpToken.address, - this.minter.address, + this.minter.address ); this.gaugesContracts = [this.lg, lg2, lg3]; this.gaugesAddress = [this.lg.address, lg2.address, lg3.address]; @@ -118,42 +203,59 @@ class TestSetup { await this.gaugeController.addType("none", 0); } - - async addType(){ + async addType() { await this.gaugeController.addType("Liquidity", this.TYPE_WEIGHTS[0]); await this.gaugeController.addType("Liquidity", this.TYPE_WEIGHTS[1]); } - async addGaugeZero(){ + async addGaugeZero() { for (let i = 0; i < 2; i++) { - await this.gaugeController.addGauge(this.gaugesAddress[i], this.GAUGE_TYPES[i], 0); + await this.gaugeController.addGauge( + this.gaugesAddress[i], + this.GAUGE_TYPES[i], + 0 + ); } } - async addGauge(){ + async addGauge() { for (let i = 0; i < 3; i++) { - await this.gaugeController.addGauge(this.gaugesAddress[i], this.GAUGE_TYPES[i], this.GAUGE_WEIGHTS[i]); + await this.gaugeController.addGauge( + this.gaugesAddress[i], + this.GAUGE_TYPES[i], + this.GAUGE_WEIGHTS[i] + ); } } - async createLock(){ - await this.token.approve(this.votingEscrow.address, BigNumber.from("1000000000000000000000000")); + async createLock() { + await this.token.approve( + this.votingEscrow.address, + BigNumber.from("1000000000000000000000000") + ); await this.votingEscrow.createLock( BigNumber.from("1000000000000000000000000"), - BigNumber.from((await ethers.provider.getBlock("latest")).timestamp).add(this.YEAR) + BigNumber.from((await ethers.provider.getBlock("latest")).timestamp).add( + this.YEAR + ) ); } - async createLP(){ + async createLP() { for (let i = 1; i < 4; i++) { - await this.mockLpToken.transfer(this.accountsAddress[i], this.ten_to_the_18); + await this.mockLpToken.transfer( + this.accountsAddress[i], + this.ten_to_the_18 + ); } for (let i = 0; i < 3; i++) { for (let t = 0; t < 3; t++) { - await this.mockLpToken.connect(this.accounts[i + 1]).approve(this.gaugesAddress[t], this.ten_to_the_18); + await this.mockLpToken + .connect(this.accounts[i + 1]) + .approve(this.gaugesAddress[t], this.ten_to_the_18); } } } } -export { EVMUtils, TestSetup }; +export { EVMUtils, TestSetup, deployContracts }; diff --git a/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts b/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts new file mode 100644 index 00000000..73f58122 --- /dev/null +++ b/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts @@ -0,0 +1,65 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; + +describe("GaugeController", function () { + let accounts; + let gaugeController; + let snapshot: SnapshotRestorer; + + const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController } = await deployContracts()); + await gaugeController.addType("none", TYPE_WEIGHTS[0]); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("GaugeController GaugecontrollerAdmin", function () { + it("test_commit_admin_only", async function () { + await expect( + gaugeController + .connect(accounts[1]) + .commitTransferOwnership(accounts[1].address) + ).to.be.revertedWith("admin only"); + }); + + it("test_apply_admin_only", async function () { + await expect( + gaugeController.connect(accounts[1]).applyTransferOwnership() + ).to.be.revertedWith("admin only"); + }); + + it("test_commit_transfer_ownership", async function () { + await gaugeController.commitTransferOwnership(accounts[1].address); + + expect(await gaugeController.admin()).to.equal( + await accounts[0].getAddress() + ); + expect(await gaugeController.futureAdmin()).to.equal(accounts[1].address); + }); + + it("test_apply_transfer_ownership", async function () { + await gaugeController.commitTransferOwnership(accounts[1].address); + await gaugeController.applyTransferOwnership(); + + expect(await gaugeController.admin()).to.equal(accounts[1].address); + }); + + it("test_apply_without_commit", async function () { + await expect(gaugeController.applyTransferOwnership()).to.be.revertedWith( + "admin not set" + ); + }); + }); +}); diff --git a/test/fork/unitary/GaugeController/gaugesWeights.test.ts b/test/fork/unitary/GaugeController/gaugesWeights.test.ts index 1f2b7eb0..b7724489 100644 --- a/test/fork/unitary/GaugeController/gaugesWeights.test.ts +++ b/test/fork/unitary/GaugeController/gaugesWeights.test.ts @@ -1,232 +1,134 @@ -import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; describe("GaugeController", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); + let accounts; + let gaugeController; + let threeGauges; + let snapshot: SnapshotRestorer; + + const WEEK = Constants.WEEK; + const YEAR = Constants.YEAR; + const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; + const GAUGE_WEIGHTS = Constants.GAUGE_WEIGHTS; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController, threeGauges } = await deployContracts()); + await gaugeController.addType("none", TYPE_WEIGHTS[0]); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); + describe("GaugeController GaugesWeights", function () { + it("test_add_gauges", async function () { + await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); + await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - describe("test gauges weights", function () { - it("test addGauges", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); - await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); - - expect(await setup.gaugeController.gauges(0)).to.equal( - setup.gaugesAddress[0] - ); - expect(await setup.gaugeController.gauges(1)).to.equal( - setup.gaugesAddress[1] - ); + expect(await gaugeController.gauges(0)).to.equal(threeGauges[0]); + expect(await gaugeController.gauges(1)).to.equal(threeGauges[1]); }); - it("test nGauges", async () => { - expect(await setup.gaugeController.nGauges()).to.equal("0"); + it("test_n_gauges", async function () { + expect(await gaugeController.nGauges()).to.equal(0); - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); - await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); + await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); + await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - expect(await setup.gaugeController.nGauges()).to.equal("2"); + expect(await gaugeController.nGauges()).to.equal(2); }); - it("test nGauges same gauge", async () => { - expect(await setup.gaugeController.nGauges()).to.equal("0"); - - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); + it("test_n_gauges_same_gauge", async function () { + await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); await expect( - setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0) - ).to.revertedWith("cannot add the same gauge twice"); - - expect(await setup.gaugeController.nGauges()).to.equal("1"); + gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]) + ).to.be.revertedWith("cannot add the same gauge twice"); }); - it("test nGaugeTypes", async () => { - expect(await setup.gaugeController.nGaugeTypes()).to.equal("1"); // unset & LiquidityGauge + it("test_n_gauge_types", async function () { + expect(await gaugeController.nGaugeTypes()).to.equal(1); - await setup.gaugeController.addType("Liquidity", 0); + await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); - expect(await setup.gaugeController.nGaugeTypes()).to.equal( - BigNumber.from("2") - ); //unset, LiquidityGauge, Liquidity + expect(await gaugeController.nGaugeTypes()).to.equal(2); }); - it("test gaugeTypes", async () => { - await setup.gaugeController.addType("Liquidity", 0); - - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 1, 0); - await setup.gaugeController.addGauge(setup.gaugesAddress[1], 0, 0); + it("test_gauge_types", async function () { + await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); + await gaugeController.addGauge(threeGauges[0], 1, GAUGE_WEIGHTS[0]); + await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - expect( - await setup.gaugeController.gaugeTypes(setup.gaugesAddress[0]) - ).to.equal("1"); - expect( - await setup.gaugeController.gaugeTypes(setup.gaugesAddress[1]) - ).to.equal("0"); + expect(await gaugeController.gaugeTypes(threeGauges[0])).to.equal(1); + expect(await gaugeController.gaugeTypes(threeGauges[1])).to.equal(0); }); - it("test gauge weight", async () => { - await setup.gaugeController.addGauge( - setup.gaugesAddress[0], - 0, - setup.ten_to_the_19 - ); + it("test_gauge_weight", async function () { + await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - expect( - await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) - ).to.equal(setup.ten_to_the_19); + expect(await gaugeController.getGaugeWeight(threeGauges[0])).to.equal( + GAUGE_WEIGHTS[0] + ); }); - it("test gauge weight as zero", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); + it("test_gauge_weight_as_zero", async function () { + await gaugeController.addGauge(threeGauges[0], 0, 0); - expect( - await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) - ).to.equal("0"); + expect(await gaugeController.getGaugeWeight(threeGauges[0])).to.equal(0); }); - it("test getGaugeWeight", async () => { - await setup.gaugeController.addGauge(setup.gaugesAddress[0], 0, 0); + it("test_set_gauge_weight", async function () { + await gaugeController.addGauge(threeGauges[0], 0, 0); + await gaugeController.changeGaugeWeight(threeGauges[0], GAUGE_WEIGHTS[0]); + await ethers.provider.send("evm_increaseTime", [WEEK.toNumber()]); - expect( - await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) - ).to.equal("0"); - - await setup.gaugeController.changeGaugeWeight( - setup.gaugesAddress[0], - setup.ten_to_the_21 - ); - - expect( - await setup.gaugeController.getGaugeWeight(setup.gaugesAddress[0]) - ).to.equal(setup.ten_to_the_21); - }); - - it("test type weight", async () => { - await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); - expect(await setup.gaugeController.getTypeWeight(1)).to.equal( - setup.TYPE_WEIGHTS[0] + expect(await gaugeController.getGaugeWeight(threeGauges[0])).to.equal( + GAUGE_WEIGHTS[0] ); - expect(await setup.gaugeController.getTypeWeight(2)).to.equal("0"); }); - it("test changeTypeWeight", async () => { - await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); - await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[1]); - await setup.gaugeController.changeTypeWeight(2, setup.TYPE_WEIGHTS[1]); - await setup.gaugeController.changeTypeWeight(1, BigNumber.from("31337")); + it("test_type_weight", async function () { + await gaugeController.addType("Insurance", 0); - expect(await setup.gaugeController.getTypeWeight(0)).to.equal(0); - expect(await setup.gaugeController.getTypeWeight(1)).to.equal("31337"); - expect(await setup.gaugeController.getTypeWeight(2)).to.equal( - setup.TYPE_WEIGHTS[1] - ); + expect(await gaugeController.getTypeWeight(0)).to.equal(TYPE_WEIGHTS[0]); + expect(await gaugeController.getTypeWeight(1)).to.equal(0); }); - it("test relative weight write", async () => { - await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[0]); - await setup.gaugeController.addType("Liquidity", setup.TYPE_WEIGHTS[1]); - - await setup.gaugeController.addGauge( - setup.gaugesAddress[0], - 1, - setup.GAUGE_WEIGHTS[0] - ); - await setup.gaugeController.addGauge( - setup.gaugesAddress[1], - 1, - setup.GAUGE_WEIGHTS[1] - ); - await setup.gaugeController.addGauge( - setup.gaugesAddress[2], - 2, - setup.GAUGE_WEIGHTS[2] - ); + it("test_change_type_weight", async function () { + await gaugeController.addType("Insurance", TYPE_WEIGHTS[0]); + await gaugeController.changeTypeWeight(0, TYPE_WEIGHTS[1]); - let gauge_type = [0, 0, 1]; - let total_weight = setup.TYPE_WEIGHTS[0] - .mul(setup.GAUGE_WEIGHTS[0]) - .add(setup.TYPE_WEIGHTS[0].mul(setup.GAUGE_WEIGHTS[1])) - .add(setup.TYPE_WEIGHTS[1].mul(setup.GAUGE_WEIGHTS[2])); - - await ethers.provider.send("evm_increaseTime", [ - setup.WEEK.mul(2).toNumber(), - ]); - await ethers.provider.send("evm_mine", []); - - let t = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - - for (let i = 0; i < 3; i++) { - await setup.gaugeController.gaugeRelativeWeightWrite( - setup.gaugesAddress[i], - t - ); - let relativeWeight = await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[i], - t - ); - expect(relativeWeight).to.equal( - setup.ten_to_the_18 - .mul(setup.GAUGE_WEIGHTS[i]) - .mul(setup.TYPE_WEIGHTS[gauge_type[i]]) - .div(total_weight) - ); - } - - await ethers.provider.send("evm_increaseTime", [ - setup.YEAR.div("2").toNumber(), - ]); - await ethers.provider.send("evm_mine", []); - - for (let i = 0; i < 3; i++) { - await setup.gaugeController.gaugeRelativeWeightWrite( - setup.gaugesAddress[i], - t - ); - let relativeWeight = await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[i], - t - ); - expect(relativeWeight).to.equal( - setup.ten_to_the_18 - .mul(setup.GAUGE_WEIGHTS[i]) - .mul(setup.TYPE_WEIGHTS[gauge_type[i]]) - .div(total_weight) - ); - } - - await ethers.provider.send("evm_increaseTime", [ - setup.YEAR.div("10").toNumber(), - ]); - await ethers.provider.send("evm_mine", []); + expect(await gaugeController.getTypeWeight(0)).to.equal(TYPE_WEIGHTS[1]); + }); - for (let i = 0; i < 3; i++) { - await setup.gaugeController.gaugeRelativeWeightWrite( - setup.gaugesAddress[i], - t - ); - let relativeWeight = await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[i], - t + it("test_relative_weight_write", async function () { + await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); + await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); + await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); + await gaugeController.addGauge(threeGauges[2], 1, GAUGE_WEIGHTS[2]); + await ethers.provider.send("evm_increaseTime", [YEAR.toNumber()]); + + const expectedWeight = TYPE_WEIGHTS[0] + .mul(GAUGE_WEIGHTS[0]) + .add(TYPE_WEIGHTS[0].mul(GAUGE_WEIGHTS[1])) + .add(TYPE_WEIGHTS[1].mul(GAUGE_WEIGHTS[2])); + + for (let i = 0; i < threeGauges.length; i++) { + const relativeWeight = await gaugeController.gaugeRelativeWeight( + threeGauges[i], + 0 ); expect(relativeWeight).to.equal( - setup.ten_to_the_18 - .mul(setup.GAUGE_WEIGHTS[i]) - .mul(setup.TYPE_WEIGHTS[gauge_type[i]]) - .div(total_weight) + GAUGE_WEIGHTS[i] + .mul(TYPE_WEIGHTS[Math.floor(i / 2)]) + .div(expectedWeight) ); } }); diff --git a/test/fork/unitary/GaugeController/timestamps.test.ts b/test/fork/unitary/GaugeController/timestamps.test.ts index 20529d03..b28d0220 100644 --- a/test/fork/unitary/GaugeController/timestamps.test.ts +++ b/test/fork/unitary/GaugeController/timestamps.test.ts @@ -1,58 +1,51 @@ -import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; describe("GaugeController", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); + let accounts; + let gaugeController; + + let snapshot: SnapshotRestorer; + + const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; + const WEEK = Constants.week; + const YEAR = Constants.year; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController } = await deployContracts()); + await gaugeController.addType("none", TYPE_WEIGHTS[0]); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); - describe("test_timestamp", function () { - it("test_timestamp", async () => { - let now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - expect(await setup.gaugeController.timeTotal()).to.equal( - now - .add(setup.WEEK) - .div(setup.WEEK) - .or(BigNumber.from("0")) - .mul(setup.WEEK) - ); + describe("GaugeController Timestamps", function () { + it("test_timestamps", async function () { + const currentTime = (await ethers.provider.getBlock("latest")).timestamp; + const expectedTime = Math.floor((currentTime + WEEK) / WEEK) * WEEK; + expect(await gaugeController.timeTotal()).to.equal(expectedTime); for (let i = 0; i < 5; i++) { - //await time.increase(YEAR.mul(BigNumber.from('11')).div(BigNumber.from('10'))); - let t = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); await ethers.provider.send("evm_increaseTime", [ - setup.YEAR.mul("11").div("10").toNumber(), + Math.floor(1.1 * YEAR), ]); - await setup.gaugeController.checkpoint(); - now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - expect(await setup.gaugeController.timeTotal()).to.equal( - now - .add(setup.WEEK) - .div(setup.WEEK) - .or(BigNumber.from("0")) - .mul(setup.WEEK) - ); //technically, blocktimestamp for this tx is "now+1", but it works fine for here because of .div(setup.WEEK) rounds down the number. + await gaugeController.checkpoint(); + + const newCurrentTime = (await ethers.provider.getBlock("latest")) + .timestamp; + const newExpectedTime = + Math.floor((newCurrentTime + WEEK) / WEEK) * WEEK; + expect(await gaugeController.timeTotal()).to.equal(newExpectedTime); } }); }); diff --git a/test/fork/unitary/GaugeController/totalWeight.test.ts b/test/fork/unitary/GaugeController/totalWeight.test.ts index 4e33dbcf..ae08f0f7 100644 --- a/test/fork/unitary/GaugeController/totalWeight.test.ts +++ b/test/fork/unitary/GaugeController/totalWeight.test.ts @@ -1,90 +1,75 @@ +import { ethers } from "hardhat"; import { expect } from "chai"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { Signer, Contract } from "ethers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; describe("GaugeController", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; + let accounts: Signer[]; + let gaugeController: Contract; + let threeGauges: Contract[]; + let snapshot: SnapshotRestorer; - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); + const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; + const GAUGE_WEIGHTS = Constants.GAUGE_WEIGHTS; + const ten_to_the_18 = Constants.ten_to_the_18; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController, threeGauges } = await deployContracts()); + await gaugeController.addType("none", TYPE_WEIGHTS[0]); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); - describe("test_total_weight", function () { - it("test_total_weight", async () => { - await setup.gaugeController.addGauge( - setup.gaugesAddress[0], - 1, - setup.GAUGE_WEIGHTS[0] - ); - expect(await setup.gaugeController.getTotalWeight()).to.equal( - setup.GAUGE_WEIGHTS[0].mul(setup.TYPE_WEIGHTS[0]) - ); - }); + describe("GaugeController TotalWeight", function () { + it("test_total_weight", async function () { + await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - it("test_changeTypeWeight", async () => { - await setup.gaugeController.addGauge( - setup.gaugesAddress[0], - 1, - setup.ten_to_the_18 + expect(await gaugeController.getTotalWeight()).to.equal( + GAUGE_WEIGHTS[0].mul(TYPE_WEIGHTS[0]) ); + }); - await setup.gaugeController.changeTypeWeight(1, BigNumber.from("31337")); + it("test_change_type_weight", async function () { + await gaugeController.addGauge(threeGauges[0], 0, ten_to_the_18); + await gaugeController.changeTypeWeight(0, 31337); - expect(await setup.gaugeController.getTotalWeight()).to.equal( - setup.ten_to_the_18.mul(BigNumber.from("31337")) + expect(await gaugeController.getTotalWeight()).to.equal( + ten_to_the_18.mul(31337) ); }); - it("test_changeGaugeWeight", async () => { - await setup.gaugeController.addGauge( - setup.gaugesAddress[0], - 1, - setup.ten_to_the_18 - ); - - await setup.gaugeController.changeGaugeWeight( - setup.gaugesAddress[0], - BigNumber.from("31337") - ); + it("test_change_gauge_weight", async function () { + await gaugeController.addGauge(threeGauges[0], 0, ten_to_the_18); + await gaugeController.changeGaugeWeight(threeGauges[0], 31337); - expect(await setup.gaugeController.getTotalWeight()).to.equal( - setup.TYPE_WEIGHTS[0].mul(BigNumber.from("31337")) + expect(await gaugeController.getTotalWeight()).to.equal( + TYPE_WEIGHTS[0].mul(31337) ); }); - it("test_multiple", async () => { - await setup.gaugeController.addGauge( - setup.gaugesAddress[0], - 1, - setup.GAUGE_WEIGHTS[0] - ); - await setup.gaugeController.addGauge( - setup.gaugesAddress[1], - 1, - setup.GAUGE_WEIGHTS[1] - ); - await setup.gaugeController.addGauge( - setup.gaugesAddress[2], - 2, - setup.GAUGE_WEIGHTS[2] - ); + it("test_multiple", async function () { + await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); + await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); + await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); + await gaugeController.addGauge(threeGauges[2], 1, GAUGE_WEIGHTS[2]); - let expected = setup.GAUGE_WEIGHTS[0] - .mul(setup.TYPE_WEIGHTS[0]) - .add(setup.GAUGE_WEIGHTS[1].mul(setup.TYPE_WEIGHTS[0])) - .add(setup.GAUGE_WEIGHTS[2].mul(setup.TYPE_WEIGHTS[1])); + const expectedTotalWeight = GAUGE_WEIGHTS[0] + .mul(TYPE_WEIGHTS[0]) + .add(GAUGE_WEIGHTS[1].mul(TYPE_WEIGHTS[0])) + .add(GAUGE_WEIGHTS[2].mul(TYPE_WEIGHTS[1])); - expect(await setup.gaugeController.getTotalWeight()).to.equal(expected); + expect(await gaugeController.getTotalWeight()).to.equal( + expectedTotalWeight + ); }); }); }); diff --git a/test/fork/unitary/GaugeController/vote.test.ts b/test/fork/unitary/GaugeController/vote.test.ts index 654146a0..4c581e7a 100644 --- a/test/fork/unitary/GaugeController/vote.test.ts +++ b/test/fork/unitary/GaugeController/vote.test.ts @@ -1,197 +1,149 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; describe("GaugeController", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - await setup.addGaugeZero(); - await setup.createLock(); + let accounts; + let gaugeController; + let threeGauges; + let votingEscrow; + let token; + + let snapshot: SnapshotRestorer; + + const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; + const ten_to_the_18 = Constants.ten_to_the_18; + const ten_to_the_24 = Constants.ten_to_the_24; + const day = Constants.day; + const year = Constants.year; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController, threeGauges, votingEscrow, token } = + await deployContracts()); + + await gaugeController.addType("none", TYPE_WEIGHTS[0]); + await gaugeController.addType("Insurance", ten_to_the_18); + await gaugeController.addGauge(threeGauges[0], 0, 0); + await gaugeController.addGauge(threeGauges[1], 1, 0); + + await token.approve(votingEscrow.address, ten_to_the_24); + await votingEscrow.createLock( + ten_to_the_24, + (await ethers.provider.getBlock("latest")).timestamp + year + ); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); - describe("test_vote", function () { - it("test_vote", async () => { - // Test if voting for a gauge with a weight of 10000 sets the user's vote power to 10000. - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], + describe("GaugeController vote", function () { + it("test_vote", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); + expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( 10000 ); - expect( - await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) - ).to.equal(BigNumber.from("10000")); //Total vote power used by user }); - it("test_vote_partial", async () => { - // Test if voting with a weight of 1234 sets the user's vote power to 1234. - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[1], + it("test_vote_partial", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[1], 1234); + expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( 1234 ); - expect( - await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) - ).to.equal(BigNumber.from("1234")); }); - it("test_vote_change", async () => { - // Test if it's possible to change the weight of the same gauge after 10 days. - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[1], - 1234 - ); + it("test_vote_change", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[1], 1234); + await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234) - ).to.revertedWith("Cannot vote so often"); + gaugeController.voteForGaugeWeights(threeGauges[1], 42) + ).to.be.revertedWith("Cannot vote so often"); - await ethers.provider.send("evm_increaseTime", [ - setup.DAY.mul("10").toNumber(), - ]); + await ethers.provider.send("evm_increaseTime", [day * 10]); + await gaugeController.voteForGaugeWeights(threeGauges[1], 42); - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[1], + expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( 42 ); - - expect( - await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) - ).to.equal(BigNumber.from("42")); }); - it("test_vote_remove", async () => { - // Test that removing a vote sets the user's voting power to 0. - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[1], - 10000 - ); + it("test_vote_remove", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[1], 10000); + await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 1234) - ).to.revertedWith("Cannot vote so often"); - - await ethers.provider.send("evm_increaseTime", [ - setup.DAY.mul("10").toNumber(), - ]); - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[1], + gaugeController.voteForGaugeWeights(threeGauges[1], 0) + ).to.be.revertedWith("Cannot vote so often"); + + await ethers.provider.send("evm_increaseTime", [day * 10]); + + await gaugeController.voteForGaugeWeights(threeGauges[1], 0); + + expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( 0 ); - - expect( - await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) - ).to.equal(setup.zero); }); - it("test_vote_multiple", async () => { - // Test if voting for multiple gauges sums up the user's voting power. - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 4000 - ); - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[1], - 6000 - ); + it("test_vote_multiple", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[0], 4000); + await gaugeController.voteForGaugeWeights(threeGauges[1], 6000); - expect( - await setup.gaugeController.voteUserPower(setup.accountsAddress[0]) - ).to.equal(BigNumber.from("10000")); + expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( + 10000 + ); }); - it("test_vote_no_balance", async () => { - // Test that users without locked tokens can't vote. + it("test_vote_no_balance", async function () { await expect( - setup.gaugeController - .connect(setup.accounts[1]) - .voteForGaugeWeights(setup.gaugesAddress[0], 10000) - ).to.revertedWith("Your token lock expires too soon"); + gaugeController + .connect(accounts[1]) + .voteForGaugeWeights(threeGauges[0], 10000) + ).to.be.revertedWith("Your token lock expires too soon"); }); - it("test_vote_expired", async () => { - // Test that users with expired token locks can't vote. - await ethers.provider.send("evm_increaseTime", [ - setup.YEAR.mul("2").toNumber(), - ]); + it("test_vote_expired", async function () { + await ethers.provider.send("evm_increaseTime", [year * 2]); await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10000) - ).to.revertedWith("Your token lock expires too soon"); + gaugeController.voteForGaugeWeights(threeGauges[0], 10000) + ).to.be.revertedWith("Your token lock expires too soon"); }); - it("test_invalid_gauge_id", async () => { - // Test that voting with an invalid gauge ID throws an error. + it("test_invalid_gauge_id", async function () { await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[2], 10000) - ).to.revertedWith("Gauge not added"); + gaugeController.voteForGaugeWeights(threeGauges[2], 10000) + ).to.be.revertedWith("Gauge not added"); }); - it("test_over_user_weight", async () => { - // Test that voting with power exceeding the user's capacity throws an error. + it("test_over_user_weight", async function () { await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 10001) - ).to.revertedWith("You used all your voting power"); + gaugeController.voteForGaugeWeights(threeGauges[0], 10001) + ).to.be.revertedWith("You used all your voting power"); }); - it("test_over_weight_multiple", async () => { - // Test that voting for multiple gauges with total power exceeding user's capacity throws an error. - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 8000 - ); - await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 4000) - ).to.revertedWith("Used too much power"); - }); - - it("test_over_weight_adjust_existing", async () => { - // Test that adjusting an existing vote to exceed the user's total voting power throws an error. - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 6000 - ); - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[1], - 3000 - ); - - await ethers.provider.send("evm_increaseTime", [ - setup.DAY.mul("10").toNumber(), - ]); + it("test_over_weight_multiple", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[0], 8000); await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 8000) - ).to.revertedWith("Used too much power"); + gaugeController.voteForGaugeWeights(threeGauges[1], 4000) + ).to.be.revertedWith("Used too much power"); }); - it("test_exceeding_voting_power", async () => { - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 6000 - ); - await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[1], 5000) - ).to.revertedWith("Used too much power"); - }); + it("test_over_weight_adjust_existing", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[0], 6000); + await gaugeController.voteForGaugeWeights(threeGauges[1], 3000); + + await ethers.provider.send("evm_increaseTime", [day * 10]); - it("test_emit_vote_event", async () => { - let now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp + 1 - ); await expect( - setup.gaugeController.voteForGaugeWeights(setup.gaugesAddress[0], 5000) - ) - .to.emit(setup.gaugeController, "VoteForGauge") - .withArgs(now, setup.accountsAddress[0], setup.gaugesAddress[0], 5000); + gaugeController.voteForGaugeWeights(threeGauges[0], 8000) + ).to.be.revertedWith("Used too much power"); }); }); }); diff --git a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts index fa5ded6e..c65be215 100644 --- a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts +++ b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts @@ -1,116 +1,108 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; describe("GaugeController", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - await setup.addGaugeZero(); - await setup.createLock(); + let accounts; + let gaugeController; + let threeGauges; + let votingEscrow; + let token; + + let snapshot: SnapshotRestorer; + + const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; + const ten_to_the_18 = Constants.ten_to_the_18; + const ten_to_the_24 = Constants.ten_to_the_24; + const year = Constants.year; + const day = Constants.day; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController, threeGauges, votingEscrow, token } = + await deployContracts()); + + await gaugeController.addType("none", TYPE_WEIGHTS[0]); + await gaugeController.addType("Insurance", ten_to_the_18); + await gaugeController.addGauge(threeGauges[0], 0, 0); + await gaugeController.addGauge(threeGauges[1], 1, 0); + + await token.approve(votingEscrow.address, ten_to_the_24); + await votingEscrow.createLock( + ten_to_the_24, + (await ethers.provider.getBlock("latest")).timestamp + year + ); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); - describe("test_vote_weight_unitary", function () { - it("test_no_immediate_effect_on_weight", async () => { - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 10000 + describe("GaugeController voteWeightUnitary", function () { + it("test_no_immediate_effect_on_weight", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); + const weight = await gaugeController.gaugeRelativeWeight( + threeGauges[0], + 0 ); - expect( - await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[0], - BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) - ) - ).to.equal(BigNumber.from("0")); + expect(weight).to.equal(0); }); - it("test_remove_vote_no_immediate_effect", async () => { - // ゲージに対して投票を行った後、時間を進めてそのゲージのチェックポイントを取ります。 - // その後、投票を取り消して、投票の取り消しは即座には影響しないことを確認します。 - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 10000 - ); - - await ethers.provider.send("evm_increaseTime", [ - setup.DAY.mul("10").toNumber(), - ]); + it("test_effect_on_following_period", async () => { + await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); - await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 0 - ); + await ethers.provider.send("evm_increaseTime", [day * 7]); + await gaugeController.checkpointGauge(threeGauges[0]); expect( - await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[0], - BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) + await gaugeController.gaugeRelativeWeight( + threeGauges[0], + ( + await ethers.provider.getBlock("latest") + ).timestamp ) - ).to.equal(setup.ten_to_the_18); + ).to.equal(ten_to_the_18); }); - it("test_effect_on_following_period", async () => { - // ゲージに対して投票を行い、一週間時間を進めた後にそのゲージの重みが変わっていることを確認します。 - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 10000 - ); + it("test_remove_vote_no_immediate_effect", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); + + await ethers.provider.send("evm_increaseTime", [day * 10]); + + await gaugeController.checkpointGauge(threeGauges[0]); + await gaugeController.voteForGaugeWeights(threeGauges[0], 0); - await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); - await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); expect( - await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[0], - BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) - ) - ).to.equal(setup.ten_to_the_18); + await gaugeController.gaugeRelativeWeight(threeGauges[0], 0) + ).to.equal(ten_to_the_18); }); - it("test_remove_vote_means_no_weight", async () => { - // ゲージに対して投票を行い、時間を進めてそのゲージのチェックポイントを取ります。 - // 投票を取り消し、さらに一週間時間を進めた後、そのゲージの重みが0になっていることを確認します。 - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 10000 - ); - await ethers.provider.send("evm_increaseTime", [ - setup.DAY.mul("10").toNumber(), - ]); - await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); + it("test_remove_vote_means_no_weight", async function () { + await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); + await ethers.provider.send("evm_increaseTime", [day * 10]); + await ethers.provider.send("evm_mine"); + + await gaugeController.checkpointGauge(threeGauges[0]); expect( - await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[0], - BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) - ) - ).to.equal(setup.ten_to_the_18); + await gaugeController.gaugeRelativeWeight(threeGauges[0], 0) + ).to.equal(ethers.utils.parseUnits("1", 18)); - await setup.gaugeController.voteForGaugeWeights( - setup.gaugesAddress[0], - 0 - ); - await ethers.provider.send("evm_increaseTime", [setup.WEEK.toNumber()]); - await setup.gaugeController.checkpointGauge(setup.gaugesAddress[0]); + await gaugeController.voteForGaugeWeights(threeGauges[0], 0); + + await ethers.provider.send("evm_increaseTime", [day * 7]); + await ethers.provider.send("evm_mine"); + await gaugeController.checkpointGauge(threeGauges[0]); expect( - await setup.gaugeController.gaugeRelativeWeight( - setup.gaugesAddress[0], - BigNumber.from((await ethers.provider.getBlock("latest")).timestamp) - ) - ).to.equal(setup.zero); + await gaugeController.gaugeRelativeWeight(threeGauges[0], 0) + ).to.equal(0); }); }); }); From 5dfe53b6666e8a96b5736a623bc26c38818ddf4a Mon Sep 17 00:00:00 2001 From: takadr Date: Thu, 9 Nov 2023 12:17:50 +0900 Subject: [PATCH 038/412] Added integration tests for GaugeController --- .../GaugeController/typesAndWeights.test.ts | 8 +- .../GaugeController/voteWeight.test.ts | 340 ++++++++++++++++++ 2 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 test/fork/integration/GaugeController/voteWeight.test.ts diff --git a/test/fork/integration/GaugeController/typesAndWeights.test.ts b/test/fork/integration/GaugeController/typesAndWeights.test.ts index 9218028d..63719c6b 100644 --- a/test/fork/integration/GaugeController/typesAndWeights.test.ts +++ b/test/fork/integration/GaugeController/typesAndWeights.test.ts @@ -123,7 +123,7 @@ describe("GaugeController", function () { await ruleAddType(); // Check - await invariants(); + await checkInvariants(); } //--------------------------------------------- randomly exceuted functions -----------------------------------------------------------// async function ruleAddType(stTypeWeight?: BigNumber) { @@ -137,7 +137,7 @@ describe("GaugeController", function () { console.log(`ruleAddType --- stTypeWeight: ${stTypeWeight.toString()}`); // Check - await invariants(); + await checkInvariants(); } async function ruleAddGauge(gaugeType?: number, stGaugeWeight?: BigNumber) { @@ -169,10 +169,10 @@ describe("GaugeController", function () { gauges.push({ contract: gauge, type: gaugeType, weight: stGaugeWeight }); // Check - await invariants(); + await checkInvariants(); } - async function invariants() { + async function checkInvariants() { await invariantGaugeWeightSums(); await invariantTotalTypeWeight(); await invariantRelativeGaugeWeight(); diff --git a/test/fork/integration/GaugeController/voteWeight.test.ts b/test/fork/integration/GaugeController/voteWeight.test.ts new file mode 100644 index 00000000..c3dcda86 --- /dev/null +++ b/test/fork/integration/GaugeController/voteWeight.test.ts @@ -0,0 +1,340 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +const WEEK = 86400 * 7; +const MAX_EXAMPLES = 10; +const SCALE = 1e20; + +type SlopeData = { + initialBias: BigNumber; + duration: number; +}; + +describe("GaugeController", function () { + let accounts: SignerWithAddress[]; + let otherAccount: SignerWithAddress; + let gaugeController: Contract; + let threeGauges: Contract[3] = []; + let mockLpToken: Contract; + let minter: Contract; + let token: Contract; + let votingEscrow: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = (await ethers.getSigners()).slice(0, 3); + otherAccount = (await ethers.getSigners())[4]; + + const MockLpToken = await ethers.getContractFactory("TestLP"); + const Token = await ethers.getContractFactory("CRV"); + const Minter = await ethers.getContractFactory("Minter"); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); + const GaugeController = await ethers.getContractFactory("GaugeController"); + const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); + + token = await Token.deploy(); + await token.deployed(); + + votingEscrow = await VotingEscrow.deploy( + token.address, + "Voting-escrowed token", + "vetoken", + "v1" + ); + await votingEscrow.deployed(); + + gaugeController = await GaugeController.deploy( + token.address, + votingEscrow.address + ); + await gaugeController.deployed(); + + minter = await Minter.deploy(token.address, gaugeController.address); + await minter.deployed(); + + mockLpToken = await MockLpToken.deploy( + "Curve LP token", + "usdCrv", + 18, + ethers.utils.parseEther("10") + ); + await mockLpToken.deployed(); + + for (let i = 0; i < 3; i++) { + threeGauges.push( + await LiquidityGauge.deploy(mockLpToken.address, minter.address) + ); + } + + await gaugeController.addType("Liquidity", ethers.utils.parseEther("1")); + for (let i = 0; i < 3; i++) { + await gaugeController.addGauge(threeGauges[i].address, 0, 0); + } + + for (let i = 0; i < 3; i++) { + await token + .connect(accounts[0]) + .transfer(accounts[i].address, ethers.utils.parseEther("100000")); + await token + .connect(accounts[i]) + .approve(votingEscrow.address, ethers.utils.parseEther("100000")); + } + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + // Helper functions to generate random variables -----> + function randomBigValue(min: BigNumber, max: BigNumber): BigNumber { + return BigNumber.from(ethers.utils.randomBytes(32)) + .mod(max.sub(min)) + .add(min); + } + function randomValue(min: number, max: number): number { + return Math.floor(Math.random() * (max - min) + min); + } + function getRandomDeposits(): BigNumber[] { + // Corresponds strategy("uint256[3]", min_value=10 ** 21, max_value=10 ** 23) + return [ + randomBigValue( + ethers.utils.parseEther("1000"), + ethers.utils.parseEther("100000") + ), + randomBigValue( + ethers.utils.parseEther("1000"), + ethers.utils.parseEther("100000") + ), + randomBigValue( + ethers.utils.parseEther("1000"), + ethers.utils.parseEther("100000") + ), + ]; + } + function getRandomLength(): number[] { + // Corresponds strategy("uint256[3]", min_value=52, max_value=100) + return [randomValue(52, 100), randomValue(52, 100), randomValue(52, 100)]; + } + function getRandomVotes(): number[][] { + // Corresponds strategy("uint[2][3]", min_value=0, max_value=5) + return [ + [randomValue(0, 5), randomValue(0, 5)], + [randomValue(0, 5), randomValue(0, 5)], + [randomValue(0, 5), randomValue(0, 5)], + ]; + } + // ------------------------------------------------ + + function generateUniqueRandomNumbers( + count: number, + min: number, + max: number + ): number[] { + const set = new Set(); + while (set.size < count) { + const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; + set.add(randomValue); + } + return Array.from(set); + } + + for (let iteration = 0; iteration < MAX_EXAMPLES; iteration++) { + it(`tests vote weight ${iteration}`, async function () { + // Test that gauge weights correctly adjust over time. + // Strategies + // --------- + // stDeposits : [int, int, int] + // Number of coins to be deposited per account + // stLength : [int, int, int] + // Policy duration in weeks + // stVotes : [(int, int), (int, int), (int, int)] + // (vote for gauge 0, vote for gauge 1) for each account, in units of 10% + + const stDeposits = getRandomDeposits(); + const stLength = getRandomLength(); + const stVotes = getRandomVotes(); + + // Dammy fixed data for debug + // const stDeposits = [ + // BigNumber.from("41798790756112223739711"), + // BigNumber.from("6086480295267465049249"), + // BigNumber.from("18446478515299272788115"), + // ]; + // const stLength = [67, 75, 55]; + // const stVotes = [ + // [3, 2], + // [0, 2], + // [3, 1], + // ]; + // Init 10 s before the week change + let t0 = await time.latest(); + let t1 = Math.floor((t0 + 2 * WEEK) / WEEK) * WEEK - 10; + // console.log(`t0: ${t0}, t1: ${t1}`); + await ethers.provider.send("evm_increaseTime", [t1 - t0]); + // Deposit for voting + let timestamp = t1; + for (let i = 0; i < accounts.length; i++) { + await votingEscrow + .connect(accounts[i]) + .createLock(stDeposits[i], timestamp + stLength[i] * WEEK); + } + + let tx; + let txReceipt; + // Place votes + let votes: number[][] = []; + for (let i = 0; i < accounts.length; i++) { + votes.push(stVotes[i].map((x) => x * 1000)); + votes[i].push(10000 - votes[i].reduce((a, b) => a + b, 0)); + for (let x = 0; x < accounts.length; x++) { + tx = await gaugeController + .connect(accounts[i]) + .voteForGaugeWeights(threeGauges[x].address, votes[i][x]); + } + txReceipt = await tx.wait(); + } + // Vote power assertions - everyone used all voting power + for (let i = 0; i < accounts.length; i++) { + expect( + await gaugeController.voteUserPower(accounts[i].address) + ).to.equal(10000); + } + // Calculate slope data, build model functions + let slopeData: SlopeData[] = []; + for (let i = 0; i < accounts.length; i++) { + const initialBias: BigNumber = ( + await votingEscrow.getLastUserSlope(accounts[i].address) + ).mul( + (await votingEscrow.locked(accounts[i].address)).end.sub(timestamp) + ); + const duration = + Math.floor((timestamp + stLength[i] * WEEK) / WEEK) * WEEK - + timestamp; + slopeData.push({ initialBias, duration }); + } + const maxDuration = Math.max( + ...slopeData.map(({ duration }) => duration) + ); + const models = (idx: number, relativeTime: number) => { + const { initialBias, duration } = slopeData[idx]; + // console.log( + // "model data: ", + // initialBias.toString(), + // duration, + // relativeTime + // ); + const coef = 1 - (relativeTime * maxDuration) / duration; + const relBias = initialBias + .mul(BigNumber.from(Math.floor(coef * SCALE).toString())) + .div(SCALE.toString()); + return relBias.lt(0) ? BigNumber.from("0") : relBias; + }; + await time.increase(WEEK * 4); + let txTimestamp = (await ethers.provider.getBlock(txReceipt.blockNumber)) + .timestamp; + while (txTimestamp < timestamp + 1.5 * maxDuration) { + // console.log( + // "blockNumber txTimestamp: ", + // txReceipt.blockNumber, + // txTimestamp + // ); + for (let i = 0; i < 3; i++) { + tx = await gaugeController + .connect(otherAccount) + .checkpointGauge(threeGauges[i].address); + } + txReceipt = await tx.wait(); + txTimestamp = (await ethers.provider.getBlock(txReceipt.blockNumber)) + .timestamp; + const relativeTime = + Math.floor((txTimestamp / WEEK) * WEEK - timestamp) / maxDuration; + // console.log( + // `Calculating relative time... txTimestamp: ${txTimestamp}, timestamp: ${timestamp}, maxDuration: ${maxDuration}` + // ); + const weights: BigNumber[] = await Promise.all( + threeGauges.map(async (gauge, i) => { + return await gaugeController.gaugeRelativeWeight( + gauge.address, + await time.latest() + ); + }) + ); + let theoreticalWeights: BigNumber[] = []; + if (relativeTime < 1) { + // console.log( + // `vote: [${votes[0]}], [${votes[1]}], [${votes[2]}], relativeTime: ${relativeTime}` + // ); + // console.log(`models: ${models(0, relativeTime).toString()}`); + theoreticalWeights = [ + votes.reduce( + (sum, vote, i) => + sum.add( + BigNumber.from(vote[0].toString()).mul( + models(i, relativeTime) + ) + ), + BigNumber.from(0) + ), + votes.reduce( + (sum, vote, i) => + sum.add( + BigNumber.from(vote[1].toString()).mul( + models(i, relativeTime) + ) + ), + BigNumber.from(0) + ), + votes.reduce( + (sum, vote, i) => + sum.add( + BigNumber.from(vote[2].toString()).mul( + models(i, relativeTime) + ) + ), + BigNumber.from(0) + ), + ]; + const totalTheoreticalWeight = theoreticalWeights.reduce( + (sum, weight) => sum.add(weight), + BigNumber.from(0) + ); + theoreticalWeights = theoreticalWeights.map((w) => + w.isZero() + ? w + : w.mul(BigNumber.from(10).pow(18)).div(totalTheoreticalWeight) + ); + } else { + theoreticalWeights = [ + BigNumber.from(0), + BigNumber.from(0), + BigNumber.from(0), + ]; + } + if (relativeTime !== 1) { + for (let i = 0; i < 3; i++) { + const weightDiff = weights[i].sub(theoreticalWeights[i]).abs(); + const maxAllowedDiff = BigNumber.from(txTimestamp - timestamp) + .div(WEEK) + .add(1) + .mul(BigNumber.from(10).pow(18)); // Adjust scale + // console.log( + // "weightDiff & maxAllowedDiff: ", + // weightDiff.toString(), + // maxAllowedDiff.toString() + // ); + expect(weightDiff.lte(maxAllowedDiff)).to.be.true; + } + } + await time.increase(WEEK * 4); + } + }); + } +}); From fad7a8d396114f162c0f2a616b604e763056ff24 Mon Sep 17 00:00:00 2001 From: takadr Date: Thu, 9 Nov 2023 15:31:20 +0900 Subject: [PATCH 039/412] Updated FeeDistributor integration tests --- .../FeeDistributor/checkpointTs.test.ts | 11 +- .../distributefFeesStateful.test.ts | 147 ++++++++++-------- 2 files changed, 87 insertions(+), 71 deletions(-) diff --git a/test/fork/integration/FeeDistributor/checkpointTs.test.ts b/test/fork/integration/FeeDistributor/checkpointTs.test.ts index a411094e..721ce3cc 100644 --- a/test/fork/integration/FeeDistributor/checkpointTs.test.ts +++ b/test/fork/integration/FeeDistributor/checkpointTs.test.ts @@ -10,6 +10,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; const DAY = 86400; const WEEK = DAY * 7; +const MAX_EXAMPLES = 10; describe("FeeDistributor", function () { let accounts: SignerWithAddress[]; @@ -70,10 +71,10 @@ describe("FeeDistributor", function () { } it("test checkpoint total supply", async function () { - const stAmount = generateUniqueRandomNumbers(10, 1 * 1e4, 100 * 1e4); - const stLocktime = generateUniqueRandomNumbers(10, 1, 52); - const stSleep = generateUniqueRandomNumbers(10, 1, 30); - for (let i = 0; i < 10; i++) { + const stAmount = generateUniqueRandomNumbers(MAX_EXAMPLES, 1e4, 100 * 1e4); + const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); + const stSleep = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 30); + for (let i = 0; i < MAX_EXAMPLES; i++) { await token .connect(accounts[i]) .approve(votingEscrow.address, ethers.constants.MaxUint256); @@ -83,7 +84,7 @@ describe("FeeDistributor", function () { } let finalLock = 0; - for (let i = 0; i < 10; i++) { + for (let i = 0; i < MAX_EXAMPLES; i++) { const sleepTime = Math.floor(stSleep[i] * 86400); await ethers.provider.send("evm_increaseTime", [sleepTime]); const lockTime = (await time.latest()) + sleepTime + WEEK * stLocktime[i]; diff --git a/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts b/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts index 1c4c8f5e..fe4a66b3 100644 --- a/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts +++ b/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts @@ -12,7 +12,8 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html const ACCOUNT_NUM = 5; -const NUMBER_OF_ATTEMPTS = 30; +const MAX_EXAMPLES = 50; +const STATEFUL_STEP_COUNT = 30; const WEEK = 86400 * 7; const YEAR = 86400 * 365; const two_to_the_256_minus_1 = BigNumber.from("2") @@ -21,11 +22,14 @@ const two_to_the_256_minus_1 = BigNumber.from("2") const MOUNT_DECIMALS = 3; // Helper functions to generate random variables -----> -function randomValue(min: number, max: number): BigNumber { +function randomBigValue(min: number, max: number): BigNumber { return BigNumber.from( Math.floor(Math.random() * (max - min) + min).toString() ); } +function randomValue(min: number, max: number): number { + return Math.floor(Math.random() * (max - min) + min); +} function getRandomAccountNum(): number { // Corresponds strategy("address", length=5) let rdm = Math.floor(Math.random() * ACCOUNT_NUM); //0~9 integer @@ -33,17 +37,18 @@ function getRandomAccountNum(): number { } function getRandomWeeks(): BigNumber { // Corresponds strategy("uint256", min_value=1, max_value=12) - return randomValue(1, 12); + return randomBigValue(1, 12); } function getRandomAmounts(): BigNumber { // Corresponds strategy("decimal", min_value=1, max_value=100, places=3) - return randomValue(1 * 10 ** MOUNT_DECIMALS, 100 * 10 ** MOUNT_DECIMALS).mul( - BigNumber.from(10).pow(18 - MOUNT_DECIMALS) - ); + return randomBigValue( + 1 * 10 ** MOUNT_DECIMALS, + 100 * 10 ** MOUNT_DECIMALS + ).mul(BigNumber.from(10).pow(18 - MOUNT_DECIMALS)); } function getRandomsTime(): BigNumber { // Corresponds strategy("uint256", min_value=0, max_value=86400 * 3) - return randomValue(0, 86400 * 3); + return randomBigValue(0, 86400 * 3); } // ------------------------------------------------ @@ -65,6 +70,11 @@ describe("FeeDistributor", function () { snapshot = await takeSnapshot(); accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); + lockedUntil = {}; + fees = {}; + userClaims = {}; + totalFees = ethers.utils.parseEther("1"); + const CRV = await ethers.getContractFactory("CRV"); const Token = await ethers.getContractFactory("MockToken"); const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); @@ -104,9 +114,11 @@ describe("FeeDistributor", function () { (await time.latest()) + YEAR * 2 ); - lockedUntil[accounts[0].address] = ( - await votingEscrow.lockedEnd(accounts[0].address) - ).toNumber(); + lockedUntil = { + [accounts[0].address]: ( + await votingEscrow.lockedEnd(accounts[0].address) + ).toNumber(), + }; // a week later we deploy the fee distributor // await ethers.provider.send("evm_increaseTime", [WEEK]); @@ -421,37 +433,37 @@ describe("FeeDistributor", function () { } // Display results-------------------------------------------------- - console.log(`Results: ${tokensPerUserPerWeek}`); - console.log(`TokensPerUserPerWeek------------`); - Object.entries(tokensPerUserPerWeek).forEach(([key, val]) => { - console.log(`${key}: ${val}`); - }); - console.log(``); - console.log(`Fees-----------`); - Object.entries(fees).forEach(([key, val]) => { - console.log(`${key}: ${val}`); - }); - console.log(``); - console.log(`Total Fee-----------`); - console.log(totalFees.toString()); - console.log(``); - console.log(`User claims---------`); - Object.entries(userClaims).forEach(([key, val]) => { - console.log(`${key}:`); - Object.entries(val).forEach(([k, v]) => { - console.log(`${k}: ${v}`); - }); - }); - console.log(``); - console.log(`User balances---------`); - for (const acct of accounts) { - console.log( - acct.address, - (await feeCoin.balanceOf(acct.address)).toString() - ); - } - console.log(`feeCoin balance of Distributor--------`); - console.log((await feeCoin.balanceOf(distributor.address)).toString()); + // console.log(`Results: ${tokensPerUserPerWeek}`); + // console.log(`TokensPerUserPerWeek------------`); + // Object.entries(tokensPerUserPerWeek).forEach(([key, val]) => { + // console.log(`${key}: ${val}`); + // }); + // console.log(``); + // console.log(`Fees-----------`); + // Object.entries(fees).forEach(([key, val]) => { + // console.log(`${key}: ${val}`); + // }); + // console.log(``); + // console.log(`Total Fee-----------`); + // console.log(totalFees.toString()); + // console.log(``); + // console.log(`User claims---------`); + // Object.entries(userClaims).forEach(([key, val]) => { + // console.log(`${key}:`); + // Object.entries(val).forEach(([k, v]) => { + // console.log(`${k}: ${v}`); + // }); + // }); + // console.log(``); + // console.log(`User balances---------`); + // for (const acct of accounts) { + // console.log( + // acct.address, + // (await feeCoin.balanceOf(acct.address)).toString() + // ); + // } + // console.log(`feeCoin balance of Distributor--------`); + // console.log((await feeCoin.balanceOf(distributor.address)).toString()); // ------------------------------------------- for (const acct of accounts) { @@ -477,30 +489,33 @@ describe("FeeDistributor", function () { ]; describe("test_deposit_withdraw_voting", function () { - it(`should distributes fee`, async () => { - // Corresponds initializer initialize_new_lock and initialize_transfer_fees - // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html#initializers - // initialize_new_lock: This is equivalent to `rule_new_lock` to make it more likely we have at least 2 accounts locked at the start of the test run. - // initialize_transfer_fees: This is equivalent to `rule_transfer_fees` to make it more likely that claimable fees are available from the start of the test. - const initializerSeed = Math.random(); - if (initializerSeed < 0.2) { - ruleNewLock(); - ruleTransferFees(); - } else if (initializerSeed < 0.4) { - ruleTransferFees(); - ruleNewLock(); - } else if (initializerSeed < 0.6) { - ruleNewLock(); - } else if (initializerSeed < 0.8) { - ruleTransferFees(); - } - - for (let x = 0; x < NUMBER_OF_ATTEMPTS; x++) { - let n = (await randomValue(0, func.length)).toNumber(); - await eval(func[n])(); - } - - await teardown(); - }); + for (let i = 0; i < MAX_EXAMPLES; i++) { + it(`should distributes fee ${i}`, async () => { + // Corresponds initializer initialize_new_lock and initialize_transfer_fees + // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html#initializers + // initialize_new_lock: This is equivalent to `rule_new_lock` to make it more likely we have at least 2 accounts locked at the start of the test run. + // initialize_transfer_fees: This is equivalent to `rule_transfer_fees` to make it more likely that claimable fees are available from the start of the test. + const initializerSeed = Math.random(); + if (initializerSeed < 0.2) { + await ruleNewLock(); + await ruleTransferFees(); + } else if (initializerSeed < 0.4) { + await ruleTransferFees(); + await ruleNewLock(); + } else if (initializerSeed < 0.6) { + await ruleNewLock(); + } else if (initializerSeed < 0.8) { + await ruleTransferFees(); + } + + const steps = randomValue(1, STATEFUL_STEP_COUNT); + for (let x = 0; x < steps; x++) { + let n = randomValue(0, func.length); + await eval(func[n])(); + } + + await teardown(); + }); + } }); }); From 1d9d63e0955c730c94612899b6d95d07c51f27dd Mon Sep 17 00:00:00 2001 From: takadr Date: Thu, 9 Nov 2023 18:52:56 +0900 Subject: [PATCH 040/412] Updated integration tests for VotingEscrow --- .../depositWithdrawVoting.test.ts | 103 +++++++++--------- .../VotingEscrow/votingEscrow.test.ts | 3 +- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts b/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts index 60db4538..4cd75157 100644 --- a/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts +++ b/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts @@ -9,7 +9,8 @@ import { import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; const ACCOUNT_NUM = 10; -const NUMBER_OF_ATTEMPTS = 30; // テストの試行回数 +const MAX_EXAMPLES = 30; // テストの試行回数 +const STATEFUL_STEP_COUNT = 20; // ruleの実行回数 const YEAR = BigNumber.from(86400 * 365); const WEEK = BigNumber.from(86400 * 7); const two_to_the_256_minus_1 = BigNumber.from("2") @@ -94,27 +95,27 @@ describe("VotingEscrow", function () { unlockTime = timestamp.add(WEEK.mul(stLockDuration)).div(WEEK).mul(WEEK); if (stValue.eq(0)) { - console.log("--revert: 1"); + // console.log("--revert: 1"); await expect( votingEscrow.connect(stAccount).createLock(stValue, unlockTime) ).to.revertedWith("need non-zero value"); } else if (votingBalances[stAccountN]["value"].gt("0")) { - console.log("--revert: 2"); + // console.log("--revert: 2"); await expect( votingEscrow.connect(stAccount).createLock(stValue, unlockTime) ).to.revertedWith("Withdraw old tokens first"); } else if (unlockTime.lte(timestamp)) { - console.log("--revert: 3"); + // console.log("--revert: 3"); await expect( votingEscrow.connect(stAccount).createLock(stValue, unlockTime) ).to.revertedWith("Can only lock until time in the future"); } else if (unlockTime.gte(timestamp.add(YEAR.mul("4")))) { - console.log("--revert: 4"); + // console.log("--revert: 4"); await expect( votingEscrow.connect(stAccount).createLock(stValue, unlockTime) ).to.revertedWith("Voting lock can be 4 years max"); } else { - console.log("--success, account:", stAccountN); + // console.log("--success, account:", stAccountN); const tx = await votingEscrow .connect(stAccount) .createLock(stValue, unlockTime); @@ -140,17 +141,17 @@ describe("VotingEscrow", function () { let timestamp = BigNumber.from(await time.latest()); if (stValue.eq(0)) { - console.log("--revert: 1"); + // console.log("--revert: 1"); await expect( votingEscrow.connect(stAccount).increaseAmount(stValue) ).to.revertedWith("dev: need non-zero value"); } else if (votingBalances[stAccountN]["value"].eq("0")) { - console.log("--revert: 2"); + // console.log("--revert: 2"); await expect( votingEscrow.connect(stAccount).increaseAmount(stValue) ).to.revertedWith("No existing lock found"); } else if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { - console.log("--revert: 3"); + // console.log("--revert: 3"); await expect( votingEscrow.connect(stAccount).increaseAmount(stValue) ).to.revertedWith("Cannot add to expired lock. Withdraw"); @@ -159,12 +160,12 @@ describe("VotingEscrow", function () { votingBalances[stAccountN]["value"] = votingBalances[stAccountN]["value"].add(stValue); - console.log( - "--success, account:", - stAccountN, - "new balance:", - votingBalances[stAccountN]["value"].toString() - ); + // console.log( + // "--success, account:", + // stAccountN, + // "new balance:", + // votingBalances[stAccountN]["value"].toString() + // ); } } @@ -185,27 +186,27 @@ describe("VotingEscrow", function () { .mul(WEEK); if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { - console.log("--revert: 1"); + // console.log("--revert: 1"); await expect( votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) ).to.revertedWith("Lock expired"); } else if (votingBalances[stAccountN]["value"].eq("0")) { - console.log("--revert: 2"); + // console.log("--revert: 2"); await expect( votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) ).to.revertedWith("Nothing is locked"); } else if (votingBalances[stAccountN]["unlockTime"].gte(unlockTime)) { - console.log("--revert: 3"); + // console.log("--revert: 3"); await expect( votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) ).to.revertedWith("Can only increase lock duration"); } else if (unlockTime.gt(timestamp.add(YEAR.mul("4")))) { - console.log("--revert: 4"); + // console.log("--revert: 4"); await expect( votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) ).to.revertedWith("Voting lock can be 4 years max"); } else { - console.log("--success, account:", stAccountN); + // console.log("--success, account:", stAccountN); const tx = await votingEscrow .connect(stAccount) .increaseUnlockTime(unlockTime); @@ -227,12 +228,12 @@ describe("VotingEscrow", function () { let timestamp = BigNumber.from(await time.latest()); if (votingBalances[stAccountN]["unlockTime"].gt(timestamp)) { - console.log("--reverted"); + // console.log("--reverted"); await expect(votingEscrow.connect(stAccount).withdraw()).to.revertedWith( "The lock didn't expire" ); } else { - console.log("--success, account:", stAccountN); + // console.log("--success, account:", stAccountN); await votingEscrow.connect(stAccount).withdraw(); votingBalances[stAccountN]["value"] = BigNumber.from("0"); } @@ -263,38 +264,17 @@ describe("VotingEscrow", function () { } } - let func = [ - "ruleCreateLock", - "ruleIncreaseAmount", - "ruleIncreaseUnlockTime", - "ruleWithdraw", - "ruleCheckpoint", - "ruleAdvanceTime", - ]; - - describe("test_deposit_withdraw_voting", function () { - //set arbitral number of repeats - for (let x = 0; x < NUMBER_OF_ATTEMPTS; x++) { - it("try " + eval("x+1"), async () => { - for (let i = 0; i < 100; i++) { - let n = (await rdmValue(func.length)).toNumber(); - await eval(func[n])(); - } - }); - } - }); - - afterEach(async () => { - console.log("=====Invariant checks====="); + async function checkInvariants() { + // console.log("=====Invariant checks====="); - console.log("invariant_token_balances"); + // console.log("invariant_token_balances"); for (let i = 0; i < ACCOUNT_NUM; i++) { expect(await token.balanceOf(accounts[i].address)).to.equal( ten_to_the_40.sub(votingBalances[i]["value"]) ); } - console.log("invariant_escrow_current_balances"); + // console.log("invariant_escrow_current_balances"); let total_supply = BigNumber.from("0"); let timestamp = BigNumber.from(await time.latest()); @@ -314,10 +294,10 @@ describe("VotingEscrow", function () { } expect(await votingEscrow["totalSupply()"]()).to.equal(total_supply); - console.log("invariant_historic_balances"); + // console.log("invariant_historic_balances"); total_supply = BigNumber.from("0"); let blocknumber = (await time.latestBlock()) - 4; - console.log(blocknumber); + // console.log(blocknumber); for (let i = 0; i < ACCOUNT_NUM; i++) { total_supply = total_supply.add( @@ -328,7 +308,32 @@ describe("VotingEscrow", function () { expect(await votingEscrow.totalSupplyAt(blocknumber)).to.equal( total_supply ); + } + + let func = [ + "ruleCreateLock", + "ruleIncreaseAmount", + "ruleIncreaseUnlockTime", + "ruleWithdraw", + "ruleCheckpoint", + "ruleAdvanceTime", + ]; + describe("test_deposit_withdraw_voting", function () { + //set arbitral number of repeats + for (let x = 0; x < MAX_EXAMPLES; x++) { + it(`Try ${x}`, async () => { + let steps = Math.floor(Math.random() * (STATEFUL_STEP_COUNT - 1)) + 1; + for (let i = 0; i < steps; i++) { + let n = (await rdmValue(func.length)).toNumber(); + await eval(func[n])(); + await checkInvariants(); + } + }); + } + }); + + afterEach(async () => { await snapshot.restore(); }); }); diff --git a/test/fork/integration/VotingEscrow/votingEscrow.test.ts b/test/fork/integration/VotingEscrow/votingEscrow.test.ts index 468075f2..96b2f55b 100644 --- a/test/fork/integration/VotingEscrow/votingEscrow.test.ts +++ b/test/fork/integration/VotingEscrow/votingEscrow.test.ts @@ -526,8 +526,9 @@ describe("Voting Powers Test", function () { } async function showStats() { + const initialBlock = (await votingEscrow.pointHistory(0)).blk.toNumber(); const latestBlock = await time.latestBlock(); - for (let i = 2; i < latestBlock; i++) { + for (let i = initialBlock; i < latestBlock; i++) { let a = await votingEscrow.balanceOfAt(alice.address, `${i}`); let b = await votingEscrow.balanceOfAt(bob.address, `${i}`); console.log(`Block ${i}: a: ${a.toString()} b: ${b.toString()}`); From f5be1a255a498eeb8fa18e0b833462778ebdd173 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 10 Nov 2023 01:11:13 +0900 Subject: [PATCH 041/412] Improved Minter test code --- test/fork/unitary/Minter/Minter.test.ts | 518 +++++++++++++----------- 1 file changed, 282 insertions(+), 236 deletions(-) diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts index 66da5f58..541bd5f7 100644 --- a/test/fork/unitary/Minter/Minter.test.ts +++ b/test/fork/unitary/Minter/Minter.test.ts @@ -1,417 +1,463 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { BigNumber, Contract } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; describe("Minter", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); - await setup.addType(); - await setup.addGauge(); - await setup.createLP(); + let accounts: any; + let minter: Contract; + let gaugeController: Contract; + let token: Contract; + let mockLpToken: Contract; + let threeGauges: String[]; + let gauges: Contract[]; + + let snapshot: SnapshotRestorer; + + const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; + const GAUGE_WEIGHTS = Constants.GAUGE_WEIGHTS; + const GAUGE_TYPES = [0, 0, 1]; + + const ten_to_the_18 = Constants.ten_to_the_18; + const ten_to_the_17 = Constants.ten_to_the_17; + const ZERO_ADDRESS = Constants.ZERO_ADDRESS; + const zero = Constants.zero; + const MONTH = Constants.MONTH; + const WEEK = Constants.WEEK; + const month = Constants.month; + const week = Constants.week; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController, minter, token, mockLpToken, threeGauges, gauges } = + await deployContracts()); + + // Set minter for the token + await token.setMinter(minter.address); + + // Skip to the start of a new epoch week + const currentWeek = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ) + .div(WEEK) + .mul(WEEK); + await ethers.provider.send("evm_setNextBlockTimestamp", [ + currentWeek.add(WEEK).toNumber(), + ]); + + // Add types and gauges + for (const weight of TYPE_WEIGHTS) { + await gaugeController.addType("Liquidity", weight); + } + for (let i = 0; i < 3; i++) { + await gaugeController.addGauge( + threeGauges[i], + GAUGE_TYPES[i], + GAUGE_WEIGHTS[i] + ); + } + for (const account of accounts) { + mockLpToken.transfer(account.address, ten_to_the_18); + } + for (let i = 1; i <= 3; i++) { + for (const gauge of threeGauges) { + await mockLpToken.connect(accounts[i]).approve(gauge, ten_to_the_18); + } + } }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); - describe("Minter Behavior", function () { // Test basic mint functionality - it("test mint", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); //gauge_address, msg.sender, mint - let expected = await setup.lg.integrateFraction( - setup.aliceAddress + it("test_mint", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); + + await ethers.provider.send("evm_increaseTime", [month]); + + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); //gauge_address, msg.sender, mint + let expected = await gauges[0].integrateFraction( + accounts[1].address ); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal( expected ); expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) ).to.equal(expected); }); // Test minting immediately after setup - it("test mint immediate", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_18, setup.aliceAddress, false); + it("test_mint_immediate", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_18, accounts[1].address, false); let t0 = BigNumber.from( (await ethers.provider.getBlock("latest")).timestamp ); - let moment = t0.add(setup.WEEK).div(setup.WEEK).mul(setup.WEEK).add("5"); + let moment = t0.add(WEEK).div(WEEK).mul(WEEK).add("5"); await ethers.provider.send("evm_setNextBlockTimestamp", [ moment.toNumber(), ]); //mint expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) ).to.equal("0"); - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); //check - let balance = await setup.token.balanceOf(setup.aliceAddress); + let balance = await token.balanceOf(accounts[1].address); expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) ).to.equal(balance); }); // Test multiple mint operations on the same gauge - it("test mint multiple same gauge", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_18, setup.aliceAddress, false); - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); - let balance = await setup.token.balanceOf(setup.aliceAddress); - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); - let expected = await setup.lg.integrateFraction( - setup.aliceAddress + it("test_mint_multiple_same_gauge", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_18, accounts[1].address, false); + await ethers.provider.send("evm_increaseTime", [month]); + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); + let balance = await token.balanceOf(accounts[1].address); + await ethers.provider.send("evm_increaseTime", [month]); + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); + let expected = await gauges[0].integrateFraction( + accounts[1].address ); - let final_balance = await setup.token.balanceOf(setup.aliceAddress); + let final_balance = await token.balanceOf(accounts[1].address); expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success expect(final_balance).to.equal(expected); //2nd mint works fine expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) ).to.equal(expected); //tracks fine }); // Test minting across multiple gauges - it("test mint multiple gauges", async () => { + it("test_mint_multiple_gauges", async () => { //setup - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); - await setup.gaugesContracts[1] - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); - await setup.gaugesContracts[2] - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); + await gauges[1] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); + await gauges[2] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); + + await ethers.provider.send("evm_increaseTime", [month]); //mint for (let i = 0; i < 3; i++) { - await setup.minter - .connect(setup.alice) - .mint(setup.gaugesAddress[i]); + await minter + .connect(accounts[1]) + .mint(threeGauges[i]); } //check let total_minted = BigNumber.from("0"); for (let i = 0; i < 3; i++) { - let gauge = setup.gaugesContracts[i]; - let minted = await setup.minter.minted( - setup.aliceAddress, + let gauge = gauges[i]; + let minted = await minter.minted( + accounts[1].address, gauge.address ); expect(minted).to.equal( - await gauge.integrateFraction(setup.aliceAddress) + await gauge.integrateFraction(accounts[1].address) ); total_minted = total_minted.add(minted); } - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal( total_minted ); }); // Test minting after withdrawing - it("test mint after withdraw", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_18, setup.aliceAddress, false); + it("test_mint_after_withdraw", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_18, accounts[1].address, false); await ethers.provider.send("evm_increaseTime", [ - setup.WEEK.mul(BigNumber.from("2")).toNumber(), + week * 2, ]); - await setup.lg - .connect(setup.alice) - .withdraw(setup.ten_to_the_18, false); - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); + await gauges[0] + .connect(accounts[1]) + .withdraw(ten_to_the_18, false); + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); expect( - (await setup.token.balanceOf(setup.aliceAddress)).gt( + (await token.balanceOf(accounts[1].address)).gt( BigNumber.from("0") ) ).to.equal(true); }); // Test multiple mints after withdrawing - it("test mint multiple after withdraw", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_18, setup.aliceAddress, false); + it("test_mint_multiple_after_withdraw", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_18, accounts[1].address, false); await ethers.provider.send("evm_increaseTime", [10]); - await setup.lg - .connect(setup.alice) - .withdraw(setup.ten_to_the_18, false); - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); + await gauges[0] + .connect(accounts[1]) + .withdraw(ten_to_the_18, false); + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); - let balance = await setup.token.balanceOf(setup.aliceAddress); + let balance = await token.balanceOf(accounts[1].address); await ethers.provider.send("evm_increaseTime", [10]); - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal( balance ); }); // Test mint without any deposit - it("test no deposit", async () => { - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( - setup.zero + it("test_no_deposit", async () => { + await minter + .connect(accounts[1]) + .mint(threeGauges[0]); + expect(await token.balanceOf(accounts[1].address)).to.equal( + zero ); expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) - ).to.equal(setup.zero); + ).to.equal(zero); }); // Test minting with the wrong gauge - it("test mint wrong gauge", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_18, setup.aliceAddress, false); + it("test_mint_wrong_gauge", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_18, accounts[1].address, false); - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); - await setup.minter - .connect(setup.alice) - .mint(setup.gaugesAddress[1]); + await ethers.provider.send("evm_increaseTime", [month]); + await minter + .connect(accounts[1]) + .mint(threeGauges[1]); //check - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( - setup.zero + expect(await token.balanceOf(accounts[1].address)).to.equal( + zero ); expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) - ).to.equal(setup.zero); + ).to.equal(zero); expect( - await setup.minter.minted( - setup.aliceAddress, - setup.gaugesAddress[1] + await minter.minted( + accounts[1].address, + threeGauges[1] ) - ).to.equal(setup.zero); + ).to.equal(zero); }); // Test minting with an invalid gauge address - it("test mint not a gauge", async () => { - await expect(setup.minter.mint(setup.aliceAddress)).to.revertedWith( + it("test_mint_not_a_gauge", async () => { + await expect(minter.mint(accounts[1].address)).to.revertedWith( "dev: gauge is not added" ); }); // Test minting before inflation begins - it("test mint before inflation begins", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_18, setup.aliceAddress, false); - expect(await setup.token.miningEpoch()).to.equal(BigNumber.from("-1")); - - await setup.minter - .connect(setup.alice) - .mint(setup.lg.address); - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( - BigNumber.from("0") - ); - expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address - ) - ).to.equal(setup.zero); + it("test_mint_before_inflation_begins", async function () { + await gauges[0].connect(accounts[1]).deposit(ten_to_the_18, accounts[1].address, false); + const startEpochTime = await token.startEpochTime(); + const currentTime = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + const timeToSleep = startEpochTime.sub(currentTime).sub(5); + await ethers.provider.send("evm_increaseTime", [timeToSleep.toNumber()]); + + await minter.connect(accounts[1]).mint(threeGauges[0]); + + expect(await token.balanceOf(accounts[1].address)).to.equal(BigNumber.from(0)); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal(BigNumber.from(0)); }); // Test mintMany function with multiple gauges - it("test mintMany function multiple gauges", async () => { + it("test_mintMany_function_multiple_gauges", async () => { //setup - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); - await setup.gaugesContracts[1] - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); - await setup.gaugesContracts[2] - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); - - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); + await gauges[1] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); + await gauges[2] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); + + await ethers.provider.send("evm_increaseTime", [month]); let addresses = [ - setup.lg.address, - setup.gaugesAddress[1], - setup.gaugesAddress[2], - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, - setup.ZERO_ADDRESS, + threeGauges[0], + threeGauges[1], + threeGauges[2], + ZERO_ADDRESS, + ZERO_ADDRESS, + ZERO_ADDRESS, + ZERO_ADDRESS, + ZERO_ADDRESS, ]; - await setup.minter.connect(setup.alice).mintMany(addresses); + await minter.connect(accounts[1]).mintMany(addresses); //check let total_minted = BigNumber.from("0"); for (let i = 0; i < 3; i++) { - let gauge = setup.gaugesContracts[i]; - let minted = await setup.minter.minted( - setup.aliceAddress, + let gauge = gauges[i]; + let minted = await minter.minted( + accounts[1].address, gauge.address ); expect(minted).to.equal( - await gauge.integrateFraction(setup.aliceAddress) + await gauge.integrateFraction(accounts[1].address) ); total_minted = total_minted.add(minted); } - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal( total_minted ); }); // Test toggling of the mint approval function - it("test toggleApproveMint function", async () => { - await setup.minter - .connect(setup.alice) - .toggleApproveMint(setup.accountsAddress[2]); + it("test_toggleApproveMint_function", async () => { + await minter + .connect(accounts[1]) + .toggleApproveMint(accounts[2].address); expect( - await setup.minter.allowedToMintFor( - setup.accountsAddress[2], - setup.aliceAddress + await minter.allowedToMintFor( + accounts[2].address, + accounts[1].address ) ).to.equal(true); - await setup.minter - .connect(setup.alice) - .toggleApproveMint(setup.accountsAddress[2]); + await minter + .connect(accounts[1]) + .toggleApproveMint(accounts[2].address); expect( - await setup.minter.allowedToMintFor( - setup.accountsAddress[2], - setup.aliceAddress + await minter.allowedToMintFor( + accounts[2].address, + accounts[1].address ) ).to.equal(false); }); // Test minting on behalf of another user - it("test mintFor function", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); + it("test_mintFor_function", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await ethers.provider.send("evm_increaseTime", [month]); - await setup.minter - .connect(setup.alice) - .toggleApproveMint(setup.accountsAddress[2]); + await minter + .connect(accounts[1]) + .toggleApproveMint(accounts[2].address); expect( - await setup.minter.allowedToMintFor( - setup.accountsAddress[2], - setup.aliceAddress + await minter.allowedToMintFor( + accounts[2].address, + accounts[1].address ) ).to.equal(true); - await setup.minter - .connect(setup.accounts[2]) - .mintFor(setup.lg.address, setup.aliceAddress); + await minter + .connect(accounts[2]) + .mintFor(threeGauges[0], accounts[1].address); - let expected = await setup.lg.integrateFraction( - setup.aliceAddress + let expected = await gauges[0].integrateFraction( + accounts[1].address ); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal( expected ); expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) ).to.equal(expected); }); // Test mintFor function when not approved - it("test mintForFail function", async () => { - await setup.lg - .connect(setup.alice) - .deposit(setup.ten_to_the_17, setup.aliceAddress, false); + it("test_mintForFail_function", async () => { + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_17, accounts[1].address, false); - await ethers.provider.send("evm_increaseTime", [setup.MONTH.toNumber()]); + await ethers.provider.send("evm_increaseTime", [month]); expect( - await setup.minter.allowedToMintFor( - setup.accountsAddress[2], - setup.aliceAddress + await minter.allowedToMintFor( + accounts[2].address, + accounts[1].address ) ).to.equal(false); - await setup.minter - .connect(setup.accounts[2]) - .mintFor(setup.lg.address, setup.aliceAddress); + await minter + .connect(accounts[2]) + .mintFor(threeGauges[0], accounts[1].address); - expect(await setup.token.balanceOf(setup.aliceAddress)).to.equal(0); + expect(await token.balanceOf(accounts[1].address)).to.equal(0); expect( - await setup.minter.minted( - setup.aliceAddress, - setup.lg.address + await minter.minted( + accounts[1].address, + threeGauges[0] ) ).to.equal(0); }); }); -}); +}); \ No newline at end of file From bd3c009d60ee47ee1d07b4f1fd5c7b2eaf7775c6 Mon Sep 17 00:00:00 2001 From: takadr Date: Fri, 10 Nov 2023 22:18:28 +0900 Subject: [PATCH 042/412] Changed type for constants from int to uint. Modified test names --- contracts/curveFork/VotingEscrow.sol | 16 ++++++++-------- test/fork/integration/Minter/components.test.ts | 2 +- .../integration/Minter/minterIntegration.test.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 94ec1bc6..54af4e18 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -40,10 +40,10 @@ contract VotingEscrow is ReentrancyGuard { uint256 end; } - int128 constant DEPOSIT_FOR_TYPE = 0; - int128 constant CREATE_LOCK_TYPE = 1; - int128 constant INCREASE_LOCK_AMOUNT = 2; - int128 constant INCREASE_UNLOCK_TIME = 3; + uint128 private constant DEPOSIT_FOR_TYPE = 0; + uint128 private constant CREATE_LOCK_TYPE = 1; + uint128 private constant INCREASE_LOCK_AMOUNT = 2; + uint128 private constant INCREASE_UNLOCK_TIME = 3; event CommitOwnership(address admin); event ApplyOwnership(address admin); @@ -51,7 +51,7 @@ contract VotingEscrow is ReentrancyGuard { address indexed provider, uint256 value, uint256 indexed locktime, - int128 _type, + uint128 _type, uint256 ts ); event Withdraw(address indexed provider, uint256 value, uint256 ts); @@ -232,7 +232,7 @@ contract VotingEscrow is ReentrancyGuard { } _uOld.bias = _uOld.slope * - int128(uint128(oldLocked_.end) - uint128(block.timestamp)); + int128(uint128(oldLocked_.end - block.timestamp)); } if (newLocked_.end > block.timestamp && newLocked_.amount > 0) { @@ -243,7 +243,7 @@ contract VotingEscrow is ReentrancyGuard { } _uNew.bias = _uNew.slope * - int128(uint128(newLocked_.end) - uint128(block.timestamp)); + int128(uint128(newLocked_.end - block.timestamp)); } // Read values of scheduled changes in the slope @@ -415,7 +415,7 @@ contract VotingEscrow is ReentrancyGuard { uint256 value_, uint256 unlockTime_, LockedBalance memory lockedBalance_, - int128 type_ + uint128 type_ ) internal { LockedBalance memory _locked = LockedBalance( lockedBalance_.amount, diff --git a/test/fork/integration/Minter/components.test.ts b/test/fork/integration/Minter/components.test.ts index bc733040..079ae64b 100644 --- a/test/fork/integration/Minter/components.test.ts +++ b/test/fork/integration/Minter/components.test.ts @@ -14,7 +14,7 @@ const SCALE = BigNumber.from((1e20).toString()); const WEEK = 86400 * 7; const MONTH = 86400 * 30; -describe("Liquidity Gauge Tests", function () { +describe("Minter components", function () { let accounts: SignerWithAddress[]; let gaugeController: Contract; let liquidityGauge: Contract; diff --git a/test/fork/integration/Minter/minterIntegration.test.ts b/test/fork/integration/Minter/minterIntegration.test.ts index 33136395..6b234f3a 100644 --- a/test/fork/integration/Minter/minterIntegration.test.ts +++ b/test/fork/integration/Minter/minterIntegration.test.ts @@ -13,7 +13,7 @@ const WEEK = 86400 * 7; const MONTH = 86400 * 30; const W = BigNumber.from(10).pow(18); -describe("Liquidity Gauge Tests", function () { +describe("Minter integration", function () { let admin, bob, charlie, dan: SignerWithAddress; let gaugeController: Contract; let threeGauges: Contract[3] = []; From b29c893b50e3a80828e05a10fcb32e1be376103e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Sat, 11 Nov 2023 16:57:11 +0900 Subject: [PATCH 043/412] Add test code for ERC20CRV --- contracts/curveFork/CRV.sol | 35 ++-- test/fork/Constants.ts | 1 + .../ERC20CRV/mintIntegration.test.ts | 94 +++++++++++ .../ERC20CRV/mintableInTimeframe.test.ts | 154 ++++++++++++++++++ test/fork/unitary/ERC20CRV/burn.test.ts | 62 +++++++ .../unitary/ERC20CRV/epochTimeSupply.test.ts | 95 +++++++++++ .../unitary/ERC20CRV/inflationDelay.test.ts | 75 +++++++++ test/fork/unitary/ERC20CRV/mint.test.ts | 88 ++++++++++ test/fork/unitary/ERC20CRV/setters.test.ts | 51 ++++++ 9 files changed, 644 insertions(+), 11 deletions(-) create mode 100644 test/fork/integration/ERC20CRV/mintIntegration.test.ts create mode 100644 test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts create mode 100644 test/fork/unitary/ERC20CRV/burn.test.ts create mode 100644 test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts create mode 100644 test/fork/unitary/ERC20CRV/inflationDelay.test.ts create mode 100644 test/fork/unitary/ERC20CRV/mint.test.ts create mode 100644 test/fork/unitary/ERC20CRV/setters.test.ts diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol index 04027e38..33182610 100644 --- a/contracts/curveFork/CRV.sol +++ b/contracts/curveFork/CRV.sol @@ -85,7 +85,7 @@ contract CRV is ERC20 { // @dev Callable by any address, but only once per epoch // Total supply becomes slightly larger if(this function is called late function updateMiningParameters() external { - require(block.timestamp >= startEpochTime + RATE_REDUCTION_TIME); // dev: too soon! + require(block.timestamp >= startEpochTime + RATE_REDUCTION_TIME,"dev: too soon!"); // dev: too soon! _updateMiningParameters(); } @@ -132,7 +132,7 @@ contract CRV is ERC20 { uint256 start, uint256 end ) external view returns (uint256) { - require(start <= end); // dev: start > end + require(start <= end, "dev: start > end"); // dev: start > end uint256 to_mint = 0; uint256 current_epoch_time = startEpochTime; uint256 current_rate = rate; @@ -145,7 +145,7 @@ contract CRV is ERC20 { RATE_REDUCTION_COEFFICIENT; } - require(end <= current_epoch_time + RATE_REDUCTION_TIME); // dev: too far in future + require(end <= current_epoch_time + RATE_REDUCTION_TIME, "dev: too far in future"); // dev: too far in future // Curve will not work in 1000 years. Darn! for (uint i; i < 999; ) { @@ -171,7 +171,7 @@ contract CRV is ERC20 { current_rate = (current_rate * RATE_REDUCTION_COEFFICIENT) / RATE_DENOMINATOR; // double-division with rounding made rate a bit less => good - require(current_rate <= INITIAL_RATE); // This should never happen + require(current_rate <= INITIAL_RATE, "This should never happen"); // This should never happen unchecked { i++; @@ -184,9 +184,8 @@ contract CRV is ERC20 { // @notice Set the minter address // @dev Only callable once, when minter has not yet been set // @param _minter Address of the minter - function setMinter(address _minter) external { - require(msg.sender == admin); // dev: admin only - require(minter == address(0)); // dev: can set the minter only once, at creation + function setMinter(address _minter) external onlyAdmin() { + require(_minter != address(0), "dev: can set the minter only once, at creation"); // dev: can set the minter only once, at creation minter = _minter; emit SetMinter(_minter); } @@ -194,8 +193,7 @@ contract CRV is ERC20 { // @notice Set the new admin. // @dev After all is set up, admin only can change the token name // @param _admin New admin address - function setAdmin(address _admin) external { - require(msg.sender == admin); // dev: admin only + function setAdmin(address _admin) external onlyAdmin() { admin = _admin; emit SetAdmin(_admin); } @@ -206,15 +204,30 @@ contract CRV is ERC20 { // @param _value The amount that will be created // @return bool success function mint(address _to, uint256 _value) external returns (bool) { - require(msg.sender == minter); // dev: minter only - require(_to != address(0)); // dev: zero address + require(msg.sender == minter, "dev: minter only"); // dev: minter only + require(_to != address(0), "dev: zero address"); // dev: zero address if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { _updateMiningParameters(); } + require(totalSupply() + _value <= _availableSupply(), "dev: exceeds allowable mint amount"); _mint(_to, _value); return true; } + + // @notice Burn `_value` tokens belonging to `msg.sender` + // @dev Emits a Transfer event with a destination of 0x00 + // @param _value The amount that will be burned + // @return bool success + function burn(uint256 _value) external returns (bool) { + _burn(msg.sender, _value); + return true; + } + + modifier onlyAdmin() { + require(admin == msg.sender, "dev: admin only"); + _; + } } \ No newline at end of file diff --git a/test/fork/Constants.ts b/test/fork/Constants.ts index 0b03ee47..1d2bf00d 100644 --- a/test/fork/Constants.ts +++ b/test/fork/Constants.ts @@ -33,6 +33,7 @@ class Constants { static ten_to_the_19 = BigNumber.from("10000000000000000000"); static ten_to_the_18 = BigNumber.from("1000000000000000000"); static ten_to_the_17 = BigNumber.from("100000000000000000"); + static ten_to_the_16 = BigNumber.from("10000000000000000"); static ten_to_the_9 = BigNumber.from("1000000000"); static a = BigNumber.from("2"); static b = BigNumber.from("5"); diff --git a/test/fork/integration/ERC20CRV/mintIntegration.test.ts b/test/fork/integration/ERC20CRV/mintIntegration.test.ts new file mode 100644 index 00000000..5b59f6b1 --- /dev/null +++ b/test/fork/integration/ERC20CRV/mintIntegration.test.ts @@ -0,0 +1,94 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber } from "ethers"; + +// Assuming you have a helper function to increase blockchain time +async function increaseTime(duration: number) { + await ethers.provider.send('evm_increaseTime', [duration]); + await ethers.provider.send('evm_mine', []); +} + +const YEAR = 365 * 24 * 60 * 60; // seconds in a year + +describe("ERC20CRV", function () { + let accounts: Signer[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("CRV"); + token = await Token.deploy(); + + await increaseTime(86401); + await token.updateMiningParameters(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("ERC20CRV MintIntegration", function () { + it("should mint the correct amount", async function () { + const duration = YEAR; // Replace with dynamic value as needed + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await increaseTime(duration); + + const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + const amount = currentTime.sub(creationTime).mul(rate); + await token.mint(accounts[1].address, amount); + + expect(await token.balanceOf(accounts[1].address)).to.equal(amount); + expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); + }); + + it("should revert on overmint", async function () { + const duration = YEAR; // Replace with dynamic value as needed + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const rate = await token.rate(); + + await increaseTime(duration); + + const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + const amount = currentTime.sub(creationTime).add(2).mul(rate); + await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith("dev: exceeds allowable mint amount"); + }); + + it("should mint multiple times correctly", async function () { + await token.setMinter(accounts[0].address); + let totalSupply = await token.totalSupply(); + let balance = BigNumber.from(0); + let epochStart = await token.startEpochTime(); + + const durations = [YEAR * 0.33, YEAR * 0.5, YEAR * 0.7]; // Replace with dynamic values as needed + + for (const time of durations) { + await increaseTime(time); + + if ((await ethers.provider.getBlock('latest')).timestamp - epochStart > YEAR) { + await token.updateMiningParameters(); + epochStart = await token.startEpochTime(); + } + + const amount = (await token.availableSupply()).sub(totalSupply); + await token.mint(accounts[1].address, amount); + + balance = balance.add(amount); + totalSupply = totalSupply.add(amount); + + expect(await token.balanceOf(accounts[1].address)).to.equal(balance); + expect(await token.totalSupply()).to.equal(totalSupply); + } + }); + }); +}); diff --git a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts new file mode 100644 index 00000000..db128176 --- /dev/null +++ b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts @@ -0,0 +1,154 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { BigNumber, Contract, Signer } from "ethers"; +import { takeSnapshot, SnapshotRestorer } from "@nomicfoundation/hardhat-network-helpers"; +import Constants from "../../Constants"; + +async function increaseTime(duration: BigNumber) { + await ethers.provider.send('evm_increaseTime', [duration.toNumber()]); + await ethers.provider.send('evm_mine', []); +} + +// Constants +const YEAR = BigNumber.from(365 * 86400); +const INITIAL_RATE = BigNumber.from(274815283); +const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)).div(YEAR).mul(YEAR); +const INITIAL_SUPPLY = BigNumber.from(1303030303); + +describe("ERC20CRV", function () { + let accounts: Signer[]; + let token: Contract; + let snapshot: SnapshotRestorer; + const year = Constants.year; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("CRV"); + token = await Token.deploy(); + + await increaseTime(BigNumber.from(86401)); + await token.updateMiningParameters(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("ERC20CRV MintableInTimeframe", function () { + + // Helper function for approximate equality + function approx(a: BigNumber, b: BigNumber, precision ): boolean { + if (a.isZero() && b.isZero()) { + return true; + } + + // Adjust precision for BigNumber + const precisionAdjusted = BigNumber.from(10).pow(18).mul(precision); + + return a.sub(b).abs().lte(a.add(b).div(precisionAdjusted)); + } + + + // Helper function for theoretical supply calculation + async function theoreticalSupply(token: Contract): Promise { + const epoch = await token.miningEpoch(); + const q = BigNumber.from(10).pow(18).div(BigNumber.from(2).pow(2)); // Equivalent to 1/2**0.25 + let S = INITIAL_SUPPLY.mul(BigNumber.from(10).pow(18)); + + if (epoch.gt(0)) { + S = S.add( + YEAR_1_SUPPLY.mul(BigNumber.from(10).pow(18)) + .mul(BigNumber.from(10).pow(18).sub(q.pow(epoch))) + .div(BigNumber.from(10).pow(18).sub(q)) + ); + } + + S = S.add( + YEAR_1_SUPPLY.div(YEAR) + .mul(q.pow(epoch)) + .mul((await ethers.provider.getBlock("latest")).timestamp - (await token.startEpochTime())) + ); + + return S; + } + + it("test_mintable_in_timeframe", async function () { + const t0 = Number(await token.startEpochTime()); + + // Ensure the exponentiation stays within safe integer limits + const exponent = BigNumber.from(10).pow(1); // Adjust the exponent as necessary + await increaseTime(exponent); + + let t1 = (await ethers.provider.getBlock("latest")).timestamp; + if (t1 - t0 >= year) { + await token.updateMiningParameters(); + } + t1 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + + const availableSupply = await token.availableSupply(); + const mintable = await token.mintableInTimeframe(t0, t1); + expect((availableSupply.sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)).gte(mintable))).to.equal(true); + if (t1 == t0) { + expect(mintable).to.equal(BigNumber.from(0)); + } else { + const tolerance = BigNumber.from("10000000"); // Adjust as needed for precision + expect(availableSupply.sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)).div(mintable).sub(1)).to.be.lt(tolerance); + } + + + // Replace this with the actual theoretical supply calculation + // const theoreticalSupply = BigNumber.from("EXPECTED_SUPPLY_CALCULATION"); + expect(approx(await theoreticalSupply(token), availableSupply, Constants.ten_to_the_16)).to.equal(true); + }); + + it("test_random_range_year_one", async function () { + const creationTime = await token.startEpochTime(); + const time1 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); + const time2 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); + const [start, end] = [creationTime.add(time1), creationTime.add(time2)]; + const sortedTimes = start.lt(end) ? [start, end] : [end, start]; + const rate = YEAR_1_SUPPLY.div(YEAR); + + expect(await token.mintableInTimeframe(sortedTimes[0], sortedTimes[1])).to.equal(rate.mul(sortedTimes[1].sub(sortedTimes[0]))); + }); + + it("test_random_range_multiple_epochs", async function () { + const creationTime = await token.startEpochTime(); + const start = creationTime.add(YEAR.mul(2)); + const duration = YEAR.mul(2); + const end = start.add(duration); + + const startEpoch = start.sub(creationTime).div(YEAR); + const endEpoch = end.sub(creationTime).div(YEAR); + const exponent = startEpoch.mul(25); + const rate = YEAR_1_SUPPLY.div(YEAR).div(BigNumber.from(2).pow(exponent.div(100))); + + for (let i = startEpoch.toNumber(); i < endEpoch.toNumber(); i++) { + await increaseTime(YEAR); + await token.updateMiningParameters(); + } + + const mintable = await token.mintableInTimeframe(start, end); + if (startEpoch.eq(endEpoch)) { + const expectedMintable = rate.mul(end.sub(start)); + expect(approx(mintable, expectedMintable, Constants.ten_to_the_16)).to.be.true; + } else { + expect(mintable.lt(rate.mul(end))).to.be.true; + } + }); + + it("test_available_supply", async function () { + const duration = BigNumber.from(100000); + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await increaseTime(duration); + + const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + const expected = initialSupply.add(now.sub(creationTime).mul(rate)); + expect(await token.availableSupply()).to.equal(expected); + }); + }); +}); diff --git a/test/fork/unitary/ERC20CRV/burn.test.ts b/test/fork/unitary/ERC20CRV/burn.test.ts new file mode 100644 index 00000000..a0f65015 --- /dev/null +++ b/test/fork/unitary/ERC20CRV/burn.test.ts @@ -0,0 +1,62 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber } from "ethers"; +import { deployContracts } from "../../helper"; + +describe("ERC20CRV", function () { + let accounts: Signer[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ token } = await deployContracts()); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("ERC20CRV EpochTimeSupply", function () { + it("test_burn", async function () { + const balance: BigNumber = await token.balanceOf(accounts[0].address); + const initialSupply: BigNumber = await token.totalSupply(); + + await token.connect(accounts[0]).burn(31337); + + expect(await token.balanceOf(accounts[0].address)).to.equal(balance.sub(31337)); + expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); + }); + + it("test_burn_not_admin", async function () { + const initialSupply: BigNumber = await token.totalSupply(); + + await token.transfer(accounts[1].address, 1000000); + await token.connect(accounts[1]).burn(31337); + + expect(await token.balanceOf(accounts[1].address)).to.equal(1000000 - 31337); + expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); + }); + + it("test_burn_all", async function () { + const initialSupply: BigNumber = await token.totalSupply(); + + await token.connect(accounts[0]).burn(initialSupply); + + expect(await token.balanceOf(accounts[0].address)).to.equal(0); + expect(await token.totalSupply()).to.equal(0); + }); + + it("test_overburn", async function () { + const initialSupply: BigNumber = await token.totalSupply(); + + await expect(token.connect(accounts[0]).burn(initialSupply.add(1))) + .to.be.revertedWith("ERC20: burn amount exceeds balance"); + }); + }); +}); diff --git a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts new file mode 100644 index 00000000..17477e59 --- /dev/null +++ b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts @@ -0,0 +1,95 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber, Contract } from "ethers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; + +describe("ERC20CRV", function () { + let accounts: any[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + const week = Constants.week; + const YEAR = Constants.YEAR; + const year = Constants.year; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ token } = await deployContracts()); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("ERC20CRV EpochTimeSupply", function () { + it("test_start_epoch_time_write", async function () { + const creationTime: BigNumber = await token.startEpochTime(); + await ethers.provider.send("evm_increaseTime", [year]); + await ethers.provider.send("evm_mine", []); + + expect(await token.startEpochTime()).to.equal(creationTime); + + await token.startEpochTimeWrite(); + + expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); + }); + + it("test_start_epoch_time_write_same_epoch", async function () { + await token.startEpochTimeWrite(); + await token.startEpochTimeWrite(); + }); + + it("test_update_mining_parameters", async function () { + const creationTime = await token.startEpochTime(); + const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + const newEpoch = creationTime.add(YEAR).sub(now); + await ethers.provider.send("evm_increaseTime", [newEpoch.toNumber()]); + await token.updateMiningParameters(); + }); + + it("test_update_mining_parameters_same_epoch", async function () { + const creationTime = await token.startEpochTime(); + const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + const newEpoch = creationTime.add(YEAR).sub(now); + await ethers.provider.send("evm_increaseTime", [newEpoch.sub(BigNumber.from("3")).toNumber()]); + await expect(token.updateMiningParameters()).to.be.revertedWith("dev: too soon!"); + }); + + it("test_mintable_in_timeframe_end_before_start", async function () { + const creationTime = await token.startEpochTime(); + await expect(token.mintableInTimeframe(creationTime.add(1), creationTime)).to.be.revertedWith("dev: start > end"); + }); + + it("test_mintable_in_timeframe_multiple_epochs", async function () { + const creationTime = await token.startEpochTime(); + + // Two epochs should not raise + const mintable = BigNumber.from("19").div(BigNumber.from("10")) + await token.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("19").div(BigNumber.from("10")))); + + // Three epochs should raise + await expect(token.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("21").div(BigNumber.from("10"))))).to.be.revertedWith("dev: too far in future"); + }); + + it("test_available_supply", async function () { + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + await ethers.provider.send("evm_increaseTime", [week]); + + const latestBlock = await ethers.provider.getBlock("latest"); + const currentTime = BigNumber.from(latestBlock.timestamp); + + const timeElapsed = currentTime.sub(creationTime); + const expected = initialSupply.add(timeElapsed.mul(rate)); + + expect(await token.availableSupply()).to.equal(expected); + }); + }); +}); diff --git a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts new file mode 100644 index 00000000..06fef4d9 --- /dev/null +++ b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts @@ -0,0 +1,75 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber, Contract } from "ethers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; + +describe("ERC20CRV", function () { + let accounts: Signer[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + const YEAR = Constants.YEAR; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ token } = await deployContracts()); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("ERC20CRV InflationDelay", function () { + it("test_rate", async function () { + expect(await token.rate()).to.equal(0); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.rate()).to.be.gt(0); + }); + + it("test_start_epoch_time", async function () { + const creationTime = await token.startEpochTime(); + // const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + // expect(creationTime).to.equal(now.add("86392").sub(YEAR)); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); + }); + + it("test_mining_epoch", async function () { + expect(await token.miningEpoch()).to.equal(-1); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.miningEpoch()).to.equal(0); + }); + + it("test_available_supply", async function () { + expect(await token.availableSupply()).to.equal(ethers.utils.parseEther("1303030303")); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.availableSupply()).to.be.gt(ethers.utils.parseEther("1303030303")); + }); + }); +}); diff --git a/test/fork/unitary/ERC20CRV/mint.test.ts b/test/fork/unitary/ERC20CRV/mint.test.ts new file mode 100644 index 00000000..f0d860a8 --- /dev/null +++ b/test/fork/unitary/ERC20CRV/mint.test.ts @@ -0,0 +1,88 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber, Contract } from "ethers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; + +describe("ERC20CRV", function () { + let accounts: Signer[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + const WEEK = Constants.WEEK; + const week = Constants.week; + const YEAR = Constants.YEAR; + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("CRV"); + token = await Token.deploy(); + await ethers.provider.send('evm_increaseTime', [86401]); + await token.updateMiningParameters(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("ERC20CRV Mint", function () { + it('test_available_supply', async function () { + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await ethers.provider.send('evm_increaseTime', [week]); + await ethers.provider.send('evm_mine', []); + + const currentBlock = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + // const currentBlock = await ethers.provider.getBlock('latest'); + const expected = initialSupply.add(currentBlock.sub(creationTime).mul(rate)); + expect(await token.availableSupply()).to.equal(expected); + }); + + it('test_mint', async function () { + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await ethers.provider.send('evm_increaseTime', [week]); + + const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + const amount = currentTime.sub(creationTime).mul(rate); + await token.mint(accounts[1].address, amount); + + expect(await token.balanceOf(accounts[1].address)).to.equal(amount); + expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); + }); + + it('test_overmint', async function () { + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const rate = await token.rate(); + + await ethers.provider.send('evm_increaseTime', [week]); + await ethers.provider.send('evm_mine', []); + + const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + const amount = currentTime.sub(creationTime).add(2).mul(rate); + await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith("dev: exceeds allowable mint amount"); + + }); + + it('test_minter_only', async function () { + await token.setMinter(accounts[0].address); + await expect(token.connect(accounts[1]).mint(accounts[1].address, 0)).to.be.revertedWith("dev: minter only"); + }); + + it('test_zero_address', async function () { + await token.setMinter(accounts[0].address); + await expect(token.mint(ZERO_ADDRESS, 0)).to.be.revertedWith("dev: zero address"); + }); + }); +}); diff --git a/test/fork/unitary/ERC20CRV/setters.test.ts b/test/fork/unitary/ERC20CRV/setters.test.ts new file mode 100644 index 00000000..e83373cf --- /dev/null +++ b/test/fork/unitary/ERC20CRV/setters.test.ts @@ -0,0 +1,51 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber, Contract } from "ethers"; +import { deployContracts } from "../../helper"; +import Constants from "../../Constants"; + +describe("ERC20CRV", function () { + let accounts: Signer[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + const WEEK = Constants.WEEK; + const week = Constants.week; + const YEAR = Constants.YEAR; + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("CRV"); + token = await Token.deploy(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("ERC20CRV Setters", function () { + it("should revert when non-admin tries to set minter", async function () { + await expect(token.connect(accounts[1]).setMinter(accounts[2].address)).to.be.revertedWith("dev: admin only"); + }); + + it("should revert when non-admin tries to set admin", async function () { + await expect(token.connect(accounts[1]).setAdmin(accounts[2].address)).to.be.revertedWith("dev: admin only"); + }); + + it("should allow admin to set minter", async function () { + await token.connect(accounts[0]).setMinter(accounts[1].address); + expect(await token.minter()).to.equal(accounts[1].address); + }); + + it("should allow admin to set admin", async function () { + await token.connect(accounts[0]).setAdmin(accounts[1].address); + expect(await token.admin()).to.equal(accounts[1].address); + }); + }); +}); From 9e7bf13ab9670028c634b7d3b65c8bff8d881edf Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 13 Nov 2023 10:26:02 +0900 Subject: [PATCH 044/412] Test code refactoring --- test/fork/Constants.ts | 11 +- test/fork/helper.ts | 195 +----------------- .../ERC20CRV/mintIntegration.test.ts | 4 +- .../ERC20CRV/mintableInTimeframe.test.ts | 7 +- .../LiquidityGauge/liquidityGauge.test.ts | 165 ++++++++------- test/fork/unitary/ERC20CRV/burn.test.ts | 7 +- .../unitary/ERC20CRV/epochTimeSupply.test.ts | 5 +- .../unitary/ERC20CRV/inflationDelay.test.ts | 7 +- test/fork/unitary/ERC20CRV/mint.test.ts | 8 +- test/fork/unitary/ERC20CRV/setters.test.ts | 12 +- .../gaugecontrollerAdmin.test.ts | 8 +- .../GaugeController/gaugesWeights.test.ts | 10 +- .../GaugeController/timestamps.test.ts | 18 +- .../GaugeController/totalWeight.test.ts | 7 +- .../fork/unitary/GaugeController/vote.test.ts | 14 +- .../GaugeController/voteWeightUnitary.test.ts | 14 +- .../unitary/LiquidityGauge/checkpoint.test.ts | 44 ++-- .../LiquidityGauge/depositWithdraw.test.ts | 104 +++++----- test/fork/unitary/LiquidityGauge/kick.test.ts | 68 +++--- test/fork/unitary/Minter/Minter.test.ts | 6 +- 20 files changed, 282 insertions(+), 432 deletions(-) diff --git a/test/fork/Constants.ts b/test/fork/Constants.ts index 1d2bf00d..8ff56354 100644 --- a/test/fork/Constants.ts +++ b/test/fork/Constants.ts @@ -9,7 +9,7 @@ class Constants { static day = 86400; static week = 86400 * 7; static month = 86400 * 30; - static year = 86400 * 265; + static year = 86400 * 365; static TYPE_WEIGHTS: BigNumber[] = [ BigNumber.from(5).mul(BigNumber.from(10).pow(17)), @@ -48,15 +48,6 @@ class Constants { BigNumber.from("2"), ]; - static TYPE_WEIGHTS = [ - Constants.ten_to_the_17.mul(Constants.b), - Constants.ten_to_the_18.mul(Constants.a), - ]; - static GAUGE_WEIGHTS = [ - Constants.ten_to_the_18.mul(Constants.a), - Constants.ten_to_the_18, - Constants.ten_to_the_17.mul(Constants.b), - ]; } export default Constants; diff --git a/test/fork/helper.ts b/test/fork/helper.ts index da03d1b1..6d73be1d 100644 --- a/test/fork/helper.ts +++ b/test/fork/helper.ts @@ -1,15 +1,4 @@ import { ethers } from "hardhat"; -import { BigNumber, Contract, Signer } from "ethers"; - -class EVMUtils { - async snapshot(): Promise { - return await ethers.provider.send("evm_snapshot", []); - } - - async restore(snapshotId: string): Promise { - return await ethers.provider.send("evm_revert", [snapshotId]); - } -} async function deployContracts() { const [creator, alice, bob, charly] = await ethers.getSigners(); @@ -75,187 +64,5 @@ async function deployContracts() { }; } -class TestSetup { - readonly DAY = BigNumber.from(86400); - readonly WEEK = BigNumber.from(86400 * 7); - readonly MONTH = BigNumber.from(86400 * 30); - readonly YEAR = BigNumber.from(86400 * 365); - - readonly name = "Token"; - readonly symbol = "Token"; - readonly decimal = 18; - readonly INITIAL_SUPPLY = BigNumber.from("1303030303000000000000000000"); - - readonly ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; - - readonly ten_to_the_24 = BigNumber.from("1000000000000000000000000"); - readonly ten_to_the_21 = BigNumber.from("1000000000000000000000"); - readonly ten_to_the_20 = BigNumber.from("100000000000000000000"); - readonly ten_to_the_19 = BigNumber.from("10000000000000000000"); - readonly ten_to_the_18 = BigNumber.from("1000000000000000000"); - readonly ten_to_the_17 = BigNumber.from("100000000000000000"); - readonly ten_to_the_9 = BigNumber.from("1000000000"); - readonly a = BigNumber.from("2"); - readonly b = BigNumber.from("5"); - readonly zero = BigNumber.from("0"); - readonly MAX_UINT256 = BigNumber.from( - "115792089237316195423570985008687907853269984665640564039457584007913129639935" - ); - - readonly GAUGE_TYPES = [ - BigNumber.from("1"), - BigNumber.from("1"), - BigNumber.from("2"), - ]; - - creator: Signer; - alice: Signer; - bob: Signer; - charly: Signer; - creatorAddress: String; - aliceAddress: String; - bobAddress: String; - charlyAddress: String; - accounts: Signer[]; - accountsAddress: String[]; - token: Contract; - votingEscrow: Contract; - gaugeController: Contract; - mockLpToken: Contract; - minter: Contract; - lg: Contract; - gaugesContracts: Contract[]; - gaugesAddress: string[]; - - TYPE_WEIGHTS: BigNumber[]; - GAUGE_WEIGHTS: BigNumber[]; - - async setup() { - [this.creator, this.alice, this.bob, this.charly] = - await ethers.getSigners(); - this.accounts = [this.creator, this.alice, this.bob, this.charly]; - this.creatorAddress = await this.creator.getAddress(); - this.aliceAddress = await this.alice.getAddress(); - this.bobAddress = await this.bob.getAddress(); - this.charlyAddress = await this.charly.getAddress(); - this.accountsAddress = [ - this.creatorAddress, - this.aliceAddress, - this.bobAddress, - this.charlyAddress, - ]; - - this.TYPE_WEIGHTS = [ - this.ten_to_the_17.mul(this.b), - this.ten_to_the_18.mul(this.a), - ]; - this.GAUGE_WEIGHTS = [ - this.ten_to_the_18.mul(this.a), - this.ten_to_the_18, - this.ten_to_the_17.mul(this.b), - ]; - - const Token = await ethers.getContractFactory("CRV"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - const GaugeController = await ethers.getContractFactory("GaugeController"); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); - const TestLP = await ethers.getContractFactory("TestLP"); - const Minter = await ethers.getContractFactory("Minter"); - - // deploy - this.token = await Token.deploy(); - this.votingEscrow = await VotingEscrow.deploy( - this.token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - this.gaugeController = await GaugeController.deploy( - this.token.address, - this.votingEscrow.address - ); - this.mockLpToken = await TestLP.deploy( - "tokenDAO LP token", - "iToken", - 18, - this.ten_to_the_21.mul("2") - ); - this.minter = await Minter.deploy( - this.token.address, - this.gaugeController.address - ); - this.lg = await LiquidityGauge.deploy( - this.mockLpToken.address, - this.minter.address - ); - const lg2 = await LiquidityGauge.deploy( - this.mockLpToken.address, - this.minter.address - ); - const lg3 = await LiquidityGauge.deploy( - this.mockLpToken.address, - this.minter.address - ); - this.gaugesContracts = [this.lg, lg2, lg3]; - this.gaugesAddress = [this.lg.address, lg2.address, lg3.address]; - await this.token.setMinter(this.minter.address); - // setup - await this.gaugeController.addType("none", 0); - } - - async addType() { - await this.gaugeController.addType("Liquidity", this.TYPE_WEIGHTS[0]); - await this.gaugeController.addType("Liquidity", this.TYPE_WEIGHTS[1]); - } - - async addGaugeZero() { - for (let i = 0; i < 2; i++) { - await this.gaugeController.addGauge( - this.gaugesAddress[i], - this.GAUGE_TYPES[i], - 0 - ); - } - } - async addGauge() { - for (let i = 0; i < 3; i++) { - await this.gaugeController.addGauge( - this.gaugesAddress[i], - this.GAUGE_TYPES[i], - this.GAUGE_WEIGHTS[i] - ); - } - } - - async createLock() { - await this.token.approve( - this.votingEscrow.address, - BigNumber.from("1000000000000000000000000") - ); - await this.votingEscrow.createLock( - BigNumber.from("1000000000000000000000000"), - BigNumber.from((await ethers.provider.getBlock("latest")).timestamp).add( - this.YEAR - ) - ); - } - - async createLP() { - for (let i = 1; i < 4; i++) { - await this.mockLpToken.transfer( - this.accountsAddress[i], - this.ten_to_the_18 - ); - } - - for (let i = 0; i < 3; i++) { - for (let t = 0; t < 3; t++) { - await this.mockLpToken - .connect(this.accounts[i + 1]) - .approve(this.gaugesAddress[t], this.ten_to_the_18); - } - } - } -} -export { EVMUtils, TestSetup, deployContracts }; +export { deployContracts }; diff --git a/test/fork/integration/ERC20CRV/mintIntegration.test.ts b/test/fork/integration/ERC20CRV/mintIntegration.test.ts index 5b59f6b1..b84f8467 100644 --- a/test/fork/integration/ERC20CRV/mintIntegration.test.ts +++ b/test/fork/integration/ERC20CRV/mintIntegration.test.ts @@ -4,6 +4,8 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { BigNumber } from "ethers"; // Assuming you have a helper function to increase blockchain time @@ -15,7 +17,7 @@ async function increaseTime(duration: number) { const YEAR = 365 * 24 * 60 * 60; // seconds in a year describe("ERC20CRV", function () { - let accounts: Signer[]; + let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; diff --git a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts index db128176..c5bdfe90 100644 --- a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts +++ b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts @@ -1,7 +1,8 @@ import { ethers } from "hardhat"; import { expect } from "chai"; -import { BigNumber, Contract, Signer } from "ethers"; +import { BigNumber, Contract } from "ethers"; import { takeSnapshot, SnapshotRestorer } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import Constants from "../../Constants"; async function increaseTime(duration: BigNumber) { @@ -10,13 +11,13 @@ async function increaseTime(duration: BigNumber) { } // Constants -const YEAR = BigNumber.from(365 * 86400); +const YEAR = Constants.YEAR; const INITIAL_RATE = BigNumber.from(274815283); const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)).div(YEAR).mul(YEAR); const INITIAL_SUPPLY = BigNumber.from(1303030303); describe("ERC20CRV", function () { - let accounts: Signer[]; + let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; const year = Constants.year; diff --git a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts index 6d5a99e3..6b622473 100644 --- a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts +++ b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts @@ -1,22 +1,41 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { BigNumber, Contract } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { deployContracts } from "../../Helper"; +import Constants from "../../Constants"; describe("LiquidityGauge", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; + let accounts: any; + let gaugeController: Contract; + let token: Contract; + let votingEscrow: Contract; + let mockLpToken: Contract; + let threeGauges: String[]; + let gauges: Contract[]; + + let snapshot: SnapshotRestorer; + const ten_to_the_18 = Constants.ten_to_the_18; + const ten_to_the_20 = Constants.ten_to_the_20; + const ten_to_the_21 = Constants.ten_to_the_21; + const MAX_UINT256 = Constants.MAX_UINT256; + const zero = Constants.zero; + const WEEK = Constants.WEEK; + beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gaugeController, threeGauges, gauges, votingEscrow, token, mockLpToken } = + await deployContracts()); + await gaugeController.addType("none", BigNumber.from("0")); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); describe("Gauge Integral Calculations", function () { @@ -47,31 +66,31 @@ describe("LiquidityGauge", function () { // 最新のブロックのタイムスタンプを取得 let checkpoint = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); // 初期レートの取得 - let checkpoint_rate = await setup.token.rate(); + let checkpoint_rate = await token.rate(); let checkpoint_supply = BigNumber.from("0"); let checkpoint_balance = BigNumber.from("0"); // タイプの追加とその重みの変更 - await setup.gaugeController.addType("Liquidity", BigNumber.from("0")); - await setup.gaugeController.changeTypeWeight(1, setup.ten_to_the_18); - await setup.gaugeController.addGauge(setup.lg.address, 1, setup.ten_to_the_18); + await gaugeController.addType("Liquidity", BigNumber.from("0")); + await gaugeController.changeTypeWeight(1, ten_to_the_18); + await gaugeController.addGauge(threeGauges[0], 1, ten_to_the_18); // LPトークンを送信 - const creatorBalance = await setup.mockLpToken.balanceOf(setup.creatorAddress); - await setup.mockLpToken.transfer( - setup.aliceAddress, + const creatorBalance = await mockLpToken.balanceOf(accounts[0].address); + await mockLpToken.transfer( + accounts[1].address, (creatorBalance).div(BigNumber.from("2")) ); - await setup.mockLpToken.transfer( - setup.bobAddress, + await mockLpToken.transfer( + accounts[2].address, (creatorBalance).div(BigNumber.from("2")) ); // 積分を更新する関数 async function update_integral() { let t1 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - let rate1 = await setup.token.rate(); - let t_epoch = await setup.token.startEpochTime(); + let rate1 = await token.rate(); + let t_epoch = await token.startEpochTime(); let rate_x_time = BigNumber.from("0"); // checkpoint >= t_epoch @@ -91,8 +110,8 @@ describe("LiquidityGauge", function () { checkpoint_rate = rate1; checkpoint = t1; - checkpoint_supply = await setup.lg.totalSupply(); - checkpoint_balance = await setup.lg.balanceOf(setup.aliceAddress); + checkpoint_supply = await gauges[0].totalSupply(); + checkpoint_balance = await gauges[0].balanceOf(accounts[1].address); } // Bobは常に預金または引き出しを行い、Aliceはそれをあまり行わない @@ -109,42 +128,42 @@ describe("LiquidityGauge", function () { if (is_withdraw) { // 引き出し処理 let amount = BigNumber.from(Math.floor(Math.random() * 10000).toString()) - .mul(await setup.lg.balanceOf(setup.bobAddress)) + .mul(await gauges[0].balanceOf(accounts[2].address)) .div(BigNumber.from("10000")); - await setup.lg.connect(setup.bob).withdraw(amount, false); + await gauges[0].connect(accounts[2]).withdraw(amount, false); await update_integral(); bob_staked = bob_staked.sub(amount); } else { // 預金処理 let amount = BigNumber.from(Math.floor(Math.random() * 10000).toString()) - .mul(await setup.mockLpToken.balanceOf(setup.bobAddress)) + .mul(await mockLpToken.balanceOf(accounts[2].address)) .div(BigNumber.from("10")) .div(BigNumber.from("10000")); - await setup.mockLpToken.connect(setup.bob).approve(setup.lg.address, amount); - await setup.lg.connect(setup.bob).deposit(amount, setup.bobAddress, false); + await mockLpToken.connect(accounts[2]).approve(threeGauges[0], amount); + await gauges[0].connect(accounts[2]).deposit(amount, accounts[2].address, false); await update_integral(); bob_staked = bob_staked.add(amount); } // Aliceの処理 if (is_alice) { - let is_withdraw_alice = (await setup.lg.balanceOf(setup.aliceAddress)) > 0 && Math.random() > 0.5; + let is_withdraw_alice = (await gauges[0].balanceOf(accounts[1].address)) > 0 && Math.random() > 0.5; if (is_withdraw_alice) { // 引き出し処理 let amount_alice = BigNumber.from(Math.floor(Math.random() * 10000).toString()) - .mul(await setup.lg.balanceOf(setup.aliceAddress)) + .mul(await gauges[0].balanceOf(accounts[1].address)) .div(BigNumber.from("10")) .div(BigNumber.from("10000")); - await setup.lg.connect(setup.alice).withdraw(amount_alice, false); + await gauges[0].connect(accounts[1]).withdraw(amount_alice, false); await update_integral(); alice_staked = alice_staked.sub(amount_alice); } else { // 預金処理 let amount_alice = BigNumber.from(Math.floor(Math.random() * 10000).toString()) - .mul(await setup.mockLpToken.balanceOf(setup.aliceAddress)) + .mul(await mockLpToken.balanceOf(accounts[1].address)) .div(BigNumber.from("10000")); - await setup.mockLpToken.connect(setup.alice).approve(setup.lg.address, amount_alice); - await setup.lg.connect(setup.alice).deposit(amount_alice, setup.aliceAddress, false); + await mockLpToken.connect(accounts[1]).approve(threeGauges[0], amount_alice); + await gauges[0].connect(accounts[1]).deposit(amount_alice, accounts[1].address, false); await update_integral(); alice_staked = alice_staked.add(amount_alice); } @@ -152,22 +171,22 @@ describe("LiquidityGauge", function () { // 同じ秒数でのチェックポイントの更新は影響しないことの確認 if (Math.random() < 0.5) { - await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); } if (Math.random() < 0.5) { - await setup.lg.connect(setup.bob).userCheckpoint(setup.bobAddress); + await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); } // 保有量の確認 - expect(await setup.lg.balanceOf(setup.aliceAddress)).to.equal(alice_staked); - expect(await setup.lg.balanceOf(setup.bobAddress)).to.equal(bob_staked); - expect(await setup.lg.totalSupply()).to.equal(alice_staked.add(bob_staked)); + expect(await gauges[0].balanceOf(accounts[1].address)).to.equal(alice_staked); + expect(await gauges[0].balanceOf(accounts[2].address)).to.equal(bob_staked); + expect(await gauges[0].totalSupply()).to.equal(alice_staked.add(bob_staked)); // ランダムな時間経過をさらにシミュレート dt = BigNumber.from(Math.floor(Math.random() * 86400 * 19).toString()).add(BigNumber.from("1")); await ethers.provider.send("evm_increaseTime", [dt.toNumber()]); - await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); await update_integral(); } }); @@ -194,90 +213,90 @@ describe("LiquidityGauge", function () { it("should distribute rewards according to vote lock status", async () => { // 2週間と5秒時間を進める await ethers.provider.send("evm_increaseTime", [ - setup.WEEK.mul(BigNumber.from("2")).add(BigNumber.from("5")).toNumber(), + WEEK.mul(BigNumber.from("2")).add(BigNumber.from("5")).toNumber(), ]); // ゲージをコントローラーに接続して適切なレートなどを設定する - await setup.gaugeController.addType("Liquidity", 0); - await setup.gaugeController.changeTypeWeight(1, setup.ten_to_the_18); - await setup.gaugeController.addGauge(setup.lg.address, 1, setup.ten_to_the_18); + await gaugeController.addType("Liquidity", 0); + await gaugeController.changeTypeWeight(1, ten_to_the_18); + await gaugeController.addGauge(threeGauges[0], 1, ten_to_the_18); // トークンの準備 - await setup.token.transfer(setup.aliceAddress, setup.ten_to_the_20); - await setup.token.transfer(setup.bobAddress, setup.ten_to_the_20); + await token.transfer(accounts[1].address, ten_to_the_20); + await token.transfer(accounts[2].address, ten_to_the_20); - await setup.token.connect(setup.alice).approve(setup.votingEscrow.address, setup.MAX_UINT256); - await setup.token.connect(setup.bob).approve(setup.votingEscrow.address, setup.MAX_UINT256); - const creatorBalance = await setup.mockLpToken.balanceOf(setup.creatorAddress); - await setup.mockLpToken.transfer( - setup.aliceAddress, + await token.connect(accounts[1]).approve(votingEscrow.address, MAX_UINT256); + await token.connect(accounts[2]).approve(votingEscrow.address, MAX_UINT256); + const creatorBalance = await mockLpToken.balanceOf(accounts[0].address); + await mockLpToken.transfer( + accounts[1].address, (creatorBalance).div(BigNumber.from("2")) ); - await setup.mockLpToken.transfer( - setup.bobAddress, + await mockLpToken.transfer( + accounts[2].address, (creatorBalance).div(BigNumber.from("2")) ); - await setup.mockLpToken.connect(setup.alice).approve(setup.lg.address, setup.MAX_UINT256); - await setup.mockLpToken.connect(setup.bob).approve(setup.lg.address, setup.MAX_UINT256); + await mockLpToken.connect(accounts[1]).approve(threeGauges[0], MAX_UINT256); + await mockLpToken.connect(accounts[2]).approve(threeGauges[0], MAX_UINT256); // Aliceがescrowにデポジットする。AliceはBOOSTを持っていることになる let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - await setup.votingEscrow.connect(setup.alice).createLock(setup.ten_to_the_20, t.add(setup.WEEK.mul(BigNumber.from("2")))); + await votingEscrow.connect(accounts[1]).createLock(ten_to_the_20, t.add(WEEK.mul(BigNumber.from("2")))); // AliceとBobが一部の流動性をデポジットする - await setup.lg.connect(setup.alice).deposit(setup.ten_to_the_21, setup.aliceAddress, false); - await setup.lg.connect(setup.bob).deposit(setup.ten_to_the_21, setup.bobAddress, false); + await gauges[0].connect(accounts[1]).deposit(ten_to_the_21, accounts[1].address, false); + await gauges[0].connect(accounts[2]).deposit(ten_to_the_21, accounts[2].address, false); let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); // 現在、Aliceは投票ロックを持っているが、Bobは持っていないことを確認する - expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.aliceAddress, now)).to.not.equal(setup.zero); - expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.bobAddress, now)).to.equal(setup.zero); + expect(await votingEscrow["balanceOf(address,uint256)"](accounts[1].address, now)).to.not.equal(zero); + expect(await votingEscrow["balanceOf(address,uint256)"](accounts[2].address, now)).to.equal(zero); // 時間を進めてチェックポイントを更新する now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(setup.WEEK.mul(BigNumber.from("4"))).toNumber()]); + await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(WEEK.mul(BigNumber.from("4"))).toNumber()]); // チェックポイント更新 await ethers.provider.send("evm_setAutomine", [false]); - await setup.lg.connect(setup.bob).userCheckpoint(setup.bobAddress); - await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); + await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_setAutomine", [true]); // 4週間後、balanceOfは0であるべき now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.aliceAddress, now)).to.equal(setup.zero); - expect(await setup.votingEscrow["balanceOf(address,uint256)"](setup.bobAddress, now)).to.equal(setup.zero); + expect(await votingEscrow["balanceOf(address,uint256)"](accounts[1].address, now)).to.equal(zero); + expect(await votingEscrow["balanceOf(address,uint256)"](accounts[2].address, now)).to.equal(zero); // AliceはTokenを投票ロックしたので、2.5倍のTokenを獲得 - let rewards_alice = await setup.lg.integrateFraction(setup.aliceAddress); - let rewards_bob = await setup.lg.integrateFraction(setup.bobAddress); + let rewards_alice = await gauges[0].integrateFraction(accounts[1].address); + let rewards_bob = await gauges[0].integrateFraction(accounts[2].address); expect(rewards_alice.mul(BigNumber.from("10000000000000000")).div(rewards_bob)).to.equal( BigNumber.from("25000000000000000") ); // 時間を進めてチェックポイントを更新: 今は誰もがTokenを投票ロックしていない now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(setup.WEEK.mul(BigNumber.from("4"))).toNumber()]); + await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(WEEK.mul(BigNumber.from("4"))).toNumber()]); await ethers.provider.send("evm_setAutomine", [false]); - await setup.lg.connect(setup.bob).userCheckpoint(setup.bobAddress); - await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); + await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_setAutomine", [true]); let old_rewards_alice = rewards_alice; let old_rewards_bob = rewards_bob; // 今、AliceはBobと同じ量を獲得した - rewards_alice = await setup.lg.integrateFraction(setup.aliceAddress); - rewards_bob = await setup.lg.integrateFraction(setup.bobAddress); + rewards_alice = await gauges[0].integrateFraction(accounts[1].address); + rewards_bob = await gauges[0].integrateFraction(accounts[2].address); console.log(rewards_alice) console.log(rewards_bob) let d_alice = rewards_alice.sub(old_rewards_alice); let d_bob = rewards_bob.sub(old_rewards_bob); - expect(d_alice.sub(d_bob)).to.equal(setup.zero); + expect(d_alice.sub(d_bob)).to.equal(zero); }); }); }); diff --git a/test/fork/unitary/ERC20CRV/burn.test.ts b/test/fork/unitary/ERC20CRV/burn.test.ts index a0f65015..363d13a8 100644 --- a/test/fork/unitary/ERC20CRV/burn.test.ts +++ b/test/fork/unitary/ERC20CRV/burn.test.ts @@ -4,11 +4,12 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber } from "ethers"; -import { deployContracts } from "../../helper"; +import { BigNumber, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; describe("ERC20CRV", function () { - let accounts: Signer[]; + let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; diff --git a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts index 17477e59..6414a3f8 100644 --- a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts +++ b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts @@ -5,11 +5,12 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber, Contract } from "ethers"; -import { deployContracts } from "../../helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("ERC20CRV", function () { - let accounts: any[]; + let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; diff --git a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts index 06fef4d9..70877afe 100644 --- a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts +++ b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts @@ -4,12 +4,13 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; -import { deployContracts } from "../../helper"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("ERC20CRV", function () { - let accounts: Signer[]; + let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; diff --git a/test/fork/unitary/ERC20CRV/mint.test.ts b/test/fork/unitary/ERC20CRV/mint.test.ts index f0d860a8..5d2081b0 100644 --- a/test/fork/unitary/ERC20CRV/mint.test.ts +++ b/test/fork/unitary/ERC20CRV/mint.test.ts @@ -5,18 +5,16 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber, Contract } from "ethers"; -import { deployContracts } from "../../helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import Constants from "../../Constants"; describe("ERC20CRV", function () { - let accounts: Signer[]; + let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; - const WEEK = Constants.WEEK; const week = Constants.week; - const YEAR = Constants.YEAR; - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + const ZERO_ADDRESS = Constants.ZERO_ADDRESS; beforeEach(async function () { snapshot = await takeSnapshot(); diff --git a/test/fork/unitary/ERC20CRV/setters.test.ts b/test/fork/unitary/ERC20CRV/setters.test.ts index e83373cf..9296115f 100644 --- a/test/fork/unitary/ERC20CRV/setters.test.ts +++ b/test/fork/unitary/ERC20CRV/setters.test.ts @@ -4,20 +4,14 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; describe("ERC20CRV", function () { - let accounts: Signer[]; + let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; - const WEEK = Constants.WEEK; - const week = Constants.week; - const YEAR = Constants.YEAR; - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; - beforeEach(async function () { snapshot = await takeSnapshot(); accounts = await ethers.getSigners(); diff --git a/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts b/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts index 73f58122..68b715fc 100644 --- a/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts +++ b/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts @@ -4,12 +4,14 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../helper"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("GaugeController", function () { - let accounts; - let gaugeController; + let accounts: SignerWithAddress[]; + let gaugeController: Contract; let snapshot: SnapshotRestorer; const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; diff --git a/test/fork/unitary/GaugeController/gaugesWeights.test.ts b/test/fork/unitary/GaugeController/gaugesWeights.test.ts index b7724489..7b416888 100644 --- a/test/fork/unitary/GaugeController/gaugesWeights.test.ts +++ b/test/fork/unitary/GaugeController/gaugesWeights.test.ts @@ -4,13 +4,15 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../helper"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("GaugeController", function () { - let accounts; - let gaugeController; - let threeGauges; + let accounts: SignerWithAddress[]; + let gaugeController: Contract; + let threeGauges: String[]; let snapshot: SnapshotRestorer; const WEEK = Constants.WEEK; diff --git a/test/fork/unitary/GaugeController/timestamps.test.ts b/test/fork/unitary/GaugeController/timestamps.test.ts index b28d0220..b5c1d287 100644 --- a/test/fork/unitary/GaugeController/timestamps.test.ts +++ b/test/fork/unitary/GaugeController/timestamps.test.ts @@ -4,18 +4,20 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../helper"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("GaugeController", function () { - let accounts; - let gaugeController; + let accounts: SignerWithAddress[]; + let gaugeController: Contract; let snapshot: SnapshotRestorer; const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - const WEEK = Constants.week; - const YEAR = Constants.year; + const week = Constants.week; + const year = Constants.year; beforeEach(async function () { snapshot = await takeSnapshot(); @@ -31,12 +33,12 @@ describe("GaugeController", function () { describe("GaugeController Timestamps", function () { it("test_timestamps", async function () { const currentTime = (await ethers.provider.getBlock("latest")).timestamp; - const expectedTime = Math.floor((currentTime + WEEK) / WEEK) * WEEK; + const expectedTime = Math.floor((currentTime + week) / week) * week; expect(await gaugeController.timeTotal()).to.equal(expectedTime); for (let i = 0; i < 5; i++) { await ethers.provider.send("evm_increaseTime", [ - Math.floor(1.1 * YEAR), + Math.floor(1.1 * year), ]); await gaugeController.checkpoint(); @@ -44,7 +46,7 @@ describe("GaugeController", function () { const newCurrentTime = (await ethers.provider.getBlock("latest")) .timestamp; const newExpectedTime = - Math.floor((newCurrentTime + WEEK) / WEEK) * WEEK; + Math.floor((newCurrentTime + week) / week) * week; expect(await gaugeController.timeTotal()).to.equal(newExpectedTime); } }); diff --git a/test/fork/unitary/GaugeController/totalWeight.test.ts b/test/fork/unitary/GaugeController/totalWeight.test.ts index ae08f0f7..96f685b2 100644 --- a/test/fork/unitary/GaugeController/totalWeight.test.ts +++ b/test/fork/unitary/GaugeController/totalWeight.test.ts @@ -4,12 +4,13 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { Signer, Contract } from "ethers"; -import { deployContracts } from "../../helper"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("GaugeController", function () { - let accounts: Signer[]; + let accounts: SignerWithAddress[]; let gaugeController: Contract; let threeGauges: Contract[]; let snapshot: SnapshotRestorer; diff --git a/test/fork/unitary/GaugeController/vote.test.ts b/test/fork/unitary/GaugeController/vote.test.ts index 4c581e7a..e710da96 100644 --- a/test/fork/unitary/GaugeController/vote.test.ts +++ b/test/fork/unitary/GaugeController/vote.test.ts @@ -4,15 +4,17 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../helper"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("GaugeController", function () { - let accounts; - let gaugeController; - let threeGauges; - let votingEscrow; - let token; + let accounts: SignerWithAddress[]; + let gaugeController: Contract; + let threeGauges: String[]; + let votingEscrow: Contract; + let token: Contract; let snapshot: SnapshotRestorer; diff --git a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts index c65be215..2ac0fd81 100644 --- a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts +++ b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts @@ -4,15 +4,17 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../helper"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("GaugeController", function () { - let accounts; - let gaugeController; - let threeGauges; - let votingEscrow; - let token; + let accounts: SignerWithAddress[]; + let gaugeController: Contract; + let threeGauges: String[]; + let votingEscrow: Contract; + let token: Contract; let snapshot: SnapshotRestorer; diff --git a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts index 50bb92aa..9088690e 100644 --- a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts +++ b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts @@ -1,46 +1,50 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; - - -const YEAR = 86400 * 365; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; +import Constants from "../../Constants"; describe("LiquidityGauge checkpoint", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; - - beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); + let accounts: SignerWithAddress; + let gauges: Contract[]; + + let snapshot: SnapshotRestorer; + const year = Constants.year; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ gauges } = await deployContracts()); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); it("test_user_checkpoint", async function () { // Assuming `userCheckpoint` is a function on your contract - await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); }); it("test_user_checkpoint_new_period", async function () { - await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); // Increase the time on the blockchain - await ethers.provider.send('evm_increaseTime', [(YEAR * 1.1)]); + await ethers.provider.send('evm_increaseTime', [(year * 1.1)]); await ethers.provider.send('evm_mine'); // this one will actually mine a new block - await setup.lg.connect(setup.alice).userCheckpoint(setup.aliceAddress); + await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); }); it("test_user_checkpoint_wrong_account", async function () { // Expect the transaction to be reverted with the specified error message await expect( - setup.lg.connect(setup.alice).userCheckpoint(setup.bobAddress) + gauges[0].connect(accounts[1]).userCheckpoint(accounts[2].address) ).to.be.revertedWith("dev: unauthorized"); }); }); diff --git a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts index b379e624..d141b353 100644 --- a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts +++ b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts @@ -1,92 +1,102 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; +import { BigNumber, Contract } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; +import Constants from "../../Constants"; describe("LiquidityGauge depositWithdraw", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; + let accounts: SignerWithAddress; + let mockLpToken: Contract; + let threeGauges: String[]; + let gauges: Contract[]; + + let snapshot: SnapshotRestorer; + + const ten_to_the_21 = Constants.ten_to_the_21; beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); - await setup.mockLpToken.approve(setup.lg.address, setup.ten_to_the_21.mul("2")); + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ mockLpToken, threeGauges, gauges } = + await deployContracts()); + await mockLpToken.approve(threeGauges[0], ten_to_the_21.mul("2")); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); it("should deposit tokens", async function () { - const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const balance = await mockLpToken.balanceOf(accounts[0].address); const depositAmount = BigNumber.from(100000); - await setup.lg.connect(setup.creator).deposit(100000, setup.creatorAddress, false); + await gauges[0].deposit(100000, accounts[0].address, false); - expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(depositAmount); - expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance.sub(depositAmount)); - expect(await setup.lg.totalSupply()).to.equal(depositAmount); - expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(depositAmount); + expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(depositAmount); + expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance.sub(depositAmount)); + expect(await gauges[0].totalSupply()).to.equal(depositAmount); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(depositAmount); }); it("should handle zero deposit", async function () { - const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); - await setup.lg.deposit(0, setup.creatorAddress, false); + const balance = await mockLpToken.balanceOf(accounts[0].address); + await gauges[0].deposit(0, accounts[0].address, false); - expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(BigNumber.from(0)); - expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance); - expect(await setup.lg.totalSupply()).to.equal(BigNumber.from(0)); - expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(BigNumber.from(0)); + expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(BigNumber.from(0)); + expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance); + expect(await gauges[0].totalSupply()).to.equal(BigNumber.from(0)); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(BigNumber.from(0)); }); it("should revert on deposit with insufficient balance", async function () { - await expect(setup.lg.connect(setup.alice).deposit(100000, setup.creatorAddress, false)).to.be.reverted; + await expect(gauges[0].connect(accounts[1]).deposit(100000, accounts[0].address, false)).to.be.reverted; }); it("should withdraw tokens", async function () { - const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const balance = await mockLpToken.balanceOf(accounts[0].address); const depositAmount = BigNumber.from(100000); - await setup.lg.deposit(depositAmount, setup.creatorAddress, false); - await setup.lg.withdraw(depositAmount, false); + await gauges[0].deposit(depositAmount, accounts[0].address, false); + await gauges[0].withdraw(depositAmount, false); - expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(0); - expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance); - expect(await setup.lg.totalSupply()).to.equal(0); - expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(0); + expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(0); + expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance); + expect(await gauges[0].totalSupply()).to.equal(0); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(0); }); it("should handle zero withdrawal", async function () { - const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const balance = await mockLpToken.balanceOf(accounts[0].address); const depositAmount = BigNumber.from(100000); - await setup.lg.deposit(depositAmount, setup.creatorAddress, false); - await setup.lg.withdraw(0, false); + await gauges[0].deposit(depositAmount, accounts[0].address, false); + await gauges[0].withdraw(0, false); - expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(depositAmount); - expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance.sub(depositAmount)); - expect(await setup.lg.totalSupply()).to.equal(depositAmount); - expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(depositAmount); + expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(depositAmount); + expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance.sub(depositAmount)); + expect(await gauges[0].totalSupply()).to.equal(depositAmount); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(depositAmount); }); it("should withdraw tokens after a new epoch", async function () { - const balance = await setup.mockLpToken.balanceOf(setup.creatorAddress); + const balance = await mockLpToken.balanceOf(accounts[0].address); const depositAmount = BigNumber.from(100000); - await setup.lg.deposit(depositAmount, setup.creatorAddress, false); + await gauges[0].deposit(depositAmount, accounts[0].address, false); await ethers.provider.send("evm_increaseTime", [86400 * 400]); await ethers.provider.send("evm_mine"); - await setup.lg.withdraw(depositAmount, false); + await gauges[0].withdraw(depositAmount, false); - expect(await setup.mockLpToken.balanceOf(setup.lg.address)).to.equal(0); - expect(await setup.mockLpToken.balanceOf(setup.creatorAddress)).to.equal(balance); - expect(await setup.lg.totalSupply()).to.equal(0); - expect(await setup.lg.balanceOf(setup.creatorAddress)).to.equal(0); + expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(0); + expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance); + expect(await gauges[0].totalSupply()).to.equal(0); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(0); }); - // Add additional tests as needed... -}); + }); diff --git a/test/fork/unitary/LiquidityGauge/kick.test.ts b/test/fork/unitary/LiquidityGauge/kick.test.ts index f70c9bba..22bb10b8 100644 --- a/test/fork/unitary/LiquidityGauge/kick.test.ts +++ b/test/fork/unitary/LiquidityGauge/kick.test.ts @@ -1,65 +1,75 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; -import { EVMUtils, TestSetup } from "../../helper"; - -const MAX_UINT256 = ethers.constants.MaxUint256; -const WEEK = 7 * 86400; -const DEPOSIT_AMOUNT = BigNumber.from("10").pow("21"); // 10^21 -const LOCK_AMOUNT = BigNumber.from("10").pow("20"); // 10^20 +import { Contract } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; +import Constants from "../../Constants"; describe("LiquidityGauge kick", function () { - let setup: TestSetup; - let evm: EVMUtils; - let snapshotId: string; + let accounts: SignerWithAddress[]; + let token: Contract; + let votingEscrow: Contract; + let mockLpToken: Contract; + let gauges: Contract[]; + + let snapshot: SnapshotRestorer; + + const MAX_UINT256 = Constants.MAX_UINT256; + const week = Constants.week; + const DEPOSIT_AMOUNT = Constants.ten_to_the_21; // 10^21 + const LOCK_AMOUNT = Constants.ten_to_the_20; // 10^20 beforeEach(async () => { - evm = new EVMUtils(); - snapshotId = await evm.snapshot(); - setup = new TestSetup(); - await setup.setup(); - await setup.token.transfer(setup.aliceAddress, LOCK_AMOUNT); - await setup.mockLpToken.transfer(setup.aliceAddress, DEPOSIT_AMOUNT); + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + ({ token, votingEscrow, mockLpToken, gauges } = + await deployContracts()); + await token.transfer(accounts[1].address, LOCK_AMOUNT); + await mockLpToken.transfer(accounts[1].address, DEPOSIT_AMOUNT); }); afterEach(async () => { - await evm.restore(snapshotId); + await snapshot.restore(); }); it("test kick functionality", async function () { // Forward time by 2 weeks + 5 seconds - await ethers.provider.send("evm_increaseTime", [2 * WEEK + 5]); + await ethers.provider.send("evm_increaseTime", [2 * week + 5]); await ethers.provider.send("evm_mine"); // Alice approves tokens to voting escrow and creates a lock - await setup.token.connect(setup.alice).approve(setup.votingEscrow.address, MAX_UINT256); - await setup.votingEscrow.connect(setup.alice).createLock(LOCK_AMOUNT, (await ethers.provider.getBlock('latest')).timestamp + 4 * WEEK); + await token.connect(accounts[1]).approve(votingEscrow.address, MAX_UINT256); + await votingEscrow.connect(accounts[1]).createLock(LOCK_AMOUNT, (await ethers.provider.getBlock('latest')).timestamp + 4 * week); // Alice approves LP tokens to Gauge and deposits - await setup.mockLpToken.connect(setup.alice).approve(setup.lg.address, MAX_UINT256); - await setup.lg.connect(setup.alice).deposit(DEPOSIT_AMOUNT, setup.aliceAddress, false); + await mockLpToken.connect(accounts[1]).approve(gauges[0].address, MAX_UINT256); + await gauges[0].connect(accounts[1]).deposit(DEPOSIT_AMOUNT, accounts[1].address, false); // Check working balance of Alice in Gauge - expect(await setup.lg.workingBalances(setup.aliceAddress)).to.equal(DEPOSIT_AMOUNT); + expect(await gauges[0].workingBalances(accounts[1].address)).to.equal(DEPOSIT_AMOUNT); // Forward time by 1 week - await ethers.provider.send("evm_increaseTime", [WEEK]); + await ethers.provider.send("evm_increaseTime", [week]); await ethers.provider.send("evm_mine"); // Bob tries to kick Alice but should fail because it's not allowed yet - await expect(setup.lg.connect(setup.bob).kick(setup.aliceAddress)).to.be.revertedWith("Not allowed"); + await expect(gauges[0].connect(accounts[1]).kick(accounts[1].address)).to.be.revertedWith("Not allowed"); // Forward time by 4 weeks - await ethers.provider.send("evm_increaseTime", [4 * WEEK]); + await ethers.provider.send("evm_increaseTime", [4 * week]); await ethers.provider.send("evm_mine"); // Now Bob kicks Alice - await setup.lg.connect(setup.bob).kick(setup.aliceAddress); + await gauges[0].connect(accounts[1]).kick(accounts[1].address); // Check the working balance of Alice after kick - expect(await setup.lg.workingBalances(setup.aliceAddress)).to.equal(LOCK_AMOUNT.mul(4)); + expect(await gauges[0].workingBalances(accounts[1].address)).to.equal(LOCK_AMOUNT.mul(4)); // Trying to kick again should fail as it's not needed - await expect(setup.lg.connect(setup.bob).kick(setup.aliceAddress)).to.be.revertedWith("Not needed"); + await expect(gauges[0].connect(accounts[1]).kick(accounts[1].address)).to.be.revertedWith("Not needed"); }); }); diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts index 541bd5f7..52e2dd3c 100644 --- a/test/fork/unitary/Minter/Minter.test.ts +++ b/test/fork/unitary/Minter/Minter.test.ts @@ -5,11 +5,12 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; describe("Minter", function () { - let accounts: any; + let accounts: SignerWithAddress[]; let minter: Contract; let gaugeController: Contract; let token: Contract; @@ -27,7 +28,6 @@ describe("Minter", function () { const ten_to_the_17 = Constants.ten_to_the_17; const ZERO_ADDRESS = Constants.ZERO_ADDRESS; const zero = Constants.zero; - const MONTH = Constants.MONTH; const WEEK = Constants.WEEK; const month = Constants.month; const week = Constants.week; From abdbcb759bfcbdf30e15ca962f2fe9d7fff43d8a Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 13 Nov 2023 10:27:27 +0900 Subject: [PATCH 045/412] code format --- contracts/Tester/TestLP.sol | 13 +- contracts/curveFork/CRV.sol | 32 +- contracts/curveFork/GaugeController.sol | 141 ++++----- contracts/curveFork/LiquidityGaugeV6.sol | 36 ++- contracts/curveFork/Minter.sol | 12 +- contracts/curveFork/VotingEscrow.sol | 7 +- contracts/curveFork/interfaces/ICRV.sol | 2 +- .../curveFork/interfaces/IGaugeController.sol | 16 +- .../curveFork/interfaces/ILiquidityGauge.sol | 5 +- contracts/curveFork/interfaces/IMinter.sol | 9 +- .../interfaces/ISmartWalletChecker.sol | 2 +- .../curveFork/interfaces/IVotingEscrow.sol | 19 +- doc/v1.5/Controller/index.md | 85 ++++-- doc/v1.5/FeeDistribution/usecase.md | 3 +- doc/v1.5/Gauge/index.md | 55 +++- doc/v1.5/GaugeController/class.md | 4 +- doc/v1.5/Minter/index.md | 42 ++- doc/v1.5/YMT/index.md | 85 ++++-- doc/v1.5/veYMT/index.md | 48 +-- doc/v1.5/veYMT/usecase.md | 2 +- test/fork/Constants.ts | 1 - test/fork/helper.ts | 1 - .../ERC20CRV/mintIntegration.test.ts | 21 +- .../ERC20CRV/mintableInTimeframe.test.ts | 83 +++-- .../LiquidityGauge/liquidityGauge.test.ts | 287 ++++++++++++------ test/fork/unitary/ERC20CRV/burn.test.ts | 15 +- .../unitary/ERC20CRV/epochTimeSupply.test.ts | 44 ++- .../unitary/ERC20CRV/inflationDelay.test.ts | 8 +- test/fork/unitary/ERC20CRV/mint.test.ts | 73 +++-- test/fork/unitary/ERC20CRV/setters.test.ts | 8 +- .../unitary/LiquidityGauge/checkpoint.test.ts | 10 +- .../LiquidityGauge/depositWithdraw.test.ts | 34 ++- test/fork/unitary/LiquidityGauge/kick.test.ts | 36 ++- test/fork/unitary/Minter/Minter.test.ts | 227 +++++--------- 34 files changed, 855 insertions(+), 611 deletions(-) diff --git a/contracts/Tester/TestLP.sol b/contracts/Tester/TestLP.sol index 20bde5da..5ac407d2 100644 --- a/contracts/Tester/TestLP.sol +++ b/contracts/Tester/TestLP.sol @@ -28,7 +28,7 @@ contract TestLP { uint256 _decimal, uint256 _supply ) { - uint256 init_supply = _supply.mul(10**_decimals); + uint256 init_supply = _supply.mul(10 ** _decimals); name = _name; symbol = _symbol; _decimals = _decimal; @@ -48,11 +48,10 @@ contract TestLP { return total_supply; } - function allowance(address _owner, address _spender) - external - view - returns (uint256) - { + function allowance( + address _owner, + address _spender + ) external view returns (uint256) { /*** *@notice Check the amount of tokens that an owner allowed to a spender *@param _owner The address which owns the funds @@ -138,4 +137,4 @@ contract TestLP { require(msg.sender == minter, "Only minter is allowed to burn"); _burn(_to, _value); } -} \ No newline at end of file +} diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol index 33182610..0d348654 100644 --- a/contracts/curveFork/CRV.sol +++ b/contracts/curveFork/CRV.sol @@ -74,18 +74,17 @@ contract CRV is ERC20 { rate = _rate; - emit UpdateMiningParameters( - block.timestamp, - _rate, - _startEpochSupply - ); + emit UpdateMiningParameters(block.timestamp, _rate, _startEpochSupply); } // @notice Update mining rate and supply at the start of the epoch // @dev Callable by any address, but only once per epoch // Total supply becomes slightly larger if(this function is called late function updateMiningParameters() external { - require(block.timestamp >= startEpochTime + RATE_REDUCTION_TIME,"dev: too soon!"); // dev: too soon! + require( + block.timestamp >= startEpochTime + RATE_REDUCTION_TIME, + "dev: too soon!" + ); // dev: too soon! _updateMiningParameters(); } @@ -145,7 +144,10 @@ contract CRV is ERC20 { RATE_REDUCTION_COEFFICIENT; } - require(end <= current_epoch_time + RATE_REDUCTION_TIME, "dev: too far in future"); // dev: too far in future + require( + end <= current_epoch_time + RATE_REDUCTION_TIME, + "dev: too far in future" + ); // dev: too far in future // Curve will not work in 1000 years. Darn! for (uint i; i < 999; ) { @@ -184,8 +186,11 @@ contract CRV is ERC20 { // @notice Set the minter address // @dev Only callable once, when minter has not yet been set // @param _minter Address of the minter - function setMinter(address _minter) external onlyAdmin() { - require(_minter != address(0), "dev: can set the minter only once, at creation"); // dev: can set the minter only once, at creation + function setMinter(address _minter) external onlyAdmin { + require( + _minter != address(0), + "dev: can set the minter only once, at creation" + ); // dev: can set the minter only once, at creation minter = _minter; emit SetMinter(_minter); } @@ -193,7 +198,7 @@ contract CRV is ERC20 { // @notice Set the new admin. // @dev After all is set up, admin only can change the token name // @param _admin New admin address - function setAdmin(address _admin) external onlyAdmin() { + function setAdmin(address _admin) external onlyAdmin { admin = _admin; emit SetAdmin(_admin); } @@ -210,7 +215,10 @@ contract CRV is ERC20 { if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { _updateMiningParameters(); } - require(totalSupply() + _value <= _availableSupply(), "dev: exceeds allowable mint amount"); + require( + totalSupply() + _value <= _availableSupply(), + "dev: exceeds allowable mint amount" + ); _mint(_to, _value); @@ -230,4 +238,4 @@ contract CRV is ERC20 { require(admin == msg.sender, "dev: admin only"); _; } -} \ No newline at end of file +} diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol index 26352052..29eb50fa 100644 --- a/contracts/curveFork/GaugeController.sol +++ b/contracts/curveFork/GaugeController.sol @@ -31,10 +31,7 @@ contract GaugeController { event CommitOwnership(address admin); event ApplyOwnership(address admin); - event AddType( - string name, - int128 typeId - ); + event AddType(string name, int128 typeId); event NewTypeWeight( int128 typeId, uint256 time, @@ -54,16 +51,12 @@ contract GaugeController { address gaugeAddr, uint256 weight ); - event NewGauge( - address addr, - int128 gaugeType, - uint256 weight - ); + event NewGauge(address addr, int128 gaugeType, uint256 weight); - uint256 constant MULTIPLIER = 10**18; + uint256 constant MULTIPLIER = 10 ** 18; // Can and will be a smart contract - address public admin; + address public admin; // Can and will be a smart contract address public futureAdmin; // CRV token @@ -71,7 +64,6 @@ contract GaugeController { // Voting escrow address public votingEscrow; - int128 public nGaugeTypes; int128 public nGauges; //number of gauges mapping(int128 => string) public gaugeTypeNames; @@ -113,10 +105,7 @@ contract GaugeController { *@param _token `Token` contract address *@param _votingEscrow `VotingEscrow` contract address */ - constructor( - address token_, - address votingEscrow_ - ) { + constructor(address token_, address votingEscrow_) { require(token_ != address(0)); require(votingEscrow_ != address(0)); @@ -138,7 +127,7 @@ contract GaugeController { /*** * @notice Apply pending ownership transfer */ - function applyTransferOwnership() external onlyAdmin{ + function applyTransferOwnership() external onlyAdmin { address _admin = futureAdmin; require(_admin != address(0), "admin not set"); admin = _admin; @@ -177,10 +166,12 @@ contract GaugeController { if (_t > block.timestamp) { timeTypeWeight[_gaugeType] = _t; } - unchecked { ++i; } + unchecked { + ++i; + } } return _w; - }else{ + } else { return 0; } } @@ -214,10 +205,12 @@ contract GaugeController { if (_t > block.timestamp) { timeSum[_gaugeType] = _t; } - unchecked { ++i; } + unchecked { + ++i; + } } return _pt.bias; - }else{ + } else { return 0; } } @@ -242,7 +235,9 @@ contract GaugeController { } _getSum(_gaugeType); _getTypeWeight(_gaugeType); - unchecked { ++_gaugeType; } + unchecked { + ++_gaugeType; + } } for (uint256 i; i < 500; ) { if (_t > block.timestamp) { @@ -258,14 +253,18 @@ contract GaugeController { uint256 _typeSum = pointsSum[_gaugeType][_t].bias; uint256 _typeWeight = pointsTypeWeight[_gaugeType][_t]; _pt += _typeSum * _typeWeight; - unchecked { ++_gaugeType; } + unchecked { + ++_gaugeType; + } } pointsTotal[_t] = _pt; if (_t > block.timestamp) { timeTotal = _t; } - unchecked { ++i; } + unchecked { + ++i; + } } return _pt; } @@ -298,10 +297,12 @@ contract GaugeController { if (_t > block.timestamp) { timeWeight[gaugeAddr_] = _t; } - unchecked { ++i; } + unchecked { + ++i; + } } return _pt.bias; - }else{ + } else { return 0; } } @@ -316,7 +317,10 @@ contract GaugeController { int128 gaugeType_, uint256 weight_ ) external onlyAdmin { - require((gaugeType_ >= 0) && (gaugeType_ < nGaugeTypes), "gauge_type 0 means unset"); //gauge_type 0 means unset + require( + (gaugeType_ >= 0) && (gaugeType_ < nGaugeTypes), + "gauge_type 0 means unset" + ); //gauge_type 0 means unset require(gaugeTypes_[addr_] == 0, "cannot add the same gauge twice"); int128 _n = nGauges; unchecked { @@ -374,11 +378,10 @@ contract GaugeController { *@param time_ Relative weight at the specified timestamp in the past or present *@return Value of relative weight normalized to 1e18 */ - function _gaugeRelativeWeight(address addr_, uint256 time_) - internal - view - returns (uint256) - { + function _gaugeRelativeWeight( + address addr_, + uint256 time_ + ) internal view returns (uint256) { uint256 _t = (time_ / WEEK) * WEEK; uint256 _totalWeight = pointsTotal[_t]; @@ -401,11 +404,10 @@ contract GaugeController { *@param time_ Relative weight at the specified timestamp in the past or present *@return Value of relative weight normalized to 1e18 */ - function gaugeRelativeWeight(address addr_, uint256 time_) - external - view - returns (uint256) - { + function gaugeRelativeWeight( + address addr_, + uint256 time_ + ) external view returns (uint256) { //default value if (time_ == 0) { time_ = block.timestamp; @@ -414,10 +416,10 @@ contract GaugeController { return _gaugeRelativeWeight(addr_, time_); } - function gaugeRelativeWeightWrite(address addr_, uint256 time_) - external - returns (uint256) - { + function gaugeRelativeWeightWrite( + address addr_, + uint256 time_ + ) external returns (uint256) { //default value if (time_ == 0) { time_ = block.timestamp; @@ -477,9 +479,10 @@ contract GaugeController { *@param typeId_ Gauge type id *@param weight_ New Gauge weight */ - function changeTypeWeight(int128 typeId_, uint256 weight_) - external onlyAdmin - { + function changeTypeWeight( + int128 typeId_, + uint256 weight_ + ) external onlyAdmin { _changeTypeWeight(typeId_, weight_); } @@ -518,9 +521,10 @@ contract GaugeController { *@param addr_ `GaugeController` contract address *@param weight_ New Gauge weight */ - function changeGaugeWeight(address addr_, uint256 weight_) - external onlyAdmin - { + function changeGaugeWeight( + address addr_, + uint256 weight_ + ) external onlyAdmin { _changeGaugeWeight(addr_, weight_); } @@ -540,20 +544,20 @@ contract GaugeController { *@param gaugeAddr_ Gauge which `msg.sender` votes for *@param userWeight_ Weight for a gauge in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0. bps = basis points */ - function voteForGaugeWeights(address gaugeAddr_, uint256 userWeight_) - external - { + function voteForGaugeWeights( + address gaugeAddr_, + uint256 userWeight_ + ) external { VotingParameter memory _vp; - _vp.slope = uint256(uint128(IVotingEscrow(votingEscrow).getLastUserSlope(msg.sender))); + _vp.slope = uint256( + uint128(IVotingEscrow(votingEscrow).getLastUserSlope(msg.sender)) + ); _vp.lockEnd = IVotingEscrow(votingEscrow).lockedEnd(msg.sender); _vp._nGauges = uint256(uint128(nGauges)); unchecked { _vp.nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; } - require( - _vp.lockEnd > _vp.nextTime, - "Your token lock expires too soon" - ); + require(_vp.lockEnd > _vp.nextTime, "Your token lock expires too soon"); require( (userWeight_ >= 0) && (userWeight_ <= 10000), "You used all your voting power" @@ -569,9 +573,7 @@ contract GaugeController { _vp.gaugeType = gaugeTypes_[gaugeAddr_] - 1; require(_vp.gaugeType >= 0, "Gauge not added"); // Prepare slopes and biases in memory - VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][ - gaugeAddr_ - ]; + VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][gaugeAddr_]; _vp.oldDt = 0; if (_oldSlope.end > _vp.nextTime) { _vp.oldDt = _oldSlope.end - _vp.nextTime; @@ -598,11 +600,9 @@ contract GaugeController { // Remove slope changes for old slopes // Schedule recording of initial slope for nextTime uint256 _oldWeightBias = _getWeight(gaugeAddr_); - uint256 _oldWeightSlope = pointsWeight[gaugeAddr_][_vp.nextTime] - .slope; + uint256 _oldWeightSlope = pointsWeight[gaugeAddr_][_vp.nextTime].slope; uint256 _oldSumBias = _getSum(_vp.gaugeType); - uint256 _oldSumSlope = pointsSum[_vp.gaugeType][_vp.nextTime] - .slope; + uint256 _oldSumSlope = pointsSum[_vp.gaugeType][_vp.nextTime].slope; pointsWeight[gaugeAddr_][_vp.nextTime].bias = max(_oldWeightBias + _newBias, _vp.oldBias) - @@ -637,12 +637,7 @@ contract GaugeController { // Record last action time lastUserVote[msg.sender][gaugeAddr_] = block.timestamp; - emit VoteForGauge( - block.timestamp, - msg.sender, - gaugeAddr_, - userWeight_ - ); + emit VoteForGauge(block.timestamp, msg.sender, gaugeAddr_, userWeight_); } /*** @@ -677,11 +672,9 @@ contract GaugeController { *@param typeId_ Type id *@return Sum of gauge weights */ - function getWeightsSumPerType(int128 typeId_) - external - view - returns (uint256) - { + function getWeightsSumPerType( + int128 typeId_ + ) external view returns (uint256) { uint256 _typeId = uint256(uint128(typeId_)); return pointsSum[typeId_][timeSum[_typeId]].bias; } @@ -694,4 +687,4 @@ contract GaugeController { require(admin == msg.sender, "admin only"); _; } -} \ No newline at end of file +} diff --git a/contracts/curveFork/LiquidityGaugeV6.sol b/contracts/curveFork/LiquidityGaugeV6.sol index 65a85714..7a7b93af 100644 --- a/contracts/curveFork/LiquidityGaugeV6.sol +++ b/contracts/curveFork/LiquidityGaugeV6.sol @@ -151,10 +151,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { uint256[100000000000000000000000000000] public periodTimestamp; uint256[100000000000000000000000000000] public integrateInvSupply; - constructor( - address lpToken_, - address minter_ - ) { + constructor(address lpToken_, address minter_) { require(lpToken == address(0)); lpToken = lpToken_; @@ -234,14 +231,18 @@ contract LiquidityGaugeV6 is ReentrancyGuard { uint256 _workingSupply = workingSupply; IGaugeController(gaugeController).checkpointGauge(address(this)); uint256 _prevWeekTime = _st.periodTime; - uint256 _weekTime = min((_st.periodTime + WEEK) / WEEK * WEEK, block.timestamp); + uint256 _weekTime = min( + ((_st.periodTime + WEEK) / WEEK) * WEEK, + block.timestamp + ); - for (uint256 i = 0; i < 500;) { + for (uint256 i = 0; i < 500; ) { uint256 dt = _weekTime - _prevWeekTime; - uint256 w = IGaugeController(gaugeController).gaugeRelativeWeight( - address(this), - (_prevWeekTime / WEEK) * WEEK - ); + uint256 w = IGaugeController(gaugeController) + .gaugeRelativeWeight( + address(this), + (_prevWeekTime / WEEK) * WEEK + ); if (_workingSupply > 0) { if ( @@ -317,7 +318,10 @@ contract LiquidityGaugeV6 is ReentrancyGuard { _rp.token = rewardTokens[i]; _rp.integral = rewardData[_rp.token].integral; - _rp.lastUpdate = min(block.timestamp, rewardData[_rp.token].periodFinish); + _rp.lastUpdate = min( + block.timestamp, + rewardData[_rp.token].periodFinish + ); _rp.duration = _rp.lastUpdate - rewardData[_rp.token].lastUpdate; if (_rp.duration != 0) { rewardData[_rp.token].lastUpdate = _rp.lastUpdate; @@ -592,7 +596,10 @@ contract LiquidityGaugeV6 is ReentrancyGuard { } function userCheckpoint(address addr_) external returns (bool) { - require(msg.sender == addr_ || msg.sender == minter, "dev: unauthorized"); + require( + msg.sender == addr_ || msg.sender == minter, + "dev: unauthorized" + ); _checkpoint(addr_); _updateLiquidityLimit(addr_, balanceOf[addr_], totalSupply); return true; @@ -674,10 +681,7 @@ contract LiquidityGaugeV6 is ReentrancyGuard { address distributor_ ) external { address currentDistributor = rewardData[rewardToken_].distributor; - require( - msg.sender == currentDistributor || - msg.sender == admin - ); + require(msg.sender == currentDistributor || msg.sender == admin); require(currentDistributor != address(0)); require(distributor_ != address(0)); diff --git a/contracts/curveFork/Minter.sol b/contracts/curveFork/Minter.sol index ef0a59f3..9c7cc88b 100644 --- a/contracts/curveFork/Minter.sol +++ b/contracts/curveFork/Minter.sol @@ -22,10 +22,7 @@ contract Minter is ReentrancyGuard { // minter -> user -> can mint? mapping(address => mapping(address => bool)) public allowedToMintFor; // A can mint for B if [A => B => true]. - constructor( - address _token, - address _controller - ) { + constructor(address _token, address _controller) { token = _token; controller = _controller; } @@ -37,9 +34,7 @@ contract Minter is ReentrancyGuard { ); ILiquidityGauge(gaugeAddr_).userCheckpoint(for_); - uint256 totalMint = ILiquidityGauge(gaugeAddr_).integrateFraction( - for_ - ); + uint256 totalMint = ILiquidityGauge(gaugeAddr_).integrateFraction(for_); uint256 _toMint = totalMint - minted[for_][gaugeAddr_]; if (_toMint != 0) { @@ -96,5 +91,4 @@ contract Minter is ReentrancyGuard { mintingUser_ ][msg.sender]; } - -} \ No newline at end of file +} diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol index 54af4e18..f20b6c8b 100644 --- a/contracts/curveFork/VotingEscrow.sol +++ b/contracts/curveFork/VotingEscrow.sol @@ -588,10 +588,7 @@ contract VotingEscrow is ReentrancyGuard { _locked.end = 0; _locked.amount = 0; - locked[msg.sender] = LockedBalance( - _locked.amount, - _locked.end - ); + locked[msg.sender] = LockedBalance(_locked.amount, _locked.end); uint256 _supplyBefore = supply; supply = _supplyBefore - _value; @@ -740,7 +737,7 @@ contract VotingEscrow is ReentrancyGuard { uint256 _blockTime = _point0.ts; if (_dBlock != 0) { - _blockTime += _dt * (block_ - _point0.blk) / _dBlock; + _blockTime += (_dt * (block_ - _point0.blk)) / _dBlock; } _upoint.bias -= diff --git a/contracts/curveFork/interfaces/ICRV.sol b/contracts/curveFork/interfaces/ICRV.sol index b07be78e..6395011e 100644 --- a/contracts/curveFork/interfaces/ICRV.sol +++ b/contracts/curveFork/interfaces/ICRV.sol @@ -10,4 +10,4 @@ interface ICRV { function rate() external view returns (uint256); function futureEpochTimeWrite() external returns (uint256); -} \ No newline at end of file +} diff --git a/contracts/curveFork/interfaces/IGaugeController.sol b/contracts/curveFork/interfaces/IGaugeController.sol index 16e3d9b7..5c0e6f07 100644 --- a/contracts/curveFork/interfaces/IGaugeController.sol +++ b/contracts/curveFork/interfaces/IGaugeController.sol @@ -11,10 +11,14 @@ interface IGaugeController { function addType(string memory name_, uint256 weight_) external; - function addGauge(address addr_, int128 gaugeType_, uint256 weight_) external; + function addGauge( + address addr_, + int128 gaugeType_, + uint256 weight_ + ) external; - function gaugeRelativeWeight(address addr, uint256 time) - external - view - returns (uint256); -} \ No newline at end of file + function gaugeRelativeWeight( + address addr, + uint256 time + ) external view returns (uint256); +} diff --git a/contracts/curveFork/interfaces/ILiquidityGauge.sol b/contracts/curveFork/interfaces/ILiquidityGauge.sol index 75b6e1b9..97c98530 100644 --- a/contracts/curveFork/interfaces/ILiquidityGauge.sol +++ b/contracts/curveFork/interfaces/ILiquidityGauge.sol @@ -8,5 +8,6 @@ interface ILiquidityGauge { function integrateFraction(address addr_) external view returns (uint256); function deposit(uint256 _value, address _addr) external; - function withdraw(uint256 _value)external; -} \ No newline at end of file + + function withdraw(uint256 _value) external; +} diff --git a/contracts/curveFork/interfaces/IMinter.sol b/contracts/curveFork/interfaces/IMinter.sol index 74dc0e60..48d4be26 100644 --- a/contracts/curveFork/interfaces/IMinter.sol +++ b/contracts/curveFork/interfaces/IMinter.sol @@ -2,14 +2,13 @@ pragma solidity 0.8.18; - interface IMinter { function token() external view returns (address); function controller() external view returns (address); - function minted(address user_, address gauge_) - external - view - returns (uint256); + function minted( + address user_, + address gauge_ + ) external view returns (uint256); } diff --git a/contracts/curveFork/interfaces/ISmartWalletChecker.sol b/contracts/curveFork/interfaces/ISmartWalletChecker.sol index e98406cb..a281ba89 100644 --- a/contracts/curveFork/interfaces/ISmartWalletChecker.sol +++ b/contracts/curveFork/interfaces/ISmartWalletChecker.sol @@ -7,4 +7,4 @@ pragma solidity 0.8.18; // for individual wallet addresses interface ISmartWalletChecker { function check(address addr_) external returns (bool); -} \ No newline at end of file +} diff --git a/contracts/curveFork/interfaces/IVotingEscrow.sol b/contracts/curveFork/interfaces/IVotingEscrow.sol index 000d8068..6d4414e9 100644 --- a/contracts/curveFork/interfaces/IVotingEscrow.sol +++ b/contracts/curveFork/interfaces/IVotingEscrow.sol @@ -7,22 +7,19 @@ interface IVotingEscrow { function lockedEnd(address addr_) external view returns (uint256); - function balanceOf(address addr_, uint256 t_) - external - view - returns (uint256); + function balanceOf( + address addr_, + uint256 t_ + ) external view returns (uint256); + + function balanceOf(address addr) external view returns (uint256); - function balanceOf(address addr)external view returns (uint256); function totalSupply(uint256 t_) external view returns (uint256); - function userPointEpoch(address _user) - external - view - returns (uint256); + function userPointEpoch(address _user) external view returns (uint256); function userPointHistoryTs( address addr, uint256 epoch ) external view returns (uint256); - -} \ No newline at end of file +} diff --git a/doc/v1.5/Controller/index.md b/doc/v1.5/Controller/index.md index dcd85056..e3bfe2e6 100644 --- a/doc/v1.5/Controller/index.md +++ b/doc/v1.5/Controller/index.md @@ -1,78 +1,99 @@ - - ## 定数 ### `WEEK: constant(uint256) = 604800` -- **説明**: 1週間の秒数。現在の値は604800秒です。 + +- **説明**: 1 週間の秒数。現在の値は 604800 秒です。 ### `WEIGHT_VOTE_DELAY: constant(uint256) = 10 * 86400` -- **説明**: 重みの投票を変更することができるのは10日に1回だけです。現在の値は10日(864,000秒)です。 + +- **説明**: 重みの投票を変更することができるのは 10 日に 1 回だけです。現在の値は 10 日(864,000 秒)です。 ### `MULTIPLIER: constant(uint256) = 10 ** 18` -- **説明**: 多数の固定小数点数を扱うための乗数。現在の値は10の18乗です。 + +- **説明**: 多数の固定小数点数を扱うための乗数。現在の値は 10 の 18 乗です。 --- ## 変数 ### `admin: public(address)` + - **説明**: 管理者のアドレス。このアドレスはスマートコントラクトに許可されています。 ### `future_admin: public(address)` + - **説明**: 未来の管理者のアドレス。所有権の移転が計画されています。 ### `token: public(address)` -- **説明**: CRVトークンのアドレス。 + +- **説明**: CRV トークンのアドレス。 ### `voting_escrow: public(address)` + - **説明**: 投票エスクローのアドレス。 ### `n_gauge_types: public(int128)` + - **説明**: 使用可能なゲージの種類の数。 ### `n_gauges: public(int128)` + - **説明**: 現在のゲージの数。 ### `gauge_type_names: public(HashMap[int128, String[64]])` + - **説明**: 各ゲージの種類の名前を格納するマップ。 ### `gauges: public(address[1000000000])` + - **説明**: ゲージのアドレスを格納するリスト。 ### `gauge_types_: HashMap[address, int128]` + - **説明**: 各ゲージアドレスとそのタイプを関連付けるマップ。 ### `vote_user_slopes: public(HashMap[address, HashMap[address, VotedSlope]])` + - **説明**: ユーザーの各ゲージアドレスに対する投票傾斜を格納するマップ。 ### `vote_user_power: public(HashMap[address, uint256])` + - **説明**: ユーザーが使用する総投票パワー。 ### `last_user_vote: public(HashMap[address, HashMap[address, uint256]])` + - **説明**: 各ゲージアドレスの最後のユーザー投票のタイムスタンプ。 ### `points_weight: public(HashMap[address, HashMap[uint256, Point]])` + - **説明**: 各ゲージアドレスと時間に対する点数の重みを格納するマップ。 ### `changes_weight: HashMap[address, HashMap[uint256, uint256]]` + - **説明**: 各ゲージアドレスと時間に対する重みの変更を格納するマップ。 ### `time_weight: public(HashMap[address, uint256])` + - **説明**: 各ゲージアドレスの最後のスケジュールされた時間。 ### `points_sum: public(HashMap[int128, HashMap[uint256, Point]])` + - **説明**: ゲージタイプと時間に対する点の合計を格納するマップ。 ### `changes_sum: HashMap[int128, HashMap[uint256, uint256]]` + - **説明**: ゲージタイプと時間に対する合計の変更を格納するマップ。 ### `time_sum: public(uint256[1000000000])` + - **説明**: ゲージタイプの最後のスケジュールされた時間。 ### `points_total: public(HashMap[uint256, uint256])` + - **説明**: 時間ごとの総重みを格納するマップ。 ### `time_total: public(uint256)` + - **説明**: 総重みの最後のスケジュールされた時間。 --- @@ -80,30 +101,35 @@ ## イベント ### `CommitOwnership` + - **説明**: 所有権のコミットイベント。 - **パラメータ**: - `admin`: 管理者のアドレス。 ### `ApplyOwnership` + - **説明**: 所有権の適用イベント。 - **パラメータ**: - `admin`: 管理者のアドレス。 ### `AddType` + - **説明**: 新しいタイプの追加イベント。 - **パラメータ**: - `name`: タイプの名前。 - - `type_id`: タイプID。 + - `type_id`: タイプ ID。 ### `NewTypeWeight` + - **説明**: 新しいタイプの重みイベント。 - **パラメータ**: - - `type_id`: タイプID。 + - `type_id`: タイプ ID。 - `time`: タイムスタンプ。 - `weight`: 重み。 - `total_weight`: 総重量。 ### `NewGaugeWeight` + - **説明**: 新しいゲージの重みイベント。 - **パラメータ**: - `gauge_address`: ゲージのアドレス。 @@ -112,6 +138,7 @@ - `total_weight`: 総重量。 ### `VoteForGauge` + - **説明**: ゲージへの投票イベント。 - **パラメータ**: - `time`: タイムスタンプ。 @@ -120,6 +147,7 @@ - `weight`: 重み。 ### `NewGauge` + - **説明**: 新しいゲージのイベント。 - **パラメータ**: - `addr`: ゲージのアドレス。 @@ -131,43 +159,52 @@ ## 関数 ### `__init__(_token: address, _voting_escrow: address)` + - **説明**: コントラクトの初期化関数。初期設定を行います。 - **パラメータ**: - `_token`: `ERC20CRV` コントラクトのアドレス。 - `_voting_escrow`: `VotingEscrow` コントラクトのアドレス。 ### `commit_transfer_ownership(addr: address)` -- **説明**: GaugeControllerの所有権を`addr`に転送するためのコミット関数。 + +- **説明**: GaugeController の所有権を`addr`に転送するためのコミット関数。 - **パラメータ**: - `addr`: 所有権が転送されるアドレス。 ### `apply_transfer_ownership()` + - **説明**: 保留中の所有権転送を適用する関数。 ### `gauge_types(_addr: address) -> int128` + - **説明**: 指定したアドレスのゲージタイプを取得する関数。 - **パラメータ**: - `_addr`: ゲージのアドレス。 ### `_get_type_weight(gauge_type: int128) -> uint256` + - **説明**: 歴史的なタイプの重みを計算し、未チェックインの週に記入します。また、次の週のタイプの重みを返します。 - **パラメータ**: - - `gauge_type`: ゲージタイプのID。 + - `gauge_type`: ゲージタイプの ID。 ### `_get_sum(gauge_type: int128) -> uint256` + - **説明**: 同じタイプのゲージの重みの合計を計算し、未チェックインの週に記入します。また、次の週の合計を返します。 - **パラメータ**: - - `gauge_type`: ゲージタイプのID。 + - `gauge_type`: ゲージタイプの ID。 ### `_get_total() -> uint256` + - **説明**: 歴史的な全体の重みを計算し、未チェックインの週に記入します。また、次の週の合計を返します。 ### `_get_weight(gauge_addr: address) -> uint256` + - **説明**: 歴史的なゲージの重みを計算し、未チェックインの週に記入します。また、次の週のゲージの重みを返します。 - **パラメータ**: - `gauge_addr`: ゲージのアドレス。 ### `add_gauge(addr: address, gauge_type: int128, weight: uint256 = 0)` + - **説明**: 指定されたタイプと重みでゲージを追加します。 - **パラメータ**: - `addr`: ゲージのアドレス。 @@ -175,71 +212,85 @@ - `weight`: ゲージの重み。 ### `checkpoint()` + - **説明**: すべてのゲージに共通のデータを記入するためのチェックポイント。 ### `checkpoint_gauge(addr: address)` + - **説明**: 特定のゲージとすべてのゲージに共通のデータを記入するためのチェックポイント。 - **パラメータ**: - `addr`: ゲージのアドレス。 ### `_gauge_relative_weight(addr: address, time: uint256) -> uint256` -- **説明**: 指定されたタイムスタンプでのゲージの相対重み(1.0を超えない)を取得します。 + +- **説明**: 指定されたタイムスタンプでのゲージの相対重み(1.0 を超えない)を取得します。 - **パラメータ**: - `addr`: ゲージのアドレス。 - `time`: 指定したタイムスタンプ。 ### `gauge_relative_weight(addr: address, time: uint256 = block.timestamp) -> uint256` + - **説明**: 指定されたタイムスタンプでのゲージの相対重みを取得します。 - **パラメータ**: - `addr`: ゲージのアドレス。 - `time`: 指定したタイムスタンプ。 ### `gauge_relative_weight_write(addr: address, time: uint256 = block.timestamp) -> uint256` + - **説明**: - 指定されたタイムスタンプでのゲージの相対重みを書き込みます。 +指定されたタイムスタンプでのゲージの相対重みを書き込みます。 + - **パラメータ**: - `addr`: ゲージのアドレス。 - `time`: 指定したタイムスタンプ。 ### `add_type(_name: String[64], weight: uint256 = 0)` + - **説明**: ゲージタイプを追加します。ゲージタイプ名 `_name` と重み `weight` を指定して追加します。 - **パラメータ**: - `_name`: ゲージタイプの名前。 - `weight`: ゲージタイプの重み。 ### `change_type_weight(type_id: int128, weight: uint256)` + - **説明**: ゲージタイプの重みを変更します。 - **パラメータ**: - - `type_id`: ゲージタイプID。 + - `type_id`: ゲージタイプ ID。 - `weight`: 新しいゲージの重み。 ### `change_gauge_weight(addr: address, weight: uint256)` + - **説明**: ゲージの重みを変更します。 - **パラメータ**: - `addr`: `GaugeController` コントラクトのアドレス。 - `weight`: 新しいゲージの重み。 ### `vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256)` + - **説明**: プールの重みを変更するための投票パワーを割り当てます。 - **パラメータ**: - `_gauge_addr`: 投票対象のゲージアドレス。 - - `_user_weight`: ゲージの重み (bps単位、0.01%の単位)。 + - `_user_weight`: ゲージの重み (bps 単位、0.01%の単位)。 ### `get_gauge_weight(addr: address) -> uint256` + - **説明**: 現在のゲージの重みを取得します。 - **パラメータ**: - `addr`: ゲージのアドレス。 ### `get_type_weight(type_id: int128) -> uint256` + - **説明**: 現在のタイプの重みを取得します。 - **パラメータ**: - - `type_id`: タイプID。 + - `type_id`: タイプ ID。 ### `get_total_weight() -> uint256` + - **説明**: 現在の合計 (タイプごとの重み付け) 重みを取得します。 ### `get_weights_sum_per_type(type_id: int128) -> uint256` + - **説明**: タイプごとのゲージの重みの合計を取得します。 - **パラメータ**: - - `type_id`: タイプID。 + - `type_id`: タイプ ID。 diff --git a/doc/v1.5/FeeDistribution/usecase.md b/doc/v1.5/FeeDistribution/usecase.md index 9816d5ae..786aac11 100644 --- a/doc/v1.5/FeeDistribution/usecase.md +++ b/doc/v1.5/FeeDistribution/usecase.md @@ -1,6 +1,5 @@ # 未完成 - ```mermaid graph LR owner{{コントラクトオーナー}} @@ -78,4 +77,4 @@ graph LR subgraph Yamato borrowing end -``` \ No newline at end of file +``` diff --git a/doc/v1.5/Gauge/index.md b/doc/v1.5/Gauge/index.md index a5ef3f8c..5db08149 100644 --- a/doc/v1.5/Gauge/index.md +++ b/doc/v1.5/Gauge/index.md @@ -2,25 +2,30 @@ ## 概要 -Gaugeは、Yamato Protocolによって提供される報酬ゲージを計算・保存するコントラクトです。 +Gauge は、Yamato Protocol によって提供される報酬ゲージを計算・保存するコントラクトです。 --- ## 定数 ### `VERSION: constant(String[8]) = "v1"` + - **説明**: コントラクトのバージョン情報。 ### `MAX_REWARDS` -- **説明**: 同時に追跡できる報酬の最大数。現在は8です。 + +- **説明**: 同時に追跡できる報酬の最大数。現在は 8 です。 ### `TOKENLESS_PRODUCTION` -- **説明**: トークンなしでの生産可能な最大値。現在の値は40です。 + +- **説明**: トークンなしでの生産可能な最大値。現在の値は 40 です。 ### `WEEK` -- **説明**: 1週間の秒数。現在の値は604800秒です。 + +- **説明**: 1 週間の秒数。現在の値は 604800 秒です。 ### 各アドレス定数 (`MINTER`, `YMT`, `veYMT`) + - **説明**: 特定の役割を持つ外部コントラクトのアドレスを定義しています。 --- @@ -28,51 +33,67 @@ Gaugeは、Yamato Protocolによって提供される報酬ゲージを計算・ ## 変数 ### `NAME` + - **説明**: ゲージの名前。 ### `TOKEN_ADDRESS` + - **説明**: 対象となるトークンのアドレス。 ### `nonces` -- **説明**: 各アドレスのnonceのマッピング。 + +- **説明**: 各アドレスの nonce のマッピング。 ### `future_epoch_time` + - **説明**: 未来のエポック時間。 ### `balanceOf` + - **説明**: 各アドレスのバランスのマッピング。 ### `totalSupply` + - **説明**: 合計供給量。 ### `working_balances` + - **説明**: 各アドレスの動作中のバランス。 ### `working_supply` + - **説明**: 動作中の供給量。 ### `reward_count`, `reward_tokens`, `reward_data` + - **説明**: 外部報酬の追跡のための変数。 ### `rewards_receiver` + - **説明**: 請求者からデフォルトの報酬受取人へのマッピング。 ### `reward_integral_for` + - **説明**: 報酬トークンに関する請求アドレスの積分のマッピング。 ### `claim_data` + - **説明**: ユーザーに関する請求データのマッピング。 ### `admin`, `future_admin`, `is_killed` + - **説明**: 管理者関連の変数。 ### `integrate_inv_supply_of`, `integrate_checkpoint_of`, `integrate_fraction` + - **説明**: 供給や報酬に関する積分の計算のための変数。 ### `inflation_rate` + - **説明**: インフレ率。 ### `period`, `period_timestamp`, `integrate_inv_supply` + - **説明**: インフレ率の変化や供給の積分に関する変数。 --- @@ -80,10 +101,12 @@ Gaugeは、Yamato Protocolによって提供される報酬ゲージを計算・ ## イベント ### `UpdateLimit` + - **説明**: ユーザーの情報が更新された際に発火。 - 各変数はユーザーの情報や更新後の情報を示します。 ### その他のイベント(`CommitOwnership`, `ApplyOwnership`) + - **説明**: 各イベントはゲージの管理やトークンの移転、許可の変更に関連する操作を示します。 --- @@ -91,73 +114,85 @@ Gaugeは、Yamato Protocolによって提供される報酬ゲージを計算・ ## 関数 ### `__init__(_lp_token: address, _admin: address)` + - **説明**: コントラクトの初期化関数。初期設定を行います。 - **パラメータ**: - `_lp_token`: 流動性プールトークンのアドレス。 - `_admin`: 管理者のアドレス。 ### `integrate_checkpoint() -> uint256` + - **説明**: 現在の期間のタイムスタンプを取得する関数。 ### `_update_liquidity_limit(addr: address, l: uint256, L: uint256)` -- **説明**: ユーザーの流動性の限界を更新する内部関数。CRVトークンの量に基づいてワーキングバランスを計算します。 + +- **説明**: ユーザーの流動性の限界を更新する内部関数。CRV トークンの量に基づいてワーキングバランスを計算します。 - **パラメータ**: - `addr`: ユーザーのアドレス。 - `l`: ユーザーの流動性の量。 - `L`: 全体の流動性の量。 ### `user_checkpoint(addr: address) -> bool` + - **説明**: ユーザー`addr`のチェックポイントを記録します。 - **パラメータ**: - `addr`: チェックポイントを記録するユーザーのアドレス。 ### `claimable_tokens(addr: address) -> uint256` + - **説明**: チェックポイントを更新して、ユーザー`addr`に対して請求可能なトークンの数を取得します。 - **パラメータ**: - `addr`: 請求可能なトークンの数を取得するユーザーのアドレス。 ### `claimed_reward(_addr: address, _token: address) -> uint256` + - **説明**: ユーザー`_addr`が既に請求した報酬トークンの数を取得します。 - **パラメータ**: - `_addr`: 既に請求した報酬トークンの数を取得するユーザーのアドレス。 - `_token`: 既に請求した報酬トークンのアドレス。 ### `claimable_reward(_user: address, _reward_token: address) -> uint256` + - **説明**: ユーザー`_user`に対して請求可能な報酬トークンの数を取得します。 - **パラメータ**: - `_user`: 請求可能な報酬トークンの数を取得するユーザーのアドレス。 - `_reward_token`: 請求可能な報酬トークンのアドレス。 ### `set_rewards_receiver(_receiver: address)` + - **説明**: 呼び出し元のデフォルトの報酬受信者を設定します。 - **パラメータ**: - `_receiver`: `claim_rewards`で報酬を受け取るアドレス。 ### `claim_rewards(_addr: address = msg.sender, _receiver: address = ZERO_ADDRESS)` + - **説明**: `_addr`の利用可能な報酬トークンを請求します。 - **パラメータ**: - `_addr`: 報酬を請求するアドレス。 - - `_receiver`: 報酬を転送するアドレス。ZERO_ADDRESSに設定されている場合、呼び出し元のデフォルトの報酬受信者を使用します。 + - `_receiver`: 報酬を転送するアドレス。ZERO_ADDRESS に設定されている場合、呼び出し元のデフォルトの報酬受信者を使用します。 ### `kick(addr: address)` + - **説明**: そのブーストを乱用している`addr`をキックします。 - **パラメータ**: - `addr`: キックするアドレス。 ### `set_killed(_is_killed: bool)` -- **説明**: このコントラクトの`killed`ステータスを設定します。`killed`としてマークされた場合、このゲージは常にレート0を返すため、CRVを生成することはできません。 + +- **説明**: このコントラクトの`killed`ステータスを設定します。`killed`としてマークされた場合、このゲージは常にレート 0 を返すため、CRV を生成することはできません。 - **パラメータ**: - `_is_killed`: 設定する`killed`のステータス。 ### `commit_transfer_ownership(addr: address)` + - **説明**: `Gauge`の所有権を`addr`に転送します。 - **パラメータ**: - `addr`: 所有権を転送するアドレス。 ### `accept_transfer_ownership()` + - **説明**: 所有権転送を受け入れます。 ### `version() -> String[8]` -- **説明**: このゲージのバージョンを取得します。 - +- **説明**: このゲージのバージョンを取得します。 diff --git a/doc/v1.5/GaugeController/class.md b/doc/v1.5/GaugeController/class.md index 085ea049..50beaf3a 100644 --- a/doc/v1.5/GaugeController/class.md +++ b/doc/v1.5/GaugeController/class.md @@ -1,7 +1,5 @@ - # GaugeController - ```mermaid classDiagram @@ -67,4 +65,4 @@ classDiagram GaugeController --|> VotingEscrow: Uses > -``` \ No newline at end of file +``` diff --git a/doc/v1.5/Minter/index.md b/doc/v1.5/Minter/index.md index e2a996cd..fee31692 100644 --- a/doc/v1.5/Minter/index.md +++ b/doc/v1.5/Minter/index.md @@ -2,80 +2,92 @@ ## 概要 -Token Minterは、特定のゲージからトークンをmintするためのコントラクトです。YMTトークンをmintする機能を提供します。また、ユーザーが他のユーザーに代わってmintする権限を与えることもできます。 +Token Minter は、特定のゲージからトークンを mint するためのコントラクトです。YMT トークンを mint する機能を提供します。また、ユーザーが他のユーザーに代わって mint する権限を与えることもできます。 --- ## インターフェース ### `Gauge`: + - **説明**: 流動性のゲージから情報を取得するためのインターフェース。 - **関数**: - `integrate_fraction(addr: address) -> uint256`: 指定したアドレスの積分値を取得する。 - `user_checkpoint(addr: address) -> bool`: ユーザーチェックポイントを更新する。 ### `YMT`: -- **説明**: トークンをmintするためのERC20トークンインターフェース。 + +- **説明**: トークンを mint するための ERC20 トークンインターフェース。 - **関数**: - - `mint(_to: address, _value: uint256) -> bool`: トークンをmintする。 + - `mint(_to: address, _value: uint256) -> bool`: トークンを mint する。 --- ## イベント ### `Minted`: -- **説明**: トークンがmintされたときにトリガーされるイベント。 + +- **説明**: トークンが mint されたときにトリガーされるイベント。 - **パラメータ**: - `recipient`: 受取人のアドレス。 - `gauge`: 使用されたゲージのアドレス。 - - `minted`: mintされたトークンの量。 + - `minted`: mint されたトークンの量。 --- ## 変数 ### `token: public(address)` -- **説明**: mintするMERC20トークンのアドレス。 + +- **説明**: mint する MERC20 トークンのアドレス。 ### `minted: public(HashMap[address, HashMap[address, uint256]])` -- **説明**: すでにmintされたトークンの量を追跡するためのマップ。 + +- **説明**: すでに mint されたトークンの量を追跡するためのマップ。 ### `allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]])` -- **説明**: あるユーザーが別のユーザーに代わってトークンをmintすることを許可するためのマップ。 + +- **説明**: あるユーザーが別のユーザーに代わってトークンを mint することを許可するためのマップ。 --- ## 関数 ### `__init__(_token: address, _controller: address)` + - **説明**: コントラクトの初期化関数。トークンとコントローラーのアドレスを設定する。 - **パラメータ**: - - `_token`: MERC20トークンのアドレス。 - - `_controller`: GaugeControllerのアドレス。 + - `_token`: MERC20 トークンのアドレス。 + - `_controller`: GaugeController のアドレス。 ### `_mint_for(gauge_addr: address, _for: address)` -- **説明**: 内部関数。指定したゲージからトークンをmintする。 + +- **説明**: 内部関数。指定したゲージからトークンを mint する。 - **パラメータ**: - `gauge_addr`: ゲージのアドレス。 - `_for`: トークンを受け取るアドレス。 ### `mint(gauge_addr: address)` -- **説明**: `msg.sender`に属するすべてのトークンをmintして送信する。 + +- **説明**: `msg.sender`に属するすべてのトークンを mint して送信する。 - **パラメータ**: - `gauge_addr`: ゲージのアドレス。 ### `mint_many(gauge_addrs: address[8])` -- **説明**: 複数のゲージから`msg.sender`に属するすべてのトークンをmintする。 + +- **説明**: 複数のゲージから`msg.sender`に属するすべてのトークンを mint する。 - **パラメータ**: - `gauge_addrs`: ゲージのアドレスのリスト。 ### `mint_for(gauge_addr: address, _for: address)` -- **説明**: `_for`のためのトークンをmintする。`msg.sender`が承認されている場合のみ可能。 + +- **説明**: `_for`のためのトークンを mint する。`msg.sender`が承認されている場合のみ可能。 - **パラメータ**: - `gauge_addr`: ゲージのアドレス。 - `_for`: トークンを受け取るアドレス。 ### `toggle_approve_mint(minting_user: address)` -- **説明**: `minting_user`が`msg.sender`の代わりにトークンをmintすることを許可または禁止する。 + +- **説明**: `minting_user`が`msg.sender`の代わりにトークンを mint することを許可または禁止する。 - **パラメータ**: - `minting_user`: 許可または禁止を切り替えるユーザーのアドレス。 diff --git a/doc/v1.5/YMT/index.md b/doc/v1.5/YMT/index.md index 73917d96..18618a46 100644 --- a/doc/v1.5/YMT/index.md +++ b/doc/v1.5/YMT/index.md @@ -2,31 +2,38 @@ ## 概要 -YMT Tokenは、piecewise-linear mining supplyを持つERC20トークンです。このトークンのサプライは線形に増加しており、マイニングパラメータは定期的に更新される。初期供給は一定であり、その後の供給レートは時間の経過とともに減少していく。 +YMT Token は、piecewise-linear mining supply を持つ ERC20 トークンです。このトークンのサプライは線形に増加しており、マイニングパラメータは定期的に更新される。初期供給は一定であり、その後の供給レートは時間の経過とともに減少していく。 --- ## 定数 ### `YEAR: constant(uint256) = 86400 * 365` -- **説明**: 1年の秒数を表す定数。具体的には、1日の秒数(86400秒)に365を掛けた値。 + +- **説明**: 1 年の秒数を表す定数。具体的には、1 日の秒数(86400 秒)に 365 を掛けた値。 ### `INITIAL_SUPPLY: constant(uint256) ` + - **説明**: トークンの初期供給量。 ### `INITIAL_RATE: constant(uint256)` + - **説明**: 初期のマイニングレート。 ### `RATE_REDUCTION_TIME: constant(uint256) = YEAR` + - **説明**: マイニングレートが減少する時間の間隔。 ### `RATE_REDUCTION_COEFFICIENT: constant(uint256)` + - **説明**: マイニングレートの減少係数。 ### `RATE_DENOMINATOR: constant(uint256) = 10 ** 18` + - **説明**: レートの分母として使用される定数。 ### `INFLATION_DELAY: constant(uint256) = 86400` + - **説明**: インフレーションが適用されるまでの遅延時間(秒)。 --- @@ -34,39 +41,51 @@ YMT Tokenは、piecewise-linear mining supplyを持つERC20トークンです。 ## 変数 ### `name: public(String[64])` + - **説明**: トークンの名前。 ### `symbol: public(String[32])` + - **説明**: トークンのシンボル。 ### `decimals: public(uint256)` + - **説明**: トークンの小数点以下の桁数。 ### `balanceOf: public(HashMap[address, uint256])` + - **説明**: 各アドレスのトークン残高を保持するマップ。 ### `allowances: HashMap[address, HashMap[address, uint256]]` + - **説明**: トークンの所有者が他のユーザーに許可したトークンの移動量。 ### `total_supply: uint256` + - **説明**: 現在のトークンの総供給量。 ### `minter: public(address)` -- **説明**: トークンをmintできるアドレス。 + +- **説明**: トークンを mint できるアドレス。 ### `admin: public(address)` + - **説明**: このスマートコントラクトの管理者アドレス。 ### `mining_epoch: public(int128)` + - **説明**: 現在のマイニングエポック。 ### `start_epoch_time: public(uint256)` + - **説明**: 現在のエポックの開始タイムスタンプ。 ### `rate: public(uint256)` + - **説明**: 現在のマイニングレート。 ### `start_epoch_supply: uint256` + - **説明**: エポック開始時の供給量。 --- @@ -74,24 +93,29 @@ YMT Tokenは、piecewise-linear mining supplyを持つERC20トークンです。 ## イベント ### Transfer: -- _from: indexed(address) -- _to: indexed(address) -- _value: uint256 + +- \_from: indexed(address) +- \_to: indexed(address) +- \_value: uint256 ### Approval: -- _owner: indexed(address) -- _spender: indexed(address) -- _value: uint256 + +- \_owner: indexed(address) +- \_spender: indexed(address) +- \_value: uint256 ### UpdateMiningParameters: + - time: uint256 - rate: uint256 - supply: uint256 ### SetMinter: + - minter: address ### SetAdmin: + - admin: address --- @@ -99,69 +123,84 @@ YMT Tokenは、piecewise-linear mining supplyを持つERC20トークンです。 ## 関数 ### `__init__(_name: String[64], _symbol: String[32], _decimals: uint256)` + - **説明**:コントラクトのコンストラクタ。初期設定を行います。 - **パラメータ**: - `_name`:トークンの名前。 - `_symbol`:トークンのシンボル。 - `_decimals`:トークンの小数点以下の桁数。 - + ### `_update_mining_parameters()` + - **説明**:採掘のレートと供給をエポックの開始時に更新します。 - **制限**:内部関数。 ### `update_mining_parameters()` + - **説明**:エポックの開始時に採掘のレートと供給を更新します。 - + ### `start_epoch_time_write() -> uint256` + - **説明**:現在の採掘エポックの開始タイムスタンプを取得し、同時に採掘パラメータを更新します。 - + ### `future_epoch_time_write() -> uint256` + - **説明**:次の採掘エポックの開始タイムスタンプを取得し、同時に採掘パラメータを更新します。 - + ### `_available_supply() -> uint256` + - **説明**:存在するトークンの数(請求済みまたは未請求)を返します。 - **制限**:内部関数。 ### `available_supply() -> uint256` + - **説明**:存在するトークンの数(請求済みまたは未請求)を返します。 - + ### `mintable_in_timeframe(start: uint256, end: uint256) -> uint256` + - **説明**:指定されたタイムフレームで発行可能なトークンの量を計算します。 - **パラメータ**: - `start`:タイムフレームの開始タイムスタンプ。 - `end`:タイムフレームの終了タイムスタンプ。 - + ### `set_minter(_minter: address)` -- **説明**:minterのアドレスを設定します。 + +- **説明**:minter のアドレスを設定します。 - **パラメータ**: - - `_minter`:minterのアドレス。 - + - `_minter`:minter のアドレス。 + ### `set_admin(_admin: address)` + - **説明**:新しい管理者のアドレスを設定します。 - **パラメータ**: - `_admin`:新しい管理者のアドレス。 - + ### `totalSupply() -> uint256` + - **説明**:存在するトークンの総量を返します。 - + ### `allowance(_owner: address, _spender: address) -> uint256` -- **説明**:_ownerが_spenderに許可したトークンの量を返します。 + +- **説明**:\_owner が\_spender に許可したトークンの量を返します。 - **パラメータ**: - `_owner`:資金の所有者のアドレス。 - `_spender`:資金を使うアドレス。 ### `transfer(_to: address, _value: uint256) -> bool` + - **説明**:`msg.sender`から`_to`へのトークンの転送。 - **パラメータ**: - `_to`:受取人のアドレス。 - `_value`:転送するトークンの量。 ### `transferFrom(_from: address, _to: address, _value: uint256) -> bool` + - **説明**:`_from`から`_to`へのトークンの転送。 - **パラメータ**: - `_from`:送信者のアドレス。 - `_to`:受取人のアドレス。 - `_value`:転送するトークンの量。 - + ### `approve(_spender: address, _value: uint256) -> bool` -- **説明**:`msg.sender`が`_spender`にトークンを使わせ \ No newline at end of file + +- **説明**:`msg.sender`が`_spender`にトークンを使わせ diff --git a/doc/v1.5/veYMT/index.md b/doc/v1.5/veYMT/index.md index 03dbab8d..fbdf43a7 100644 --- a/doc/v1.5/veYMT/index.md +++ b/doc/v1.5/veYMT/index.md @@ -2,8 +2,8 @@ ## 概要 -YMT トークンをロックし、移転不可のveYMT トークンを発行する。 -ロック期間は最大4年間、最小単位は1週間で、veYMTはロック後の時間経過により線形に減衰する。1YMTを4年間ロックすると1veYMTが発行される。 +YMT トークンをロックし、移転不可の veYMT トークンを発行する。 +ロック期間は最大 4 年間、最小単位は 1 週間で、veYMT はロック後の時間経過により線形に減衰する。1YMT を 4 年間ロックすると 1veYMT が発行される。 #### 参考 @@ -86,11 +86,11 @@ veYMT のある時点での状態を格納するための構造体 ### `point_history: public(Point[])` -veYMTのグローバルな状態を epoch ごとに記録する配列 +veYMT のグローバルな状態を epoch ごとに記録する配列 ### `user_point_history: public(address => Point[])` -veYMTのユーザごとの状態を user epoch ごとに記録する配列 +veYMT のユーザごとの状態を user epoch ごとに記録する配列 ### `user_point_epoch: public(address => uint256)` @@ -102,11 +102,11 @@ veYMTのユーザごとの状態を user epoch ごとに記録する配列 ### `controller: public(address)` -Aragon互換性のため +Aragon 互換性のため ### `transfersEnabled: public(bool)` -Aragon互換性のため +Aragon 互換性のため ### `name: public(string)` @@ -134,7 +134,7 @@ The goal is to prevent tokenizing the escrow Checker for whitelisted (smart contract) wallets which are allowed to deposit The goal is to prevent tokenizing the escrow -CurveのVotingEscrowでは現在下記コントラクトが登録されている +Curve の VotingEscrow では現在下記コントラクトが登録されている https://etherscan.io/address/0xca719728Ef172d0961768581fdF35CB116e0B7a4#readContract ### `admin: public(address)` @@ -151,16 +151,16 @@ https://etherscan.io/address/0xca719728Ef172d0961768581fdF35CB116e0B7a4#readCont 初期化 -- adminにmsg.senderを設定 -- tokenにtoken_addrを設定 -- point_history[0].blkにblock.numberを設定 -- point_history[0].tsにblock.timestampを設定 -- controllerにmsg.senderを設定 -- transfersEnabledにTrueを設定 -- decimalsにtokenのdecimalsと同じ値を設定 -- nameに\_nameを設定 -- symbolに\_symbolを設定 -- versionに\_versionを設定 +- admin に msg.sender を設定 +- token に token_addr を設定 +- point_history[0].blk に block.number を設定 +- point_history[0].ts に block.timestamp を設定 +- controller に msg.sender を設定 +- transfersEnabled に True を設定 +- decimals に token の decimals と同じ値を設定 +- name に\_name を設定 +- symbol に\_symbol を設定 +- version に\_version を設定 ### `commit_transfer_ownership(addr: address) external` @@ -211,7 +211,7 @@ https://etherscan.io/address/0xca719728Ef172d0961768581fdF35CB116e0B7a4#readCont - integrate_checkpoint_of - integrate_fraction - ポイント履歴の最後の時点から 最大 255 週分の履歴を作成する。255 週以上の期間に渡って履歴がない場合(=ユーザ操作がない場合)は正しい計算ができなくなる -- integrate_inv_supplyを更新する +- integrate_inv_supply を更新する ### `_deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128) internal` @@ -249,28 +249,28 @@ https://etherscan.io/address/0xca719728Ef172d0961768581fdF35CB116e0B7a4#readCont ### `balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256 external view` -- 指定したアドレスの指定したタイムスタンプ時点でのveYMT残高を返す +- 指定したアドレスの指定したタイムスタンプ時点での veYMT 残高を返す - \_t が最後に記録されたユーザのポイント履歴より前の場合は失敗する ### `balanceOfAt(addr: address, _block: uint256) -> uint256 external view` -- 指定したアドレスの指定したブロック高時点でのveYMT残高を返す +- 指定したアドレスの指定したブロック高時点での veYMT 残高を返す ### `supply_at(point: Point, t: uint256) -> uint256 internal view` -- 指定したポイントを起点に指定したタイムスタンプ時点での 総veYMT残高を返す +- 指定したポイントを起点に指定したタイムスタンプ時点での 総 veYMT 残高を返す - 255 週以上ポイントが記録されていない状況の場合は正しい計算ができなくなる ### `totalSupply(t: uint256 = block.timestamp) -> uint256 external view` -- 最後に記録されたポイントを起点に、指定したタイムスタンプ時点の総veYMT残高を返す +- 最後に記録されたポイントを起点に、指定したタイムスタンプ時点の総 veYMT 残高を返す ### `totalSupplyAt(_block: uint256) -> uint256 external view` -- 指定したブロック高時点での総veYMT残高を返す +- 指定したブロック高時点での総 veYMT 残高を返す ### `changeController(_newController: address) external` - controller のみ - controller を変更する -- Aragon互換性のためのダミー関数 \ No newline at end of file +- Aragon 互換性のためのダミー関数 diff --git a/doc/v1.5/veYMT/usecase.md b/doc/v1.5/veYMT/usecase.md index c9f0045f..5495236b 100644 --- a/doc/v1.5/veYMT/usecase.md +++ b/doc/v1.5/veYMT/usecase.md @@ -153,4 +153,4 @@ graph LR end -``` \ No newline at end of file +``` diff --git a/test/fork/Constants.ts b/test/fork/Constants.ts index 8ff56354..f00a9c15 100644 --- a/test/fork/Constants.ts +++ b/test/fork/Constants.ts @@ -47,7 +47,6 @@ class Constants { BigNumber.from("1"), BigNumber.from("2"), ]; - } export default Constants; diff --git a/test/fork/helper.ts b/test/fork/helper.ts index 6d73be1d..67021869 100644 --- a/test/fork/helper.ts +++ b/test/fork/helper.ts @@ -64,5 +64,4 @@ async function deployContracts() { }; } - export { deployContracts }; diff --git a/test/fork/integration/ERC20CRV/mintIntegration.test.ts b/test/fork/integration/ERC20CRV/mintIntegration.test.ts index b84f8467..2351e7c3 100644 --- a/test/fork/integration/ERC20CRV/mintIntegration.test.ts +++ b/test/fork/integration/ERC20CRV/mintIntegration.test.ts @@ -10,8 +10,8 @@ import { BigNumber } from "ethers"; // Assuming you have a helper function to increase blockchain time async function increaseTime(duration: number) { - await ethers.provider.send('evm_increaseTime', [duration]); - await ethers.provider.send('evm_mine', []); + await ethers.provider.send("evm_increaseTime", [duration]); + await ethers.provider.send("evm_mine", []); } const YEAR = 365 * 24 * 60 * 60; // seconds in a year @@ -45,7 +45,9 @@ describe("ERC20CRV", function () { await increaseTime(duration); - const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const amount = currentTime.sub(creationTime).mul(rate); await token.mint(accounts[1].address, amount); @@ -61,9 +63,13 @@ describe("ERC20CRV", function () { await increaseTime(duration); - const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const amount = currentTime.sub(creationTime).add(2).mul(rate); - await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith("dev: exceeds allowable mint amount"); + await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( + "dev: exceeds allowable mint amount" + ); }); it("should mint multiple times correctly", async function () { @@ -77,7 +83,10 @@ describe("ERC20CRV", function () { for (const time of durations) { await increaseTime(time); - if ((await ethers.provider.getBlock('latest')).timestamp - epochStart > YEAR) { + if ( + (await ethers.provider.getBlock("latest")).timestamp - epochStart > + YEAR + ) { await token.updateMiningParameters(); epochStart = await token.startEpochTime(); } diff --git a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts index c5bdfe90..4742c98d 100644 --- a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts +++ b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts @@ -1,19 +1,24 @@ import { ethers } from "hardhat"; import { expect } from "chai"; import { BigNumber, Contract } from "ethers"; -import { takeSnapshot, SnapshotRestorer } from "@nomicfoundation/hardhat-network-helpers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import Constants from "../../Constants"; async function increaseTime(duration: BigNumber) { - await ethers.provider.send('evm_increaseTime', [duration.toNumber()]); - await ethers.provider.send('evm_mine', []); + await ethers.provider.send("evm_increaseTime", [duration.toNumber()]); + await ethers.provider.send("evm_mine", []); } // Constants const YEAR = Constants.YEAR; const INITIAL_RATE = BigNumber.from(274815283); -const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)).div(YEAR).mul(YEAR); +const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)) + .div(YEAR) + .mul(YEAR); const INITIAL_SUPPLY = BigNumber.from(1303030303); describe("ERC20CRV", function () { @@ -35,11 +40,10 @@ describe("ERC20CRV", function () { afterEach(async () => { await snapshot.restore(); }); - - describe("ERC20CRV MintableInTimeframe", function () { + describe("ERC20CRV MintableInTimeframe", function () { // Helper function for approximate equality - function approx(a: BigNumber, b: BigNumber, precision ): boolean { + function approx(a: BigNumber, b: BigNumber, precision): boolean { if (a.isZero() && b.isZero()) { return true; } @@ -50,7 +54,6 @@ describe("ERC20CRV", function () { return a.sub(b).abs().lte(a.add(b).div(precisionAdjusted)); } - // Helper function for theoretical supply calculation async function theoreticalSupply(token: Contract): Promise { const epoch = await token.miningEpoch(); @@ -68,7 +71,10 @@ describe("ERC20CRV", function () { S = S.add( YEAR_1_SUPPLY.div(YEAR) .mul(q.pow(epoch)) - .mul((await ethers.provider.getBlock("latest")).timestamp - (await token.startEpochTime())) + .mul( + (await ethers.provider.getBlock("latest")).timestamp - + (await token.startEpochTime()) + ) ); return S; @@ -76,33 +82,47 @@ describe("ERC20CRV", function () { it("test_mintable_in_timeframe", async function () { const t0 = Number(await token.startEpochTime()); - + // Ensure the exponentiation stays within safe integer limits const exponent = BigNumber.from(10).pow(1); // Adjust the exponent as necessary await increaseTime(exponent); - + let t1 = (await ethers.provider.getBlock("latest")).timestamp; if (t1 - t0 >= year) { await token.updateMiningParameters(); } - t1 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + t1 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); const availableSupply = await token.availableSupply(); const mintable = await token.mintableInTimeframe(t0, t1); - expect((availableSupply.sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)).gte(mintable))).to.equal(true); + expect( + availableSupply + .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) + .gte(mintable) + ).to.equal(true); if (t1 == t0) { expect(mintable).to.equal(BigNumber.from(0)); } else { const tolerance = BigNumber.from("10000000"); // Adjust as needed for precision - expect(availableSupply.sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)).div(mintable).sub(1)).to.be.lt(tolerance); + expect( + availableSupply + .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) + .div(mintable) + .sub(1) + ).to.be.lt(tolerance); } - // Replace this with the actual theoretical supply calculation // const theoreticalSupply = BigNumber.from("EXPECTED_SUPPLY_CALCULATION"); - expect(approx(await theoreticalSupply(token), availableSupply, Constants.ten_to_the_16)).to.equal(true); + expect( + approx( + await theoreticalSupply(token), + availableSupply, + Constants.ten_to_the_16 + ) + ).to.equal(true); }); - + it("test_random_range_year_one", async function () { const creationTime = await token.startEpochTime(); const time1 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); @@ -110,30 +130,35 @@ describe("ERC20CRV", function () { const [start, end] = [creationTime.add(time1), creationTime.add(time2)]; const sortedTimes = start.lt(end) ? [start, end] : [end, start]; const rate = YEAR_1_SUPPLY.div(YEAR); - - expect(await token.mintableInTimeframe(sortedTimes[0], sortedTimes[1])).to.equal(rate.mul(sortedTimes[1].sub(sortedTimes[0]))); + + expect( + await token.mintableInTimeframe(sortedTimes[0], sortedTimes[1]) + ).to.equal(rate.mul(sortedTimes[1].sub(sortedTimes[0]))); }); - + it("test_random_range_multiple_epochs", async function () { const creationTime = await token.startEpochTime(); const start = creationTime.add(YEAR.mul(2)); const duration = YEAR.mul(2); const end = start.add(duration); - + const startEpoch = start.sub(creationTime).div(YEAR); const endEpoch = end.sub(creationTime).div(YEAR); const exponent = startEpoch.mul(25); - const rate = YEAR_1_SUPPLY.div(YEAR).div(BigNumber.from(2).pow(exponent.div(100))); - + const rate = YEAR_1_SUPPLY.div(YEAR).div( + BigNumber.from(2).pow(exponent.div(100)) + ); + for (let i = startEpoch.toNumber(); i < endEpoch.toNumber(); i++) { await increaseTime(YEAR); await token.updateMiningParameters(); } - + const mintable = await token.mintableInTimeframe(start, end); if (startEpoch.eq(endEpoch)) { const expectedMintable = rate.mul(end.sub(start)); - expect(approx(mintable, expectedMintable, Constants.ten_to_the_16)).to.be.true; + expect(approx(mintable, expectedMintable, Constants.ten_to_the_16)).to + .be.true; } else { expect(mintable.lt(rate.mul(end))).to.be.true; } @@ -144,10 +169,12 @@ describe("ERC20CRV", function () { const creationTime = await token.startEpochTime(); const initialSupply = await token.totalSupply(); const rate = await token.rate(); - + await increaseTime(duration); - - const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const expected = initialSupply.add(now.sub(creationTime).mul(rate)); expect(await token.availableSupply()).to.equal(expected); }); diff --git a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts index 6b622473..2080b380 100644 --- a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts +++ b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts @@ -25,12 +25,17 @@ describe("LiquidityGauge", function () { const zero = Constants.zero; const WEEK = Constants.WEEK; - beforeEach(async () => { snapshot = await takeSnapshot(); accounts = await ethers.getSigners(); - ({ gaugeController, threeGauges, gauges, votingEscrow, token, mockLpToken } = - await deployContracts()); + ({ + gaugeController, + threeGauges, + gauges, + votingEscrow, + token, + mockLpToken, + } = await deployContracts()); await gaugeController.addType("none", BigNumber.from("0")); }); @@ -39,7 +44,6 @@ describe("LiquidityGauge", function () { }); describe("Gauge Integral Calculations", function () { - /** * テスト: test_gauge_integral * 1. AliceとBobの保有量と積分の初期化を行う。 @@ -62,72 +66,85 @@ describe("LiquidityGauge", function () { let alice_staked = BigNumber.from("0"); let bob_staked = BigNumber.from("0"); let integral = BigNumber.from("0"); - + // 最新のブロックのタイムスタンプを取得 - let checkpoint = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + let checkpoint = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); // 初期レートの取得 let checkpoint_rate = await token.rate(); let checkpoint_supply = BigNumber.from("0"); let checkpoint_balance = BigNumber.from("0"); - + // タイプの追加とその重みの変更 await gaugeController.addType("Liquidity", BigNumber.from("0")); await gaugeController.changeTypeWeight(1, ten_to_the_18); await gaugeController.addGauge(threeGauges[0], 1, ten_to_the_18); - + // LPトークンを送信 const creatorBalance = await mockLpToken.balanceOf(accounts[0].address); await mockLpToken.transfer( accounts[1].address, - (creatorBalance).div(BigNumber.from("2")) + creatorBalance.div(BigNumber.from("2")) ); await mockLpToken.transfer( accounts[2].address, - (creatorBalance).div(BigNumber.from("2")) + creatorBalance.div(BigNumber.from("2")) ); - + // 積分を更新する関数 async function update_integral() { - let t1 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + let t1 = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); let rate1 = await token.rate(); let t_epoch = await token.startEpochTime(); let rate_x_time = BigNumber.from("0"); - + // checkpoint >= t_epoch if (checkpoint.gte(t_epoch)) { // t1 - checkpoint * rate1 rate_x_time = t1.sub(checkpoint).mul(rate1); } else { // t_epoch - checkpoint * checkpoint_rate ( t1 - t_epoch * rate1) - rate_x_time = t_epoch.sub(checkpoint).mul(checkpoint_rate).add(t1.sub(t_epoch).mul(rate1)); + rate_x_time = t_epoch + .sub(checkpoint) + .mul(checkpoint_rate) + .add(t1.sub(t_epoch).mul(rate1)); } - + // checkpoint_supply > 0 if (checkpoint_supply.gt(BigNumber.from("0"))) { // integral + rate_x_time * checkpoint_balance / checkpoint_supply - integral = integral.add(rate_x_time.mul(checkpoint_balance).div(checkpoint_supply)); + integral = integral.add( + rate_x_time.mul(checkpoint_balance).div(checkpoint_supply) + ); } - + checkpoint_rate = rate1; checkpoint = t1; checkpoint_supply = await gauges[0].totalSupply(); checkpoint_balance = await gauges[0].balanceOf(accounts[1].address); } - + // Bobは常に預金または引き出しを行い、Aliceはそれをあまり行わない for (let i = 0; i < 10; i++) { let is_alice = Math.random() < 0.2; - + // ランダムな時間経過をシミュレート - let dt = BigNumber.from(Math.floor(Math.random() * 86400 * 73).toString()).add(BigNumber.from("1")); + let dt = BigNumber.from( + Math.floor(Math.random() * 86400 * 73).toString() + ).add(BigNumber.from("1")); await ethers.provider.send("evm_increaseTime", [dt.toNumber()]); - + // Bobの処理 let is_withdraw = i > 0 && Math.random() < 0.5; - + if (is_withdraw) { // 引き出し処理 - let amount = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + let amount = BigNumber.from( + Math.floor(Math.random() * 10000).toString() + ) .mul(await gauges[0].balanceOf(accounts[2].address)) .div(BigNumber.from("10000")); await gauges[0].connect(accounts[2]).withdraw(amount, false); @@ -135,22 +152,32 @@ describe("LiquidityGauge", function () { bob_staked = bob_staked.sub(amount); } else { // 預金処理 - let amount = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + let amount = BigNumber.from( + Math.floor(Math.random() * 10000).toString() + ) .mul(await mockLpToken.balanceOf(accounts[2].address)) .div(BigNumber.from("10")) .div(BigNumber.from("10000")); - await mockLpToken.connect(accounts[2]).approve(threeGauges[0], amount); - await gauges[0].connect(accounts[2]).deposit(amount, accounts[2].address, false); + await mockLpToken + .connect(accounts[2]) + .approve(threeGauges[0], amount); + await gauges[0] + .connect(accounts[2]) + .deposit(amount, accounts[2].address, false); await update_integral(); bob_staked = bob_staked.add(amount); } - + // Aliceの処理 if (is_alice) { - let is_withdraw_alice = (await gauges[0].balanceOf(accounts[1].address)) > 0 && Math.random() > 0.5; + let is_withdraw_alice = + (await gauges[0].balanceOf(accounts[1].address)) > 0 && + Math.random() > 0.5; if (is_withdraw_alice) { // 引き出し処理 - let amount_alice = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + let amount_alice = BigNumber.from( + Math.floor(Math.random() * 10000).toString() + ) .mul(await gauges[0].balanceOf(accounts[1].address)) .div(BigNumber.from("10")) .div(BigNumber.from("10000")); @@ -159,63 +186,81 @@ describe("LiquidityGauge", function () { alice_staked = alice_staked.sub(amount_alice); } else { // 預金処理 - let amount_alice = BigNumber.from(Math.floor(Math.random() * 10000).toString()) + let amount_alice = BigNumber.from( + Math.floor(Math.random() * 10000).toString() + ) .mul(await mockLpToken.balanceOf(accounts[1].address)) .div(BigNumber.from("10000")); - await mockLpToken.connect(accounts[1]).approve(threeGauges[0], amount_alice); - await gauges[0].connect(accounts[1]).deposit(amount_alice, accounts[1].address, false); + await mockLpToken + .connect(accounts[1]) + .approve(threeGauges[0], amount_alice); + await gauges[0] + .connect(accounts[1]) + .deposit(amount_alice, accounts[1].address, false); await update_integral(); alice_staked = alice_staked.add(amount_alice); } } - + // 同じ秒数でのチェックポイントの更新は影響しないことの確認 if (Math.random() < 0.5) { - await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); + await gauges[0] + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); } if (Math.random() < 0.5) { - await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); + await gauges[0] + .connect(accounts[2]) + .userCheckpoint(accounts[2].address); } - + // 保有量の確認 - expect(await gauges[0].balanceOf(accounts[1].address)).to.equal(alice_staked); - expect(await gauges[0].balanceOf(accounts[2].address)).to.equal(bob_staked); - expect(await gauges[0].totalSupply()).to.equal(alice_staked.add(bob_staked)); - + expect(await gauges[0].balanceOf(accounts[1].address)).to.equal( + alice_staked + ); + expect(await gauges[0].balanceOf(accounts[2].address)).to.equal( + bob_staked + ); + expect(await gauges[0].totalSupply()).to.equal( + alice_staked.add(bob_staked) + ); + // ランダムな時間経過をさらにシミュレート - dt = BigNumber.from(Math.floor(Math.random() * 86400 * 19).toString()).add(BigNumber.from("1")); + dt = BigNumber.from( + Math.floor(Math.random() * 86400 * 19).toString() + ).add(BigNumber.from("1")); await ethers.provider.send("evm_increaseTime", [dt.toNumber()]); - - await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); + + await gauges[0] + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); await update_integral(); } }); }); - - - /** - * test_mining_with_votelock の全体的な流れ: - * - * 1. 2週間と5秒の時間を進める。 - * 2. ゲージとコントローラーをセットアップし、適切なレートを設定する。 - * 3. AliceとBobにトークンを転送し、それぞれのアドレスに関連する承認を設定する。 - * 4. Aliceは投票のエスクローにトークンをロックすることで、BOOSTを取得する。 - * 5. AliceとBobはそれぞれ流動性をデポジットする。 - * 6. Aliceの投票ロックの存在とBobの投票ロックの不在を確認する。 - * 7. 時間をさらに進め、両方のユーザーのチェックポイントを更新する。 - * 8. 4週間後、AliceとBobの投票エスクローのバランスが0であることを確認する。 - * 9. Aliceが投票ロックでTokenを獲得したため、彼女はBobの2.5倍のリワードを獲得することを確認する。 - * 10. さらに時間を進め、両方のユーザーのチェックポイントを更新する。 - * 11. 最終的に、AliceとBobが同じ量のリワードを獲得していることを確認する。 - */ + /** + * test_mining_with_votelock の全体的な流れ: + * + * 1. 2週間と5秒の時間を進める。 + * 2. ゲージとコントローラーをセットアップし、適切なレートを設定する。 + * 3. AliceとBobにトークンを転送し、それぞれのアドレスに関連する承認を設定する。 + * 4. Aliceは投票のエスクローにトークンをロックすることで、BOOSTを取得する。 + * 5. AliceとBobはそれぞれ流動性をデポジットする。 + * 6. Aliceの投票ロックの存在とBobの投票ロックの不在を確認する。 + * 7. 時間をさらに進め、両方のユーザーのチェックポイントを更新する。 + * 8. 4週間後、AliceとBobの投票エスクローのバランスが0であることを確認する。 + * 9. Aliceが投票ロックでTokenを獲得したため、彼女はBobの2.5倍のリワードを獲得することを確認する。 + * 10. さらに時間を進め、両方のユーザーのチェックポイントを更新する。 + * 11. 最終的に、AliceとBobが同じ量のリワードを獲得していることを確認する。 + */ describe("Mining with Vote Locking", function () { it("should distribute rewards according to vote lock status", async () => { // 2週間と5秒時間を進める await ethers.provider.send("evm_increaseTime", [ WEEK.mul(BigNumber.from("2")).add(BigNumber.from("5")).toNumber(), ]); - + // ゲージをコントローラーに接続して適切なレートなどを設定する await gaugeController.addType("Liquidity", 0); await gaugeController.changeTypeWeight(1, ten_to_the_18); @@ -225,39 +270,71 @@ describe("LiquidityGauge", function () { await token.transfer(accounts[1].address, ten_to_the_20); await token.transfer(accounts[2].address, ten_to_the_20); - await token.connect(accounts[1]).approve(votingEscrow.address, MAX_UINT256); - await token.connect(accounts[2]).approve(votingEscrow.address, MAX_UINT256); + await token + .connect(accounts[1]) + .approve(votingEscrow.address, MAX_UINT256); + await token + .connect(accounts[2]) + .approve(votingEscrow.address, MAX_UINT256); const creatorBalance = await mockLpToken.balanceOf(accounts[0].address); await mockLpToken.transfer( accounts[1].address, - (creatorBalance).div(BigNumber.from("2")) + creatorBalance.div(BigNumber.from("2")) ); await mockLpToken.transfer( accounts[2].address, - (creatorBalance).div(BigNumber.from("2")) + creatorBalance.div(BigNumber.from("2")) ); - await mockLpToken.connect(accounts[1]).approve(threeGauges[0], MAX_UINT256); - await mockLpToken.connect(accounts[2]).approve(threeGauges[0], MAX_UINT256); - + await mockLpToken + .connect(accounts[1]) + .approve(threeGauges[0], MAX_UINT256); + await mockLpToken + .connect(accounts[2]) + .approve(threeGauges[0], MAX_UINT256); + // Aliceがescrowにデポジットする。AliceはBOOSTを持っていることになる - let t = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + let t = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + + await votingEscrow + .connect(accounts[1]) + .createLock(ten_to_the_20, t.add(WEEK.mul(BigNumber.from("2")))); - await votingEscrow.connect(accounts[1]).createLock(ten_to_the_20, t.add(WEEK.mul(BigNumber.from("2")))); - // AliceとBobが一部の流動性をデポジットする - await gauges[0].connect(accounts[1]).deposit(ten_to_the_21, accounts[1].address, false); - await gauges[0].connect(accounts[2]).deposit(ten_to_the_21, accounts[2].address, false); - let now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_21, accounts[1].address, false); + await gauges[0] + .connect(accounts[2]) + .deposit(ten_to_the_21, accounts[2].address, false); + let now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + // 現在、Aliceは投票ロックを持っているが、Bobは持っていないことを確認する - expect(await votingEscrow["balanceOf(address,uint256)"](accounts[1].address, now)).to.not.equal(zero); - expect(await votingEscrow["balanceOf(address,uint256)"](accounts[2].address, now)).to.equal(zero); - + expect( + await votingEscrow["balanceOf(address,uint256)"]( + accounts[1].address, + now + ) + ).to.not.equal(zero); + expect( + await votingEscrow["balanceOf(address,uint256)"]( + accounts[2].address, + now + ) + ).to.equal(zero); + // 時間を進めてチェックポイントを更新する - now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(WEEK.mul(BigNumber.from("4"))).toNumber()]); - + now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + await ethers.provider.send("evm_setNextBlockTimestamp", [ + now.add(WEEK.mul(BigNumber.from("4"))).toNumber(), + ]); + // チェックポイント更新 await ethers.provider.send("evm_setAutomine", [false]); await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); @@ -266,21 +343,39 @@ describe("LiquidityGauge", function () { await ethers.provider.send("evm_setAutomine", [true]); // 4週間後、balanceOfは0であるべき - now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - expect(await votingEscrow["balanceOf(address,uint256)"](accounts[1].address, now)).to.equal(zero); - expect(await votingEscrow["balanceOf(address,uint256)"](accounts[2].address, now)).to.equal(zero); - + now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + expect( + await votingEscrow["balanceOf(address,uint256)"]( + accounts[1].address, + now + ) + ).to.equal(zero); + expect( + await votingEscrow["balanceOf(address,uint256)"]( + accounts[2].address, + now + ) + ).to.equal(zero); + // AliceはTokenを投票ロックしたので、2.5倍のTokenを獲得 - let rewards_alice = await gauges[0].integrateFraction(accounts[1].address); + let rewards_alice = await gauges[0].integrateFraction( + accounts[1].address + ); let rewards_bob = await gauges[0].integrateFraction(accounts[2].address); - expect(rewards_alice.mul(BigNumber.from("10000000000000000")).div(rewards_bob)).to.equal( - BigNumber.from("25000000000000000") - ); - + expect( + rewards_alice.mul(BigNumber.from("10000000000000000")).div(rewards_bob) + ).to.equal(BigNumber.from("25000000000000000")); + // 時間を進めてチェックポイントを更新: 今は誰もがTokenを投票ロックしていない - now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - await ethers.provider.send("evm_setNextBlockTimestamp", [now.add(WEEK.mul(BigNumber.from("4"))).toNumber()]); - + now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + await ethers.provider.send("evm_setNextBlockTimestamp", [ + now.add(WEEK.mul(BigNumber.from("4"))).toNumber(), + ]); + await ethers.provider.send("evm_setAutomine", [false]); await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); @@ -291,11 +386,11 @@ describe("LiquidityGauge", function () { // 今、AliceはBobと同じ量を獲得した rewards_alice = await gauges[0].integrateFraction(accounts[1].address); rewards_bob = await gauges[0].integrateFraction(accounts[2].address); - console.log(rewards_alice) - console.log(rewards_bob) + console.log(rewards_alice); + console.log(rewards_bob); let d_alice = rewards_alice.sub(old_rewards_alice); let d_bob = rewards_bob.sub(old_rewards_bob); - + expect(d_alice.sub(d_bob)).to.equal(zero); }); }); diff --git a/test/fork/unitary/ERC20CRV/burn.test.ts b/test/fork/unitary/ERC20CRV/burn.test.ts index 363d13a8..1e5ebce1 100644 --- a/test/fork/unitary/ERC20CRV/burn.test.ts +++ b/test/fork/unitary/ERC20CRV/burn.test.ts @@ -27,10 +27,12 @@ describe("ERC20CRV", function () { it("test_burn", async function () { const balance: BigNumber = await token.balanceOf(accounts[0].address); const initialSupply: BigNumber = await token.totalSupply(); - + await token.connect(accounts[0]).burn(31337); - expect(await token.balanceOf(accounts[0].address)).to.equal(balance.sub(31337)); + expect(await token.balanceOf(accounts[0].address)).to.equal( + balance.sub(31337) + ); expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); }); @@ -40,7 +42,9 @@ describe("ERC20CRV", function () { await token.transfer(accounts[1].address, 1000000); await token.connect(accounts[1]).burn(31337); - expect(await token.balanceOf(accounts[1].address)).to.equal(1000000 - 31337); + expect(await token.balanceOf(accounts[1].address)).to.equal( + 1000000 - 31337 + ); expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); }); @@ -56,8 +60,9 @@ describe("ERC20CRV", function () { it("test_overburn", async function () { const initialSupply: BigNumber = await token.totalSupply(); - await expect(token.connect(accounts[0]).burn(initialSupply.add(1))) - .to.be.revertedWith("ERC20: burn amount exceeds balance"); + await expect( + token.connect(accounts[0]).burn(initialSupply.add(1)) + ).to.be.revertedWith("ERC20: burn amount exceeds balance"); }); }); }); diff --git a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts index 6414a3f8..c0c8897b 100644 --- a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts +++ b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts @@ -48,7 +48,9 @@ describe("ERC20CRV", function () { it("test_update_mining_parameters", async function () { const creationTime = await token.startEpochTime(); - const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const newEpoch = creationTime.add(YEAR).sub(now); await ethers.provider.send("evm_increaseTime", [newEpoch.toNumber()]); await token.updateMiningParameters(); @@ -56,26 +58,46 @@ describe("ERC20CRV", function () { it("test_update_mining_parameters_same_epoch", async function () { const creationTime = await token.startEpochTime(); - const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const newEpoch = creationTime.add(YEAR).sub(now); - await ethers.provider.send("evm_increaseTime", [newEpoch.sub(BigNumber.from("3")).toNumber()]); - await expect(token.updateMiningParameters()).to.be.revertedWith("dev: too soon!"); + await ethers.provider.send("evm_increaseTime", [ + newEpoch.sub(BigNumber.from("3")).toNumber(), + ]); + await expect(token.updateMiningParameters()).to.be.revertedWith( + "dev: too soon!" + ); }); it("test_mintable_in_timeframe_end_before_start", async function () { const creationTime = await token.startEpochTime(); - await expect(token.mintableInTimeframe(creationTime.add(1), creationTime)).to.be.revertedWith("dev: start > end"); + await expect( + token.mintableInTimeframe(creationTime.add(1), creationTime) + ).to.be.revertedWith("dev: start > end"); }); it("test_mintable_in_timeframe_multiple_epochs", async function () { const creationTime = await token.startEpochTime(); // Two epochs should not raise - const mintable = BigNumber.from("19").div(BigNumber.from("10")) - await token.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("19").div(BigNumber.from("10")))); + const mintable = BigNumber.from("19").div(BigNumber.from("10")); + await token.mintableInTimeframe( + creationTime, + creationTime + .add(YEAR) + .mul(BigNumber.from("19").div(BigNumber.from("10"))) + ); // Three epochs should raise - await expect(token.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("21").div(BigNumber.from("10"))))).to.be.revertedWith("dev: too far in future"); + await expect( + token.mintableInTimeframe( + creationTime, + creationTime + .add(YEAR) + .mul(BigNumber.from("21").div(BigNumber.from("10"))) + ) + ).to.be.revertedWith("dev: too far in future"); }); it("test_available_supply", async function () { @@ -83,13 +105,13 @@ describe("ERC20CRV", function () { const initialSupply = await token.totalSupply(); const rate = await token.rate(); await ethers.provider.send("evm_increaseTime", [week]); - + const latestBlock = await ethers.provider.getBlock("latest"); const currentTime = BigNumber.from(latestBlock.timestamp); - + const timeElapsed = currentTime.sub(creationTime); const expected = initialSupply.add(timeElapsed.mul(rate)); - + expect(await token.availableSupply()).to.equal(expected); }); }); diff --git a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts index 70877afe..cf488d50 100644 --- a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts +++ b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts @@ -63,14 +63,18 @@ describe("ERC20CRV", function () { }); it("test_available_supply", async function () { - expect(await token.availableSupply()).to.equal(ethers.utils.parseEther("1303030303")); + expect(await token.availableSupply()).to.equal( + ethers.utils.parseEther("1303030303") + ); await ethers.provider.send("evm_increaseTime", [86401]); await ethers.provider.send("evm_mine", []); await token.updateMiningParameters(); - expect(await token.availableSupply()).to.be.gt(ethers.utils.parseEther("1303030303")); + expect(await token.availableSupply()).to.be.gt( + ethers.utils.parseEther("1303030303") + ); }); }); }); diff --git a/test/fork/unitary/ERC20CRV/mint.test.ts b/test/fork/unitary/ERC20CRV/mint.test.ts index 5d2081b0..7d93ae15 100644 --- a/test/fork/unitary/ERC20CRV/mint.test.ts +++ b/test/fork/unitary/ERC20CRV/mint.test.ts @@ -21,7 +21,7 @@ describe("ERC20CRV", function () { accounts = await ethers.getSigners(); const Token = await ethers.getContractFactory("CRV"); token = await Token.deploy(); - await ethers.provider.send('evm_increaseTime', [86401]); + await ethers.provider.send("evm_increaseTime", [86401]); await token.updateMiningParameters(); }); @@ -29,58 +29,71 @@ describe("ERC20CRV", function () { await snapshot.restore(); }); describe("ERC20CRV Mint", function () { - it('test_available_supply', async function () { + it("test_available_supply", async function () { const creationTime = await token.startEpochTime(); const initialSupply = await token.totalSupply(); const rate = await token.rate(); - - await ethers.provider.send('evm_increaseTime', [week]); - await ethers.provider.send('evm_mine', []); - - const currentBlock = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + + await ethers.provider.send("evm_increaseTime", [week]); + await ethers.provider.send("evm_mine", []); + + const currentBlock = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); // const currentBlock = await ethers.provider.getBlock('latest'); - const expected = initialSupply.add(currentBlock.sub(creationTime).mul(rate)); + const expected = initialSupply.add( + currentBlock.sub(creationTime).mul(rate) + ); expect(await token.availableSupply()).to.equal(expected); }); - - it('test_mint', async function () { + + it("test_mint", async function () { await token.setMinter(accounts[0].address); const creationTime = await token.startEpochTime(); const initialSupply = await token.totalSupply(); const rate = await token.rate(); - - await ethers.provider.send('evm_increaseTime', [week]); - - const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + + await ethers.provider.send("evm_increaseTime", [week]); + + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const amount = currentTime.sub(creationTime).mul(rate); await token.mint(accounts[1].address, amount); - + expect(await token.balanceOf(accounts[1].address)).to.equal(amount); expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); }); - - it('test_overmint', async function () { + + it("test_overmint", async function () { await token.setMinter(accounts[0].address); const creationTime = await token.startEpochTime(); const rate = await token.rate(); - - await ethers.provider.send('evm_increaseTime', [week]); - await ethers.provider.send('evm_mine', []); - - const currentTime = BigNumber.from((await ethers.provider.getBlock('latest')).timestamp); + + await ethers.provider.send("evm_increaseTime", [week]); + await ethers.provider.send("evm_mine", []); + + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const amount = currentTime.sub(creationTime).add(2).mul(rate); - await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith("dev: exceeds allowable mint amount"); - + await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( + "dev: exceeds allowable mint amount" + ); }); - - it('test_minter_only', async function () { + + it("test_minter_only", async function () { await token.setMinter(accounts[0].address); - await expect(token.connect(accounts[1]).mint(accounts[1].address, 0)).to.be.revertedWith("dev: minter only"); + await expect( + token.connect(accounts[1]).mint(accounts[1].address, 0) + ).to.be.revertedWith("dev: minter only"); }); - - it('test_zero_address', async function () { + + it("test_zero_address", async function () { await token.setMinter(accounts[0].address); - await expect(token.mint(ZERO_ADDRESS, 0)).to.be.revertedWith("dev: zero address"); + await expect(token.mint(ZERO_ADDRESS, 0)).to.be.revertedWith( + "dev: zero address" + ); }); }); }); diff --git a/test/fork/unitary/ERC20CRV/setters.test.ts b/test/fork/unitary/ERC20CRV/setters.test.ts index 9296115f..2c5c0eb2 100644 --- a/test/fork/unitary/ERC20CRV/setters.test.ts +++ b/test/fork/unitary/ERC20CRV/setters.test.ts @@ -25,11 +25,15 @@ describe("ERC20CRV", function () { describe("ERC20CRV Setters", function () { it("should revert when non-admin tries to set minter", async function () { - await expect(token.connect(accounts[1]).setMinter(accounts[2].address)).to.be.revertedWith("dev: admin only"); + await expect( + token.connect(accounts[1]).setMinter(accounts[2].address) + ).to.be.revertedWith("dev: admin only"); }); it("should revert when non-admin tries to set admin", async function () { - await expect(token.connect(accounts[1]).setAdmin(accounts[2].address)).to.be.revertedWith("dev: admin only"); + await expect( + token.connect(accounts[1]).setAdmin(accounts[2].address) + ).to.be.revertedWith("dev: admin only"); }); it("should allow admin to set minter", async function () { diff --git a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts index 9088690e..ef2dfbc6 100644 --- a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts +++ b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts @@ -12,10 +12,10 @@ import Constants from "../../Constants"; describe("LiquidityGauge checkpoint", function () { let accounts: SignerWithAddress; let gauges: Contract[]; - + let snapshot: SnapshotRestorer; const year = Constants.year; - + beforeEach(async function () { snapshot = await takeSnapshot(); accounts = await ethers.getSigners(); @@ -33,10 +33,10 @@ describe("LiquidityGauge checkpoint", function () { it("test_user_checkpoint_new_period", async function () { await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); - + // Increase the time on the blockchain - await ethers.provider.send('evm_increaseTime', [(year * 1.1)]); - await ethers.provider.send('evm_mine'); // this one will actually mine a new block + await ethers.provider.send("evm_increaseTime", [year * 1.1]); + await ethers.provider.send("evm_mine"); // this one will actually mine a new block await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); }); diff --git a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts index d141b353..82b45e6a 100644 --- a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts +++ b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts @@ -22,8 +22,7 @@ describe("LiquidityGauge depositWithdraw", function () { beforeEach(async () => { snapshot = await takeSnapshot(); accounts = await ethers.getSigners(); - ({ mockLpToken, threeGauges, gauges } = - await deployContracts()); + ({ mockLpToken, threeGauges, gauges } = await deployContracts()); await mockLpToken.approve(threeGauges[0], ten_to_the_21.mul("2")); }); @@ -37,23 +36,33 @@ describe("LiquidityGauge depositWithdraw", function () { await gauges[0].deposit(100000, accounts[0].address, false); expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(depositAmount); - expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance.sub(depositAmount)); + expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal( + balance.sub(depositAmount) + ); expect(await gauges[0].totalSupply()).to.equal(depositAmount); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(depositAmount); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal( + depositAmount + ); }); it("should handle zero deposit", async function () { const balance = await mockLpToken.balanceOf(accounts[0].address); await gauges[0].deposit(0, accounts[0].address, false); - expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(BigNumber.from(0)); + expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal( + BigNumber.from(0) + ); expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance); expect(await gauges[0].totalSupply()).to.equal(BigNumber.from(0)); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(BigNumber.from(0)); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal( + BigNumber.from(0) + ); }); it("should revert on deposit with insufficient balance", async function () { - await expect(gauges[0].connect(accounts[1]).deposit(100000, accounts[0].address, false)).to.be.reverted; + await expect( + gauges[0].connect(accounts[1]).deposit(100000, accounts[0].address, false) + ).to.be.reverted; }); it("should withdraw tokens", async function () { @@ -77,9 +86,13 @@ describe("LiquidityGauge depositWithdraw", function () { await gauges[0].withdraw(0, false); expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(depositAmount); - expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance.sub(depositAmount)); + expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal( + balance.sub(depositAmount) + ); expect(await gauges[0].totalSupply()).to.equal(depositAmount); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(depositAmount); + expect(await gauges[0].balanceOf(accounts[0].address)).to.equal( + depositAmount + ); }); it("should withdraw tokens after a new epoch", async function () { @@ -98,5 +111,4 @@ describe("LiquidityGauge depositWithdraw", function () { expect(await gauges[0].totalSupply()).to.equal(0); expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(0); }); - - }); +}); diff --git a/test/fork/unitary/LiquidityGauge/kick.test.ts b/test/fork/unitary/LiquidityGauge/kick.test.ts index 22bb10b8..b3debae7 100644 --- a/test/fork/unitary/LiquidityGauge/kick.test.ts +++ b/test/fork/unitary/LiquidityGauge/kick.test.ts @@ -15,7 +15,7 @@ describe("LiquidityGauge kick", function () { let votingEscrow: Contract; let mockLpToken: Contract; let gauges: Contract[]; - + let snapshot: SnapshotRestorer; const MAX_UINT256 = Constants.MAX_UINT256; @@ -26,8 +26,7 @@ describe("LiquidityGauge kick", function () { beforeEach(async () => { snapshot = await takeSnapshot(); accounts = await ethers.getSigners(); - ({ token, votingEscrow, mockLpToken, gauges } = - await deployContracts()); + ({ token, votingEscrow, mockLpToken, gauges } = await deployContracts()); await token.transfer(accounts[1].address, LOCK_AMOUNT); await mockLpToken.transfer(accounts[1].address, DEPOSIT_AMOUNT); }); @@ -43,21 +42,34 @@ describe("LiquidityGauge kick", function () { // Alice approves tokens to voting escrow and creates a lock await token.connect(accounts[1]).approve(votingEscrow.address, MAX_UINT256); - await votingEscrow.connect(accounts[1]).createLock(LOCK_AMOUNT, (await ethers.provider.getBlock('latest')).timestamp + 4 * week); + await votingEscrow + .connect(accounts[1]) + .createLock( + LOCK_AMOUNT, + (await ethers.provider.getBlock("latest")).timestamp + 4 * week + ); // Alice approves LP tokens to Gauge and deposits - await mockLpToken.connect(accounts[1]).approve(gauges[0].address, MAX_UINT256); - await gauges[0].connect(accounts[1]).deposit(DEPOSIT_AMOUNT, accounts[1].address, false); + await mockLpToken + .connect(accounts[1]) + .approve(gauges[0].address, MAX_UINT256); + await gauges[0] + .connect(accounts[1]) + .deposit(DEPOSIT_AMOUNT, accounts[1].address, false); // Check working balance of Alice in Gauge - expect(await gauges[0].workingBalances(accounts[1].address)).to.equal(DEPOSIT_AMOUNT); + expect(await gauges[0].workingBalances(accounts[1].address)).to.equal( + DEPOSIT_AMOUNT + ); // Forward time by 1 week await ethers.provider.send("evm_increaseTime", [week]); await ethers.provider.send("evm_mine"); // Bob tries to kick Alice but should fail because it's not allowed yet - await expect(gauges[0].connect(accounts[1]).kick(accounts[1].address)).to.be.revertedWith("Not allowed"); + await expect( + gauges[0].connect(accounts[1]).kick(accounts[1].address) + ).to.be.revertedWith("Not allowed"); // Forward time by 4 weeks await ethers.provider.send("evm_increaseTime", [4 * week]); @@ -67,9 +79,13 @@ describe("LiquidityGauge kick", function () { await gauges[0].connect(accounts[1]).kick(accounts[1].address); // Check the working balance of Alice after kick - expect(await gauges[0].workingBalances(accounts[1].address)).to.equal(LOCK_AMOUNT.mul(4)); + expect(await gauges[0].workingBalances(accounts[1].address)).to.equal( + LOCK_AMOUNT.mul(4) + ); // Trying to kick again should fail as it's not needed - await expect(gauges[0].connect(accounts[1]).kick(accounts[1].address)).to.be.revertedWith("Not needed"); + await expect( + gauges[0].connect(accounts[1]).kick(accounts[1].address) + ).to.be.revertedWith("Not needed"); }); }); diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts index 52e2dd3c..6c489e67 100644 --- a/test/fork/unitary/Minter/Minter.test.ts +++ b/test/fork/unitary/Minter/Minter.test.ts @@ -23,7 +23,7 @@ describe("Minter", function () { const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; const GAUGE_WEIGHTS = Constants.GAUGE_WEIGHTS; const GAUGE_TYPES = [0, 0, 1]; - + const ten_to_the_18 = Constants.ten_to_the_18; const ten_to_the_17 = Constants.ten_to_the_17; const ZERO_ADDRESS = Constants.ZERO_ADDRESS; @@ -84,23 +84,14 @@ describe("Minter", function () { await ethers.provider.send("evm_increaseTime", [month]); - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); //gauge_address, msg.sender, mint - let expected = await gauges[0].integrateFraction( - accounts[1].address - ); + await minter.connect(accounts[1]).mint(threeGauges[0]); //gauge_address, msg.sender, mint + let expected = await gauges[0].integrateFraction(accounts[1].address); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await token.balanceOf(accounts[1].address)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal(expected); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( expected ); - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal(expected); }); // Test minting immediately after setup @@ -118,24 +109,16 @@ describe("Minter", function () { ]); //mint - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal("0"); - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( + "0" + ); + await minter.connect(accounts[1]).mint(threeGauges[0]); //check let balance = await token.balanceOf(accounts[1].address); - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal(balance); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( + balance + ); }); // Test multiple mint operations on the same gauge @@ -144,27 +127,18 @@ describe("Minter", function () { .connect(accounts[1]) .deposit(ten_to_the_18, accounts[1].address, false); await ethers.provider.send("evm_increaseTime", [month]); - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); + await minter.connect(accounts[1]).mint(threeGauges[0]); let balance = await token.balanceOf(accounts[1].address); await ethers.provider.send("evm_increaseTime", [month]); - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); - let expected = await gauges[0].integrateFraction( - accounts[1].address - ); + await minter.connect(accounts[1]).mint(threeGauges[0]); + let expected = await gauges[0].integrateFraction(accounts[1].address); let final_balance = await token.balanceOf(accounts[1].address); expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success expect(final_balance).to.equal(expected); //2nd mint works fine - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal(expected); //tracks fine + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( + expected + ); //tracks fine }); // Test minting across multiple gauges @@ -184,9 +158,7 @@ describe("Minter", function () { //mint for (let i = 0; i < 3; i++) { - await minter - .connect(accounts[1]) - .mint(threeGauges[i]); + await minter.connect(accounts[1]).mint(threeGauges[i]); } //check @@ -194,19 +166,14 @@ describe("Minter", function () { for (let i = 0; i < 3; i++) { let gauge = gauges[i]; - let minted = await minter.minted( - accounts[1].address, - gauge.address - ); + let minted = await minter.minted(accounts[1].address, gauge.address); expect(minted).to.equal( await gauge.integrateFraction(accounts[1].address) ); total_minted = total_minted.add(minted); } - expect(await token.balanceOf(accounts[1].address)).to.equal( - total_minted - ); + expect(await token.balanceOf(accounts[1].address)).to.equal(total_minted); }); // Test minting after withdrawing @@ -215,21 +182,13 @@ describe("Minter", function () { .connect(accounts[1]) .deposit(ten_to_the_18, accounts[1].address, false); - await ethers.provider.send("evm_increaseTime", [ - week * 2, - ]); + await ethers.provider.send("evm_increaseTime", [week * 2]); - await gauges[0] - .connect(accounts[1]) - .withdraw(ten_to_the_18, false); - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); + await gauges[0].connect(accounts[1]).withdraw(ten_to_the_18, false); + await minter.connect(accounts[1]).mint(threeGauges[0]); expect( - (await token.balanceOf(accounts[1].address)).gt( - BigNumber.from("0") - ) + (await token.balanceOf(accounts[1].address)).gt(BigNumber.from("0")) ).to.equal(true); }); @@ -240,39 +199,24 @@ describe("Minter", function () { .deposit(ten_to_the_18, accounts[1].address, false); await ethers.provider.send("evm_increaseTime", [10]); - await gauges[0] - .connect(accounts[1]) - .withdraw(ten_to_the_18, false); - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); + await gauges[0].connect(accounts[1]).withdraw(ten_to_the_18, false); + await minter.connect(accounts[1]).mint(threeGauges[0]); let balance = await token.balanceOf(accounts[1].address); await ethers.provider.send("evm_increaseTime", [10]); - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); + await minter.connect(accounts[1]).mint(threeGauges[0]); - expect(await token.balanceOf(accounts[1].address)).to.equal( - balance - ); + expect(await token.balanceOf(accounts[1].address)).to.equal(balance); }); // Test mint without any deposit it("test_no_deposit", async () => { - await minter - .connect(accounts[1]) - .mint(threeGauges[0]); - expect(await token.balanceOf(accounts[1].address)).to.equal( + await minter.connect(accounts[1]).mint(threeGauges[0]); + expect(await token.balanceOf(accounts[1].address)).to.equal(zero); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( zero ); - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal(zero); }); // Test minting with the wrong gauge @@ -282,26 +226,16 @@ describe("Minter", function () { .deposit(ten_to_the_18, accounts[1].address, false); await ethers.provider.send("evm_increaseTime", [month]); - await minter - .connect(accounts[1]) - .mint(threeGauges[1]); + await minter.connect(accounts[1]).mint(threeGauges[1]); //check - expect(await token.balanceOf(accounts[1].address)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal(zero); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( + zero + ); + expect(await minter.minted(accounts[1].address, threeGauges[1])).to.equal( zero ); - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal(zero); - expect( - await minter.minted( - accounts[1].address, - threeGauges[1] - ) - ).to.equal(zero); }); // Test minting with an invalid gauge address @@ -313,16 +247,24 @@ describe("Minter", function () { // Test minting before inflation begins it("test_mint_before_inflation_begins", async function () { - await gauges[0].connect(accounts[1]).deposit(ten_to_the_18, accounts[1].address, false); + await gauges[0] + .connect(accounts[1]) + .deposit(ten_to_the_18, accounts[1].address, false); const startEpochTime = await token.startEpochTime(); - const currentTime = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); const timeToSleep = startEpochTime.sub(currentTime).sub(5); await ethers.provider.send("evm_increaseTime", [timeToSleep.toNumber()]); - + await minter.connect(accounts[1]).mint(threeGauges[0]); - - expect(await token.balanceOf(accounts[1].address)).to.equal(BigNumber.from(0)); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal(BigNumber.from(0)); + + expect(await token.balanceOf(accounts[1].address)).to.equal( + BigNumber.from(0) + ); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( + BigNumber.from(0) + ); }); // Test mintMany function with multiple gauges @@ -357,41 +299,26 @@ describe("Minter", function () { for (let i = 0; i < 3; i++) { let gauge = gauges[i]; - let minted = await minter.minted( - accounts[1].address, - gauge.address - ); + let minted = await minter.minted(accounts[1].address, gauge.address); expect(minted).to.equal( await gauge.integrateFraction(accounts[1].address) ); total_minted = total_minted.add(minted); } - expect(await token.balanceOf(accounts[1].address)).to.equal( - total_minted - ); + expect(await token.balanceOf(accounts[1].address)).to.equal(total_minted); }); // Test toggling of the mint approval function it("test_toggleApproveMint_function", async () => { - await minter - .connect(accounts[1]) - .toggleApproveMint(accounts[2].address); + await minter.connect(accounts[1]).toggleApproveMint(accounts[2].address); expect( - await minter.allowedToMintFor( - accounts[2].address, - accounts[1].address - ) + await minter.allowedToMintFor(accounts[2].address, accounts[1].address) ).to.equal(true); - await minter - .connect(accounts[1]) - .toggleApproveMint(accounts[2].address); + await minter.connect(accounts[1]).toggleApproveMint(accounts[2].address); expect( - await minter.allowedToMintFor( - accounts[2].address, - accounts[1].address - ) + await minter.allowedToMintFor(accounts[2].address, accounts[1].address) ).to.equal(false); }); @@ -403,33 +330,21 @@ describe("Minter", function () { await ethers.provider.send("evm_increaseTime", [month]); - await minter - .connect(accounts[1]) - .toggleApproveMint(accounts[2].address); + await minter.connect(accounts[1]).toggleApproveMint(accounts[2].address); expect( - await minter.allowedToMintFor( - accounts[2].address, - accounts[1].address - ) + await minter.allowedToMintFor(accounts[2].address, accounts[1].address) ).to.equal(true); await minter .connect(accounts[2]) .mintFor(threeGauges[0], accounts[1].address); - let expected = await gauges[0].integrateFraction( - accounts[1].address - ); + let expected = await gauges[0].integrateFraction(accounts[1].address); expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await token.balanceOf(accounts[1].address)).to.equal( + expect(await token.balanceOf(accounts[1].address)).to.equal(expected); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( expected ); - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal(expected); }); // Test mintFor function when not approved @@ -441,10 +356,7 @@ describe("Minter", function () { await ethers.provider.send("evm_increaseTime", [month]); expect( - await minter.allowedToMintFor( - accounts[2].address, - accounts[1].address - ) + await minter.allowedToMintFor(accounts[2].address, accounts[1].address) ).to.equal(false); await minter @@ -452,12 +364,9 @@ describe("Minter", function () { .mintFor(threeGauges[0], accounts[1].address); expect(await token.balanceOf(accounts[1].address)).to.equal(0); - expect( - await minter.minted( - accounts[1].address, - threeGauges[0] - ) - ).to.equal(0); + expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( + 0 + ); }); }); -}); \ No newline at end of file +}); From 9679c4be3a1566e223d30bdd62ebe7ac04b0f0be Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 13 Nov 2023 16:01:40 +0900 Subject: [PATCH 046/412] ERC20CRV variables changed to CamelCase --- contracts/curveFork/CRV.sol | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol index 0d348654..c2484650 100644 --- a/contracts/curveFork/CRV.sol +++ b/contracts/curveFork/CRV.sol @@ -41,8 +41,8 @@ contract CRV is ERC20 { uint256 startEpochSupply; constructor() ERC20("Curve", "CRV") { - uint256 init_supply = INITIAL_SUPPLY * 10 ** decimals(); - _mint(msg.sender, init_supply); + uint256 initSupply = INITIAL_SUPPLY * 10 ** decimals(); + _mint(msg.sender, initSupply); admin = msg.sender; @@ -52,7 +52,7 @@ contract CRV is ERC20 { RATE_REDUCTION_TIME; miningEpoch = -1; rate = 0; - startEpochSupply = init_supply; + startEpochSupply = initSupply; } // @dev Update mining rate and supply at the start of the epoch @@ -133,47 +133,47 @@ contract CRV is ERC20 { ) external view returns (uint256) { require(start <= end, "dev: start > end"); // dev: start > end uint256 to_mint = 0; - uint256 current_epoch_time = startEpochTime; - uint256 current_rate = rate; + uint256 currentEpochTime = startEpochTime; + uint256 currentRate = rate; // Special case if(end is in future (not yet minted) epoch - if (end > current_epoch_time + RATE_REDUCTION_TIME) { - current_epoch_time += RATE_REDUCTION_TIME; - current_rate = - (current_rate * RATE_DENOMINATOR) / + if (end > currentEpochTime + RATE_REDUCTION_TIME) { + currentEpochTime += RATE_REDUCTION_TIME; + currentRate = + (currentRate * RATE_DENOMINATOR) / RATE_REDUCTION_COEFFICIENT; } require( - end <= current_epoch_time + RATE_REDUCTION_TIME, + end <= currentEpochTime + RATE_REDUCTION_TIME, "dev: too far in future" ); // dev: too far in future // Curve will not work in 1000 years. Darn! for (uint i; i < 999; ) { - if (end >= current_epoch_time) { - uint256 current_end = end; - if (current_end > current_epoch_time + RATE_REDUCTION_TIME) { - current_end = current_epoch_time + RATE_REDUCTION_TIME; + if (end >= currentEpochTime) { + uint256 currentEnd = end; + if (currentEnd > currentEpochTime + RATE_REDUCTION_TIME) { + currentEnd = currentEpochTime + RATE_REDUCTION_TIME; } - uint256 current_start = start; - if (current_start >= current_epoch_time + RATE_REDUCTION_TIME) { + uint256 currentStart = start; + if (currentStart >= currentEpochTime + RATE_REDUCTION_TIME) { break; // We should never get here but what if... - } else if (current_start < current_epoch_time) { - current_start = current_epoch_time; + } else if (currentStart < currentEpochTime) { + currentStart = currentEpochTime; } - to_mint += current_rate * (current_end - current_start); - if (start >= current_epoch_time) { + to_mint += currentRate * (currentEnd - currentStart); + if (start >= currentEpochTime) { break; } } - current_epoch_time -= RATE_REDUCTION_TIME; - current_rate = - (current_rate * RATE_REDUCTION_COEFFICIENT) / + currentEpochTime -= RATE_REDUCTION_TIME; + currentRate = + (currentRate * RATE_REDUCTION_COEFFICIENT) / RATE_DENOMINATOR; // double-division with rounding made rate a bit less => good - require(current_rate <= INITIAL_RATE, "This should never happen"); // This should never happen + require(currentRate <= INITIAL_RATE, "This should never happen"); // This should never happen unchecked { i++; From bf63cacafaec20277ff7a72229083d62a425de79 Mon Sep 17 00:00:00 2001 From: takadr Date: Wed, 15 Nov 2023 10:13:39 +0900 Subject: [PATCH 047/412] Fixed ts warning --- test/fork/integration/ERC20CRV/mintIntegration.test.ts | 2 +- .../fork/integration/ERC20CRV/mintableInTimeframe.test.ts | 4 ++-- test/fork/integration/FeeDistributor/checkpointTs.test.ts | 2 +- test/fork/integration/GaugeController/voteWeight.test.ts | 2 +- .../integration/LiquidityGauge/liquidityGauge.test.ts | 2 +- test/fork/integration/VotingEscrow/votingEscrow.test.ts | 2 +- .../VotingEscrow/zeroBalanceAtUnlockTime.test.ts | 2 +- test/fork/unitary/ERC20CRV/burn.test.ts | 2 +- test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts | 2 +- test/fork/unitary/ERC20CRV/inflationDelay.test.ts | 2 +- test/fork/unitary/ERC20CRV/mint.test.ts | 2 +- test/fork/unitary/ERC20CRV/setters.test.ts | 2 +- test/fork/unitary/FeeDistributor/checkpoints.test.ts | 6 ++++-- test/fork/unitary/FeeDistributor/claimMany.test.ts | 6 ++++-- test/fork/unitary/FeeDistributor/feeDistribution.test.ts | 6 ++++-- test/fork/unitary/FeeDistributor/killFeeDistro.test.ts | 6 ++++-- .../unitary/GaugeController/gaugecontrollerAdmin.test.ts | 4 ++-- test/fork/unitary/GaugeController/gaugesWeights.test.ts | 4 ++-- test/fork/unitary/GaugeController/timestamps.test.ts | 4 ++-- test/fork/unitary/GaugeController/totalWeight.test.ts | 6 +++--- test/fork/unitary/GaugeController/vote.test.ts | 2 +- .../unitary/GaugeController/voteWeightUnitary.test.ts | 8 ++++---- test/fork/unitary/LiquidityGauge/checkpoint.test.ts | 2 +- test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts | 2 +- test/fork/unitary/LiquidityGauge/kick.test.ts | 2 +- test/fork/unitary/Minter/Minter.test.ts | 4 ++-- test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts | 2 +- tsconfig.json | 3 ++- 28 files changed, 51 insertions(+), 42 deletions(-) diff --git a/test/fork/integration/ERC20CRV/mintIntegration.test.ts b/test/fork/integration/ERC20CRV/mintIntegration.test.ts index 2351e7c3..8fd54e97 100644 --- a/test/fork/integration/ERC20CRV/mintIntegration.test.ts +++ b/test/fork/integration/ERC20CRV/mintIntegration.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { BigNumber } from "ethers"; // Assuming you have a helper function to increase blockchain time diff --git a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts index 4742c98d..f392031d 100644 --- a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts +++ b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts @@ -5,7 +5,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import Constants from "../../Constants"; async function increaseTime(duration: BigNumber) { @@ -91,7 +91,7 @@ describe("ERC20CRV", function () { if (t1 - t0 >= year) { await token.updateMiningParameters(); } - t1 = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + t1 = (await ethers.provider.getBlock("latest")).timestamp; const availableSupply = await token.availableSupply(); const mintable = await token.mintableInTimeframe(t0, t1); diff --git a/test/fork/integration/FeeDistributor/checkpointTs.test.ts b/test/fork/integration/FeeDistributor/checkpointTs.test.ts index 721ce3cc..0507b80d 100644 --- a/test/fork/integration/FeeDistributor/checkpointTs.test.ts +++ b/test/fork/integration/FeeDistributor/checkpointTs.test.ts @@ -6,7 +6,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; const DAY = 86400; const WEEK = DAY * 7; diff --git a/test/fork/integration/GaugeController/voteWeight.test.ts b/test/fork/integration/GaugeController/voteWeight.test.ts index c3dcda86..ff527572 100644 --- a/test/fork/integration/GaugeController/voteWeight.test.ts +++ b/test/fork/integration/GaugeController/voteWeight.test.ts @@ -6,7 +6,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; const WEEK = 86400 * 7; const MAX_EXAMPLES = 10; diff --git a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts index 2080b380..fafae31b 100644 --- a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts +++ b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts @@ -5,7 +5,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../Helper"; +import { deployContracts } from "../../helper"; import Constants from "../../Constants"; describe("LiquidityGauge", function () { diff --git a/test/fork/integration/VotingEscrow/votingEscrow.test.ts b/test/fork/integration/VotingEscrow/votingEscrow.test.ts index 96b2f55b..e47f0740 100644 --- a/test/fork/integration/VotingEscrow/votingEscrow.test.ts +++ b/test/fork/integration/VotingEscrow/votingEscrow.test.ts @@ -6,7 +6,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; type Stage = { blockNumber: number; diff --git a/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts b/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts index f2a77509..80938dab 100644 --- a/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts +++ b/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts @@ -6,7 +6,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; const WEEK = 86400 * 7; diff --git a/test/fork/unitary/ERC20CRV/burn.test.ts b/test/fork/unitary/ERC20CRV/burn.test.ts index 1e5ebce1..333e1fef 100644 --- a/test/fork/unitary/ERC20CRV/burn.test.ts +++ b/test/fork/unitary/ERC20CRV/burn.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { deployContracts } from "../../Helper"; describe("ERC20CRV", function () { diff --git a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts index c0c8897b..925fef6a 100644 --- a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts +++ b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; diff --git a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts index cf488d50..f78ad71b 100644 --- a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts +++ b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; diff --git a/test/fork/unitary/ERC20CRV/mint.test.ts b/test/fork/unitary/ERC20CRV/mint.test.ts index 7d93ae15..e9027d20 100644 --- a/test/fork/unitary/ERC20CRV/mint.test.ts +++ b/test/fork/unitary/ERC20CRV/mint.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import Constants from "../../Constants"; describe("ERC20CRV", function () { diff --git a/test/fork/unitary/ERC20CRV/setters.test.ts b/test/fork/unitary/ERC20CRV/setters.test.ts index 2c5c0eb2..fbe98e49 100644 --- a/test/fork/unitary/ERC20CRV/setters.test.ts +++ b/test/fork/unitary/ERC20CRV/setters.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; describe("ERC20CRV", function () { let accounts: SignerWithAddress[]; diff --git a/test/fork/unitary/FeeDistributor/checkpoints.test.ts b/test/fork/unitary/FeeDistributor/checkpoints.test.ts index 4f56b82a..d499b0f7 100644 --- a/test/fork/unitary/FeeDistributor/checkpoints.test.ts +++ b/test/fork/unitary/FeeDistributor/checkpoints.test.ts @@ -6,14 +6,16 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; const DAY = 86400; const WEEK = DAY * 7; const YEAR = DAY * 365; describe("FeeDistributor", () => { - let alice, bob, charlie: SignerWithAddress; + let alice: SignerWithAddress, + bob: SignerWithAddress, + charlie: SignerWithAddress; let distributor: Contract; let votingEscrow: Contract; diff --git a/test/fork/unitary/FeeDistributor/claimMany.test.ts b/test/fork/unitary/FeeDistributor/claimMany.test.ts index e131ad11..c831cc16 100644 --- a/test/fork/unitary/FeeDistributor/claimMany.test.ts +++ b/test/fork/unitary/FeeDistributor/claimMany.test.ts @@ -6,14 +6,16 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; const DAY = 86400; const WEEK = DAY * 7; describe("FeeDistributor", () => { let snapshot: SnapshotRestorer; - let alice, bob, charlie: SignerWithAddress; + let alice: SignerWithAddress, + bob: SignerWithAddress, + charlie: SignerWithAddress; let distributor: Contract; let votingEscrow: Contract; diff --git a/test/fork/unitary/FeeDistributor/feeDistribution.test.ts b/test/fork/unitary/FeeDistributor/feeDistribution.test.ts index 5388f881..e00878ad 100644 --- a/test/fork/unitary/FeeDistributor/feeDistribution.test.ts +++ b/test/fork/unitary/FeeDistributor/feeDistribution.test.ts @@ -6,13 +6,15 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; const DAY = 86400; const WEEK = DAY * 7; describe("FeeDistributor", () => { - let alice, bob, charlie: SignerWithAddress; + let alice: SignerWithAddress, + bob: SignerWithAddress, + charlie: SignerWithAddress; let distributor: Contract; let votingEscrow: Contract; let token: Contract; diff --git a/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts b/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts index 8e374f1c..e5ce78c2 100644 --- a/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts +++ b/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts @@ -6,10 +6,12 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; describe("FeeDistributor", () => { - let alice, bob, charlie: SignerWithAddress; + let alice: SignerWithAddress, + bob: SignerWithAddress, + charlie: SignerWithAddress; let distributor: Contract; let votingEscrow: Contract; diff --git a/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts b/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts index 68b715fc..4e8f63f5 100644 --- a/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts +++ b/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts @@ -5,8 +5,8 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { deployContracts } from "../../Helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { deployContracts } from "../../helper"; import Constants from "../../Constants"; describe("GaugeController", function () { diff --git a/test/fork/unitary/GaugeController/gaugesWeights.test.ts b/test/fork/unitary/GaugeController/gaugesWeights.test.ts index 7b416888..78fb97e3 100644 --- a/test/fork/unitary/GaugeController/gaugesWeights.test.ts +++ b/test/fork/unitary/GaugeController/gaugesWeights.test.ts @@ -5,8 +5,8 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { deployContracts } from "../../Helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { deployContracts } from "../../helper"; import Constants from "../../Constants"; describe("GaugeController", function () { diff --git a/test/fork/unitary/GaugeController/timestamps.test.ts b/test/fork/unitary/GaugeController/timestamps.test.ts index b5c1d287..d57f4206 100644 --- a/test/fork/unitary/GaugeController/timestamps.test.ts +++ b/test/fork/unitary/GaugeController/timestamps.test.ts @@ -5,8 +5,8 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { deployContracts } from "../../Helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { deployContracts } from "../../helper"; import Constants from "../../Constants"; describe("GaugeController", function () { diff --git a/test/fork/unitary/GaugeController/totalWeight.test.ts b/test/fork/unitary/GaugeController/totalWeight.test.ts index 96f685b2..0b55cbb9 100644 --- a/test/fork/unitary/GaugeController/totalWeight.test.ts +++ b/test/fork/unitary/GaugeController/totalWeight.test.ts @@ -5,14 +5,14 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { deployContracts } from "../../Helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { deployContracts } from "../../helper"; import Constants from "../../Constants"; describe("GaugeController", function () { let accounts: SignerWithAddress[]; let gaugeController: Contract; - let threeGauges: Contract[]; + let threeGauges: string[]; let snapshot: SnapshotRestorer; const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; diff --git a/test/fork/unitary/GaugeController/vote.test.ts b/test/fork/unitary/GaugeController/vote.test.ts index e710da96..c6d004e8 100644 --- a/test/fork/unitary/GaugeController/vote.test.ts +++ b/test/fork/unitary/GaugeController/vote.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; diff --git a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts index 2ac0fd81..af433400 100644 --- a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts +++ b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts @@ -5,8 +5,8 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { deployContracts } from "../../Helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { deployContracts } from "../../helper"; import Constants from "../../Constants"; describe("GaugeController", function () { @@ -89,7 +89,7 @@ describe("GaugeController", function () { await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); await ethers.provider.send("evm_increaseTime", [day * 10]); - await ethers.provider.send("evm_mine"); + await ethers.provider.send("evm_mine", []); await gaugeController.checkpointGauge(threeGauges[0]); expect( @@ -99,7 +99,7 @@ describe("GaugeController", function () { await gaugeController.voteForGaugeWeights(threeGauges[0], 0); await ethers.provider.send("evm_increaseTime", [day * 7]); - await ethers.provider.send("evm_mine"); + await ethers.provider.send("evm_mine", []); await gaugeController.checkpointGauge(threeGauges[0]); expect( diff --git a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts index ef2dfbc6..a7756157 100644 --- a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts +++ b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts @@ -5,7 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; diff --git a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts index 82b45e6a..788607d2 100644 --- a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts +++ b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts @@ -5,7 +5,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; diff --git a/test/fork/unitary/LiquidityGauge/kick.test.ts b/test/fork/unitary/LiquidityGauge/kick.test.ts index b3debae7..955d391a 100644 --- a/test/fork/unitary/LiquidityGauge/kick.test.ts +++ b/test/fork/unitary/LiquidityGauge/kick.test.ts @@ -5,7 +5,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { deployContracts } from "../../Helper"; import Constants from "../../Constants"; diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts index 6c489e67..aee0ab1d 100644 --- a/test/fork/unitary/Minter/Minter.test.ts +++ b/test/fork/unitary/Minter/Minter.test.ts @@ -5,8 +5,8 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { deployContracts } from "../../Helper"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { deployContracts } from "../../helper"; import Constants from "../../Constants"; describe("Minter", function () { diff --git a/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts b/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts index f0cfbab0..a9b3b039 100644 --- a/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts +++ b/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts @@ -5,7 +5,7 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; describe("VotingEscrow", () => { let votingEscrow: Contract; diff --git a/tsconfig.json b/tsconfig.json index d8907572..e3b073ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "src/**/*.ts", "deploy/**/*.ts", "./node_modules/hardhat/hardhat-ethers/src/type-extensions.d.ts", - "./typechain/**/*.ts" + "./typechain/**/*.ts", + "./test" ] } From 2cdf64b0bdd2833af8160a1c209917d27bbf7e1a Mon Sep 17 00:00:00 2001 From: takadr Date: Wed, 15 Nov 2023 10:15:07 +0900 Subject: [PATCH 048/412] Replaced "changeTokenBalance" assertion --- test/fork/unitary/FeeDistributor/claimMany.test.ts | 11 +++++------ .../unitary/FeeDistributor/feeDistribution.test.ts | 8 +++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/test/fork/unitary/FeeDistributor/claimMany.test.ts b/test/fork/unitary/FeeDistributor/claimMany.test.ts index c831cc16..0f8616ca 100644 --- a/test/fork/unitary/FeeDistributor/claimMany.test.ts +++ b/test/fork/unitary/FeeDistributor/claimMany.test.ts @@ -147,12 +147,11 @@ describe("FeeDistributor", () => { const expected = await distributor.connect(alice).callStatic["claim()"](); expect(expected).to.above(0); - expect( - await distributor - .connect(alice) - .claimMany(Array(20).fill(alice.address)) - .toString() - ).to.changeTokenBalance(coinA, alice, expected); + + const balanceBefore = await coinA.balanceOf(alice.address); + await distributor.connect(alice).claimMany(Array(20).fill(alice.address)); + const balanceAfter = await coinA.balanceOf(alice.address); + expect(balanceAfter.sub(balanceBefore)).to.be.eq(expected); }); }); }); diff --git a/test/fork/unitary/FeeDistributor/feeDistribution.test.ts b/test/fork/unitary/FeeDistributor/feeDistribution.test.ts index e00878ad..5499e9fc 100644 --- a/test/fork/unitary/FeeDistributor/feeDistribution.test.ts +++ b/test/fork/unitary/FeeDistributor/feeDistribution.test.ts @@ -81,9 +81,11 @@ describe("FeeDistributor", () => { await time.increase(2 * WEEK); await distributor.connect(alice)["claim()"](); - expect( - await distributor.connect(alice)["claim()"]() - ).to.changeTokenBalance(coinA, alice, 0); + + const balanceBefore = await coinA.balanceOf(alice.address); + await distributor.connect(alice)["claim()"](); + const balanceAfter = await coinA.balanceOf(alice.address); + expect(balanceAfter.sub(balanceBefore)).to.be.eq(0); }); it("test_deposited_during", async function () { From 741ece960b22469d3e8b11f3873dc0abb6c2ad1a Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 15 Nov 2023 10:27:02 +0900 Subject: [PATCH 049/412] Hardhat config restored --- hardhat.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hardhat.config.ts b/hardhat.config.ts index 59a34dbf..854917be 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -64,6 +64,15 @@ module.exports = { }, }, ], + }, + networks: { + hardhat: { + allowUnlimitedContractSize: true, + accounts: { + count: 200, + accountsBalance: "1000000000000000000000000", + }, + }, localhost: { url: "http://127.0.0.1:8545", }, From 8f36e67bb6c4cf1fa7cdee17bca8222fa85c4a90 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 15 Nov 2023 12:49:47 +0900 Subject: [PATCH 050/412] Faster contract test code. --- test/param/version.ts | 62 ++++++++++++++++++++++ test/unit/BurnCJPY.test.ts | 44 +++++++++------ test/unit/CurrencyOS.test.ts | 34 ++++++++---- test/unit/Events.test.ts | 21 ++++---- test/unit/FlashLock.test.ts | 42 ++++++++++----- test/unit/MintCJPY.test.ts | 41 +++++++++----- test/unit/Pool.test.ts | 20 +++++-- test/unit/PriceChangeAndRedemption.test.ts | 42 ++++++++++----- test/unit/PriceFeed.test.ts | 20 +++++-- test/unit/PriorityRegistry.test.ts | 29 +++++++--- test/unit/Yamato.test.ts | 49 ++++++++--------- 11 files changed, 290 insertions(+), 114 deletions(-) create mode 100644 test/param/version.ts diff --git a/test/param/version.ts b/test/param/version.ts new file mode 100644 index 00000000..08698a9e --- /dev/null +++ b/test/param/version.ts @@ -0,0 +1,62 @@ +export interface Version { + CJPY: string; + ChainLinkMock: string; + Currency: string; + CurrencyOS: string; + Dependencies: string; + FeePool: string; + Import: string; + Interfaces: string; + Minter: string; + OracleMockBase: string; + Pool: string; + PriceFeed: string; + PriorityRegistry: string; + ScoreRegistry: string; + TellorCallerMock: string; + Vester: string; + VesterFactory: string; + YMT: string; + Yamato: string; + YamatoBorrower: string; + YamatoDepositor: string; + YamatoRedeemer: string; + YamatoRepayer: string; + YamatoSweeper: string; + YamatoWithdrawer: string; + YmtOS: string; + curveFork: string; + veYMT: string; +} + + +export const contractVersion : Version = { + "CJPY": "CJPY", + "ChainLinkMock": "ChainLinkMock", + "Currency": "Currency", + "CurrencyOS": "CurrencyOSV2", + "Dependencies": "Dependencies", + "FeePool": "FeePool", + "Import": "Import", + "Interfaces": "Interfaces", + "Minter": "Minter", + "OracleMockBase": "OracleMockBase", + "Pool": "PoolV2", + "PriceFeed": "PriceFeedV3", + "PriorityRegistry": "PriorityRegistryV6", + "ScoreRegistry": "ScoreRegistry", + "TellorCallerMock": "TellorCallerMock", + "Vester": "Vester", + "VesterFactory": "VesterFactory", + "YMT": "YMT", + "Yamato": "YamatoV3", + "YamatoBorrower": "YamatoBorrower", + "YamatoDepositor": "YamatoDepositorV2", + "YamatoRedeemer": "YamatoRedeemerV4", + "YamatoRepayer": "YamatoRepayerV2", + "YamatoSweeper": "YamatoSweeperV2", + "YamatoWithdrawer": "YamatoWithdrawerV2", + "YmtOS": "YmtOS", + "curveFork": "curveFork", + "veYMT": "veYMT" +} diff --git a/test/unit/BurnCJPY.test.ts b/test/unit/BurnCJPY.test.ts index 1a0f7215..a002646c 100644 --- a/test/unit/BurnCJPY.test.ts +++ b/test/unit/BurnCJPY.test.ts @@ -2,6 +2,10 @@ import { ethers } from "hardhat"; import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { toERC20 } from "../param/helper"; import { ChainLinkMock, @@ -34,6 +38,7 @@ import { PriorityRegistry__factory, } from "../../typechain"; import { getProxy, getLinkedProxy } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -57,7 +62,9 @@ describe("burnCurrency :: contract Yamato", () => { let ownerAddress: string; let userAddress: string; - beforeEach(async () => { + let snapshot: SnapshotRestorer; + + before(async () => { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); userAddress = await accounts[1].getAddress(); @@ -91,25 +98,24 @@ describe("burnCurrency :: contract Yamato", () => { ).wait(); PriceFeed = await getProxy( - "PriceFeed", - [ChainLinkEthUsd.address, ChainLinkUsdJpy.address], - 3 + contractVersion["PriceFeed"], + [ChainLinkEthUsd.address, ChainLinkUsdJpy.address] ); CJPY = await (( await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy("FeePool", []); + FeePool = await getProxy(contractVersion["FeePool"], []); - CurrencyOS = await getProxy("CurrencyOS", [ + CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ CJPY.address, PriceFeed.address, FeePool.address, ]); Yamato = await getLinkedProxy( - "Yamato", + contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] ); @@ -117,15 +123,15 @@ describe("burnCurrency :: contract Yamato", () => { YamatoDepositor = await getLinkedProxy< YamatoDepositor, YamatoDepositor__factory - >("YamatoDepositor", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [Yamato.address], ["PledgeLib"]); YamatoBorrower = await getLinkedProxy< YamatoBorrower, YamatoBorrower__factory - >("YamatoBorrower", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoBorrower"], [Yamato.address], ["PledgeLib"]); YamatoRepayer = await getLinkedProxy( - "YamatoRepayer", + contractVersion["YamatoRepayer"], [Yamato.address], ["PledgeLib"] ); @@ -133,25 +139,25 @@ describe("burnCurrency :: contract Yamato", () => { YamatoWithdrawer = await getLinkedProxy< YamatoWithdrawer, YamatoWithdrawer__factory - >("YamatoWithdrawer", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoWithdrawer"], [Yamato.address], ["PledgeLib"]); YamatoRedeemer = await getLinkedProxy< YamatoRedeemer, YamatoRedeemer__factory - >("YamatoRedeemer", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRedeemer"], [Yamato.address], ["PledgeLib"]); YamatoSweeper = await getLinkedProxy( - "YamatoSweeper", + contractVersion["YamatoSweeper"], [Yamato.address], ["PledgeLib"] ); - Pool = await getProxy("Pool", [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); PriorityRegistry = await getLinkedProxy< PriorityRegistry, PriorityRegistry__factory - >("PriorityRegistry", [Yamato.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); await ( await Yamato.setDeps( @@ -169,6 +175,14 @@ describe("burnCurrency :: contract Yamato", () => { await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("redeem()", function () { let PRICE; const MCR = BigNumber.from(130); diff --git a/test/unit/CurrencyOS.test.ts b/test/unit/CurrencyOS.test.ts index 52b771c2..0564f9eb 100644 --- a/test/unit/CurrencyOS.test.ts +++ b/test/unit/CurrencyOS.test.ts @@ -2,6 +2,10 @@ import { ethers } from "hardhat"; import { FakeContract, smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { CurrencyOS, CJPY, @@ -20,6 +24,7 @@ import { FeePool__factory, } from "../../typechain"; import { getFakeProxy, getProxy } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -40,25 +45,26 @@ describe("CurrencyOS", () => { let accounts: Signer[]; let ownerAddress: string; let userAddress: string; + let snapshot: SnapshotRestorer; - beforeEach(async () => { + before(async () => { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); userAddress = await accounts[1].getAddress(); mockCJPY = await smock.fake("CJPY"); mockFeed = await getFakeProxy("PriceFeed"); - mockFeePool = await getFakeProxy("FeePool"); - mockYamato = await getFakeProxy("Yamato"); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockYamato = await getFakeProxy(contractVersion["Yamato"]); mockYamatoDepositor = await getFakeProxy( - "YamatoDepositor" + contractVersion["YamatoDepositor"] ); - mockYamatoBorrower = await getFakeProxy("YamatoBorrower"); - mockYamatoRepayer = await getFakeProxy("YamatoRepayer"); + mockYamatoBorrower = await getFakeProxy(contractVersion["YamatoBorrower"]); + mockYamatoRepayer = await getFakeProxy(contractVersion["YamatoRepayer"]); mockYamatoWithdrawer = await getFakeProxy( - "YamatoWithdrawer" + contractVersion["YamatoWithdrawer"] ); - mockYamatoRedeemer = await getFakeProxy("YamatoRedeemer"); - mockYamatoSweeper = await getFakeProxy("YamatoSweeper"); + mockYamatoRedeemer = await getFakeProxy(contractVersion["YamatoRedeemer"]); + mockYamatoSweeper = await getFakeProxy(contractVersion["YamatoSweeper"]); mockYamato.depositor.returns(mockYamatoDepositor.address); mockYamato.borrower.returns(mockYamatoBorrower.address); mockYamato.repayer.returns(mockYamatoRepayer.address); @@ -67,7 +73,7 @@ describe("CurrencyOS", () => { mockYamato.sweeper.returns(mockYamatoSweeper.address); mockYamato.permitDeps.returns(true); - currencyOS = await getProxy("CurrencyOS", [ + currencyOS = await getProxy(contractVersion["CurrencyOS"], [ mockCJPY.address, mockFeed.address, mockFeePool.address, @@ -77,6 +83,14 @@ describe("CurrencyOS", () => { mockCJPY.burn.returns(0); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("addYamato()", function () { it(`fails to add new Yamato for non-governer.`, async function () { await expect( diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index 142fd473..1a1bffff 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -32,6 +32,7 @@ import { } from "../../typechain"; import { encode, toERC20 } from "../param/helper"; import { getFakeProxy, getLinkedProxy, getProxy } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -85,42 +86,42 @@ describe("story Events", function () { mockCurrencyOS.currency.returns(mockCJPY.address); yamato = await getLinkedProxy( - "Yamato", + contractVersion["Yamato"], [mockCurrencyOS.address], ["PledgeLib"] ); yamatoDepositor = await getLinkedProxy< YamatoDepositor, YamatoDepositor__factory - >("YamatoDepositor", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); yamatoBorrower = await getLinkedProxy< YamatoBorrower, YamatoBorrower__factory - >("YamatoBorrower", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); yamatoRepayer = await getLinkedProxy< YamatoRepayer, YamatoRepayer__factory - >("YamatoRepayer", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRepayer"], [yamato.address], ["PledgeLib"]); yamatoWithdrawer = await getLinkedProxy< YamatoWithdrawer, YamatoWithdrawer__factory - >("YamatoWithdrawer", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); yamatoRedeemer = await getLinkedProxy< YamatoRedeemer, YamatoRedeemer__factory - >("YamatoRedeemer", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); yamatoSweeper = await getLinkedProxy< YamatoSweeper, YamatoSweeper__factory - >("YamatoSweeper", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoSweeper"], [yamato.address], ["PledgeLib"]); mockPriorityRegistry = await getFakeProxy( - "PriorityRegistryV6" + contractVersion["PriorityRegistry"] ); await ( @@ -331,7 +332,7 @@ describe("story Events", function () { mockCJPY = await smock.fake("CJPY"); mockCJPY.transfer.returns(0); - mockFeePool = await getFakeProxy("FeePool"); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); mockFeed = await getFakeProxy("PriceFeed"); mockCurrencyOS = await smock.fake("CurrencyOS"); mockCurrencyOS.priceFeed.returns(mockFeed.address); @@ -367,7 +368,7 @@ describe("story Events", function () { mockYamatoRedeemer.yamato.returns(yamatoDummy.address); mockYamatoSweeper.yamato.returns(yamatoDummy.address); - pool = await getProxy("Pool", [yamatoDummy.address]); + pool = await getProxy(contractVersion["Pool"], [yamatoDummy.address]); await (await yamatoDummy.setPool(pool.address)).wait(); }); diff --git a/test/unit/FlashLock.test.ts b/test/unit/FlashLock.test.ts index 27c46e72..cac0265e 100644 --- a/test/unit/FlashLock.test.ts +++ b/test/unit/FlashLock.test.ts @@ -2,6 +2,10 @@ import { ethers } from "hardhat"; import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { toERC20 } from "../param/helper"; import { ChainLinkMock, @@ -36,6 +40,7 @@ import { SameBlockClient__factory, } from "../../typechain"; import { getProxy, getLinkedProxy } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -60,7 +65,9 @@ describe("FlashLock :: contract Yamato", () => { let userAddress: string; let SameBlockClient: SameBlockClient; - beforeEach(async () => { + let snapshot: SnapshotRestorer; + + before(async () => { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); userAddress = await accounts[1].getAddress(); @@ -94,25 +101,24 @@ describe("FlashLock :: contract Yamato", () => { ).wait(); PriceFeed = await getProxy( - "PriceFeed", + contractVersion["PriceFeed"], [ChainLinkEthUsd.address, ChainLinkUsdJpy.address], - 3 ); CJPY = await (( await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy("FeePool", []); + FeePool = await getProxy(contractVersion["FeePool"], []); - CurrencyOS = await getProxy("CurrencyOS", [ + CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ CJPY.address, PriceFeed.address, FeePool.address, ]); Yamato = await getLinkedProxy( - "Yamato", + contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] ); @@ -120,15 +126,15 @@ describe("FlashLock :: contract Yamato", () => { YamatoDepositor = await getLinkedProxy< YamatoDepositor, YamatoDepositor__factory - >("YamatoDepositor", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [Yamato.address], ["PledgeLib"]); YamatoBorrower = await getLinkedProxy< YamatoBorrower, YamatoBorrower__factory - >("YamatoBorrower", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoBorrower"], [Yamato.address], ["PledgeLib"]); YamatoRepayer = await getLinkedProxy( - "YamatoRepayer", + contractVersion["YamatoRepayer"], [Yamato.address], ["PledgeLib"] ); @@ -136,25 +142,25 @@ describe("FlashLock :: contract Yamato", () => { YamatoWithdrawer = await getLinkedProxy< YamatoWithdrawer, YamatoWithdrawer__factory - >("YamatoWithdrawer", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoWithdrawer"], [Yamato.address], ["PledgeLib"]); YamatoRedeemer = await getLinkedProxy< YamatoRedeemer, YamatoRedeemer__factory - >("YamatoRedeemer", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRedeemer"], [Yamato.address], ["PledgeLib"]); YamatoSweeper = await getLinkedProxy( - "YamatoSweeper", + contractVersion["YamatoSweeper"], [Yamato.address], ["PledgeLib"] ); - Pool = await getProxy("Pool", [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); PriorityRegistry = await getLinkedProxy< PriorityRegistry, PriorityRegistry__factory - >("PriorityRegistry", [Yamato.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); await ( await Yamato.setDeps( @@ -178,6 +184,14 @@ describe("FlashLock :: contract Yamato", () => { await (await PriceFeed.fetchPrice()).wait(); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("depositAndBorrow()", function () { it(`should be reverted`, async function () { const PRICE = await PriceFeed.lastGoodPrice(); diff --git a/test/unit/MintCJPY.test.ts b/test/unit/MintCJPY.test.ts index be310758..bbac9456 100644 --- a/test/unit/MintCJPY.test.ts +++ b/test/unit/MintCJPY.test.ts @@ -2,6 +2,10 @@ import { ethers } from "hardhat"; import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { toERC20 } from "../param/helper"; import { ChainLinkMock, @@ -34,6 +38,7 @@ import { PriorityRegistry__factory, } from "../../typechain"; import { getProxy, getLinkedProxy } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -56,8 +61,9 @@ describe("MintCJPY :: contract Yamato", () => { let accounts: Signer[]; let ownerAddress: string; let userAddress: string; + let snapshot: SnapshotRestorer; - beforeEach(async () => { + before(async () => { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); userAddress = await accounts[1].getAddress(); @@ -91,25 +97,24 @@ describe("MintCJPY :: contract Yamato", () => { ).wait(); PriceFeed = await getProxy( - "PriceFeed", + contractVersion["PriceFeed"], [ChainLinkEthUsd.address, ChainLinkUsdJpy.address], - 3 ); CJPY = await (( await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy("FeePool", []); + FeePool = await getProxy(contractVersion["FeePool"], []); - CurrencyOS = await getProxy("CurrencyOS", [ + CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ CJPY.address, PriceFeed.address, FeePool.address, ]); Yamato = await getLinkedProxy( - "Yamato", + contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] ); @@ -117,15 +122,15 @@ describe("MintCJPY :: contract Yamato", () => { YamatoDepositor = await getLinkedProxy< YamatoDepositor, YamatoDepositor__factory - >("YamatoDepositor", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [Yamato.address], ["PledgeLib"]); YamatoBorrower = await getLinkedProxy< YamatoBorrower, YamatoBorrower__factory - >("YamatoBorrower", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoBorrower"], [Yamato.address], ["PledgeLib"]); YamatoRepayer = await getLinkedProxy( - "YamatoRepayer", + contractVersion["YamatoRepayer"], [Yamato.address], ["PledgeLib"] ); @@ -133,25 +138,25 @@ describe("MintCJPY :: contract Yamato", () => { YamatoWithdrawer = await getLinkedProxy< YamatoWithdrawer, YamatoWithdrawer__factory - >("YamatoDepositor", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [Yamato.address], ["PledgeLib"]); YamatoRedeemer = await getLinkedProxy< YamatoRedeemer, YamatoRedeemer__factory - >("YamatoRedeemer", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRedeemer"], [Yamato.address], ["PledgeLib"]); YamatoSweeper = await getLinkedProxy( - "YamatoSweeper", + contractVersion["YamatoSweeper"], [Yamato.address], ["PledgeLib"] ); - Pool = await getProxy("Pool", [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); PriorityRegistry = await getLinkedProxy< PriorityRegistry, PriorityRegistry__factory - >("PriorityRegistry", [Yamato.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); await ( await Yamato.setDeps( @@ -169,6 +174,14 @@ describe("MintCJPY :: contract Yamato", () => { await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("borrow()", function () { it(`should mint CJPY`, async function () { await (await PriceFeed.fetchPrice()).wait(); diff --git a/test/unit/Pool.test.ts b/test/unit/Pool.test.ts index ce829ed8..20eab938 100644 --- a/test/unit/Pool.test.ts +++ b/test/unit/Pool.test.ts @@ -2,20 +2,34 @@ import { ethers } from "hardhat"; import { FakeContract, smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber, Wallet } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { Pool, Yamato, Pool__factory } from "../../typechain"; import { getFakeProxy, getProxy } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); describe("contract Pool", function () { let pool: Pool; let accounts; + let snapshot: SnapshotRestorer; - beforeEach(async () => { + before(async () => { accounts = await ethers.getSigners(); - let mockYamato = await getFakeProxy("Yamato"); + let mockYamato = await getFakeProxy(contractVersion["Yamato"]); mockYamato.permitDeps.returns(false); - pool = await getProxy("Pool", [mockYamato.address]); + pool = await getProxy(contractVersion["Pool"], [mockYamato.address]); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); }); describe("receive() from EOA", function () { diff --git a/test/unit/PriceChangeAndRedemption.test.ts b/test/unit/PriceChangeAndRedemption.test.ts index b0f38f22..06798b65 100644 --- a/test/unit/PriceChangeAndRedemption.test.ts +++ b/test/unit/PriceChangeAndRedemption.test.ts @@ -2,6 +2,10 @@ import { ethers } from "hardhat"; import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber, Wallet } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { toERC20 } from "../param/helper"; import { ChainLinkMock, @@ -41,6 +45,7 @@ import { assertCollIntegrity, } from "../../src/testUtil"; import { isToken } from "typescript"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -64,7 +69,9 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { let ownerAddress: string; let userAddress: string; - beforeEach(async () => { + let snapshot: SnapshotRestorer; + + before(async () => { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); userAddress = await accounts[1].getAddress(); @@ -101,9 +108,8 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { await (await ChainLinkUsdJpy.setLastPrice(877000)).wait(); PriceFeed = await getProxy( - "PriceFeed", + contractVersion["PriceFeed"], [ChainLinkEthUsd.address, ChainLinkUsdJpy.address], - 3 ); await (await PriceFeed.fetchPrice()).wait(); @@ -111,9 +117,9 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy("FeePool", []); + FeePool = await getProxy(contractVersion["FeePool"], []); - CurrencyOS = await getProxy("CurrencyOS", [ + CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ CJPY.address, PriceFeed.address, FeePool.address, @@ -124,7 +130,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { ).address; Yamato = await getLinkedProxy( - "Yamato", + contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] ); @@ -132,15 +138,15 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { YamatoDepositor = await getLinkedProxy< YamatoDepositor, YamatoDepositor__factory - >("YamatoDepositor", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [Yamato.address], ["PledgeLib"]); YamatoBorrower = await getLinkedProxy< YamatoBorrower, YamatoBorrower__factory - >("YamatoBorrower", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoBorrower"], [Yamato.address], ["PledgeLib"]); YamatoRepayer = await getLinkedProxy( - "YamatoRepayer", + contractVersion["YamatoRepayer"], [Yamato.address], ["PledgeLib"] ); @@ -148,25 +154,25 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { YamatoWithdrawer = await getLinkedProxy< YamatoWithdrawer, YamatoWithdrawer__factory - >("YamatoWithdrawer", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoWithdrawer"], [Yamato.address], ["PledgeLib"]); YamatoRedeemer = await getLinkedProxy< YamatoRedeemer, YamatoRedeemer__factory - >("YamatoRedeemer", [Yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRedeemer"], [Yamato.address], ["PledgeLib"]); YamatoSweeper = await getLinkedProxy( - "YamatoSweeper", + contractVersion["YamatoSweeper"], [Yamato.address], ["PledgeLib"] ); - Pool = await getProxy("Pool", [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); PriorityRegistry = await getLinkedProxy< PriorityRegistryV6, PriorityRegistryV6__factory - >("PriorityRegistry", [Yamato.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); await ( await Yamato.setDeps( @@ -185,6 +191,14 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("redeem()", function () { let PRICE; const MCR = BigNumber.from(130); diff --git a/test/unit/PriceFeed.test.ts b/test/unit/PriceFeed.test.ts index adc74fe2..f8a18d0a 100644 --- a/test/unit/PriceFeed.test.ts +++ b/test/unit/PriceFeed.test.ts @@ -2,12 +2,17 @@ import { ethers } from "hardhat"; import { FakeContract, smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { ChainLinkMock, PriceFeedV3, PriceFeedV3__factory, } from "../../typechain"; import { getProxy } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -112,7 +117,9 @@ async function setMocks(conf: MockConf) { } describe("PriceFeed", function () { let lastMockInput; - beforeEach(async () => { + let snapshot: SnapshotRestorer; + + before(async () => { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); // https://github.com/liquity/dev/blob/main/packages/contracts/contracts/Dependencies/AggregatorV3Interface.sol @@ -138,9 +145,8 @@ describe("PriceFeed", function () { await setMocks(lastMockInput); feed = await getProxy( - "PriceFeed", + contractVersion["PriceFeed"], [mockAggregatorV3EthUsd.address, mockAggregatorV3JpyUsd.address], - 3 ); assertChainlink( @@ -150,6 +156,14 @@ describe("PriceFeed", function () { ); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("constructor()", function () { it(`should NOT revoke ownership`, async function () { expect(await feed.governance()).to.eq(await feed.signer.getAddress()); diff --git a/test/unit/PriorityRegistry.test.ts b/test/unit/PriorityRegistry.test.ts index 1c1a982e..5b33a7f3 100644 --- a/test/unit/PriorityRegistry.test.ts +++ b/test/unit/PriorityRegistry.test.ts @@ -2,6 +2,10 @@ import { ethers } from "hardhat"; import { FakeContract, smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { CurrencyOS, FeePool, @@ -16,6 +20,7 @@ import { } from "../../typechain"; import { getFakeProxy, getLinkedProxy } from "../../src/testUtil"; import { describe } from "mocha"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -32,13 +37,15 @@ describe("contract PriorityRegistry", function () { let address0: string; const PRICE = BigNumber.from(410000).mul(1e18 + ""); - beforeEach(async () => { + let snapshot: SnapshotRestorer; + + before(async () => { accounts = await ethers.getSigners(); address0 = await accounts[0].getAddress(); - mockFeed = await getFakeProxy("PriceFeedV3"); - mockFeePool = await getFakeProxy("FeePool"); - mockCurrencyOS = await getFakeProxy("CurrencyOSV2"); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockCurrencyOS = await getFakeProxy(contractVersion["CurrencyOS"]); const PledgeLib = ( await (( await ethers.getContractFactory("PledgeLib") @@ -49,7 +56,7 @@ describe("contract PriorityRegistry", function () { libraries: { PledgeLib }, }) ); - mockYamato = await getFakeProxy("Yamato"); + mockYamato = await getFakeProxy(contractVersion["Yamato"]); mockYamato.currencyOS.returns(mockCurrencyOS.address); mockCJPY = await smock.fake("CJPY"); @@ -69,7 +76,7 @@ describe("contract PriorityRegistry", function () { priorityRegistryWithYamatoMock = await getLinkedProxy< PriorityRegistryV6, PriorityRegistryV6__factory - >("PriorityRegistry", [mockYamato.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [mockYamato.address], ["PledgeLib"]); /* For onlyYamato tests @@ -81,13 +88,21 @@ describe("contract PriorityRegistry", function () { priorityRegistry = await getLinkedProxy< PriorityRegistryV6, PriorityRegistryV6__factory - >("PriorityRegistry", [yamatoDummy.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [yamatoDummy.address], ["PledgeLib"]); await ( await yamatoDummy.setPriorityRegistry(priorityRegistry.address) ).wait(); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("upsert()", function () { it(`fails due to the call from EOA.`, async function () { /* diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index 307983f1..0016c71c 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -37,6 +37,7 @@ import { getLinkedProxy, getTCR, } from "../../src/testUtil"; +import { contractVersion } from "../param/version"; chai.use(smock.matchers); @@ -68,8 +69,8 @@ describe("contract Yamato - pure func quickier tests", function () { ownerAddress = await accounts[0].getAddress(); mockPool = await smock.fake("Pool"); - mockFeePool = await getFakeProxy("FeePool"); - mockFeed = await getFakeProxy("PriceFeedV3"); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); mockYMT = await smock.fake("YMT"); mockCJPY = await smock.fake("CJPY"); mockCurrencyOS = await smock.fake("CurrencyOS"); @@ -84,7 +85,7 @@ describe("contract Yamato - pure func quickier tests", function () { mockCurrencyOS.currency.returns(mockCJPY.address); yamato = await getLinkedProxy( - "Yamato", + contractVersion["Yamato"], [mockCurrencyOS.address], ["PledgeLib"] ); @@ -92,15 +93,15 @@ describe("contract Yamato - pure func quickier tests", function () { yamatoDepositor = await getLinkedProxy< YamatoDepositor, YamatoDepositor__factory - >("YamatoDepositor", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); yamatoBorrower = await getLinkedProxy< YamatoBorrower, YamatoBorrower__factory - >("YamatoBorrower", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); yamatoRepayer = await getLinkedProxy( - "YamatoRepayer", + contractVersion["YamatoRepayer"], [yamato.address], ["PledgeLib"] ); @@ -108,15 +109,15 @@ describe("contract Yamato - pure func quickier tests", function () { yamatoWithdrawer = await getLinkedProxy< YamatoWithdrawer, YamatoWithdrawer__factory - >("YamatoWithdrawer", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); yamatoRedeemer = await getLinkedProxy< YamatoRedeemer, YamatoRedeemer__factory - >("YamatoRedeemer", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); yamatoSweeper = await getLinkedProxy( - "YamatoSweeper", + contractVersion["YamatoSweeper"], [yamato.address], ["PledgeLib"] ); @@ -129,7 +130,7 @@ describe("contract Yamato - pure func quickier tests", function () { )).deploy(mockCurrencyOS.address); // This has test funcs to size Yamato contract mockPriorityRegistry = await getFakeProxy( - "PriorityRegistry" + contractVersion["PriorityRegistry"] ); await ( @@ -146,13 +147,13 @@ describe("contract Yamato - pure func quickier tests", function () { ).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec - pool = await getProxy("Pool", [yamato.address]); + pool = await getProxy(contractVersion["Pool"], [yamato.address]); // Note: Will use later for the redeem() test priorityRegistry = await getLinkedProxy< PriorityRegistry, PriorityRegistry__factory - >("PriorityRegistry", [yamato.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); await ( await yamatoDummy.setPriorityRegistry(priorityRegistry.address) @@ -265,8 +266,8 @@ describe("contract Yamato", function () { ownerAddress = await accounts[0].getAddress(); mockPool = await smock.fake("Pool"); - mockFeePool = await getFakeProxy("FeePool"); - mockFeed = await getFakeProxy("PriceFeedV3"); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); mockYMT = await smock.fake("YMT"); mockCJPY = await smock.fake("CJPY"); mockCurrencyOS = await smock.fake("CurrencyOS"); @@ -281,7 +282,7 @@ describe("contract Yamato", function () { mockCurrencyOS.currency.returns(mockCJPY.address); yamato = await getLinkedProxy( - "Yamato", + contractVersion["Yamato"], [mockCurrencyOS.address], ["PledgeLib"] ); @@ -289,15 +290,15 @@ describe("contract Yamato", function () { yamatoDepositor = await getLinkedProxy< YamatoDepositor, YamatoDepositor__factory - >("YamatoDepositor", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); yamatoBorrower = await getLinkedProxy< YamatoBorrower, YamatoBorrower__factory - >("YamatoBorrower", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); yamatoRepayer = await getLinkedProxy( - "YamatoRepayer", + contractVersion["YamatoRepayer"], [yamato.address], ["PledgeLib"] ); @@ -305,15 +306,15 @@ describe("contract Yamato", function () { yamatoWithdrawer = await getLinkedProxy< YamatoWithdrawer, YamatoWithdrawer__factory - >("YamatoWithdrawer", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); yamatoRedeemer = await getLinkedProxy< YamatoRedeemer, YamatoRedeemer__factory - >("YamatoRedeemer", [yamato.address], ["PledgeLib"]); + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); yamatoSweeper = await getLinkedProxy( - "YamatoSweeper", + contractVersion["YamatoSweeper"], [yamato.address], ["PledgeLib"] ); @@ -326,7 +327,7 @@ describe("contract Yamato", function () { )).deploy(mockCurrencyOS.address); // This has test funcs to size Yamato contract mockPriorityRegistry = await getFakeProxy( - "PriorityRegistry" + contractVersion["PriorityRegistry"] ); await ( @@ -343,13 +344,13 @@ describe("contract Yamato", function () { ).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec - pool = await getProxy("Pool", [yamato.address]); + pool = await getProxy(contractVersion["Pool"], [yamato.address]); // Note: Will use later for the redeem() test priorityRegistry = await getLinkedProxy< PriorityRegistry, PriorityRegistry__factory - >("PriorityRegistry", [yamato.address], ["PledgeLib"]); + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); await ( await yamatoDummy.setPriorityRegistry(priorityRegistry.address) From 3fc574aff95d19ea94abfaca74209ec8bde6a802 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 20 Nov 2023 14:52:09 +0900 Subject: [PATCH 051/412] Adding veYMT contracts --- contracts/CurrencyOSV3.sol | 195 +++++ contracts/Interfaces/ICurrencyOSV3.sol | 28 + contracts/Interfaces/IScoreController.sol | 36 + contracts/Interfaces/IScoreRegistry.sol | 23 + contracts/Interfaces/IYMT.sol | 27 +- contracts/Interfaces/IYamatoV4.sol | 23 + contracts/Interfaces/IYmtMinter.sol | 20 + contracts/Interfaces/IveYMT.sol | 29 +- contracts/ScoreRegistry.sol | 262 +++++++ contracts/ScoreWeightController.sol | 698 ++++++++++++++++++ contracts/YMT.sol | 241 ++++++- contracts/YamatoBorrowerV2.sol | 125 ++++ contracts/YamatoRedeemerV5.sol | 316 ++++++++ contracts/YamatoRepayerV3.sol | 106 +++ contracts/YamatoSweeperV3.sol | 202 ++++++ contracts/YamatoV4.sol | 623 ++++++++++++++++ contracts/YmtMinter.sol | 127 ++++ contracts/veYMT.sol | 830 +++++++++++++++++++++- 18 files changed, 3849 insertions(+), 62 deletions(-) create mode 100644 contracts/CurrencyOSV3.sol create mode 100644 contracts/Interfaces/ICurrencyOSV3.sol create mode 100644 contracts/Interfaces/IScoreController.sol create mode 100644 contracts/Interfaces/IScoreRegistry.sol create mode 100644 contracts/Interfaces/IYamatoV4.sol create mode 100644 contracts/Interfaces/IYmtMinter.sol create mode 100644 contracts/ScoreRegistry.sol create mode 100644 contracts/ScoreWeightController.sol create mode 100644 contracts/YamatoBorrowerV2.sol create mode 100644 contracts/YamatoRedeemerV5.sol create mode 100644 contracts/YamatoRepayerV3.sol create mode 100644 contracts/YamatoSweeperV3.sol create mode 100644 contracts/YamatoV4.sol create mode 100644 contracts/YmtMinter.sol diff --git a/contracts/CurrencyOSV3.sol b/contracts/CurrencyOSV3.sol new file mode 100644 index 00000000..d7b0c293 --- /dev/null +++ b/contracts/CurrencyOSV3.sol @@ -0,0 +1,195 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Interfaces/ICurrency.sol"; +import "./Interfaces/ICurrencyOSV3.sol"; +import "./Interfaces/IYMT.sol"; +import "./veYMT.sol"; +import "./Interfaces/IYamatoV3.sol"; +import "./Dependencies/UUPSBase.sol"; +import "hardhat/console.sol"; + +contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { + string constant CURRENCY_SLOT_ID = "deps.Currency"; + string constant PRICEFEED_SLOT_ID = "deps.PriceFeed"; + string constant FEEPOOL_SLOT_ID = "deps.FeePool"; + string constant YMT_SLOT_ID = "deps.YMT"; + string constant VEYMT_SLOT_ID = "deps.veYMT"; + string constant MINTER_SLOT_ID = "deps.Minter"; + string constant WEIGHT_CONTROLLER_SLOT_ID = "deps.ScoreWeightController"; + + /* + =========================== + !!! DANGER ZONE BEGINS !!! + =========================== + */ + address[] public yamatoes; + + /* + =========================== + !!! DANGER ZONE ENDED !!! + =========================== + */ + + event YamatoAdded(address _yamatoAddr); + + function initialize( + address currencyAddr, + address feedAddr, + address feePoolAddr, + address ymatAddr, + address veymtAddr, + address minterAddr, + address scoreWeightControllerAddr + ) public initializer { + __UUPSBase_init(); + + bytes32 CURRENCY_KEY = bytes32(keccak256(abi.encode(CURRENCY_SLOT_ID))); + bytes32 PRICEFEED_KEY = bytes32( + keccak256(abi.encode(PRICEFEED_SLOT_ID)) + ); + bytes32 FEEPOOL_KEY = bytes32(keccak256(abi.encode(FEEPOOL_SLOT_ID))); + bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); + bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); + bytes32 WEIGHT_CONTROLLER_KEY = bytes32( + keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) + ); + assembly { + sstore(CURRENCY_KEY, currencyAddr) + sstore(PRICEFEED_KEY, feedAddr) + sstore(FEEPOOL_KEY, feePoolAddr) + sstore(YMT_KEY, ymatAddr) + sstore(VEYMT_KEY, veymtAddr) + sstore(MINTER_KEY, minterAddr) + sstore(WEIGHT_CONTROLLER_KEY, scoreWeightControllerAddr) + } + } + + function setPriceFeed(address feedAddr) external onlyGovernance { + bytes32 PRICEFEED_KEY = bytes32( + keccak256(abi.encode(PRICEFEED_SLOT_ID)) + ); + assembly { + sstore(PRICEFEED_KEY, feedAddr) + } + } + + modifier onlyYamato() { + if (yamatoes.length == 0) { + revert("No Yamato is registered."); + } else { + require(_permitMe(), "You are not Yamato deps."); + _; + } + } + + /* + ===================== + Public Functions + ===================== + */ + function addYamato(address _yamatoAddr) external onlyGovernance { + require(!exists(_yamatoAddr), "Duplicated Yamato."); + yamatoes.push(_yamatoAddr); + emit YamatoAdded(_yamatoAddr); + } + + function mintCurrency( + address to, + uint256 amount + ) public override onlyYamato { + ICurrency(currency()).mint(to, amount); + } + + function burnCurrency( + address to, + uint256 amount + ) public override onlyYamato { + ICurrency(currency()).burn(to, amount); + } + + /* + ===================== + Getter Functions + ===================== + */ + function currency() public view override returns (address _currency) { + bytes32 CURRENCY_KEY = bytes32(keccak256(abi.encode(CURRENCY_SLOT_ID))); + assembly { + _currency := sload(CURRENCY_KEY) + } + } + + function priceFeed() public view override returns (address _feed) { + bytes32 PRICEFEED_KEY = bytes32( + keccak256(abi.encode(PRICEFEED_SLOT_ID)) + ); + assembly { + _feed := sload(PRICEFEED_KEY) + } + } + + function feePool() public view override returns (address _feePool) { + bytes32 FEEPOOL_KEY = bytes32(keccak256(abi.encode(FEEPOOL_SLOT_ID))); + assembly { + _feePool := sload(FEEPOOL_KEY) + } + } + + function YMT() public view override returns (address _YMT) { + bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + assembly { + _YMT := sload(YMT_KEY) + } + } + + function veYMT() public view override returns (address _veYMT) { + bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); + assembly { + _veYMT := sload(VEYMT_KEY) + } + } + + function minter() public view override returns (address _minter) { + bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); + assembly { + _minter := sload(MINTER_KEY) + } + } + + function scoreWeightController() + public + view + override + returns (address _scoreWeightController) + { + bytes32 WEIGHT_CONTROLLER_KEY = bytes32( + keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) + ); + assembly { + _scoreWeightController := sload(WEIGHT_CONTROLLER_KEY) + } + } + + function exists(address _yamato) public view returns (bool) { + for (uint256 i = 0; i < yamatoes.length; i++) { + if (yamatoes[i] == _yamato) return true; + } + return false; + } + + function _permitMe() internal view returns (bool) { + for (uint256 i = 0; i < yamatoes.length; i++) { + if (IYamato(yamatoes[i]).permitDeps(msg.sender)) return true; + } + return false; + } +} diff --git a/contracts/Interfaces/ICurrencyOSV3.sol b/contracts/Interfaces/ICurrencyOSV3.sol new file mode 100644 index 00000000..dbf73af1 --- /dev/null +++ b/contracts/Interfaces/ICurrencyOSV3.sol @@ -0,0 +1,28 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly +interface ICurrencyOSV3 { + function mintCurrency(address to, uint256 amount) external; + + function burnCurrency(address to, uint256 amount) external; + + function priceFeed() external view returns (address); + + function feePool() external view returns (address); + + function currency() external view returns (address); + + function YMT() external view returns (address); + + function veYMT() external view returns (address); + + function minter() external view returns (address); + + function scoreWeightController() external view returns (address); +} diff --git a/contracts/Interfaces/IScoreController.sol b/contracts/Interfaces/IScoreController.sol new file mode 100644 index 00000000..87e3d46f --- /dev/null +++ b/contracts/Interfaces/IScoreController.sol @@ -0,0 +1,36 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +interface IScoreController { + function scoreTypes(address addr_) external view returns (uint256); + + function votingEscrow() external view returns (address); + + function checkpointScore(address addr) external; + + function checkpoint(address addr_) external; + + function updateScoreLimit(address addr_, uint256 l_, uint256 L_) external; + + function userCheckpoint(address addr_) external returns (bool); + + function addType(string memory name_, uint256 weight_) external; + + function addCurrency( + address addr_, + int128 scoreType_, + uint256 weight_ + ) external; + + function scoreRelativeWeight( + address addr, + uint256 time + ) external view returns (uint256); +} diff --git a/contracts/Interfaces/IScoreRegistry.sol b/contracts/Interfaces/IScoreRegistry.sol new file mode 100644 index 00000000..dfc40d81 --- /dev/null +++ b/contracts/Interfaces/IScoreRegistry.sol @@ -0,0 +1,23 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +interface IScoreRegistry { + function checkpoint(address addr_) external; + + function updateScoreLimit( + address addr_, + uint256 debt_, + uint256 totaldebt_ + ) external; + + function userCheckpoint(address addr_) external returns (bool); + + function integrateFraction(address addr_) external view returns (uint256); +} diff --git a/contracts/Interfaces/IYMT.sol b/contracts/Interfaces/IYMT.sol index d9a6a5bc..02f330a0 100644 --- a/contracts/Interfaces/IYMT.sol +++ b/contracts/Interfaces/IYMT.sol @@ -2,10 +2,12 @@ pragma solidity 0.8.4; /* * SPDX-License-Identifier: GPL-3.0-or-later - * Written by 0xMotoko (0xmotoko@pm.me) - * Copyright (C) 2021 Yamato Protocol (DeFiGeek Community Japan) + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) */ +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + /** * @dev Interface of the ERC20 standard as defined in the EIP. */ @@ -13,5 +15,24 @@ interface IYMT { /** * @dev mint token for recipient. Assuming onlyGovernance */ - function mint(address to, uint256 amount) external; + function mint(address to_, uint256 value_) external returns (bool); + + function decimals() external view returns (uint8); + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + function transfer( + address recipient, + uint256 amount + ) external returns (bool); + + function approve(address spender_, uint256 value_) external; + + function rate() external view returns (uint256); + + function futureEpochTimeWrite() external returns (uint256); } diff --git a/contracts/Interfaces/IYamatoV4.sol b/contracts/Interfaces/IYamatoV4.sol new file mode 100644 index 00000000..5715a718 --- /dev/null +++ b/contracts/Interfaces/IYamatoV4.sol @@ -0,0 +1,23 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./IYamato.sol"; + +interface IYamatoV4 { + function setPledges(IYamato.Pledge[] memory _pledges) external; + + function collFloor() external view returns (uint256); + + function maxRedeemableCount() external view returns (uint256); + + function CHECKPOINT_BUFFER() external view returns (uint256); + + function scoreRegistry() external view returns (address); +} diff --git a/contracts/Interfaces/IYmtMinter.sol b/contracts/Interfaces/IYmtMinter.sol new file mode 100644 index 00000000..a6c2c6b7 --- /dev/null +++ b/contracts/Interfaces/IYmtMinter.sol @@ -0,0 +1,20 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +interface IYmtMinter { + function token() external view returns (address); + + function controller() external view returns (address); + + function minted( + address user_, + address score_ + ) external view returns (uint256); +} diff --git a/contracts/Interfaces/IveYMT.sol b/contracts/Interfaces/IveYMT.sol index db043c00..0ea97927 100644 --- a/contracts/Interfaces/IveYMT.sol +++ b/contracts/Interfaces/IveYMT.sol @@ -2,23 +2,32 @@ pragma solidity 0.8.4; /* * SPDX-License-Identifier: GPL-3.0-or-later - * Written by 0xMotoko (0xmotoko@pm.me) - * Copyright (C) 2021 Yamato Protocol (DeFiGeek Community Japan) + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) */ //solhint-disable max-line-length //solhint-disable no-inline-assembly interface IveYMT { - function mintableInTimeframe( - uint256 _start, - uint256 _end - ) external view returns (uint256); + function totalSupply() external view returns (uint256); + + function getLastUserSlope(address addr_) external view returns (int128); - function balanceOfAt( - address _addr, - uint256 _at + function lockedEnd(address addr_) external view returns (uint256); + + function balanceOf( + address addr_, + uint256 t_ ) external view returns (uint256); - function totalSupplyAt(uint256 _at) external view returns (uint256); + function balanceOf(address addr) external view returns (uint256); + + function totalSupply(uint256 t_) external view returns (uint256); + + function userPointEpoch(address _user) external view returns (uint256); + + function userPointHistoryTs( + address addr, + uint256 epoch + ) external view returns (uint256); } diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol new file mode 100644 index 00000000..496ba590 --- /dev/null +++ b/contracts/ScoreRegistry.sol @@ -0,0 +1,262 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "./Dependencies/YamatoAction.sol"; +import "./Interfaces/IScoreController.sol"; +import "./Interfaces/IYMT.sol"; +import "./Interfaces/IYmtMinter.sol"; +import "./Interfaces/IveYMT.sol"; + +contract ScoreRegistry is YamatoAction { + event UpdateLiquidityLimit( + address user, + uint256 originalBalance, + uint256 originalSupply, + uint256 workingBalance, + uint256 workingSupply + ); + event CommitOwnership(address indexed admin); + event ApplyOwnership(address indexed admin); + + // to avoid "stack too deep" + struct CheckPointParameters { + int128 period; + uint256 periodTime; + uint256 integrateInvSupply; + uint256 inflationParams; + uint256 rate; + uint256 newRate; + uint256 prevFutureEpoch; + uint256 workingBalance; + uint256 workingSupply; + } + + // Constants + uint256 public constant TOKENLESS_PRODUCTION = 40; + uint256 public constant WEEK = 604800; + + // ERC20 + mapping(address => uint256) public balanceOf; + uint256 public totalSupply; + + // Score + address public admin; + + address public token; + address public votingEscrow; + address public minter; + address public scoreController; + + bool public isKilled; + + // [future_epoch_time uint40][inflation_rate uint216] + uint256 public futureEpochTime; + uint256 public inflationRate; + + mapping(address => uint256) public workingBalances; + uint256 public workingSupply; + + // 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint + mapping(address => uint256) public integrateInvSupplyOf; + mapping(address => uint256) public integrateCheckpointOf; + + // ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint + mapping(address => uint256) public integrateFraction; + + // The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint + int128 public period; + + // Using dynamic array instead of fixed 100000000000000000000000000000 array to avoid warning about collisions + uint256[100000000000000000000000000000] public periodTimestamp; + uint256[100000000000000000000000000000] public integrateInvSupply; + + function initialize(address minter_, address yamato_) public initializer { + __YamatoAction_init(yamato_); + minter = minter_; + token = IYmtMinter(minter).token(); + scoreController = IYmtMinter(minter).controller(); + votingEscrow = IScoreController(scoreController).votingEscrow(); + + periodTimestamp[0] = block.timestamp; + admin = msg.sender; + + // Assuming you have the YMT interface defined somewhere for the following line + inflationRate = IYMT(token).rate(); + futureEpochTime = IYMT(token).futureEpochTimeWrite(); + } + + function checkpoint(address addr) public onlyYamato { + CheckPointParameters memory _st; + + _st.period = period; + _st.periodTime = periodTimestamp[uint256(uint128(_st.period))]; + _st.integrateInvSupply = integrateInvSupply[ + uint256(uint128(_st.period)) + ]; + + _st.rate = inflationRate; + _st.prevFutureEpoch = futureEpochTime; + _st.newRate = _st.rate; + + if (_st.prevFutureEpoch >= _st.periodTime) { + futureEpochTime = IYMT(token).futureEpochTimeWrite(); + _st.newRate = IYMT(token).rate(); + inflationRate = _st.newRate; + } + + if (isKilled) { + _st.rate = 0; + _st.newRate = 0; + } + + if (block.timestamp > _st.periodTime) { + uint256 _workingSupply = workingSupply; + IScoreController(scoreController).checkpointScore(address(this)); + uint256 _prevWeekTime = _st.periodTime; + uint256 _weekTime = min( + ((_st.periodTime + WEEK) / WEEK) * WEEK, + block.timestamp + ); + + for (uint256 i = 0; i < 500; ) { + uint256 dt = _weekTime - _prevWeekTime; + uint256 w = IScoreController(scoreController) + .scoreRelativeWeight( + address(this), + (_prevWeekTime / WEEK) * WEEK + ); + + if (_workingSupply > 0) { + if ( + _st.prevFutureEpoch >= _prevWeekTime && + _st.prevFutureEpoch < _weekTime + ) { + _st.integrateInvSupply += + (_st.rate * + w * + (_st.prevFutureEpoch - _prevWeekTime)) / + _workingSupply; + _st.rate = _st.newRate; + _st.integrateInvSupply += + (_st.rate * w * (_weekTime - _st.prevFutureEpoch)) / + _workingSupply; + } else { + _st.integrateInvSupply += + (_st.rate * w * dt) / + _workingSupply; + } + } + + if (_weekTime == block.timestamp) { + break; + } + _prevWeekTime = _weekTime; + _weekTime = min(_weekTime + WEEK, block.timestamp); + unchecked { + ++i; + } + } + } + + _st.period += 1; + period = _st.period; + periodTimestamp[uint256(uint128(_st.period))] = block.timestamp; + integrateInvSupply[uint256(uint128(_st.period))] = _st + .integrateInvSupply; + + uint256 _workingBalance = workingBalances[addr]; + integrateFraction[addr] += + (_workingBalance * + (_st.integrateInvSupply - integrateInvSupplyOf[addr])) / + 10 ** 18; + integrateInvSupplyOf[addr] = _st.integrateInvSupply; + integrateCheckpointOf[addr] = block.timestamp; + } + + function updateScoreLimit( + address addr_, + uint256 debt_, + uint256 totaldebt_ + ) public onlyYamato { + uint256 _votingBalance = IveYMT(votingEscrow).balanceOf(addr_); + uint256 _votingTotal = IveYMT(votingEscrow).totalSupply(); + + uint256 _lim = (debt_ * TOKENLESS_PRODUCTION) / 100; + if (_votingTotal > 0) { + _lim += + (((totaldebt_ * _votingBalance) / _votingTotal) * + (100 - TOKENLESS_PRODUCTION)) / + 100; + } + + _lim = min(debt_, _lim); + uint256 _oldBal = workingBalances[addr_]; + workingBalances[addr_] = _lim; + uint256 _workingSupply = workingSupply + _lim - _oldBal; + workingSupply = _workingSupply; + + emit UpdateLiquidityLimit( + addr_, + debt_, + totaldebt_, + _lim, + _workingSupply + ); + } + + function userCheckpoint(address addr_) external onlyYamato returns (bool) { + require( + msg.sender == addr_ || msg.sender == minter, + "dev: unauthorized" + ); + checkpoint(addr_); + updateScoreLimit(addr_, balanceOf[addr_], totalSupply); + return true; + } + + function kick(address addr_) external { + uint256 _tLast = integrateCheckpointOf[addr_]; + uint256 _tVe = IveYMT(votingEscrow).userPointHistoryTs( + addr_, + IveYMT(votingEscrow).userPointEpoch(addr_) + ); + uint256 _balance = balanceOf[addr_]; + + require( + IveYMT(votingEscrow).balanceOf(addr_) == 0 || _tVe > _tLast, + "Not allowed" + ); + require( + workingBalances[addr_] > (_balance * TOKENLESS_PRODUCTION) / 100, + "Not needed" + ); + + checkpoint(addr_); + updateScoreLimit(addr_, balanceOf[addr_], totalSupply); + } + + function setKilled(bool isKilled_) external onlyAdmin { + isKilled = isKilled_; + } + + function integrateCheckpoint() external view returns (uint256) { + return periodTimestamp[uint256(uint128(period))]; + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + modifier onlyAdmin() { + require(msg.sender == admin, "only owner"); + _; + } +} diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol new file mode 100644 index 00000000..1650cdb7 --- /dev/null +++ b/contracts/ScoreWeightController.sol @@ -0,0 +1,698 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +/*** + *@title Score Controller + *@notice Controls liquidity scores and the issuance of token through the scores + */ + +//dao-contracts +import "./Interfaces/IveYMT.sol"; +import "./Dependencies/UUPSBase.sol"; + +contract ScoreWeightController is UUPSBase { + // 7 * 86400 seconds - all future times are rounded by week + // uint256 constant WEEK = 604800; + uint256 constant WEEK = 7 days; + + // Cannot change weight votes more often than once in 10 days. + uint256 constant WEIGHT_VOTE_DELAY = 10 days; + + struct Point { + uint256 bias; + uint256 slope; + } + + struct VotedSlope { + uint256 slope; + uint256 power; + uint256 end; + } + + event CommitOwnership(address admin); + event ApplyOwnership(address admin); + event AddType(string name, int128 typeId); + event NewTypeWeight( + int128 typeId, + uint256 time, + uint256 weight, + uint256 totalWeight + ); + + event NewScoreWeight( + address scoreAddress, + uint256 time, + uint256 weight, + uint256 totalWeight + ); + event VoteForScore( + uint256 time, + address user, + address scoreAddr, + uint256 weight + ); + event NewScore(address addr, int128 scoreType, uint256 weight); + + uint256 constant MULTIPLIER = 10 ** 18; + + // Can and will be a smart contract + address public admin; + // Can and will be a smart contract + address public futureAdmin; + // YMT token + address public token; + // Voting escrow + address public votingEscrow; + + int128 public nScoreTypes; + int128 public nScores; //number of scores + mapping(int128 => string) public scoreTypeNames; + + // Needed for enumeration + address[1000000000] public scores; + + // we increment values by 1 prior to storing them here so we can rely on a value + // of zero as meaning the score has not been set mapping(address => int128) scoreTypes; + mapping(address => int128) public scoreTypes_; + + mapping(address => mapping(address => VotedSlope)) public voteUserSlopes; // user -> score_addr -> VotedSlope + + mapping(address => uint256) public voteUserPower; // Total vote power used by user + mapping(address => mapping(address => uint256)) public lastUserVote; // Last user vote's timestamp for each score address + + // Past and scheduled points for score weight, sum of weights per type, total weight + // Point is for bias+slope + // changes_* are for changes in slope + // time_* are for the last change timestamp + // timestamps are rounded to whole weeks + + mapping(address => mapping(uint256 => Point)) public pointsWeight; // score_addr -> time -> Point + mapping(address => mapping(uint256 => uint256)) public changesWeight; // score_addr -> time -> slope + mapping(address => uint256) public timeWeight; // score_addr -> last scheduled time (next week) + + mapping(int128 => mapping(uint256 => Point)) public pointsSum; // type_id -> time -> Point + mapping(int128 => mapping(uint256 => uint256)) public changesSum; // type_id -> time -> slope + uint256[1000000000] public timeSum; // type_id -> last scheduled time (next week) + + mapping(uint256 => uint256) public pointsTotal; // time -> total weight + uint256 public timeTotal; // last scheduled time + + mapping(int128 => mapping(uint256 => uint256)) public pointsTypeWeight; // type_id -> time -> type weight + uint256[1000000000] public timeTypeWeight; // type_id -> last scheduled time (next week) + + /*** + *@notice Contract constructor + *@param _token `Token` contract address + *@param _votingEscrow `VotingEscrow` contract address + */ + function initialize( + address token_, + address votingEscrow_ + ) public initializer { + require(token_ != address(0)); + require(votingEscrow_ != address(0)); + + __UUPSBase_init(); + admin = msg.sender; + token = token_; + votingEscrow = votingEscrow_; + timeTotal = (block.timestamp / WEEK) * WEEK; + } + + /*** + * @notice Transfer ownership of ScoreController to `addr` + * @param addr_ Address to have ownership transferred to + */ + function commitTransferOwnership(address addr_) external onlyAdmin { + futureAdmin = addr_; + emit CommitOwnership(addr_); + } + + /*** + * @notice Apply pending ownership transfer + */ + function applyTransferOwnership() external onlyAdmin { + address _admin = futureAdmin; + require(_admin != address(0), "admin not set"); + admin = _admin; + emit ApplyOwnership(_admin); + } + + /*** + *@notice Get score type for address + *@param addr_ Score address + *@return Score type id + */ + function scoreTypes(address addr_) external view returns (int128) { + int128 _scoreType = scoreTypes_[addr_]; + require(_scoreType != 0, "dev: score is not added"); + + return _scoreType - 1; + } + + /*** + *@notice Fill historic type weights week-over-week for missed checkins + * and return the type weight for the future week + *@param _score_type Score type id + *@return Type weight + */ + function _getTypeWeight(int128 scoreType_) internal returns (uint256) { + uint256 _scoreType = uint256(uint128(scoreType_)); + uint256 _t = timeTypeWeight[_scoreType]; + if (_t > 0) { + uint256 _w = pointsTypeWeight[scoreType_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + pointsTypeWeight[scoreType_][_t] = _w; + if (_t > block.timestamp) { + timeTypeWeight[_scoreType] = _t; + } + unchecked { + ++i; + } + } + return _w; + } else { + return 0; + } + } + + /*** + *@notice Fill sum of score weights for the same type week-over-week for + * missed checkins and return the sum for the future week + *@param scoreType_ Score type id + *@return Sum of weights + */ + function _getSum(int128 scoreType_) internal returns (uint256) { + uint256 _scoreType = uint256(uint128(scoreType_)); + uint256 _t = timeSum[_scoreType]; + if (_t > 0) { + Point memory _pt = pointsSum[scoreType_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + uint256 _dBias = _pt.slope * WEEK; + if (_pt.bias > _dBias) { + _pt.bias -= _dBias; + uint256 _dSlope = changesSum[scoreType_][_t]; + _pt.slope -= _dSlope; + } else { + _pt.bias = 0; + _pt.slope = 0; + } + pointsSum[scoreType_][_t] = _pt; + if (_t > block.timestamp) { + timeSum[_scoreType] = _t; + } + unchecked { + ++i; + } + } + return _pt.bias; + } else { + return 0; + } + } + + /*** + *@notice Fill historic total weights week-over-week for missed checkins + * and return the total for the future week + *@return Total weight + */ + function _getTotal() internal returns (uint256) { + uint256 _t = timeTotal; + int128 _nScoreTypes = nScoreTypes; + if (_t > block.timestamp) { + // If we have already checkpointed - still need to change the value + _t -= WEEK; + } + uint256 _pt = pointsTotal[_t]; + + for (int128 _scoreType; _scoreType < 100; ) { + if (_scoreType == _nScoreTypes) { + break; + } + _getSum(_scoreType); + _getTypeWeight(_scoreType); + unchecked { + ++_scoreType; + } + } + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + _pt = 0; + // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) + for (int128 _scoreType; _scoreType < 100; ) { + if (_scoreType == _nScoreTypes) { + break; + } + uint256 _typeSum = pointsSum[_scoreType][_t].bias; + uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; + _pt += _typeSum * _typeWeight; + unchecked { + ++_scoreType; + } + } + pointsTotal[_t] = _pt; + + if (_t > block.timestamp) { + timeTotal = _t; + } + unchecked { + ++i; + } + } + return _pt; + } + + /*** + *@notice Fill historic score weights week-over-week for missed checkins + * and return the total for the future week + *@param scoreAddr_ Address of the score + *@return Score weight + */ + function _getWeight(address scoreAddr_) internal returns (uint256) { + uint256 _t = timeWeight[scoreAddr_]; + if (_t > 0) { + Point memory _pt = pointsWeight[scoreAddr_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + uint256 _dBias = _pt.slope * WEEK; + if (_pt.bias > _dBias) { + _pt.bias -= _dBias; + uint256 _dSlope = changesWeight[scoreAddr_][_t]; + _pt.slope -= _dSlope; + } else { + _pt.bias = 0; + _pt.slope = 0; + } + pointsWeight[scoreAddr_][_t] = _pt; + if (_t > block.timestamp) { + timeWeight[scoreAddr_] = _t; + } + unchecked { + ++i; + } + } + return _pt.bias; + } else { + return 0; + } + } + + /*** + *@notice Add Currency `addr` of type `score_type` with weight `weight` + *@param addr_ Score address + *@param scoreType_ Score type + */ + function addCurrency( + address addr_, + int128 scoreType_, + uint256 weight_ + ) external onlyAdmin { + require( + (scoreType_ >= 0) && (scoreType_ < nScoreTypes), + "score_type 0 means unset" + ); //score_type 0 means unset + require(scoreTypes_[addr_] == 0, "cannot add the same score twice"); + int128 _n = nScores; + unchecked { + nScores = _n + 1; + } + scores[uint256(uint128(_n))] = addr_; + uint256 _scoreType = uint256(uint128(scoreType_)); + scoreTypes_[addr_] = scoreType_ + 1; + uint256 _nextTime; + unchecked { + _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + + if (weight_ > 0) { + uint256 _typeWeight = _getTypeWeight(scoreType_); + uint256 _oldSum = _getSum(scoreType_); + uint256 _oldTotal = _getTotal(); + + pointsSum[scoreType_][_nextTime].bias = weight_ + _oldSum; + timeSum[_scoreType] = _nextTime; + pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); + timeTotal = _nextTime; + + pointsWeight[addr_][_nextTime].bias = weight_; + } + if (timeSum[_scoreType] == 0) { + timeSum[_scoreType] = _nextTime; + } + timeWeight[addr_] = _nextTime; + + emit NewScore(addr_, scoreType_, weight_); + } + + /*** + * @notice Checkpoint to fill data common for all scores + */ + function checkpoint() external { + _getTotal(); + } + + /*** + *@notice Checkpoint to fill data for both a specific score and common for all scores + *@param addr_ Score address + */ + function checkpointScore(address addr_) external { + _getWeight(addr_); + _getTotal(); + } + + /*** + *@notice Get Score relative weight (not more than 1.0) normalized to 1e18 + * (e.g. 1.0 == 1e18). Inflation which will be received by it is + * inflation_rate * relative_weight / 1e18 + *@param addr_ Score address + *@param time_ Relative weight at the specified timestamp in the past or present + *@return Value of relative weight normalized to 1e18 + */ + function _scoreRelativeWeight( + address addr_, + uint256 time_ + ) internal view returns (uint256) { + uint256 _t = (time_ / WEEK) * WEEK; + uint256 _totalWeight = pointsTotal[_t]; + + if (_totalWeight > 0) { + int128 _scoreType = scoreTypes_[addr_] - 1; + uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; + uint256 _scoreWeight = pointsWeight[addr_][_t].bias; + + return (MULTIPLIER * _typeWeight * _scoreWeight) / _totalWeight; + } else { + return 0; + } + } + + /*** + *@notice Get Score relative weight (not more than 1.0) normalized to 1e18 + * (e.g. 1.0 == 1e18). Inflation which will be received by it is + * inflation_rate * relative_weight / 1e18 + *@param addr_ Score address + *@param time_ Relative weight at the specified timestamp in the past or present + *@return Value of relative weight normalized to 1e18 + */ + function scoreRelativeWeight( + address addr_, + uint256 time_ + ) external view returns (uint256) { + //default value + if (time_ == 0) { + time_ = block.timestamp; + } + + return _scoreRelativeWeight(addr_, time_); + } + + function scoreRelativeWeightWrite( + address addr_, + uint256 time_ + ) external returns (uint256) { + //default value + if (time_ == 0) { + time_ = block.timestamp; + } + + _getWeight(addr_); + _getTotal(); // Also calculates get_sum + return _scoreRelativeWeight(addr_, time_); + } + + /*** + *@notice Change type weight + *@param typeId_ Type id + *@param weight_ New type weight + */ + function _changeTypeWeight(int128 typeId_, uint256 weight_) internal { + uint256 _oldWeight = _getTypeWeight(typeId_); + uint256 _oldSum = _getSum(typeId_); + uint256 _totalWeight = _getTotal(); + uint256 _nextTime; + unchecked { + _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + uint256 _typeId = uint256(uint128(typeId_)); + + _totalWeight = + _totalWeight + + (_oldSum * weight_) - + (_oldSum * _oldWeight); + pointsTotal[_nextTime] = _totalWeight; + pointsTypeWeight[typeId_][_nextTime] = weight_; + timeTotal = _nextTime; + timeTypeWeight[_typeId] = _nextTime; + + emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); + } + + /*** + *@notice Add score type with name `name_` and weight `weight` //ex. type=1, Liquidity, 1*1e18 + *@param name_ Name of score type + *@param weight_ Weight of score type + */ + function addType(string memory name_, uint256 weight_) external onlyAdmin { + int128 _typeId = nScoreTypes; + scoreTypeNames[_typeId] = name_; + unchecked { + nScoreTypes = _typeId + 1; + } + if (weight_ != 0) { + _changeTypeWeight(_typeId, weight_); + emit AddType(name_, _typeId); + } + } + + /*** + *@notice Change score type `type_id` weight to `weight` + *@param typeId_ Score type id + *@param weight_ New Score weight + */ + function changeTypeWeight( + int128 typeId_, + uint256 weight_ + ) external onlyAdmin { + _changeTypeWeight(typeId_, weight_); + } + + function _changeScoreWeight(address addr_, uint256 weight_) internal { + // Change score weight + // Only needed when testing in reality + int128 _scoreType = scoreTypes_[addr_] - 1; + uint256 _oldScoreWeight = _getWeight(addr_); + uint256 _typeWeight = _getTypeWeight(_scoreType); + uint256 _oldSum = _getSum(_scoreType); + uint256 _totalWeight = _getTotal(); + uint256 _nextTime; + unchecked { + _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + + pointsWeight[addr_][_nextTime].bias = weight_; + timeWeight[addr_] = _nextTime; + + uint256 newSum = _oldSum + weight_ - _oldScoreWeight; + pointsSum[_scoreType][_nextTime].bias = newSum; + timeSum[uint256(uint128(_scoreType))] = _nextTime; + + _totalWeight = + _totalWeight + + (newSum * _typeWeight) - + (_oldSum * _typeWeight); + pointsTotal[_nextTime] = _totalWeight; + timeTotal = _nextTime; + + emit NewScoreWeight(addr_, block.timestamp, weight_, _totalWeight); + } + + /*** + *@notice Change weight of score `addr` to `weight` + *@param addr_ `ScoreController` contract address + *@param weight_ New Score weight + */ + function changeScoreWeight( + address addr_, + uint256 weight_ + ) external onlyAdmin { + _changeScoreWeight(addr_, weight_); + } + + struct VotingParameter { + //to avoid "Stack too deep" issue + uint256 slope; + uint256 lockEnd; + uint256 _nScores; + uint256 nextTime; + int128 scoreType; + uint256 oldDt; + uint256 oldBias; + } + + /**** + *@notice Allocate voting power for changing pool weights + *@param scoreAddr_ Score which `msg.sender` votes for + *@param userWeight_ Weight for a score in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0. bps = basis points + */ + function voteForScoreWeights( + address scoreAddr_, + uint256 userWeight_ + ) external { + VotingParameter memory _vp; + _vp.slope = uint256( + uint128(IveYMT(votingEscrow).getLastUserSlope(msg.sender)) + ); + _vp.lockEnd = IveYMT(votingEscrow).lockedEnd(msg.sender); + _vp._nScores = uint256(uint128(nScores)); + unchecked { + _vp.nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + require(_vp.lockEnd > _vp.nextTime, "Your token lock expires too soon"); + require( + (userWeight_ >= 0) && (userWeight_ <= 10000), + "You used all your voting power" + ); + unchecked { + require( + block.timestamp >= + lastUserVote[msg.sender][scoreAddr_] + WEIGHT_VOTE_DELAY, + "Cannot vote so often" + ); + } + + _vp.scoreType = scoreTypes_[scoreAddr_] - 1; + require(_vp.scoreType >= 0, "Score not added"); + // Prepare slopes and biases in memory + VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][scoreAddr_]; + _vp.oldDt = 0; + if (_oldSlope.end > _vp.nextTime) { + _vp.oldDt = _oldSlope.end - _vp.nextTime; + } + _vp.oldBias = _oldSlope.slope * _vp.oldDt; + VotedSlope memory _newSlope = VotedSlope({ + slope: (_vp.slope * userWeight_) / 10000, + end: _vp.lockEnd, + power: userWeight_ + }); + uint256 _newDt = _vp.lockEnd - _vp.nextTime; // dev: raises when expired + uint256 _newBias = _newSlope.slope * _newDt; + + // Check and update powers (weights) used + uint256 _powerUsed = voteUserPower[msg.sender]; + _powerUsed = _powerUsed + _newSlope.power - _oldSlope.power; + voteUserPower[msg.sender] = _powerUsed; + require( + (_powerUsed >= 0) && (_powerUsed <= 10000), + "Used too much power" + ); + + //// Remove old and schedule new slope changes + // Remove slope changes for old slopes + // Schedule recording of initial slope for nextTime + uint256 _oldWeightBias = _getWeight(scoreAddr_); + uint256 _oldWeightSlope = pointsWeight[scoreAddr_][_vp.nextTime].slope; + uint256 _oldSumBias = _getSum(_vp.scoreType); + uint256 _oldSumSlope = pointsSum[_vp.scoreType][_vp.nextTime].slope; + + pointsWeight[scoreAddr_][_vp.nextTime].bias = + max(_oldWeightBias + _newBias, _vp.oldBias) - + _vp.oldBias; + pointsSum[_vp.scoreType][_vp.nextTime].bias = + max(_oldSumBias + _newBias, _vp.oldBias) - + _vp.oldBias; + if (_oldSlope.end > _vp.nextTime) { + pointsWeight[scoreAddr_][_vp.nextTime].slope = + max(_oldWeightSlope + _newSlope.slope, _oldSlope.slope) - + _oldSlope.slope; + pointsSum[_vp.scoreType][_vp.nextTime].slope = + max(_oldSumSlope + _newSlope.slope, _oldSlope.slope) - + _oldSlope.slope; + } else { + pointsWeight[scoreAddr_][_vp.nextTime].slope += _newSlope.slope; + pointsSum[_vp.scoreType][_vp.nextTime].slope += _newSlope.slope; + } + if (_oldSlope.end > block.timestamp) { + // Cancel old slope changes if they still didn't happen + changesWeight[scoreAddr_][_oldSlope.end] -= _oldSlope.slope; + changesSum[_vp.scoreType][_oldSlope.end] -= _oldSlope.slope; + } + // Add slope changes for new slopes + changesWeight[scoreAddr_][_newSlope.end] += _newSlope.slope; + changesSum[_vp.scoreType][_newSlope.end] += _newSlope.slope; + + _getTotal(); + + voteUserSlopes[msg.sender][scoreAddr_] = _newSlope; + + // Record last action time + lastUserVote[msg.sender][scoreAddr_] = block.timestamp; + + emit VoteForScore(block.timestamp, msg.sender, scoreAddr_, userWeight_); + } + + /*** + *@notice Get current score weight + *@param addr_ Score address + *@return Score weight + */ + function getScoreWeight(address addr_) external view returns (uint256) { + return pointsWeight[addr_][timeWeight[addr_]].bias; + } + + /*** + *@notice Get current type weight + *@param typeId_ Type id + *@return Type weight + */ + function getTypeWeight(int128 typeId_) external view returns (uint256) { + uint256 _typeId = uint256(uint128(typeId_)); + return pointsTypeWeight[typeId_][timeTypeWeight[_typeId]]; + } + + /*** + *@notice Get current total (type-weighted) weight + *@return Total weight + */ + function getTotalWeight() external view returns (uint256) { + return pointsTotal[timeTotal]; + } + + /*** + *@notice Get sum of score weights per type + *@param typeId_ Type id + *@return Sum of score weights + */ + function getWeightsSumPerType( + int128 typeId_ + ) external view returns (uint256) { + uint256 _typeId = uint256(uint128(typeId_)); + return pointsSum[typeId_][timeSum[_typeId]].bias; + } + + function max(uint256 _a, uint256 _b) internal pure returns (uint256) { + return _a >= _b ? _a : _b; + } + + modifier onlyAdmin() { + require(admin == msg.sender, "admin only"); + _; + } +} diff --git a/contracts/YMT.sol b/contracts/YMT.sol index 15aebb62..57b8570a 100644 --- a/contracts/YMT.sol +++ b/contracts/YMT.sol @@ -2,55 +2,240 @@ pragma solidity 0.8.4; /* * SPDX-License-Identifier: GPL-3.0-or-later - * Written by 0xMotoko (0xmotoko@pm.me) - * Copyright (C) 2021 Yamato Protocol (DeFiGeek Community Japan) + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) */ //solhint-disable max-line-length //solhint-disable no-inline-assembly import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; -import "./Interfaces/IYMT.sol"; /** - * @author 0xMotoko * @title YMT Token * @notice Divident. Inflatable but the rate is to be decreasing. */ -contract YMT is IYMT, ERC20Permit { - address ymtOSProxy; +contract YMT is ERC20Permit { + event UpdateMiningParameters(uint256 time, uint256 rate, uint256 supply); + event SetMinter(address minter); + event SetAdmin(address admin); - constructor( - uint256 initialSupply, - address _ymtOSProxy - ) ERC20Permit("Yamato") ERC20("Yamato", "YMT") { - _mint(msg.sender, initialSupply); - ymtOSProxy = _ymtOSProxy; + address public minter; + address public admin; + + // General constants + uint256 constant YEAR = 365 days; + + // Supply parameters + uint256 constant INITIAL_SUPPLY = 450_000_000; + uint256 constant INITIAL_RATE = (55_000_000 * 10 ** 18) / YEAR; // leading to 45% premine + uint256 constant RATE_REDUCTION_TIME = YEAR; + uint256 constant RATE_REDUCTION_COEFFICIENT = 1_111_111_111_111_111_111; // 10 * 1e18 / 9 + uint256 constant RATE_DENOMINATOR = 10 ** 18; + uint256 constant INFLATION_DELAY = 1 days; + + // Supply variables + int128 public miningEpoch; + uint256 public startEpochTime; + uint256 public rate; + + uint256 startEpochSupply; + + constructor() ERC20Permit("Yamato") ERC20("Yamato", "YMT") { + uint256 initSupply = INITIAL_SUPPLY * 10 ** decimals(); + _mint(msg.sender, initSupply); + + admin = msg.sender; + + startEpochTime = + block.timestamp + + INFLATION_DELAY - + RATE_REDUCTION_TIME; + miningEpoch = -1; + rate = 0; + startEpochSupply = initSupply; } - modifier onlyYmtOSProxy() { - require(msg.sender == ymtOSProxy, "You are not Yamato contract."); - _; + // @dev Update mining rate and supply at the start of the epoch + // Any modifying mining call must also call this + function _updateMiningParameters() internal { + uint256 _rate = rate; + uint256 _startEpochSupply = startEpochSupply; + + startEpochTime += RATE_REDUCTION_TIME; + miningEpoch += 1; + + if (_rate == 0) { + _rate = INITIAL_RATE; + } else { + _startEpochSupply += _rate * RATE_REDUCTION_TIME; + startEpochSupply = _startEpochSupply; + _rate = (_rate * RATE_DENOMINATOR) / RATE_REDUCTION_COEFFICIENT; + } + + rate = _rate; + + emit UpdateMiningParameters(block.timestamp, _rate, _startEpochSupply); + } + + // @notice Update mining rate and supply at the start of the epoch + // @dev Callable by any address, but only once per epoch + // Total supply becomes slightly larger if(this function is called late + function updateMiningParameters() external { + require( + block.timestamp >= startEpochTime + RATE_REDUCTION_TIME, + "dev: too soon!" + ); // dev: too soon! + _updateMiningParameters(); + } + + // @notice Get timestamp of the current mining epoch start + // while simultaneously updating mining parameters + // @return Timestamp of the epoch + function startEpochTimeWrite() external returns (uint256) { + uint256 _startEpochTime = startEpochTime; + if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); + return startEpochTime; + } else { + return _startEpochTime; + } + } + + // @notice Get timestamp of the next mining epoch start + // while simultaneously updating mining parameters + // @return Timestamp of the next epoch + function futureEpochTimeWrite() external returns (uint256) { + uint256 _startEpochTime = startEpochTime; + if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); + return startEpochTime + RATE_REDUCTION_TIME; + } else { + return _startEpochTime + RATE_REDUCTION_TIME; + } + } + + function _availableSupply() internal view returns (uint256) { + return startEpochSupply + (block.timestamp - startEpochTime) * rate; + } + + // @notice Current number of tokens in existence (claimed or unclaimed) + function availableSupply() external view returns (uint256) { + return _availableSupply(); + } + + // @notice How much supply is mintable from start timestamp till end timestamp + // @param start Start of the time interval (timestamp) + // @param end End of the time interval (timestamp) + // @return Tokens mintable from `start` till `end` + function mintableInTimeframe( + uint256 start, + uint256 end + ) external view returns (uint256) { + require(start <= end, "dev: start > end"); // dev: start > end + uint256 to_mint = 0; + uint256 currentEpochTime = startEpochTime; + uint256 currentRate = rate; + + // Special case if(end is in future (not yet minted) epoch + if (end > currentEpochTime + RATE_REDUCTION_TIME) { + currentEpochTime += RATE_REDUCTION_TIME; + currentRate = + (currentRate * RATE_DENOMINATOR) / + RATE_REDUCTION_COEFFICIENT; + } + + require( + end <= currentEpochTime + RATE_REDUCTION_TIME, + "dev: too far in future" + ); // dev: too far in future + + // Curve will not work in 1000 years. Darn! + for (uint i; i < 999; ) { + if (end >= currentEpochTime) { + uint256 currentEnd = end; + if (currentEnd > currentEpochTime + RATE_REDUCTION_TIME) { + currentEnd = currentEpochTime + RATE_REDUCTION_TIME; + } + + uint256 currentStart = start; + if (currentStart >= currentEpochTime + RATE_REDUCTION_TIME) { + break; // We should never get here but what if... + } else if (currentStart < currentEpochTime) { + currentStart = currentEpochTime; + } + to_mint += currentRate * (currentEnd - currentStart); + if (start >= currentEpochTime) { + break; + } + } + + currentEpochTime -= RATE_REDUCTION_TIME; + currentRate = + (currentRate * RATE_REDUCTION_COEFFICIENT) / + RATE_DENOMINATOR; // double-division with rounding made rate a bit less => good + require(currentRate <= INITIAL_RATE, "This should never happen"); // This should never happen + + unchecked { + i++; + } + } + + return to_mint; } - function mint( - address to, - uint256 amount - ) public virtual override onlyYmtOSProxy { - _mint(to, amount); + // @notice Set the minter address + // @dev Only callable once, when minter has not yet been set + // @param _minter Address of the minter + function setMinter(address _minter) external onlyAdmin { + require( + _minter != address(0), + "dev: can set the minter only once, at creation" + ); // dev: can set the minter only once, at creation + minter = _minter; + emit SetMinter(_minter); + } + + // @notice Set the new admin. + // @dev After all is set up, admin only can change the token name + // @param _admin New admin address + function setAdmin(address _admin) external onlyAdmin { + admin = _admin; + emit SetAdmin(_admin); } - function approve( - address spender, - uint256 amount - ) public virtual override returns (bool) { + // @notice Mint `_value` tokens and assign them to `_to` + // @dev Emits a Transfer event originating from 0x00 + // @param _to The account that will receive the created tokens + // @param _value The amount that will be created + // @return bool success + function mint(address _to, uint256 _value) external returns (bool) { + require(msg.sender == minter, "dev: minter only"); // dev: minter only + require(_to != address(0), "dev: zero address"); // dev: zero address + + if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); + } require( - _msgSender() != spender, - "sender and spender shouldn't be the same." + totalSupply() + _value <= _availableSupply(), + "dev: exceeds allowable mint amount" ); - require(amount > 0, "Amount is zero."); - _approve(_msgSender(), spender, amount); + _mint(_to, _value); + return true; } + + // @notice Burn `_value` tokens belonging to `msg.sender` + // @dev Emits a Transfer event with a destination of 0x00 + // @param _value The amount that will be burned + // @return bool success + function burn(uint256 _value) external returns (bool) { + _burn(msg.sender, _value); + return true; + } + + modifier onlyAdmin() { + require(admin == msg.sender, "dev: admin only"); + _; + } } diff --git a/contracts/YamatoBorrowerV2.sol b/contracts/YamatoBorrowerV2.sol new file mode 100644 index 00000000..a30d3fd4 --- /dev/null +++ b/contracts/YamatoBorrowerV2.sol @@ -0,0 +1,125 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Pool.sol"; +import "./YMT.sol"; +import "./PriceFeedV3.sol"; +import "./Dependencies/YamatoAction.sol"; +import "./Dependencies/PledgeLib.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Interfaces/IYamato.sol"; +import "./Interfaces/IYamatoV4.sol"; +import "./Interfaces/IFeePool.sol"; +import "./Interfaces/ICurrencyOS.sol"; +import "./Interfaces/IYamatoBorrower.sol"; +import "./Interfaces/IPriorityRegistryV6.sol"; +import "./Interfaces/IScoreRegistry.sol"; +import "hardhat/console.sol"; + +/// @title Yamato Borrower Contract + +contract YamatoBorrower is IYamatoBorrower, YamatoAction { + using PledgeLib for IYamato.Pledge; + using PledgeLib for uint256; + + function initialize(address _yamato) public initializer { + __YamatoAction_init(_yamato); + } + + function runBorrow( + address _sender, + uint256 _borrowAmountInCurrency + ) public override onlyYamato returns (uint256 fee) { + /* + 1. Ready + */ + IPriceFeedV3(priceFeed()).fetchPrice(); + IYamato.Pledge memory pledge = IYamato(yamato()).getPledge(_sender); + (, uint256 totalDebt, , , , ) = IYamato(yamato()).getStates(); + uint256 _ICRAfter = pledge.addDebt(_borrowAmountInCurrency).getICR( + priceFeed() + ); + fee = (_borrowAmountInCurrency * _ICRAfter.FR()) / 10000; + uint256 returnableCurrency = _borrowAmountInCurrency - fee; + + IScoreRegistry _scoreRegistry = IScoreRegistry( + IYamatoV4(yamato()).scoreRegistry() + ); + _scoreRegistry.checkpoint(_sender); + + /* + 2. Validate + */ + require( + !IYamato(yamato()).checkFlashLock(_sender), + "Those can't be called in the same block." + ); + require(pledge.isCreated, "This pledge is not created yet."); + require( + _ICRAfter >= uint256(IYamato(yamato()).MCR()) * 100, + "This minting is invalid because of too large borrowing." + ); + require(fee > 0, "fee must be more than zero."); + require( + returnableCurrency > 0, + "(borrow - fee) must be more than zero." + ); + + /* + 3. Set FlashLock + */ + IYamato(yamato()).setFlashLock(_sender); + + /* + 4. Add debt to a pledge in memory + */ + pledge.debt += _borrowAmountInCurrency; + + /* + 5. Add PriorityRegistry change + */ + pledge.priority = IPriorityRegistryV6(priorityRegistry()).upsert( + pledge + ); + + /* + 6. Commit to pledge + */ + IYamato(yamato()).setPledge(pledge.owner, pledge); + + /* + 7. Update totalDebt + */ + IYamato(yamato()).setTotalDebt(totalDebt + _borrowAmountInCurrency); + + /* + 8. Update score + */ + _scoreRegistry.updateScoreLimit( + _sender, + pledge.debt, + totalDebt + _borrowAmountInCurrency + ); + + /* + 9. Borrowed fund & fee transfer + */ + ICurrencyOS(currencyOS()).mintCurrency(_sender, returnableCurrency); // onlyYamato + + if ( + IPool(pool()).redemptionReserve() / 5 <= + IPool(pool()).sweepReserve() + ) { + IPool(pool()).depositRedemptionReserve(fee); + } else { + IPool(pool()).depositSweepReserve(fee); + } + } +} diff --git a/contracts/YamatoRedeemerV5.sol b/contracts/YamatoRedeemerV5.sol new file mode 100644 index 00000000..beb22e7d --- /dev/null +++ b/contracts/YamatoRedeemerV5.sol @@ -0,0 +1,316 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Pool.sol"; +import "./YMT.sol"; +import "./PriceFeedV3.sol"; +import "./Dependencies/YamatoAction.sol"; +import "./Dependencies/PledgeLib.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Interfaces/IYamato.sol"; +import "./Interfaces/IYamatoV4.sol"; +import "./Interfaces/IFeePool.sol"; +import "./Interfaces/ICurrencyOS.sol"; +import "./Interfaces/IYamatoRedeemer.sol"; +import "./Interfaces/IYamatoRedeemerV4.sol"; +import "./Interfaces/IPriorityRegistry.sol"; +import "./Interfaces/IPriorityRegistryV6.sol"; +import "./Interfaces/IScoreRegistry.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "hardhat/console.sol"; + +/// @title Yamato Redeemer Contract + +contract YamatoRedeemerV is IYamatoRedeemerV4, YamatoAction { + using PledgeLib for IYamato.Pledge; + using PledgeLib for uint256; + + function initialize(address _yamato) public initializer { + __YamatoAction_init(_yamato); + } + + function runRedeem( + IYamatoRedeemer.RunRedeemArgs memory _args + ) public override onlyYamato returns (IYamatoRedeemer.RedeemedArgs memory) { + IYamatoRedeemerV4.RunRedeemVars memory vars; + IPriorityRegistryV6 _prv6 = IPriorityRegistryV6(priorityRegistry()); + ICurrencyOS _currencyOS = ICurrencyOS(currencyOS()); + IERC20 _cjpy = IERC20(_currencyOS.currency()); + IYamato _yamato = IYamato(yamato()); + + IScoreRegistry _scoreRegistry = IScoreRegistry( + IYamatoV4(yamato()).scoreRegistry() + ); + + vars.ethPriceInCurrency = IPriceFeedV3(priceFeed()).fetchPrice(); + if (_args.isCoreRedemption) { + _args.wantToRedeemCurrencyAmount = IPool(pool()) + .redemptionReserve(); + require( + _args.wantToRedeemCurrencyAmount > 0, + "The redemption reserve is empty." + ); + } else { + require( + _cjpy.balanceOf(_args.sender) >= + _args.wantToRedeemCurrencyAmount, + "Insufficient currency balance to redeem." + ); + } + vars._GRR = _yamato.GRR(); + vars._mcrPercent = uint256(_yamato.MCR()); + vars._mcrPertenk = vars._mcrPercent * 100; + + /* + On memory update: Get redemption candidates with calculating after-redemption state + */ + vars._nextICR = _prv6.LICR(); + vars._nextICR = vars._nextICR == 0 ? 1 : vars._nextICR; + vars._nextout = _prv6.rankedQueueNextout(vars._nextICR); + vars._nextin = _prv6.rankedQueueTotalLen(vars._nextICR); + vars._maxCount = IYamatoV4(yamato()).maxRedeemableCount(); + vars._bulkedPledges = new IYamato.Pledge[](vars._maxCount * 2); + vars._skippedPledges = new IYamato.Pledge[](vars._maxCount); + vars._pledgesOwner = new address[](vars._maxCount); + vars._checkpoint = + vars._mcrPercent + + IYamatoV4(yamato()).CHECKPOINT_BUFFER(); + + while (true) { + address _pledgeAddr = _prv6.rankedQueuePop(vars._nextICR); + + if (vars._nextICR >= vars._checkpoint) { + // Note: This case conditioned as + // vars._activePledgeLength - vars._count == 0 || vars._nextICR >= vars._checkpoint + // but removed the first clause for simplicity. + // It would cause worse gas consumption in pledge-lacking cases but it's okay. + break; /* inf loop checker */ + } + + if (_pledgeAddr == address(0)) { + vars._nextICR++; + continue; /* That rank has been exhausted */ + } + + IYamato.Pledge memory _pledge = _yamato.getPledge(_pledgeAddr); + + /* + Score checkpoint + */ + _scoreRegistry.checkpoint(_pledge.owner); + + uint256 _ICRpertenk = _pledge.getICRWithPrice( + vars.ethPriceInCurrency + ); + + if ( + vars._nextICR == vars._mcrPercent && + _ICRpertenk == vars._mcrPertenk /* priority=realICR=MCR */ + ) { + vars._nextICR++; + vars._nextout = _prv6.rankedQueueNextout(vars._nextICR); + vars._nextin = _prv6.rankedQueueTotalLen(vars._nextICR); + continue; /* To avoid "just-on-MCR" pledges */ + } else { + vars._toBeRedeemedFragment = _pledge.toBeRedeemed( + vars._mcrPertenk, + _ICRpertenk, + vars.ethPriceInCurrency + ); + + if ( + vars._toBeRedeemedFragment == 0 && + _ICRpertenk >= vars._mcrPertenk + ) { + vars._skippedPledges[vars._skipCount] = _pledge; + vars._skipCount++; + continue; /* To skip until next poppables. This must be upserted below to refresh obsoleted priority. */ + } + + if ( + vars._toBeRedeemed + vars._toBeRedeemedFragment > + _args.wantToRedeemCurrencyAmount + ) { + vars._toBeRedeemedFragment = + _args.wantToRedeemCurrencyAmount - + vars + ._toBeRedeemed; /* Limiting redeeming amount within the amount sender has. */ + } + + vars._toBeRedeemedFragmentInEth = + (vars._toBeRedeemedFragment * 1e18) / + vars.ethPriceInCurrency; + /* state update for redeemed pledge */ + + require( + vars._toBeRedeemedFragmentInEth <= _pledge.coll, + "redemption fragment can't be bigger than coll." + ); + uint256 _dustyDiff = _pledge.coll - + vars._toBeRedeemedFragmentInEth; + uint256 _debtAfter = _pledge.debt - vars._toBeRedeemedFragment; + bool _isSweepableWithColl = (_dustyDiff > 0) && + ((_dustyDiff * vars.ethPriceInCurrency) / 1e18 < + _debtAfter); + uint256 _increasedToBeRedeemedFragment = vars + ._toBeRedeemedFragment + + (_dustyDiff * vars.ethPriceInCurrency) / + 1e18; + bool _hasRoundingBudget = vars._toBeRedeemed + + _increasedToBeRedeemedFragment <= + _args.wantToRedeemCurrencyAmount; + + if (_isSweepableWithColl && _hasRoundingBudget) { + /* Rounding a dusty collateral */ + vars._toBeRedeemedFragmentInEth += _dustyDiff; + require( + vars._toBeRedeemedFragmentInEth == _pledge.coll, + "For ICR=0% pledge, coll must be zero." + ); + vars._toBeRedeemedFragment = _increasedToBeRedeemedFragment; + } + + _pledge.debt -= vars._toBeRedeemedFragment; + _pledge.coll -= vars._toBeRedeemedFragmentInEth; + + vars._toBeRedeemed += vars._toBeRedeemedFragment; + vars._toBeRedeemedInEth += vars._toBeRedeemedFragmentInEth; + vars._pledgesOwner[vars._count] = _pledge.owner; + vars._bulkedPledges[vars._count] = _pledge; + vars._count++; + + if (vars._toBeRedeemed == _args.wantToRedeemCurrencyAmount) { + break; /* Could pile up money as sender wants. */ + } + if (vars._count >= vars._maxCount) { + break; /* count reached to the target */ + } + } + } + require(vars._toBeRedeemed > 0, "No pledges are redeemed."); + require( + vars._toBeRedeemed <= _args.wantToRedeemCurrencyAmount, + "Redeeming amount exceeds bearer's balance." + ); + + /* + Merge skipped pledges to re-redeem later + */ + for (uint256 i; i < vars._maxCount; ) { + vars._bulkedPledges[vars._maxCount + i] = vars._skippedPledges[i]; + unchecked { + ++i; // Note: gas saving + } + } + + /* + External tx: bulkUpsert and LICR update + */ + uint256[] memory _priorities = _prv6.bulkUpsert(vars._bulkedPledges); + + /* + On memory update: priority + */ + for (uint256 i; i < vars._bulkedPledges.length; i++) { + vars._bulkedPledges[i].priority = _priorities[i]; + } + + /* + External tx: setPledges + */ + + IYamatoV4(yamato()).setPledges(vars._bulkedPledges); + + /* + External tx: setTotalColl, setTotalDebt + */ + (uint256 totalColl, uint256 totalDebt, , , , ) = IYamato(yamato()) + .getStates(); + IYamato(yamato()).setTotalDebt(totalDebt - vars._toBeRedeemed); + IYamato(yamato()).setTotalColl(totalColl - vars._toBeRedeemedInEth); + + /* + Update score + */ + for (uint256 i; i < vars._bulkedPledges.length; i++) { + IYamato.Pledge memory _pledge = vars._bulkedPledges[i]; + _scoreRegistry.updateScoreLimit( + _pledge.owner, + _pledge.debt, + totalDebt - vars._toBeRedeemed + ); + } + + /* + 8. Update score + */ + // _scoreRegistry.updateScoreLimit(_args.sender, _pledge.debt, totalDebt - vars._toBeRedeemed); + + /* + Handle compensations + */ + address _redemptionBearer; + address _returningDestination; + if (_args.isCoreRedemption) { + /* + [ Core Redemption - Pool Subtotal ] + (-) Redemption Reserve (Currency) + v + v + (+) Fee Pool (ETH) + */ + _redemptionBearer = pool(); + _returningDestination = feePool(); + IPool(pool()).useRedemptionReserve(vars._toBeRedeemed); + } else { + /* + [ Normal Redemption - Account Subtotal ] + (-) Bearer Balance (Currency) + v + v + (+) Bearer Balance (ETH) + */ + _redemptionBearer = _args.sender; + _returningDestination = _args.sender; + } + + // Burn 100% + _currencyOS.burnCurrency(_redemptionBearer, vars._toBeRedeemed); + + // Pay 99% in ETH + vars._effectiveRedemptionAmountInCurrency = + (vars._toBeRedeemed * (100 - vars._GRR)) / + 100; + vars._effectiveRedemptionAmount = + (vars._toBeRedeemedInEth * (100 - vars._GRR)) / + 100; + IPool(pool()).sendETH( + _returningDestination, + vars._effectiveRedemptionAmount + ); + + /* + 4. Pay 1% gas compensation in ETH + */ + vars._gasCompensationInETH = + vars._toBeRedeemedInEth - + vars._effectiveRedemptionAmount; + IPool(pool()).sendETH(_args.sender, vars._gasCompensationInETH); + + return + IYamatoRedeemer.RedeemedArgs( + vars._effectiveRedemptionAmountInCurrency, + vars._effectiveRedemptionAmount, + vars._pledgesOwner, + vars.ethPriceInCurrency, + vars._gasCompensationInETH + ); + } +} diff --git a/contracts/YamatoRepayerV3.sol b/contracts/YamatoRepayerV3.sol new file mode 100644 index 00000000..a604b509 --- /dev/null +++ b/contracts/YamatoRepayerV3.sol @@ -0,0 +1,106 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Pool.sol"; +import "./YMT.sol"; +import "./PriceFeedV3.sol"; +import "./Dependencies/YamatoAction.sol"; +import "./Dependencies/PledgeLib.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Interfaces/IYamato.sol"; +import "./Interfaces/IYamatoV4.sol"; +import "./Interfaces/IFeePool.sol"; +import "./Interfaces/ICurrencyOS.sol"; +import "./Interfaces/ICurrency.sol"; +import "./Interfaces/IYamatoRepayer.sol"; +import "./Interfaces/IPriorityRegistryV6.sol"; +import "./Interfaces/IScoreRegistry.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "hardhat/console.sol"; + +/// @title Yamato Repayer Contract + +contract YamatoRepayerV3 is IYamatoRepayer, YamatoAction { + function initialize(address _yamato) public initializer { + __YamatoAction_init(_yamato); + } + + /// @dev Package-separated repay function which only be called by YamatoV(n).sol + function runRepay( + address _sender, + uint256 _repayAmountInCurrency + ) public override onlyYamato { + /* + 1. Get feed and Pledge + */ + IPriceFeedV3(priceFeed()).fetchPrice(); + IYamato.Pledge memory pledge = IYamato(yamato()).getPledge(_sender); + (, uint256 totalDebt, , , , ) = IYamato(yamato()).getStates(); + uint256 senderBalance = IERC20(ICurrencyOS(currencyOS()).currency()) + .balanceOf(_sender); + + IScoreRegistry _scoreRegistry = IScoreRegistry( + IYamatoV4(yamato()).scoreRegistry() + ); + _scoreRegistry.checkpoint(_sender); + + /* + 2. Check repayability + */ + require(_repayAmountInCurrency > 0, "You are repaying no Currency"); + require(pledge.debt > 0, "You can't repay for a zero-debt pledge."); + require( + _repayAmountInCurrency <= senderBalance, + "You are trying to repay more than you have." + ); // V2 (Dec 7, 2021) + require( + _repayAmountInCurrency <= pledge.debt, + "You are trying to repay more than your debt." + ); // V2 (Dec 7, 2021) + + /* + 2. Compose a pledge in memory + */ + pledge.debt -= _repayAmountInCurrency; + + /* + 3. Add PriorityRegistry update result to a pledge in memory + */ + pledge.priority = IPriorityRegistryV6(priorityRegistry()).upsert( + pledge + ); + + /* + 4. Commit a pledge in memory to YamatoStore + */ + IYamato(yamato()).setPledge(pledge.owner, pledge); + + /* + 5. Commit totalDebt + */ + IYamato(yamato()).setTotalDebt(totalDebt - _repayAmountInCurrency); + + /* + 6. Update score + */ + _scoreRegistry.updateScoreLimit( + _sender, + pledge.debt, + totalDebt - _repayAmountInCurrency + ); + + /* + 7-1. Charge Currency + 7-2. Return coll to the repayer + */ + ICurrencyOS(currencyOS()).burnCurrency(_sender, _repayAmountInCurrency); + } +} diff --git a/contracts/YamatoSweeperV3.sol b/contracts/YamatoSweeperV3.sol new file mode 100644 index 00000000..718d8b05 --- /dev/null +++ b/contracts/YamatoSweeperV3.sol @@ -0,0 +1,202 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Pool.sol"; +import "./YMT.sol"; +import "./PriceFeedV3.sol"; +import "./Dependencies/YamatoAction.sol"; +import "./Dependencies/PledgeLib.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Dependencies/LiquityMath.sol"; +import "./Interfaces/IYamato.sol"; +import "./Interfaces/IYamatoV4.sol"; +import "./Interfaces/IFeePool.sol"; +import "./Interfaces/ICurrencyOS.sol"; +import "./Interfaces/IYamatoSweeper.sol"; +import "./Interfaces/IPriorityRegistryV6.sol"; +import "./Interfaces/IScoreRegistry.sol"; +import "hardhat/console.sol"; + +/// @title Yamato Sweeper Contract + +contract YamatoSweeperV3 is IYamatoSweeper, YamatoAction { + using PledgeLib for IYamato.Pledge; + using PledgeLib for uint256; + + function initialize(address _yamato) public initializer { + __YamatoAction_init(_yamato); + } + + /// @dev no reentrancy guard because action funcs are protected by permitDeps() + function runSweep( + address _sender + ) + public + override + onlyYamato + returns ( + uint256 _sweptAmount, + uint256 gasCompensationInCurrency, + address[] memory _pledgesOwner + ) + { + IPriceFeedV3(priceFeed()).fetchPrice(); + + IYamatoSweeper.Vars memory vars; + + IScoreRegistry _scoreRegistry = IScoreRegistry( + IYamatoV4(yamato()).scoreRegistry() + ); + + vars._GRR = IYamato(yamato()).GRR(); + vars._currencyOS = ICurrencyOS(currencyOS()); + vars.sweepReserve = IPool(pool()).sweepReserve(); + vars._poolBalance = IERC20(vars._currencyOS.currency()).balanceOf( + pool() + ); + vars._sweepingAmountTmp = LiquityMath._min( + vars.sweepReserve, + vars._poolBalance + ); + vars._sweepingAmount = + (vars._sweepingAmountTmp * (100 - vars._GRR)) / + 100; + vars._gasCompensationInCurrency = + vars._sweepingAmountTmp - + vars._sweepingAmount; + + if (vars._sweepingAmountTmp > 0 && vars._sweepingAmount == 0) { + revert("Sweep budget is too small to pay gas reward."); + } + require( + vars._sweepingAmount > 0, + "Sweep failure: sweep reserve is empty." + ); + vars._reminder = vars._sweepingAmount; + vars._maxCount = IYamatoV4(yamato()).maxRedeemableCount(); + vars._pledgesOwner = new address[](vars._maxCount); + vars._bulkedPledges = new IYamato.Pledge[](vars._maxCount); + + /* + 1. Sweeping + */ + IYamato _yamato = IYamato(yamato()); + IPriorityRegistryV6 _prv6 = IPriorityRegistryV6(priorityRegistry()); + require(_prv6.rankedQueueLen(0) > 0, "No sweepables."); + while (true) { + address _pledgeAddr = _prv6.rankedQueuePop(0); + + if (_pledgeAddr == address(0)) { + break; + } + + IYamato.Pledge memory _pledge = _yamato.getPledge(_pledgeAddr); + + /* + Score checkpoint + */ + _scoreRegistry.checkpoint(_pledge.owner); + + uint256 _pledgeDebt = _pledge.debt; + + if (_pledgeDebt >= vars._reminder) { + _pledge.debt = _pledgeDebt - vars._reminder; + vars._toBeSwept += vars._reminder; + vars._reminder = 0; + } else { + _pledge.debt = 0; + vars._toBeSwept += _pledgeDebt; + vars._reminder -= _pledgeDebt; + } + + _pledge.coll = 0; // Note: Sometimes very tiny coll would be there but ignore it. Don't reduce totalColl. + + vars._pledgesOwner[vars._loopCount] = _pledge.owner; // Note: For event + vars._bulkedPledges[vars._loopCount] = _pledge; + + vars._loopCount++; + + if (vars._toBeSwept >= vars._sweepingAmount) { + break; /* redeeming amount reached to the target */ + } + if (vars._loopCount >= vars._maxCount) { + break; + } + } + require(vars._toBeSwept > 0, "At least a pledge should be swept."); + require(vars._sweepingAmount >= vars._toBeSwept, "Too much sweeping."); + + /* + Update pledges + */ + for (uint256 i; i < vars._bulkedPledges.length; i++) { + IYamato.Pledge memory _pledge = vars._bulkedPledges[i]; + if (_pledge.debt == 0) { + _prv6.remove(_pledge); + _yamato.setPledge(_pledge.owner, _pledge.nil()); + } else { + _prv6.upsert(_pledge); + _yamato.setPledge(_pledge.owner, _pledge); + } + } + + /* + Update global state for 99% + */ + (, uint256 totalDebt, , , , ) = _yamato.getStates(); + _yamato.setTotalDebt(totalDebt - vars._toBeSwept); + + /* + Update score + */ + for (uint256 i; i < vars._bulkedPledges.length; i++) { + IYamato.Pledge memory _pledge = vars._bulkedPledges[i]; + _scoreRegistry.updateScoreLimit( + _pledge.owner, + _pledge.debt, + totalDebt - vars._toBeSwept + ); + } + + /* + Reserve reduction for 99% + */ + IPool(pool()).useSweepReserve(vars._toBeSwept); + vars._currencyOS.burnCurrency(pool(), vars._toBeSwept); + + /* + Gas compensation for 1% + */ + IPool(pool()).sendCurrency(_sender, vars._gasCompensationInCurrency); // Not sendETH. But redemption returns in ETH and so it's a bit weird. + IPool(pool()).useSweepReserve(vars._gasCompensationInCurrency); + + return ( + vars._toBeSwept, + vars._gasCompensationInCurrency, + vars._pledgesOwner + ); + } + + /// @dev Deprecated in V2. + function sweepDebt( + IYamato.Pledge memory sPledge, + uint256 maxSweeplable + ) + public + override + onlyYamato + returns (IYamato.Pledge memory, uint256, uint256) + { + IYamato.Pledge memory sPledge; + uint256 reminder; + uint256 sweepingAmount; + return (sPledge, reminder, sweepingAmount); + } +} diff --git a/contracts/YamatoV4.sol b/contracts/YamatoV4.sol new file mode 100644 index 00000000..b7504697 --- /dev/null +++ b/contracts/YamatoV4.sol @@ -0,0 +1,623 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Pool.sol"; +import "./YMT.sol"; +import "./Interfaces/IYamatoDepositor.sol"; +import "./Interfaces/IYamatoBorrower.sol"; +import "./Interfaces/IYamatoRepayer.sol"; +import "./Interfaces/IYamatoWithdrawer.sol"; +import "./Interfaces/IYamatoRedeemer.sol"; +import "./Interfaces/IYamatoSweeper.sol"; +import "./Dependencies/YamatoStore.sol"; +import "./Dependencies/YamatoBase.sol"; +import "./Dependencies/PledgeLib.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Interfaces/IYamato.sol"; +import "./Interfaces/IYamatoV4.sol"; +import "./Interfaces/IFeePool.sol"; +import "./Interfaces/ICurrencyOS.sol"; +import "hardhat/console.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; + +/// @title Yamato Pledge Manager Contract +contract YamatoV4 is + IYamato, + IYamatoV4, + YamatoStore, + ReentrancyGuardUpgradeable, + PausableUpgradeable +{ + /* + =========================== + Lib for struct Pledge + =========================== + */ + using PledgeLib for IYamato.Pledge; + using PledgeLib for uint256; + + /* + =========================== + ~~~ SAFE HAVEN ~~~ + =========================== + - Constants don't take slots + - You can add or remove them in upgrade timing + - Read more => https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#avoid-initial-values-in-field-declarations + */ + + uint8 public constant override MCR = 130; // MinimumCollateralizationRatio in pertenk + uint8 public constant RRR = 80; // RedemptionReserveRate in pertenk + uint8 public constant SRR = 20; // SweepReserveRate in pertenk + uint8 public constant override GRR = 1; // GasReserveRate in pertenk + // TODO: Comment-in here later + uint256 public constant override(IYamatoV4) collFloor = 1e17; // 0.1 ETH is the floor + uint256 public constant override(IYamatoV4) maxRedeemableCount = 50; // 5ETH is the max redeemable amount per a tx. + uint256 public constant override(IYamatoV4) CHECKPOINT_BUFFER = 55; + + // Use hash-slot pointer. You will be less anxious to modularise contracts later. + string constant CURRENCY_OS_SLOT_ID = "deps.CurrencyOS"; + string constant YAMATO_DEPOSITOR_SLOT_ID = "deps.YamatoDepositor"; + string constant YAMATO_BORROWER_SLOT_ID = "deps.YamatoBorrower"; + string constant YAMATO_REPAYER_SLOT_ID = "deps.YamatoRepayer"; + string constant YAMATO_WITHDRAWER_SLOT_ID = "deps.YamatoWithdrawer"; + string constant YAMATO_REDEEMER_SLOT_ID = "deps.YamatoRedeemer"; + string constant YAMATO_SWEEPER_SLOT_ID = "deps.YamatoSweeper"; + string constant POOL_SLOT_ID = "deps.Pool"; + string constant PRIORITY_REGISTRY_SLOT_ID = "deps.PriorityRegistry"; + string constant SCORE_REGISTRY_SLOT_ID = "deps.ScoreRegistry"; + + /* + =========================== + ~~~ SAFE HAVEN ENDED ~~~ + =========================== + */ + + /* + =========================== + !!! DANGER ZONE BEGINS !!! + =========================== + - Proxy patterns (UUPS) stores state onto ERC1967Proxy via `delegatecall` opcode. + - So modifying storage slot order in the next version of implementation would cause storage layout confliction. + - You can check whether your change will conflict or not by using `@openzeppelin/upgrades` + - Read more => https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts + */ + uint256 totalColl; + uint256 totalDebt; + + mapping(address => Pledge) pledges; + mapping(address => FlashLockData) flashlocks; // sender => + ... dep,bor,rep,wit,red,swe + + /* + =========================== + !!! DANGER ZONE ENDED !!! + =========================== + */ + + /* + ============================== + Modifier + ============================== + - onlyYamato + */ + modifier onlyYamato() override { + require(permitDeps(msg.sender), "You are not Yamato contract."); + _; + } + + /* + ============================== + Set-up functions + ============================== + - initialize + - setDeps + */ + function initialize(address _currencyOS) public initializer { + bytes32 CURRENCY_OS_KEY = bytes32( + keccak256(abi.encode(CURRENCY_OS_SLOT_ID)) + ); + assembly { + sstore(CURRENCY_OS_KEY, _currencyOS) + } + + __ReentrancyGuard_init(); + __Pausable_init(); + __YamatoStore_init(address(this)); + } + + /// @dev UUPS enforces you to modularise your contract into many because UUPS takes ~15KB/24.576KB of contract size limitation. + function setDeps( + address _yamatoDepositor, + address _yamatoBorrower, + address _yamatoRepayer, + address _yamatoWithdrawer, + address _yamatoRedeemer, + address _yamatoSweeper, + address _pool, + address _priorityRegistry, + address _scoreRegistry + ) external onlyGovernance { + /* + [ Deployment Order ] + Currency.deploy() + FeePool.deploy() + PriceFeed.deploy() + CurrencyOS.deploy(Currency,FeePool,PriceFeed) + Yamato.deploy(CurrencyOS) + YamatoDepositor.deploy(Yamato) + YamatoBorrower.deploy(Yamato) + YamatoRepayer.deploy(Yamato) + YamatoWithdrawer.deploy(Yamato) + YamatoRedeemer.deploy(Yamato) + YamatoSweeper.deploy(Yamato) + Pool.deploy(Yamato) + PriorityRegistry.deploy(Yamato) + Yamato.setDeps(YamatoDepositor,YamatoBorrower,YamatoRepayer,YamatoWithdrawer,YamatoRedeemer,YamatoSweeper,Pool,PriorityRegistry) + */ + bytes32 YAMATO_DEPOSITOR_KEY = bytes32( + keccak256(abi.encode(YAMATO_DEPOSITOR_SLOT_ID)) + ); + bytes32 YAMATO_BORROWER_KEY = bytes32( + keccak256(abi.encode(YAMATO_BORROWER_SLOT_ID)) + ); + bytes32 YAMATO_REPAYER_KEY = bytes32( + keccak256(abi.encode(YAMATO_REPAYER_SLOT_ID)) + ); + bytes32 YAMATO_WITHDRAWER_KEY = bytes32( + keccak256(abi.encode(YAMATO_WITHDRAWER_SLOT_ID)) + ); + bytes32 YAMATO_REDEEMER_KEY = bytes32( + keccak256(abi.encode(YAMATO_REDEEMER_SLOT_ID)) + ); + bytes32 YAMATO_SWEEPER_KEY = bytes32( + keccak256(abi.encode(YAMATO_SWEEPER_SLOT_ID)) + ); + bytes32 POOL_KEY = bytes32(keccak256(abi.encode(POOL_SLOT_ID))); + bytes32 PRIORITY_REGISTRY_KEY = bytes32( + keccak256(abi.encode(PRIORITY_REGISTRY_SLOT_ID)) + ); + bytes32 SCORE_REGISTRY_KEY = bytes32( + keccak256(abi.encode(SCORE_REGISTRY_SLOT_ID)) + ); + assembly { + sstore(YAMATO_DEPOSITOR_KEY, _yamatoDepositor) + sstore(YAMATO_BORROWER_KEY, _yamatoBorrower) + sstore(YAMATO_REPAYER_KEY, _yamatoRepayer) + sstore(YAMATO_WITHDRAWER_KEY, _yamatoWithdrawer) + sstore(YAMATO_REDEEMER_KEY, _yamatoRedeemer) + sstore(YAMATO_SWEEPER_KEY, _yamatoSweeper) + sstore(POOL_KEY, _pool) + sstore(PRIORITY_REGISTRY_KEY, _priorityRegistry) + sstore(SCORE_REGISTRY_KEY, _scoreRegistry) + } + } + + /* + ============================== + Storing functions + ============================== + - setPledge + - setTotalColl + - setTotalDebt + - setDepositAndBorrowLocks + - setWithdrawLocks + */ + /// @dev Only-yamato-package state mutation func + function setPledge( + address _owner, + Pledge memory _p + ) public override onlyYamato { + Pledge storage p = pledges[_owner]; + if (_p.debt == 0 && _p.coll == 0) { + _p.owner = address(0); + _p.isCreated = false; + _p.priority = 0; + } + if (p.coll != _p.coll) { + p.coll = _p.coll; + } + if (p.debt != _p.debt) { + p.debt = _p.debt; + } + if (p.owner != _p.owner) { + p.owner = _p.owner; + } + if (p.isCreated != _p.isCreated) { + p.isCreated = _p.isCreated; + } + if (p.priority != _p.priority) { + p.priority = _p.priority; + } + } + + /// @dev Only-yamato-package state mutation func + function setPledges( + Pledge[] memory _pledges + ) public override(IYamatoV4) onlyYamato { + for (uint256 i; i < _pledges.length; i++) { + Pledge memory _p = _pledges[i]; + if (_p.isCreated == false) { + continue; + } + setPledge(_p.owner, _p); + } + } + + /// @dev Only-yamato-package state mutation func + /// @dev totalColl is theoretically as same as all pledges.coll - but it can be differ from Pool.balance due to selfdestruct(address) + function setTotalColl(uint256 _totalColl) public override onlyYamato { + totalColl = _totalColl; + } + + /// @dev Only-yamato-package state mutation func + /// @dev totalDebt is theoretically as same as Currency.totalSupply() + function setTotalDebt(uint256 _totalDebt) public override onlyYamato { + totalDebt = _totalDebt; + } + + /// @dev Only-yamato-package state mutation func + /// @dev deposit-borrow-withdraw should not be in the same block to avoid flashloan attack. + function checkFlashLock( + address _owner + ) public view override onlyYamato returns (bool _isLocked) { + FlashLockData storage lock = flashlocks[_owner]; + if (lock.lockedBlockHeight == block.number) { + return _isLocked = true; + } + } + + /// @dev Only-yamato-package state mutation func + function setFlashLock(address _owner) public override onlyYamato { + FlashLockData storage lock = flashlocks[_owner]; + require( + lock.lockedBlockHeight <= block.number, + "FlashLock.lockedBlockHeight can't be more than currenct blockheight." + ); + + lock.lockedBlockHeight = block.number; + } + + /* + ============================== + Single Pledge Actions + ============================== + - deposit + - borrow + - repay + - withdraw + */ + + /// @notice Make a Pledge with ETH. "Top-up" supported. + /// @dev We haven't supported ERC-20 pledges and pool + function deposit() public payable nonReentrant whenNotPaused { + IYamatoDepositor(depositor()).runDeposit{value: msg.value}(msg.sender); + emit Deposited(msg.sender, msg.value); + } + + /// @notice Borrow in Currency. In that currency-unit (e.g., CJPY), 15.84%=RR, 0.16%=RRGas, 3.96%=SR, 0.4%=SRGas + /// @dev This function can't be executed just the same block with your deposit + /// @param borrowAmountInCurrency maximal redeemable amount + function borrow(uint256 borrowAmountInCurrency) public whenNotPaused { + uint256 fee = IYamatoBorrower(borrower()).runBorrow( + msg.sender, + borrowAmountInCurrency + ); + emit Borrowed(msg.sender, borrowAmountInCurrency, fee); + } + + /// @notice Recover the collateral of one's pledge. + /// @dev Need allowance. TCR will go up. + /// @param currencyAmount maximal redeemable amount + function repay(uint256 currencyAmount) public { + IYamatoRepayer(repayer()).runRepay(msg.sender, currencyAmount); + emit Repaid(msg.sender, currencyAmount); + } + + /// @notice Withdraw collaterals from one's pledge. + /// @dev Need reentrancy guard. TCR will go down. + /// @param ethAmount withdrawal amount + function withdraw(uint256 ethAmount) public nonReentrant { + IYamatoWithdrawer(withdrawer()).runWithdraw(msg.sender, ethAmount); + emit Withdrawn(msg.sender, ethAmount); + } + + /* + ============================== + Multi Pledge Actions + ============================== + - redeem + - sweep + */ + + /// @notice Retrieve ETH collaterals from Pledges by burning Currency + /// @dev Need allowance. Lowest ICR Pledges get redeemed first. TCR will go up. coll=0 pledges are to be remained. + /// @param maxRedemptionCurrencyAmount maximal redeemable amount + /// @param isCoreRedemption A flag for who to pay + function redeem( + uint256 maxRedemptionCurrencyAmount, + bool isCoreRedemption + ) public nonReentrant whenNotPaused { + IYamatoRedeemer.RedeemedArgs memory _args = IYamatoRedeemer(redeemer()) + .runRedeem( + IYamatoRedeemer.RunRedeemArgs( + msg.sender, + maxRedemptionCurrencyAmount, + isCoreRedemption + ) + ); + + emit Redeemed( + msg.sender, + _args.totalRedeemedCurrencyAmount, + _args.totalRedeemedEthAmount, + _args._pledgesOwner + ); + emit RedeemedMeta( + msg.sender, + _args.ethPriceInCurrency, + isCoreRedemption, + _args.gasCompensationInETH + ); + } + + /// @notice Initialize all pledges such that ICR is 0 (= (0*price)/debt ) + /// @dev Will be run by incentivised DAO member. Scan all pledges and filter debt>0, coll=0. Pay gas compensation from the 1% of SweepReserve at most, and as same as 1% of the actual sweeping amount. + function sweep() public nonReentrant whenNotPaused { + ( + uint256 _sweptAmount, + uint256 gasCompensationInCurrency, + address[] memory _pledgesOwner + ) = IYamatoSweeper(sweeper()).runSweep(msg.sender); + + emit Swept( + msg.sender, + _sweptAmount, + gasCompensationInCurrency, + _pledgesOwner + ); + } + + /* + ============================== + Internal Helpers + ============================== + - toggle + */ + + /// @dev Pausable + function toggle() external onlyGovernance { + if (paused()) { + _unpause(); + } else { + _pause(); + } + } + + /* + ============================== + State Getter Function + ============================== + - getPledge + - getStates + - getIndividualStates + */ + + /// @notice To give pledge access to YmtOS + /// @dev Interface can't return "struct memory" from public state variable + function getPledge( + address _owner + ) public view override returns (Pledge memory) { + return pledges[_owner]; + } + + /// @notice Provide the data of public storage. + function getStates() + public + view + override + returns (uint256, uint256, uint8, uint8, uint8, uint8) + { + return (totalColl, totalDebt, MCR, RRR, SRR, GRR); + } + + /// @notice Provide the data of individual pledge. + function getIndividualStates( + address owner + ) + public + view + returns ( + uint256 coll, + uint256 debt, + bool isCreated, + FlashLockData memory lock + ) + { + Pledge memory pledge = pledges[owner]; + return (pledge.coll, pledge.debt, pledge.isCreated, flashlocks[owner]); + } + + // @dev Yamato.sol must override it with correct logic. + function yamato() public view override returns (address) { + return address(this); + } + + /// @dev Get pool UUPS proxy address from slot + function pool() public view override returns (address _pool) { + bytes32 POOL_KEY = bytes32(keccak256(abi.encode(POOL_SLOT_ID))); + assembly { + _pool := sload(POOL_KEY) + } + } + + /// @dev Get priorityRegistry UUPS proxy address from slot + function priorityRegistry() + public + view + override + returns (address _priorityRegistry) + { + bytes32 PRIORITY_REGISTRY_KEY = bytes32( + keccak256(abi.encode(PRIORITY_REGISTRY_SLOT_ID)) + ); + assembly { + _priorityRegistry := sload(PRIORITY_REGISTRY_KEY) + } + } + + /// @dev Get scoreRegistry UUPS proxy address from slot + function scoreRegistry() + public + view + override + returns (address _scoreRegistry) + { + bytes32 SCORE_REGISTRY_KEY = bytes32( + keccak256(abi.encode(SCORE_REGISTRY_SLOT_ID)) + ); + assembly { + _scoreRegistry := sload(SCORE_REGISTRY_KEY) + } + } + + /// @dev Get depositor UUPS proxy address from slot + function depositor() public view override returns (address _depositor) { + bytes32 YAMATO_DEPOSITOR_KEY = bytes32( + keccak256(abi.encode(YAMATO_DEPOSITOR_SLOT_ID)) + ); + assembly { + _depositor := sload(YAMATO_DEPOSITOR_KEY) + } + } + + /// @dev Get borrower UUPS proxy address from slot + function borrower() public view override returns (address _borrower) { + bytes32 YAMATO_BORROWER_KEY = bytes32( + keccak256(abi.encode(YAMATO_BORROWER_SLOT_ID)) + ); + assembly { + _borrower := sload(YAMATO_BORROWER_KEY) + } + } + + /// @dev Get repayer UUPS proxy address from slot + function repayer() public view override returns (address _repayer) { + bytes32 YAMATO_REPAYER_KEY = bytes32( + keccak256(abi.encode(YAMATO_REPAYER_SLOT_ID)) + ); + assembly { + _repayer := sload(YAMATO_REPAYER_KEY) + } + } + + /// @dev Get withdrawer UUPS proxy address from slot + function withdrawer() public view override returns (address _withdrawer) { + bytes32 YAMATO_WITHDRAWER_KEY = bytes32( + keccak256(abi.encode(YAMATO_WITHDRAWER_SLOT_ID)) + ); + assembly { + _withdrawer := sload(YAMATO_WITHDRAWER_KEY) + } + } + + /// @dev Get redeemer UUPS proxy address from slot + function redeemer() public view override returns (address _redeemer) { + bytes32 YAMATO_REDEEMER_KEY = bytes32( + keccak256(abi.encode(YAMATO_REDEEMER_SLOT_ID)) + ); + assembly { + _redeemer := sload(YAMATO_REDEEMER_KEY) + } + } + + /// @dev Get sweeper UUPS proxy address from slot + function sweeper() public view override returns (address _sweeper) { + bytes32 YAMATO_SWEEPER_KEY = bytes32( + keccak256(abi.encode(YAMATO_SWEEPER_SLOT_ID)) + ); + assembly { + _sweeper := sload(YAMATO_SWEEPER_KEY) + } + } + + /// @dev Yamato.sol must override it with correct logic. + function currencyOS() + public + view + override(IYamato, YamatoBase) + returns (address _currencyOS) + { + bytes32 CURRENCY_OS_KEY = bytes32( + keccak256(abi.encode(CURRENCY_OS_SLOT_ID)) + ); + assembly { + _currencyOS := sload(CURRENCY_OS_KEY) + } + } + + /// @dev Yamato.sol must override it with correct logic. + function feePool() public view override returns (address) { + return ICurrencyOS(currencyOS()).feePool(); + } + + /// @dev Yamato.sol must override it with correct logic. + function priceFeed() + public + view + override(IYamato, YamatoBase) + returns (address) + { + return ICurrencyOS(currencyOS()).priceFeed(); + } + + /// @dev All YamatoStores and YamatoActions except Yamato.sol are NOT needed to modify these funcs. Just write the same signature and don't fill inside. Yamato.sol must override it with correct logic. + function permitDeps( + address _sender + ) public view override(IYamato, YamatoBase) returns (bool) { + bool permit; + address[10] memory deps = getDeps(); + for (uint256 i = 0; i < deps.length; i++) { + if (_sender == deps[i]) permit = true; + } + return permit; + } + + /// @dev Get package-deps to check onlyYamato-permitDeps logic + function getDeps() public view returns (address[10] memory) { + return [ + address(this), + depositor(), + borrower(), + repayer(), + withdrawer(), + redeemer(), + sweeper(), + pool(), + priorityRegistry(), + scoreRegistry() + ]; + } + + /* + * Only for test + */ + + /// @dev For test + function setPriorityRegistry( + address _priorityRegistry + ) external onlyGovernance { + bytes32 PRIORITY_REGISTRY_KEY = bytes32( + keccak256(abi.encode(PRIORITY_REGISTRY_SLOT_ID)) + ); + assembly { + sstore(PRIORITY_REGISTRY_KEY, _priorityRegistry) + } + } +} diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol new file mode 100644 index 00000000..27f18b06 --- /dev/null +++ b/contracts/YmtMinter.sol @@ -0,0 +1,127 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Interfaces/IYMT.sol"; +import "./Interfaces/IScoreRegistry.sol"; +import "./Interfaces/IScoreController.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; + +/*** + *@title Token Minter + */ + +contract YmtMinter is + UUPSBase, + ReentrancyGuardUpgradeable, + PausableUpgradeable +{ + event Minted(address indexed recipient, address score, uint256 minted); + + address public token; + address public controller; + + // user -> score -> value + mapping(address => mapping(address => uint256)) public minted; // minted amount of user from specific score. + + // minter -> user -> can mint? + mapping(address => mapping(address => bool)) public allowedToMintFor; // A can mint for B if [A => B => true]. + + function initialize( + address _token, + address _controller + ) public initializer { + token = _token; + controller = _controller; + __ReentrancyGuard_init(); + __Pausable_init(); + } + + function _mintFor(address scoreAddr_, address for_) internal { + require( + IScoreController(controller).scoreTypes(scoreAddr_) >= 0, + "dev: score is not added" + ); + + IScoreRegistry(scoreAddr_).userCheckpoint(for_); + uint256 totalMint = IScoreRegistry(scoreAddr_).integrateFraction(for_); + uint256 _toMint = totalMint - minted[for_][scoreAddr_]; + + if (_toMint != 0) { + IYMT(token).mint(for_, _toMint); + minted[for_][scoreAddr_] = totalMint; + + emit Minted(for_, scoreAddr_, totalMint); + } + } + + /*** + *@notice Mint everything which belongs to `msg.sender` and send to them + *@param scoreAddr_ `LiquidityScore` address to get mintable amount from + */ + function mint(address scoreAddr_) external nonReentrant { + _mintFor(scoreAddr_, msg.sender); + } + + /*** + *@notice Mint everything which belongs to `msg.sender` across multiple scores + *@param scoreAddrs_ List of `LiquidityScore` addresses + *@dev address[8]: 8 has randomly decided and has no meaning. + */ + function mintMany(address[8] memory scoreAddrs_) external nonReentrant { + for (uint256 i; i < 8; ) { + if (scoreAddrs_[i] == address(0)) { + break; + } + _mintFor(scoreAddrs_[i], msg.sender); + unchecked { + ++i; + } + } + } + + /*** + *@notice Mint tokens for `for_` + *@dev Only possible when `msg.sender` has been approved via `toggle_approve_mint` + *@param scoreAddr_ `LiquidityScore` address to get mintable amount from + *@param for_ Address to mint to + */ + function mintFor(address scoreAddr_, address for_) external nonReentrant { + if (allowedToMintFor[msg.sender][for_]) { + _mintFor(scoreAddr_, for_); + } + } + + /*** + *@notice allow `mintingUser` to mint for `msg.sender` + *@param mintingUser_ Address to toggle permission for + */ + function toggleApproveMint(address mintingUser_) external { + allowedToMintFor[mintingUser_][msg.sender] = !allowedToMintFor[ + mintingUser_ + ][msg.sender]; + } + + /* + ============================== + Internal Helpers + ============================== + - toggle + */ + + /// @dev Pausable + function toggle() external onlyGovernance { + if (paused()) { + _unpause(); + } else { + _pause(); + } + } +} diff --git a/contracts/veYMT.sol b/contracts/veYMT.sol index 6fd0ef73..8d93e1a8 100644 --- a/contracts/veYMT.sol +++ b/contracts/veYMT.sol @@ -2,46 +2,834 @@ pragma solidity 0.8.4; /* * SPDX-License-Identifier: GPL-3.0-or-later - * Written by 0xMotoko (0xmotoko@pm.me) - * Copyright (C) 2021 Yamato Protocol (DeFiGeek Community Japan) + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) */ //solhint-disable max-line-length //solhint-disable no-inline-assembly -import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; -import "./Interfaces/IveYMT.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "./Interfaces/IYMT.sol"; /** - * @author 0xMotoko * @title veYMT Token * @notice Locked YMT. An ERC-20 but w/o transfer() */ -contract veYMT is IveYMT { - string name; - string symbol; +contract veYMT is ReentrancyGuard { + // Voting escrow to have time-weighted votes + // Votes have a weight depending on time, so that users are committed + // to the future of (whatever they are voting for). + // The weight in this implementation is linear, and lock cannot be more than maxtime: + // w ^ + // 1 + / + // | / + // | / + // | / + // |/ + // 0 +--------+------> time + // maxtime (4 years?) - constructor() { - name = "Voting-escrow Yamato"; + struct Point { + int128 bias; + int128 slope; // - dweight / dt + uint256 ts; //timestamp + uint256 blk; // block + } + + // We cannot really do block numbers per se b/c slope is per time, not per block + // and per block could be fairly bad b/c Ethereum changes blocktimes. + // What we can do is to extrapolate ***At functions + struct LockedBalance { + int128 amount; + uint256 end; + } + + uint128 private constant DEPOSIT_FOR_TYPE = 0; + uint128 private constant CREATE_LOCK_TYPE = 1; + uint128 private constant INCREASE_LOCK_AMOUNT = 2; + uint128 private constant INCREASE_UNLOCK_TIME = 3; + + event CommitOwnership(address admin); + event ApplyOwnership(address admin); + event Deposit( + address indexed provider, + uint256 value, + uint256 indexed locktime, + uint128 _type, + uint256 ts + ); + event Withdraw(address indexed provider, uint256 value, uint256 ts); + event Supply(uint256 prevSupply, uint256 supply); + + uint256 public constant WEEK = 7 * 86400; // all future times are rounded by week + uint256 public constant MAXTIME = 4 * 365 * 86400; // 4 years + uint256 public constant MULTIPLIER = 1e18; + + address public token; + uint256 public supply; + + mapping(address => LockedBalance) public locked; + + //everytime user deposit/withdraw/change_locktime, these values will be updated; + uint256 public epoch; + + mapping(uint256 => Point) public pointHistory; // epoch -> unsigned point. + mapping(address => mapping(uint256 => Point)) public userPointHistory; // user -> Point[user_epoch] + mapping(address => uint256) public userPointEpoch; + mapping(uint256 => int128) public slopeChanges; // time -> signed slope change + + // Aragon's view methods for compatibility + address public controller; + bool public transfersEnabled; + + string public name; + string public symbol; + uint256 public decimals; + + // Checker for whitelisted (smart contract) wallets which are allowed to deposit + // The goal is to prevent tokenizing the escrow + address public futureSmartWalletChecker; + address public smartWalletChecker; + + address public admin; + address public futureAdmin; + + /*** + * @notice Contract constructor + * @param tokenAddr_ `YMT` token address + */ + constructor(address tokenAddr_) { + admin = msg.sender; + token = tokenAddr_; + pointHistory[0].blk = block.number; + pointHistory[0].ts = block.timestamp; + controller = msg.sender; + transfersEnabled = true; + + uint256 decimalsFromToken = IYMT(tokenAddr_).decimals(); + require( + decimalsFromToken <= 255, + "Decimals should be less than or equal to 255" + ); + decimals = decimalsFromToken; + + name = "Voting-escrowed Yamato"; symbol = "veYMT"; } - function mintableInTimeframe( - uint256 _start, - uint256 _end - ) public view override returns (uint256) { - return 1; + /*** + * @notice Transfer ownership of VotingEscrow contract to `addr_` + * @param addr Address to have ownership transferred to + */ + function commitTransferOwnership(address addr_) external onlyAdmin { + futureAdmin = addr_; + emit CommitOwnership(addr_); + } + + /*** + * @notice Apply ownership transfer + */ + function applyTransferOwnership() external onlyAdmin { + address _admin = futureAdmin; + require(_admin != address(0), "admin not set"); + admin = _admin; + emit ApplyOwnership(_admin); + } + + /*** + * @notice Set an external contract to check for approved smart contract wallets + * @param addr_ Address of Smart contract checker + */ + function commitSmartWalletChecker(address addr_) external onlyAdmin { + futureSmartWalletChecker = addr_; + } + + /*** + * @notice Apply setting external contract to check approved smart contract wallets + */ + function applySmartWalletChecker() external onlyAdmin { + smartWalletChecker = futureSmartWalletChecker; + } + + /*** + * @notice Get the most recently recorded rate of voting power decrease for `addr` + * @param addr_ Address of the user wallet + * @return Value of the slope + */ + function getLastUserSlope(address addr_) external view returns (int128) { + uint256 _uEpoch = userPointEpoch[addr_]; + return userPointHistory[addr_][_uEpoch].slope; + } + + /*** + * @notice Get the timestamp for checkpoint `idx_` for `addr_` + * @param addr_ User wallet address + * @param idx_ User epoch number + * @return Epoch time of the checkpoint + */ + function userPointHistoryTs( + address addr_, + uint256 idx_ + ) external view returns (uint256) { + return userPointHistory[addr_][idx_].ts; } + /*** + * @notice Get timestamp when `addr_`'s lock finishes + * @param addr_ User wallet + * @return Epoch time of the lock end + */ + function lockedEnd(address addr_) external view returns (uint256) { + return locked[addr_].end; + } + + /*** + * @notice Record global and per-user data to checkpoint + * @param addr_ User's wallet address. No user checkpoint if 0x0 + * @param oldLocked_ Pevious locked amount / end lock time for the user + * @param newLocked_ New locked amount / end lock time for the user + */ + function _checkpoint( + address addr_, + LockedBalance memory oldLocked_, + LockedBalance memory newLocked_ + ) internal { + Point memory _uOld; + Point memory _uNew; + int128 _oldDSlope = 0; + int128 _newDSlope = 0; + uint256 _epoch = epoch; + + if (addr_ != address(0)) { + // Calculate slopes and biases + // Kept at zero when they have to + if (oldLocked_.end > block.timestamp && oldLocked_.amount > 0) { + unchecked { + _uOld.slope = int128(oldLocked_.amount / int256(MAXTIME)); + } + _uOld.bias = + _uOld.slope * + int128(uint128(oldLocked_.end - block.timestamp)); + } + + if (newLocked_.end > block.timestamp && newLocked_.amount > 0) { + unchecked { + _uNew.slope = int128( + uint128(newLocked_.amount) / uint128(MAXTIME) + ); + } + _uNew.bias = + _uNew.slope * + int128(uint128(newLocked_.end - block.timestamp)); + } + + // Read values of scheduled changes in the slope + // old_locked.end can be in the past and in the future + // new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros + _oldDSlope = slopeChanges[oldLocked_.end]; + if (newLocked_.end != 0) { + if (newLocked_.end == oldLocked_.end) { + _newDSlope = _oldDSlope; + } else { + _newDSlope = slopeChanges[newLocked_.end]; + } + } + } + + Point memory _lastPoint = Point({ + bias: 0, + slope: 0, + ts: block.timestamp, + blk: block.number + }); + if (_epoch > 0) { + _lastPoint = Point({ + bias: pointHistory[_epoch].bias, + slope: pointHistory[_epoch].slope, + ts: pointHistory[_epoch].ts, + blk: pointHistory[_epoch].blk + }); + } + uint256 _lastCheckpoint = _lastPoint.ts; + + // initial_last_point is used for extrapolation to calculate block number + // (approximately, for *At methods) and save them + // as we cannot figure that out exactly from inside the contract + Point memory _initialLastPoint = Point({ + bias: _lastPoint.bias, + slope: _lastPoint.slope, + ts: _lastPoint.ts, + blk: _lastPoint.blk + }); + uint256 _blockSlope = 0; + if (block.timestamp > _lastPoint.ts) { + _blockSlope = + (MULTIPLIER * (block.number - _lastPoint.blk)) / + (block.timestamp - _lastPoint.ts); + } + // If last point is already recorded in this block, slope=0 + // But that's ok b/c we know the block in such case + + // Go over weeks to fill history and calculate what the current point is + uint256 _ti; + unchecked { + _ti = (_lastCheckpoint / WEEK) * WEEK; + } + for (uint256 i; i < 255; ) { + // Hopefully it won't happen that this won't get used in 5 years! + // If it does, users will be able to withdraw but vote weight will be broken + _ti += WEEK; + int128 _dSlope = 0; + if (_ti > block.timestamp) { + _ti = block.timestamp; + } else { + _dSlope = slopeChanges[_ti]; + } + + _lastPoint.bias -= + _lastPoint.slope * + int128(uint128(_ti) - uint128(_lastCheckpoint)); + _lastPoint.slope += _dSlope; + + if (_lastPoint.bias < 0) { + // This can happen + _lastPoint.bias = 0; + } + if (_lastPoint.slope < 0) { + // This cannot happen - just in case + _lastPoint.slope = 0; + } + + _lastCheckpoint = _ti; + _lastPoint.ts = _ti; + _lastPoint.blk = + _initialLastPoint.blk + + (_blockSlope * (_ti - _initialLastPoint.ts)) / + MULTIPLIER; + + _epoch += 1; + + if (_ti == block.timestamp) { + _lastPoint.blk = block.number; + break; + } else { + pointHistory[_epoch] = Point({ + bias: _lastPoint.bias, + slope: _lastPoint.slope, + ts: _lastPoint.ts, + blk: _lastPoint.blk + }); + } + unchecked { + ++i; + } + } + + epoch = _epoch; + // Now point_history is filled until t=now + + if (addr_ != address(0)) { + // If last point was in this block, the slope change has been applied already + // But in such case we have 0 slope(s) + _lastPoint.slope += (_uNew.slope - _uOld.slope); + _lastPoint.bias += (_uNew.bias - _uOld.bias); + if (_lastPoint.slope < 0) { + _lastPoint.slope = 0; + } + if (_lastPoint.bias < 0) { + _lastPoint.bias = 0; + } + } + + // Record the changed point into history + pointHistory[_epoch] = _lastPoint; + + address _addr = addr_; // To avoid being "Stack Too Deep" + + if (_addr != address(0)) { + // Schedule the slope changes (slope is going down) + // We subtract new_user_slope from [new_locked.end] + // and add old_user_slope to [old_locked.end] + if (oldLocked_.end > block.timestamp) { + // old_dslope was - u_old.slope, so we cancel that + _oldDSlope += _uOld.slope; + if (newLocked_.end == oldLocked_.end) { + _oldDSlope -= _uNew.slope; // It was a new deposit, not extension + } + slopeChanges[oldLocked_.end] = _oldDSlope; + } + + if (newLocked_.end > block.timestamp) { + if (newLocked_.end > oldLocked_.end) { + _newDSlope -= _uNew.slope; // old slope disappeared at this point + slopeChanges[newLocked_.end] = _newDSlope; + } + // else: we recorded it already in old_dslope + } + + // Now handle user history + uint256 _userEpoch; + unchecked { + _userEpoch = userPointEpoch[_addr] + 1; + } + userPointEpoch[_addr] = _userEpoch; + + _uNew.ts = block.timestamp; + _uNew.blk = block.number; + userPointHistory[_addr][_userEpoch] = _uNew; + } + } + + /*** + * @notice Deposit and lock tokens for a user + * @param addr_ User's wallet address + * @param value_ Amount to deposit + * @param unlockTime_ New time when to unlock the tokens, or 0 if unchanged + * @param lockedBalance_ Previous locked amount / timestamp + */ + function _depositFor( + address addr_, + uint256 value_, + uint256 unlockTime_, + LockedBalance memory lockedBalance_, + uint128 type_ + ) internal { + LockedBalance memory _locked = LockedBalance( + lockedBalance_.amount, + lockedBalance_.end + ); + LockedBalance memory _oldLocked = LockedBalance( + lockedBalance_.amount, + lockedBalance_.end + ); + + uint256 _supplyBefore = supply; + supply = _supplyBefore + value_; + + // Adding to existing lock, or if a lock is expired - creating a new one + _locked.amount += int128(uint128(value_)); + if (unlockTime_ != 0) { + _locked.end = unlockTime_; + } + locked[addr_] = _locked; + + // Possibilities: + // Both old_locked.end could be current or expired (>/< block.timestamp) + // value == 0 (extend lock) or value > 0 (add to lock or extend lock) + // _locked.end > block.timestamp (always) + _checkpoint(addr_, _oldLocked, _locked); + + if (value_ != 0) { + require( + IYMT(token).transferFrom(addr_, address(this), value_), + "Transfer failed" + ); + } + + emit Deposit(addr_, value_, _locked.end, type_, block.timestamp); + emit Supply(_supplyBefore, _supplyBefore + value_); + } + + /*** + * @notice Record global data to checkpoint + */ + function checkpoint() external { + LockedBalance memory _old; + LockedBalance memory _new; + _checkpoint(address(0), _old, _new); + } + + /*** + * @notice Deposit `_value` tokens for `_addr` and add to the lock + * @dev Anyone (even a smart contract) can deposit for someone else, but + cannot extend their locktime and deposit for a brand new user + * @param addr_ User's wallet address + * @param value_ Amount to add to user's lock + */ + function depositFor(address addr_, uint256 value_) external nonReentrant { + LockedBalance memory _locked = locked[addr_]; + + require(value_ > 0, "Need non-zero value"); + require(_locked.amount > 0, "No existing lock found"); + require( + _locked.end > block.timestamp, + "Cannot add to expired lock. Withdraw" + ); + + _depositFor(addr_, value_, 0, locked[addr_], DEPOSIT_FOR_TYPE); + } + + /*** + * @notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time` + * @param value_ Amount to deposit + * @param unlockTime_ Epoch time when tokens unlock, rounded down to whole weeks + */ + function createLock( + uint256 value_, + uint256 unlockTime_ + ) external nonReentrant { + uint256 _unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; + LockedBalance memory _locked = locked[msg.sender]; + + require(value_ > 0, "Need non-zero value"); + require(_locked.amount == 0, "Withdraw old tokens first"); + require( + _unlockTimeRounded > block.timestamp, + "Can only lock until time in the future" + ); + require( + _unlockTimeRounded <= block.timestamp + MAXTIME, + "Voting lock can be 4 years max" + ); + + _depositFor( + msg.sender, + value_, + _unlockTimeRounded, + _locked, + CREATE_LOCK_TYPE + ); + } + + /*** + * @notice Deposit `_value` additional tokens for `msg.sender` + without modifying the unlock time + * @param value_ Amount of tokens to deposit and add to the lock + */ + function increaseAmount(uint256 value_) external nonReentrant { + LockedBalance memory _locked = locked[msg.sender]; + + require(value_ > 0, "Need non-zero value"); + require(_locked.amount > 0, "No existing lock found"); + require( + _locked.end > block.timestamp, + "Cannot add to expired lock. Withdraw" + ); + + _depositFor(msg.sender, value_, 0, _locked, INCREASE_LOCK_AMOUNT); + } + + /*** + * @notice Extend the unlock time for `msg.sender` to `_unlock_time` + * @param unlockTime_ New epoch time for unlocking + */ + function increaseUnlockTime(uint256 unlockTime_) external nonReentrant { + LockedBalance memory _locked = locked[msg.sender]; + uint256 _unlockTimeRounded; + unchecked { + _unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; + } + + require(_locked.end > block.timestamp, "Lock expired"); + require(_locked.amount > 0, "Nothing is locked"); + require( + _unlockTimeRounded > _locked.end, + "Can only increase lock duration" + ); + require( + _unlockTimeRounded <= block.timestamp + MAXTIME, + "Voting lock can be 4 years max" + ); + + _depositFor( + msg.sender, + 0, + _unlockTimeRounded, + _locked, + INCREASE_UNLOCK_TIME + ); + } + + /*** + * @notice Withdraw all tokens for `msg.sender` + * @dev Only possible if the lock has expired + */ + function withdraw() external nonReentrant { + LockedBalance memory _locked = LockedBalance( + locked[msg.sender].amount, + locked[msg.sender].end + ); + require(block.timestamp >= _locked.end, "The lock didn't expire"); + uint256 _value = uint256(int256(_locked.amount)); + + LockedBalance memory _oldLocked = LockedBalance( + locked[msg.sender].amount, + locked[msg.sender].end + ); + + _locked.end = 0; + _locked.amount = 0; + locked[msg.sender] = LockedBalance(_locked.amount, _locked.end); + uint256 _supplyBefore = supply; + supply = _supplyBefore - _value; + + // old_locked can have either expired <= timestamp or zero end + // _locked has only 0 end + // Both can have >= 0 amount + _checkpoint(msg.sender, _oldLocked, _locked); + + require(IYMT(token).transfer(msg.sender, _value), "Transfer failed"); + + emit Withdraw(msg.sender, _value, block.timestamp); + emit Supply(_supplyBefore, _supplyBefore - _value); + } + + // The following ERC20/minime-compatible methods are not real balanceOf and supply! + // They measure the weights for the purpose of voting, so they don't represent + // real coins. + + /*** + * @notice Binary search to estimate epoch for block number + * @param block_ Block to find + * @param maxEpoch_ Don't go beyond this epoch + * @return Approximate epoch for block + */ + function findBlockEpoch( + uint256 block_, + uint256 maxEpoch_ + ) internal view returns (uint256) { + // Binary search + uint256 _min = 0; + uint256 _max = maxEpoch_; + unchecked { + for (uint256 i; i < 128; i++) { + // Will be always enough for 128-bit numbers + if (_min >= _max) { + break; + } + uint256 _mid = (_min + _max + 1) / 2; + if (pointHistory[_mid].blk <= block_) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + } + return _min; + } + + /*** + * @notice Get the current voting power for `msg.sender` + * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility + * @param addr_ User wallet address + * @param t_ Epoch time to return voting power at + * @return User voting power + */ + function balanceOf( + address addr_, + uint256 t_ + ) external view returns (uint256) { + uint256 epoch_ = userPointEpoch[addr_]; + if (epoch_ == 0) { + return 0; + } else { + Point memory lastPoint = userPointHistory[addr_][epoch_]; + lastPoint.bias -= + lastPoint.slope * + int128(int256(t_) - int256(lastPoint.ts)); + if (lastPoint.bias < 0) { + lastPoint.bias = 0; + } + return uint256(int256(lastPoint.bias)); + } + } + + /*** + * @notice Get the current voting power for `msg.sender` + * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility + * @param addr_ User wallet address + * @return User voting power + */ + function balanceOf(address addr_) external view returns (uint256) { + uint256 epoch_ = userPointEpoch[addr_]; + if (epoch_ == 0) { + return 0; + } else { + Point memory lastPoint = userPointHistory[addr_][epoch_]; + lastPoint.bias -= + lastPoint.slope * + int128(int256(block.timestamp) - int256(lastPoint.ts)); + if (lastPoint.bias < 0) { + lastPoint.bias = 0; + } + return uint256(int256(lastPoint.bias)); + } + } + + /*** + * @notice Measure voting power of `addr` at block height `_block` + * @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime + * @param addr_ User's wallet address + * @param block_ Block to calculate the voting power at + * @return Voting power + */ function balanceOfAt( - address _addr, - uint256 _at - ) public view override returns (uint256) { - return 1; + address addr_, + uint256 block_ + ) external view returns (uint256) { + // Copying and pasting totalSupply code because Vyper cannot pass by + // reference yet + require(block_ <= block.number, "Cannot look up future block"); + + // Binary search + uint256 min_ = 0; + uint256 max_ = userPointEpoch[addr_]; + + unchecked { + for (uint i = 0; i < 128; i++) { + // Will be always enough for 128-bit numbers + if (min_ >= max_) { + break; + } + uint256 mid_ = (min_ + max_ + 1) / 2; + if (userPointHistory[addr_][mid_].blk <= block_) { + min_ = mid_; + } else { + max_ = mid_ - 1; + } + } + } + + Point memory _upoint = userPointHistory[addr_][min_]; + uint256 _maxEpoch = epoch; + uint256 _epoch = findBlockEpoch(block_, _maxEpoch); + Point memory _point0 = pointHistory[_epoch]; + uint256 _dBlock = 0; + uint256 _dt = 0; + + if (_epoch < _maxEpoch) { + Point memory _point1 = pointHistory[_epoch + 1]; + _dBlock = _point1.blk - _point0.blk; + _dt = _point1.ts - _point0.ts; + } else { + _dBlock = block.number - _point0.blk; + _dt = block.timestamp - _point0.ts; + } + + uint256 _blockTime = _point0.ts; + if (_dBlock != 0) { + _blockTime += (_dt * (block_ - _point0.blk)) / _dBlock; + } + + _upoint.bias -= + _upoint.slope * + int128(int256(_blockTime) - int256(_upoint.ts)); + if (_upoint.bias >= 0) { + return uint256(int256(_upoint.bias)); + } else { + return 0; + } + } + + /*** + * @notice Calculate total voting power at some point in the past + * @param point_ The point (bias/slope) to start search from + * @param t_ Time to calculate the total voting power at + * @return Total voting power at that time + */ + function supplyAt( + Point memory point_, + uint256 t_ + ) internal view returns (uint256) { + Point memory _lastPoint = point_; + uint256 _ti; + unchecked { + _ti = (_lastPoint.ts / WEEK) * WEEK; + } + for (uint256 i; i < 255; ) { + _ti += WEEK; + int128 _dSlope = 0; + if (_ti > t_) { + _ti = t_; + } else { + _dSlope = slopeChanges[_ti]; + } + _lastPoint.bias -= + _lastPoint.slope * + int128(int256(_ti) - int256(_lastPoint.ts)); + if (_ti == t_) { + break; + } + _lastPoint.slope += _dSlope; + _lastPoint.ts = _ti; + unchecked { + ++i; + } + } + + if (_lastPoint.bias < 0) { + _lastPoint.bias = 0; + } + return uint256(int256(_lastPoint.bias)); + } + + /*** + * @notice Calculate total voting power + * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + * @param t_ Time to calculate the total voting power at + * @return Total voting power + */ + function totalSupply(uint256 t_) external view returns (uint256) { + uint256 _epoch = epoch; + Point memory _lastPoint = pointHistory[_epoch]; + return supplyAt(_lastPoint, t_); + } + + /*** + * @notice Calculate total voting power + * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + * @return Total voting power + */ + function totalSupply() external view returns (uint256) { + uint256 _epoch = epoch; + Point memory _lastPoint = pointHistory[_epoch]; + return supplyAt(_lastPoint, block.timestamp); + } + + /*** + * @notice Calculate total voting power at some point in the past + * @param block_ Block to calculate the total voting power at + * @return Total voting power at `_block` + */ + function totalSupplyAt(uint256 block_) external view returns (uint256) { + require(block_ <= block.number, "Invalid block number"); + uint256 _epoch = epoch; + uint256 _targetEpoch = findBlockEpoch(block_, _epoch); + + Point memory _point = pointHistory[_targetEpoch]; + uint256 _dt = 0; + if (_targetEpoch < _epoch) { + Point memory _pointNext = pointHistory[_targetEpoch + 1]; + if (_point.blk != _pointNext.blk) { + _dt = + ((block_ - _point.blk) * (_pointNext.ts - _point.ts)) / + (_pointNext.blk - _point.blk); + } + } else { + if (_point.blk != block.number) { + _dt = + ((block_ - _point.blk) * (block.timestamp - _point.ts)) / + (block.number - _point.blk); + } + } + // Now _dt contains info on how far are we beyond point + return supplyAt(_point, _point.ts + _dt); + } + + /*** + * @dev Dummy method required for Aragon compatibility + */ + function changeController(address newController) external { + require( + msg.sender == controller, + "Only the controller can call this function" + ); + controller = newController; } - function totalSupplyAt(uint256 _at) public view override returns (uint256) { - return 1; + modifier onlyAdmin() { + require(admin == msg.sender, "admin only"); + _; } } From 4acef22b3f740d65e97ea7d25fc0674c054dfd58 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 20 Nov 2023 14:53:25 +0900 Subject: [PATCH 052/412] code format --- test/param/version.ts | 61 +++++++++++----------- test/unit/BurnCJPY.test.ts | 18 ++++--- test/unit/CurrencyOS.test.ts | 25 +++++---- test/unit/Events.test.ts | 4 +- test/unit/FlashLock.test.ts | 20 ++++--- test/unit/MintCJPY.test.ts | 20 ++++--- test/unit/Pool.test.ts | 4 +- test/unit/PriceChangeAndRedemption.test.ts | 20 ++++--- test/unit/PriceFeed.test.ts | 2 +- test/unit/PriorityRegistry.test.ts | 10 +++- test/unit/Yamato.test.ts | 8 ++- 11 files changed, 114 insertions(+), 78 deletions(-) diff --git a/test/param/version.ts b/test/param/version.ts index 08698a9e..9fafdc15 100644 --- a/test/param/version.ts +++ b/test/param/version.ts @@ -29,34 +29,33 @@ export interface Version { veYMT: string; } - -export const contractVersion : Version = { - "CJPY": "CJPY", - "ChainLinkMock": "ChainLinkMock", - "Currency": "Currency", - "CurrencyOS": "CurrencyOSV2", - "Dependencies": "Dependencies", - "FeePool": "FeePool", - "Import": "Import", - "Interfaces": "Interfaces", - "Minter": "Minter", - "OracleMockBase": "OracleMockBase", - "Pool": "PoolV2", - "PriceFeed": "PriceFeedV3", - "PriorityRegistry": "PriorityRegistryV6", - "ScoreRegistry": "ScoreRegistry", - "TellorCallerMock": "TellorCallerMock", - "Vester": "Vester", - "VesterFactory": "VesterFactory", - "YMT": "YMT", - "Yamato": "YamatoV3", - "YamatoBorrower": "YamatoBorrower", - "YamatoDepositor": "YamatoDepositorV2", - "YamatoRedeemer": "YamatoRedeemerV4", - "YamatoRepayer": "YamatoRepayerV2", - "YamatoSweeper": "YamatoSweeperV2", - "YamatoWithdrawer": "YamatoWithdrawerV2", - "YmtOS": "YmtOS", - "curveFork": "curveFork", - "veYMT": "veYMT" -} +export const contractVersion: Version = { + CJPY: "CJPY", + ChainLinkMock: "ChainLinkMock", + Currency: "Currency", + CurrencyOS: "CurrencyOSV2", + Dependencies: "Dependencies", + FeePool: "FeePool", + Import: "Import", + Interfaces: "Interfaces", + Minter: "Minter", + OracleMockBase: "OracleMockBase", + Pool: "PoolV2", + PriceFeed: "PriceFeedV3", + PriorityRegistry: "PriorityRegistryV6", + ScoreRegistry: "ScoreRegistry", + TellorCallerMock: "TellorCallerMock", + Vester: "Vester", + VesterFactory: "VesterFactory", + YMT: "YMT", + Yamato: "YamatoV3", + YamatoBorrower: "YamatoBorrower", + YamatoDepositor: "YamatoDepositorV2", + YamatoRedeemer: "YamatoRedeemerV4", + YamatoRepayer: "YamatoRepayerV2", + YamatoSweeper: "YamatoSweeperV2", + YamatoWithdrawer: "YamatoWithdrawerV2", + YmtOS: "YmtOS", + curveFork: "curveFork", + veYMT: "veYMT", +}; diff --git a/test/unit/BurnCJPY.test.ts b/test/unit/BurnCJPY.test.ts index a002646c..85ef625c 100644 --- a/test/unit/BurnCJPY.test.ts +++ b/test/unit/BurnCJPY.test.ts @@ -106,13 +106,15 @@ describe("burnCurrency :: contract Yamato", () => { await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy(contractVersion["FeePool"], []); + FeePool = await getProxy( + contractVersion["FeePool"], + [] + ); - CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ - CJPY.address, - PriceFeed.address, - FeePool.address, - ]); + CurrencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, PriceFeed.address, FeePool.address] + ); Yamato = await getLinkedProxy( contractVersion["Yamato"], @@ -152,7 +154,9 @@ describe("burnCurrency :: contract Yamato", () => { ["PledgeLib"] ); - Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [ + Yamato.address, + ]); PriorityRegistry = await getLinkedProxy< PriorityRegistry, diff --git a/test/unit/CurrencyOS.test.ts b/test/unit/CurrencyOS.test.ts index 0564f9eb..7fdf84dc 100644 --- a/test/unit/CurrencyOS.test.ts +++ b/test/unit/CurrencyOS.test.ts @@ -58,13 +58,21 @@ describe("CurrencyOS", () => { mockYamatoDepositor = await getFakeProxy( contractVersion["YamatoDepositor"] ); - mockYamatoBorrower = await getFakeProxy(contractVersion["YamatoBorrower"]); - mockYamatoRepayer = await getFakeProxy(contractVersion["YamatoRepayer"]); + mockYamatoBorrower = await getFakeProxy( + contractVersion["YamatoBorrower"] + ); + mockYamatoRepayer = await getFakeProxy( + contractVersion["YamatoRepayer"] + ); mockYamatoWithdrawer = await getFakeProxy( contractVersion["YamatoWithdrawer"] ); - mockYamatoRedeemer = await getFakeProxy(contractVersion["YamatoRedeemer"]); - mockYamatoSweeper = await getFakeProxy(contractVersion["YamatoSweeper"]); + mockYamatoRedeemer = await getFakeProxy( + contractVersion["YamatoRedeemer"] + ); + mockYamatoSweeper = await getFakeProxy( + contractVersion["YamatoSweeper"] + ); mockYamato.depositor.returns(mockYamatoDepositor.address); mockYamato.borrower.returns(mockYamatoBorrower.address); mockYamato.repayer.returns(mockYamatoRepayer.address); @@ -73,11 +81,10 @@ describe("CurrencyOS", () => { mockYamato.sweeper.returns(mockYamatoSweeper.address); mockYamato.permitDeps.returns(true); - currencyOS = await getProxy(contractVersion["CurrencyOS"], [ - mockCJPY.address, - mockFeed.address, - mockFeePool.address, - ]); + currencyOS = await getProxy( + contractVersion["CurrencyOS"], + [mockCJPY.address, mockFeed.address, mockFeePool.address] + ); mockCJPY.mint.returns(0); mockCJPY.burn.returns(0); diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index 1a1bffff..b07558a6 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -368,7 +368,9 @@ describe("story Events", function () { mockYamatoRedeemer.yamato.returns(yamatoDummy.address); mockYamatoSweeper.yamato.returns(yamatoDummy.address); - pool = await getProxy(contractVersion["Pool"], [yamatoDummy.address]); + pool = await getProxy(contractVersion["Pool"], [ + yamatoDummy.address, + ]); await (await yamatoDummy.setPool(pool.address)).wait(); }); diff --git a/test/unit/FlashLock.test.ts b/test/unit/FlashLock.test.ts index cac0265e..96554d5c 100644 --- a/test/unit/FlashLock.test.ts +++ b/test/unit/FlashLock.test.ts @@ -102,20 +102,22 @@ describe("FlashLock :: contract Yamato", () => { PriceFeed = await getProxy( contractVersion["PriceFeed"], - [ChainLinkEthUsd.address, ChainLinkUsdJpy.address], + [ChainLinkEthUsd.address, ChainLinkUsdJpy.address] ); CJPY = await (( await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy(contractVersion["FeePool"], []); + FeePool = await getProxy( + contractVersion["FeePool"], + [] + ); - CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ - CJPY.address, - PriceFeed.address, - FeePool.address, - ]); + CurrencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, PriceFeed.address, FeePool.address] + ); Yamato = await getLinkedProxy( contractVersion["Yamato"], @@ -155,7 +157,9 @@ describe("FlashLock :: contract Yamato", () => { ["PledgeLib"] ); - Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [ + Yamato.address, + ]); PriorityRegistry = await getLinkedProxy< PriorityRegistry, diff --git a/test/unit/MintCJPY.test.ts b/test/unit/MintCJPY.test.ts index bbac9456..0358f8cc 100644 --- a/test/unit/MintCJPY.test.ts +++ b/test/unit/MintCJPY.test.ts @@ -98,20 +98,22 @@ describe("MintCJPY :: contract Yamato", () => { PriceFeed = await getProxy( contractVersion["PriceFeed"], - [ChainLinkEthUsd.address, ChainLinkUsdJpy.address], + [ChainLinkEthUsd.address, ChainLinkUsdJpy.address] ); CJPY = await (( await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy(contractVersion["FeePool"], []); + FeePool = await getProxy( + contractVersion["FeePool"], + [] + ); - CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ - CJPY.address, - PriceFeed.address, - FeePool.address, - ]); + CurrencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, PriceFeed.address, FeePool.address] + ); Yamato = await getLinkedProxy( contractVersion["Yamato"], @@ -151,7 +153,9 @@ describe("MintCJPY :: contract Yamato", () => { ["PledgeLib"] ); - Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [ + Yamato.address, + ]); PriorityRegistry = await getLinkedProxy< PriorityRegistry, diff --git a/test/unit/Pool.test.ts b/test/unit/Pool.test.ts index 20eab938..90d7311d 100644 --- a/test/unit/Pool.test.ts +++ b/test/unit/Pool.test.ts @@ -21,7 +21,9 @@ describe("contract Pool", function () { accounts = await ethers.getSigners(); let mockYamato = await getFakeProxy(contractVersion["Yamato"]); mockYamato.permitDeps.returns(false); - pool = await getProxy(contractVersion["Pool"], [mockYamato.address]); + pool = await getProxy(contractVersion["Pool"], [ + mockYamato.address, + ]); }); beforeEach(async () => { diff --git a/test/unit/PriceChangeAndRedemption.test.ts b/test/unit/PriceChangeAndRedemption.test.ts index 06798b65..2a9d4d9f 100644 --- a/test/unit/PriceChangeAndRedemption.test.ts +++ b/test/unit/PriceChangeAndRedemption.test.ts @@ -109,7 +109,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { PriceFeed = await getProxy( contractVersion["PriceFeed"], - [ChainLinkEthUsd.address, ChainLinkUsdJpy.address], + [ChainLinkEthUsd.address, ChainLinkUsdJpy.address] ); await (await PriceFeed.fetchPrice()).wait(); @@ -117,13 +117,15 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy(contractVersion["FeePool"], []); + FeePool = await getProxy( + contractVersion["FeePool"], + [] + ); - CurrencyOS = await getProxy(contractVersion["CurrencyOS"], [ - CJPY.address, - PriceFeed.address, - FeePool.address, - ]); + CurrencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, PriceFeed.address, FeePool.address] + ); const PledgeLib = ( await (await ethers.getContractFactory("PledgeLib")).deploy() @@ -167,7 +169,9 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { ["PledgeLib"] ); - Pool = await getProxy(contractVersion["Pool"], [Yamato.address]); + Pool = await getProxy(contractVersion["Pool"], [ + Yamato.address, + ]); PriorityRegistry = await getLinkedProxy< PriorityRegistryV6, diff --git a/test/unit/PriceFeed.test.ts b/test/unit/PriceFeed.test.ts index f8a18d0a..cf9a73a1 100644 --- a/test/unit/PriceFeed.test.ts +++ b/test/unit/PriceFeed.test.ts @@ -146,7 +146,7 @@ describe("PriceFeed", function () { feed = await getProxy( contractVersion["PriceFeed"], - [mockAggregatorV3EthUsd.address, mockAggregatorV3JpyUsd.address], + [mockAggregatorV3EthUsd.address, mockAggregatorV3JpyUsd.address] ); assertChainlink( diff --git a/test/unit/PriorityRegistry.test.ts b/test/unit/PriorityRegistry.test.ts index 5b33a7f3..365faca4 100644 --- a/test/unit/PriorityRegistry.test.ts +++ b/test/unit/PriorityRegistry.test.ts @@ -45,7 +45,9 @@ describe("contract PriorityRegistry", function () { mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); mockFeePool = await getFakeProxy(contractVersion["FeePool"]); - mockCurrencyOS = await getFakeProxy(contractVersion["CurrencyOS"]); + mockCurrencyOS = await getFakeProxy( + contractVersion["CurrencyOS"] + ); const PledgeLib = ( await (( await ethers.getContractFactory("PledgeLib") @@ -88,7 +90,11 @@ describe("contract PriorityRegistry", function () { priorityRegistry = await getLinkedProxy< PriorityRegistryV6, PriorityRegistryV6__factory - >(contractVersion["PriorityRegistry"], [yamatoDummy.address], ["PledgeLib"]); + >( + contractVersion["PriorityRegistry"], + [yamatoDummy.address], + ["PledgeLib"] + ); await ( await yamatoDummy.setPriorityRegistry(priorityRegistry.address) diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index 0016c71c..e252e537 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -147,7 +147,9 @@ describe("contract Yamato - pure func quickier tests", function () { ).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec - pool = await getProxy(contractVersion["Pool"], [yamato.address]); + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); // Note: Will use later for the redeem() test priorityRegistry = await getLinkedProxy< @@ -344,7 +346,9 @@ describe("contract Yamato", function () { ).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec - pool = await getProxy(contractVersion["Pool"], [yamato.address]); + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); // Note: Will use later for the redeem() test priorityRegistry = await getLinkedProxy< From 43f88d145fb1c8e38b59fe54bb7cc939ae102c2c Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 21 Nov 2023 11:18:11 +0900 Subject: [PATCH 053/412] Fix Contract build error. --- contracts/CurrencyOSV3.sol | 48 ++++++++++++++++++++---------- contracts/FeePool.sol | 4 +-- contracts/Interfaces/IveYMTOld.sol | 23 ++++++++++++++ contracts/YamatoBorrowerV2.sol | 2 +- contracts/YamatoRedeemerV5.sol | 2 +- contracts/YamatoV4.sol | 15 ++++++---- contracts/YmtMinter.sol | 1 + contracts/YmtOS.sol | 6 ++-- 8 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 contracts/Interfaces/IveYMTOld.sol diff --git a/contracts/CurrencyOSV3.sol b/contracts/CurrencyOSV3.sol index d7b0c293..00e4cf6d 100644 --- a/contracts/CurrencyOSV3.sol +++ b/contracts/CurrencyOSV3.sol @@ -43,11 +43,7 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { function initialize( address currencyAddr, address feedAddr, - address feePoolAddr, - address ymatAddr, - address veymtAddr, - address minterAddr, - address scoreWeightControllerAddr + address feePoolAddr ) public initializer { __UUPSBase_init(); @@ -56,20 +52,10 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { keccak256(abi.encode(PRICEFEED_SLOT_ID)) ); bytes32 FEEPOOL_KEY = bytes32(keccak256(abi.encode(FEEPOOL_SLOT_ID))); - bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); - bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); - bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); - bytes32 WEIGHT_CONTROLLER_KEY = bytes32( - keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) - ); assembly { sstore(CURRENCY_KEY, currencyAddr) sstore(PRICEFEED_KEY, feedAddr) sstore(FEEPOOL_KEY, feePoolAddr) - sstore(YMT_KEY, ymatAddr) - sstore(VEYMT_KEY, veymtAddr) - sstore(MINTER_KEY, minterAddr) - sstore(WEIGHT_CONTROLLER_KEY, scoreWeightControllerAddr) } } @@ -82,6 +68,38 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { } } + function setYMT(address ymtAddr) external onlyGovernance { + bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + assembly { + sstore(YMT_KEY, ymtAddr) + } + } + + function setVeYMT(address veymtAddr) external onlyGovernance { + bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); + assembly { + sstore(VEYMT_KEY, veymtAddr) + } + } + + function setYmtMinter(address minterAddr) external onlyGovernance { + bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); + assembly { + sstore(MINTER_KEY, minterAddr) + } + } + + function setScoreWeightController( + address scoreWeightControllerAddr + ) external onlyGovernance { + bytes32 WEIGHT_CONTROLLER_KEY = bytes32( + keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) + ); + assembly { + sstore(WEIGHT_CONTROLLER_KEY, scoreWeightControllerAddr) + } + } + modifier onlyYamato() { if (yamatoes.length == 0) { revert("No Yamato is registered."); diff --git a/contracts/FeePool.sol b/contracts/FeePool.sol index 65a4f97f..d51a0428 100644 --- a/contracts/FeePool.sol +++ b/contracts/FeePool.sol @@ -9,7 +9,7 @@ pragma solidity 0.8.4; //solhint-disable max-line-length //solhint-disable no-inline-assembly -import "./Interfaces/IveYMT.sol"; +import "./Interfaces/IveYMTOld.sol"; import "./Interfaces/IFeePool.sol"; import "./Dependencies/UUPSBase.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; @@ -64,7 +64,7 @@ contract FeePool is IFeePool, UUPSBase, ReentrancyGuardUpgradeable { modifier onlyVeYMT() { require( - IveYMT(veYMT()).balanceOfAt(msg.sender, block.number) > 0, + IveYMTOld(veYMT()).balanceOfAt(msg.sender, block.number) > 0, "You are not a veYMT holder." ); _; diff --git a/contracts/Interfaces/IveYMTOld.sol b/contracts/Interfaces/IveYMTOld.sol new file mode 100644 index 00000000..0211b3d8 --- /dev/null +++ b/contracts/Interfaces/IveYMTOld.sol @@ -0,0 +1,23 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +interface IveYMTOld { + function mintableInTimeframe( + uint256 _start, + uint256 _end + ) external view returns (uint256); + + function balanceOfAt( + address _addr, + uint256 _at + ) external view returns (uint256); + + function totalSupplyAt(uint256 _at) external view returns (uint256); +} diff --git a/contracts/YamatoBorrowerV2.sol b/contracts/YamatoBorrowerV2.sol index a30d3fd4..3af9ea9f 100644 --- a/contracts/YamatoBorrowerV2.sol +++ b/contracts/YamatoBorrowerV2.sol @@ -25,7 +25,7 @@ import "hardhat/console.sol"; /// @title Yamato Borrower Contract -contract YamatoBorrower is IYamatoBorrower, YamatoAction { +contract YamatoBorrowerV2 is IYamatoBorrower, YamatoAction { using PledgeLib for IYamato.Pledge; using PledgeLib for uint256; diff --git a/contracts/YamatoRedeemerV5.sol b/contracts/YamatoRedeemerV5.sol index beb22e7d..32a4ca34 100644 --- a/contracts/YamatoRedeemerV5.sol +++ b/contracts/YamatoRedeemerV5.sol @@ -28,7 +28,7 @@ import "hardhat/console.sol"; /// @title Yamato Redeemer Contract -contract YamatoRedeemerV is IYamatoRedeemerV4, YamatoAction { +contract YamatoRedeemerV5 is IYamatoRedeemerV4, YamatoAction { using PledgeLib for IYamato.Pledge; using PledgeLib for uint256; diff --git a/contracts/YamatoV4.sol b/contracts/YamatoV4.sol index b7504697..e66b7abd 100644 --- a/contracts/YamatoV4.sol +++ b/contracts/YamatoV4.sol @@ -118,6 +118,7 @@ contract YamatoV4 is ============================== - initialize - setDeps + - setScreRegistory */ function initialize(address _currencyOS) public initializer { bytes32 CURRENCY_OS_KEY = bytes32( @@ -141,8 +142,7 @@ contract YamatoV4 is address _yamatoRedeemer, address _yamatoSweeper, address _pool, - address _priorityRegistry, - address _scoreRegistry + address _priorityRegistry ) external onlyGovernance { /* [ Deployment Order ] @@ -183,9 +183,6 @@ contract YamatoV4 is bytes32 PRIORITY_REGISTRY_KEY = bytes32( keccak256(abi.encode(PRIORITY_REGISTRY_SLOT_ID)) ); - bytes32 SCORE_REGISTRY_KEY = bytes32( - keccak256(abi.encode(SCORE_REGISTRY_SLOT_ID)) - ); assembly { sstore(YAMATO_DEPOSITOR_KEY, _yamatoDepositor) sstore(YAMATO_BORROWER_KEY, _yamatoBorrower) @@ -195,6 +192,14 @@ contract YamatoV4 is sstore(YAMATO_SWEEPER_KEY, _yamatoSweeper) sstore(POOL_KEY, _pool) sstore(PRIORITY_REGISTRY_KEY, _priorityRegistry) + } + } + + function setScoreRegistory(address _scoreRegistry) external onlyGovernance { + bytes32 SCORE_REGISTRY_KEY = bytes32( + keccak256(abi.encode(SCORE_REGISTRY_SLOT_ID)) + ); + assembly { sstore(SCORE_REGISTRY_KEY, _scoreRegistry) } } diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index 27f18b06..d8b967bd 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -11,6 +11,7 @@ pragma solidity 0.8.4; import "./Interfaces/IYMT.sol"; import "./Interfaces/IScoreRegistry.sol"; import "./Interfaces/IScoreController.sol"; +import "./Dependencies/UUPSBase.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; diff --git a/contracts/YmtOS.sol b/contracts/YmtOS.sol index f494d36c..a91c2776 100644 --- a/contracts/YmtOS.sol +++ b/contracts/YmtOS.sol @@ -1,4 +1,5 @@ pragma solidity 0.8.4; +// This contract is not in use. /* * SPDX-License-Identifier: GPL-3.0-or-later @@ -11,6 +12,7 @@ pragma solidity 0.8.4; import "./veYMT.sol"; import "./Interfaces/IYMT.sol"; +import "./Interfaces/IveYMTOld.sol"; import "./Interfaces/IYamato.sol"; import "./Interfaces/IYmtOS.sol"; import "./Dependencies/UUPSBase.sol"; @@ -102,7 +104,7 @@ contract YmtOS is IYmtOS, UUPSBase { uint256 _cycle = getLastCycle(); uint256 _at = _cycle * CYCLE_SIZE; uint256[2] memory _range = getLastCycleBlockRange(); - uint256 _mintableInTimeframe = IveYMT(veYMT()).mintableInTimeframe( + uint256 _mintableInTimeframe = IveYMTOld(veYMT()).mintableInTimeframe( _range[0], _range[1] ); @@ -177,7 +179,7 @@ contract YmtOS is IYmtOS, UUPSBase { IYamato.Pledge memory _pledge, uint256 _at ) public view returns (uint256) { - uint256 veScore = IveYMT(veYMT()).balanceOfAt(_pledge.owner, _at); + uint256 veScore = IveYMTOld(veYMT()).balanceOfAt(_pledge.owner, _at); // TODO: Do some magic :) From 4bb51715662f464b05e56c91744a20080b2eebed Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 21 Nov 2023 11:26:44 +0900 Subject: [PATCH 054/412] Already existing test code made to work --- test/param/version.ts | 18 ++--- test/unit/BurnCJPY.test.ts | 45 ++++++++++-- test/unit/Events.test.ts | 49 ++++++++++++-- test/unit/FlashLock.test.ts | 45 ++++++++++-- test/unit/MintCJPY.test.ts | 45 ++++++++++-- test/unit/OracleMock.test.ts | 2 +- test/unit/PriceChangeAndRedemption.test.ts | 61 +++++++++++++---- test/unit/Yamato.test.ts | 79 +++++++++++++++++++--- 8 files changed, 297 insertions(+), 47 deletions(-) diff --git a/test/param/version.ts b/test/param/version.ts index 9fafdc15..850ab3e7 100644 --- a/test/param/version.ts +++ b/test/param/version.ts @@ -7,12 +7,13 @@ export interface Version { FeePool: string; Import: string; Interfaces: string; - Minter: string; + YmtMinter: string; OracleMockBase: string; Pool: string; PriceFeed: string; PriorityRegistry: string; ScoreRegistry: string; + ScoreWeightController: string; TellorCallerMock: string; Vester: string; VesterFactory: string; @@ -33,27 +34,28 @@ export const contractVersion: Version = { CJPY: "CJPY", ChainLinkMock: "ChainLinkMock", Currency: "Currency", - CurrencyOS: "CurrencyOSV2", + CurrencyOS: "CurrencyOSV3", Dependencies: "Dependencies", FeePool: "FeePool", Import: "Import", Interfaces: "Interfaces", - Minter: "Minter", + YmtMinter: "YmtMinter", OracleMockBase: "OracleMockBase", Pool: "PoolV2", PriceFeed: "PriceFeedV3", PriorityRegistry: "PriorityRegistryV6", ScoreRegistry: "ScoreRegistry", + ScoreWeightController: "ScoreWeightController", TellorCallerMock: "TellorCallerMock", Vester: "Vester", VesterFactory: "VesterFactory", YMT: "YMT", - Yamato: "YamatoV3", - YamatoBorrower: "YamatoBorrower", + Yamato: "YamatoV4", + YamatoBorrower: "YamatoBorrowerV2", YamatoDepositor: "YamatoDepositorV2", - YamatoRedeemer: "YamatoRedeemerV4", - YamatoRepayer: "YamatoRepayerV2", - YamatoSweeper: "YamatoSweeperV2", + YamatoRedeemer: "YamatoRedeemerV5", + YamatoRepayer: "YamatoRepayerV3", + YamatoSweeper: "YamatoSweeperV3", YamatoWithdrawer: "YamatoWithdrawerV2", YmtOS: "YmtOS", curveFork: "curveFork", diff --git a/test/unit/BurnCJPY.test.ts b/test/unit/BurnCJPY.test.ts index 85ef625c..39e99c0f 100644 --- a/test/unit/BurnCJPY.test.ts +++ b/test/unit/BurnCJPY.test.ts @@ -13,7 +13,7 @@ import { FeePool, CurrencyOS, CJPY, - Yamato, + YamatoV4, YamatoDepositor, YamatoBorrower, YamatoRepayer, @@ -22,12 +22,17 @@ import { YamatoSweeper, PriorityRegistry, Pool, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, ChainLinkMock__factory, PriceFeedV3__factory, FeePool__factory, CurrencyOS__factory, CJPY__factory, - Yamato__factory, + YamatoV4__factory, YamatoDepositor__factory, YamatoBorrower__factory, YamatoRepayer__factory, @@ -36,6 +41,11 @@ import { YamatoSweeper__factory, Pool__factory, PriorityRegistry__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, } from "../../typechain"; import { getProxy, getLinkedProxy } from "../../src/testUtil"; import { contractVersion } from "../param/version"; @@ -49,7 +59,7 @@ describe("burnCurrency :: contract Yamato", () => { let CJPY: CJPY; let FeePool: FeePool; let CurrencyOS: CurrencyOS; - let Yamato: Yamato; + let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; let YamatoBorrower: YamatoBorrower; let YamatoRepayer: YamatoRepayer; @@ -58,6 +68,11 @@ describe("burnCurrency :: contract Yamato", () => { let YamatoSweeper: YamatoSweeper; let Pool: Pool; let PriorityRegistry: PriorityRegistry; + let ScoreRegistry: ScoreRegistry; + let YmtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; let userAddress: string; @@ -116,7 +131,7 @@ describe("burnCurrency :: contract Yamato", () => { [CJPY.address, PriceFeed.address, FeePool.address] ); - Yamato = await getLinkedProxy( + Yamato = await getLinkedProxy( contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] @@ -163,6 +178,27 @@ describe("burnCurrency :: contract Yamato", () => { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + ScoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + YmtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, ScoreWeightController.address] + ); + + ScoreRegistry = await getProxy( + contractVersion["ScoreRegistry"], + [YmtMinter.address, Yamato.address] + ); + await ( await Yamato.setDeps( YamatoDepositor.address, @@ -175,6 +211,7 @@ describe("burnCurrency :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); + await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); }); diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index b07558a6..23ec4446 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -10,7 +10,7 @@ import { PriceFeedV3, PriorityRegistryV6, PriorityRegistryV6__factory, - Yamato, + YamatoV4, YamatoDepositor, YamatoBorrower, YamatoRepayer, @@ -18,7 +18,12 @@ import { YamatoRedeemer, YamatoSweeper, YamatoDummy, - Yamato__factory, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + YamatoV4__factory, YamatoDepositor__factory, YamatoBorrower__factory, YamatoRepayer__factory, @@ -27,8 +32,12 @@ import { YamatoSweeper__factory, YamatoDummy__factory, FeePool__factory, - YMT, Pool__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, } from "../../typechain"; import { encode, toERC20 } from "../param/helper"; import { getFakeProxy, getLinkedProxy, getProxy } from "../../src/testUtil"; @@ -45,7 +54,7 @@ describe("story Events", function () { let mockCJPY: FakeContract; let mockCurrencyOS: FakeContract; let mockPriorityRegistry: FakeContract; - let yamato: Yamato; + let yamato: YamatoV4; let yamatoDepositor: YamatoDepositor; let yamatoBorrower: YamatoBorrower; let yamatoRepayer: YamatoRepayer; @@ -53,6 +62,11 @@ describe("story Events", function () { let yamatoRedeemer: YamatoRedeemer; let yamatoSweeper: YamatoSweeper; let priorityRegistry: PriorityRegistryV6; + let ScoreRegistry: ScoreRegistry; + let YmtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let ScoreWeightController: ScoreWeightController; let PRICE: BigNumber; let MCR: BigNumber; let accounts: Signer[]; @@ -85,7 +99,7 @@ describe("story Events", function () { mockCurrencyOS.feePool.returns(mockFeePool.address); mockCurrencyOS.currency.returns(mockCJPY.address); - yamato = await getLinkedProxy( + yamato = await getLinkedProxy( contractVersion["Yamato"], [mockCurrencyOS.address], ["PledgeLib"] @@ -124,6 +138,29 @@ describe("story Events", function () { contractVersion["PriorityRegistry"] ); + YMT = await (( + await ethers.getContractFactory("YMT") + )).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + ScoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + YmtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, ScoreWeightController.address] + ); + + ScoreRegistry = await getProxy( + contractVersion["ScoreRegistry"], + [YmtMinter.address, yamato.address] + ); + await ( await yamato.setDeps( yamatoDepositor.address, @@ -136,7 +173,7 @@ describe("story Events", function () { mockPriorityRegistry.address ) ).wait(); - + await (await yamato.setScoreRegistory(ScoreRegistry.address)).wait(); PRICE = BigNumber.from(260000).mul(1e18 + ""); MCR = BigNumber.from(130); diff --git a/test/unit/FlashLock.test.ts b/test/unit/FlashLock.test.ts index 96554d5c..facfc54d 100644 --- a/test/unit/FlashLock.test.ts +++ b/test/unit/FlashLock.test.ts @@ -13,7 +13,7 @@ import { FeePool, CurrencyOS, CJPY, - Yamato, + YamatoV4, YamatoDepositor, YamatoBorrower, YamatoRepayer, @@ -21,6 +21,11 @@ import { YamatoRedeemer, YamatoSweeper, PriorityRegistry, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, Pool, SameBlockClient, ChainLinkMock__factory, @@ -28,7 +33,7 @@ import { FeePool__factory, CurrencyOS__factory, CJPY__factory, - Yamato__factory, + YamatoV4__factory, YamatoDepositor__factory, YamatoBorrower__factory, YamatoRepayer__factory, @@ -37,6 +42,11 @@ import { YamatoSweeper__factory, Pool__factory, PriorityRegistry__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, SameBlockClient__factory, } from "../../typechain"; import { getProxy, getLinkedProxy } from "../../src/testUtil"; @@ -51,7 +61,7 @@ describe("FlashLock :: contract Yamato", () => { let CJPY: CJPY; let FeePool: FeePool; let CurrencyOS: CurrencyOS; - let Yamato: Yamato; + let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; let YamatoBorrower: YamatoBorrower; let YamatoRepayer: YamatoRepayer; @@ -60,6 +70,11 @@ describe("FlashLock :: contract Yamato", () => { let YamatoSweeper: YamatoSweeper; let Pool: Pool; let PriorityRegistry: PriorityRegistry; + let ScoreRegistry: ScoreRegistry; + let YmtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; let userAddress: string; @@ -119,7 +134,7 @@ describe("FlashLock :: contract Yamato", () => { [CJPY.address, PriceFeed.address, FeePool.address] ); - Yamato = await getLinkedProxy( + Yamato = await getLinkedProxy( contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] @@ -166,6 +181,27 @@ describe("FlashLock :: contract Yamato", () => { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + ScoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + YmtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, ScoreWeightController.address] + ); + + ScoreRegistry = await getProxy( + contractVersion["ScoreRegistry"], + [YmtMinter.address, Yamato.address] + ); + await ( await Yamato.setDeps( YamatoDepositor.address, @@ -178,6 +214,7 @@ describe("FlashLock :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); + await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); diff --git a/test/unit/MintCJPY.test.ts b/test/unit/MintCJPY.test.ts index 0358f8cc..2927659a 100644 --- a/test/unit/MintCJPY.test.ts +++ b/test/unit/MintCJPY.test.ts @@ -13,7 +13,7 @@ import { FeePool, CurrencyOS, CJPY, - Yamato, + YamatoV4, YamatoDepositor, YamatoBorrower, YamatoRepayer, @@ -21,13 +21,18 @@ import { YamatoRedeemer, YamatoSweeper, PriorityRegistry, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, Pool, ChainLinkMock__factory, PriceFeedV3__factory, FeePool__factory, CurrencyOS__factory, CJPY__factory, - Yamato__factory, + YamatoV4__factory, YamatoDepositor__factory, YamatoBorrower__factory, YamatoRepayer__factory, @@ -36,6 +41,11 @@ import { YamatoSweeper__factory, Pool__factory, PriorityRegistry__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, } from "../../typechain"; import { getProxy, getLinkedProxy } from "../../src/testUtil"; import { contractVersion } from "../param/version"; @@ -49,7 +59,7 @@ describe("MintCJPY :: contract Yamato", () => { let CJPY: CJPY; let FeePool: FeePool; let CurrencyOS: CurrencyOS; - let Yamato: Yamato; + let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; let YamatoBorrower: YamatoBorrower; let YamatoRepayer: YamatoRepayer; @@ -58,6 +68,11 @@ describe("MintCJPY :: contract Yamato", () => { let YamatoSweeper: YamatoSweeper; let Pool: Pool; let PriorityRegistry: PriorityRegistry; + let ScoreRegistry: ScoreRegistry; + let YmtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; let userAddress: string; @@ -115,7 +130,7 @@ describe("MintCJPY :: contract Yamato", () => { [CJPY.address, PriceFeed.address, FeePool.address] ); - Yamato = await getLinkedProxy( + Yamato = await getLinkedProxy( contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] @@ -162,6 +177,27 @@ describe("MintCJPY :: contract Yamato", () => { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + ScoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + YmtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, ScoreWeightController.address] + ); + + ScoreRegistry = await getProxy( + contractVersion["ScoreRegistry"], + [YmtMinter.address, Yamato.address] + ); + await ( await Yamato.setDeps( YamatoDepositor.address, @@ -174,6 +210,7 @@ describe("MintCJPY :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); + await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); }); diff --git a/test/unit/OracleMock.test.ts b/test/unit/OracleMock.test.ts index ae01242f..845592c6 100644 --- a/test/unit/OracleMock.test.ts +++ b/test/unit/OracleMock.test.ts @@ -14,7 +14,7 @@ let chainlinkInitialRoundId = BigNumber.from("30000000000000000001").add(1); let priceDeviationRange = 0.01; let accounts; -describe("OracleMockBase", function () { +describe.only("OracleMockBase", function () { beforeEach(async () => { const spec1 = ( await ethers.getContractFactory("ChainLinkMock") diff --git a/test/unit/PriceChangeAndRedemption.test.ts b/test/unit/PriceChangeAndRedemption.test.ts index 2a9d4d9f..7685c343 100644 --- a/test/unit/PriceChangeAndRedemption.test.ts +++ b/test/unit/PriceChangeAndRedemption.test.ts @@ -13,7 +13,7 @@ import { FeePool, CurrencyOS, CJPY, - Yamato, + YamatoV4, YamatoDepositor, YamatoBorrower, YamatoRepayer, @@ -21,12 +21,17 @@ import { YamatoRedeemer, YamatoSweeper, PriorityRegistryV6, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, Pool, ChainLinkMock__factory, PriceFeedV3__factory, CurrencyOS__factory, CJPY__factory, - Yamato__factory, + YamatoV4__factory, YamatoDepositor__factory, YamatoBorrower__factory, YamatoRepayer__factory, @@ -36,6 +41,11 @@ import { Pool__factory, FeePool__factory, PriorityRegistryV6__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, } from "../../typechain"; import { getProxy, @@ -56,7 +66,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { let CJPY: CJPY; let FeePool: FeePool; let CurrencyOS: CurrencyOS; - let Yamato: Yamato; + let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; let YamatoBorrower: YamatoBorrower; let YamatoRepayer: YamatoRepayer; @@ -65,6 +75,11 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { let YamatoSweeper: YamatoSweeper; let Pool: Pool; let PriorityRegistry: PriorityRegistryV6; + let ScoreRegistry: ScoreRegistry; + let YmtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; let userAddress: string; @@ -131,7 +146,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { await (await ethers.getContractFactory("PledgeLib")).deploy() ).address; - Yamato = await getLinkedProxy( + Yamato = await getLinkedProxy( contractVersion["Yamato"], [CurrencyOS.address], ["PledgeLib"] @@ -178,6 +193,27 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { PriorityRegistryV6__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + ScoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + YmtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, ScoreWeightController.address] + ); + + ScoreRegistry = await getProxy( + contractVersion["ScoreRegistry"], + [YmtMinter.address, Yamato.address] + ); + await ( await Yamato.setDeps( YamatoDepositor.address, @@ -190,6 +226,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); + await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); @@ -1361,7 +1398,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(90); }); - it("should redeem within 10m gas and sweep within 16m gas", async () => { + it("should redeem within 11m gas and sweep within 16m gas", async () => { let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); let ethBalanceBefore = await Yamato.provider.getBalance(redeemerAddr); @@ -1378,7 +1415,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(10000000); + expect(txReceipt1.gasUsed).to.be.lt(11000000); let sweepablePledge = await Yamato.getPledge( await PriorityRegistry.getRankedQueue( 0, @@ -1409,7 +1446,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(100); }); - it("should redeem within 10m gas and sweep within 16m gas", async () => { + it("should redeem within 11m gas and sweep within 16m gas", async () => { const price = await PriceFeed.getPrice(); let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); @@ -1427,7 +1464,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(10000000); + expect(txReceipt1.gasUsed).to.be.lt(11000000); expect( (await Yamato.getPledge(await accounts[1].getAddress())).coll @@ -1464,7 +1501,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(110); }); - it("should redeem within 10m gas and sweep within 16m gas", async () => { + it("should redeem within 11m gas and sweep within 16m gas", async () => { let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); let ethBalanceBefore = await Yamato.provider.getBalance(redeemerAddr); @@ -1481,7 +1518,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(10000000); + expect(txReceipt1.gasUsed).to.be.lt(11000000); let ethBalanceAfter = await Yamato.provider.getBalance(redeemerAddr); let redemptionReturn = ethBalanceAfter.sub(ethBalanceBefore); @@ -1508,7 +1545,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(120); }); - it("should redeem within 10m gas and sweep within 16m gas", async () => { + it("should redeem within 11m gas and sweep within 16m gas", async () => { let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); let ethBalanceBefore = await Yamato.provider.getBalance(redeemerAddr); @@ -1525,7 +1562,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(10000000); + expect(txReceipt1.gasUsed).to.be.lt(11000000); let ethBalanceAfter = await Yamato.provider.getBalance(redeemerAddr); let redemptionReturn = ethBalanceAfter.sub(ethBalanceBefore); diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index e252e537..592345ca 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -10,7 +10,7 @@ import { PriceFeedV3, PriorityRegistry, PriorityRegistry__factory, - Yamato, + YamatoV4, YamatoDepositor, YamatoBorrower, YamatoRepayer, @@ -18,7 +18,12 @@ import { YamatoRedeemer, YamatoSweeper, YamatoDummy, - Yamato__factory, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + YamatoV4__factory, YamatoDepositor__factory, YamatoBorrower__factory, YamatoRepayer__factory, @@ -26,9 +31,13 @@ import { YamatoRedeemer__factory, YamatoSweeper__factory, YamatoDummy__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, FeePool__factory, Pool__factory, - YMT, } from "../../typechain"; import { encode, toERC20 } from "../param/helper"; import { @@ -49,7 +58,7 @@ describe("contract Yamato - pure func quickier tests", function () { let mockCJPY: FakeContract; let mockCurrencyOS: FakeContract; let mockPriorityRegistry: FakeContract; - let yamato: Yamato; + let yamato: YamatoV4; let yamatoDepositor: YamatoDepositor; let yamatoBorrower: YamatoBorrower; let yamatoRepayer: YamatoRepayer; @@ -57,6 +66,11 @@ describe("contract Yamato - pure func quickier tests", function () { let yamatoRedeemer: YamatoRedeemer; let yamatoSweeper: YamatoSweeper; let yamatoDummy: YamatoDummy; + let ScoreRegistry: ScoreRegistry; + let YmtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let ScoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; let PRICE: BigNumber; @@ -84,7 +98,7 @@ describe("contract Yamato - pure func quickier tests", function () { mockCurrencyOS.feePool.returns(mockFeePool.address); mockCurrencyOS.currency.returns(mockCJPY.address); - yamato = await getLinkedProxy( + yamato = await getLinkedProxy( contractVersion["Yamato"], [mockCurrencyOS.address], ["PledgeLib"] @@ -133,6 +147,27 @@ describe("contract Yamato - pure func quickier tests", function () { contractVersion["PriorityRegistry"] ); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + ScoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + YmtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, ScoreWeightController.address] + ); + + ScoreRegistry = await getProxy( + contractVersion["ScoreRegistry"], + [YmtMinter.address, yamato.address] + ); + await ( await yamato.setDeps( yamatoDepositor.address, @@ -145,6 +180,7 @@ describe("contract Yamato - pure func quickier tests", function () { mockPriorityRegistry.address ) ).wait(); + await (await yamato.setScoreRegistory(ScoreRegistry.address)).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec pool = await getProxy(contractVersion["Pool"], [ @@ -240,7 +276,7 @@ describe("contract Yamato - pure func quickier tests", function () { }); }); -describe("contract Yamato", function () { +describe.only("contract Yamato", function () { let mockPool: FakeContract; let mockFeePool: FakeContract; let mockFeed: FakeContract; @@ -248,7 +284,7 @@ describe("contract Yamato", function () { let mockCJPY: FakeContract; let mockCurrencyOS: FakeContract; let mockPriorityRegistry: FakeContract; - let yamato: Yamato; + let yamato: YamatoV4; let yamatoDepositor: YamatoDepositor; let yamatoBorrower: YamatoBorrower; let yamatoRepayer: YamatoRepayer; @@ -258,6 +294,11 @@ describe("contract Yamato", function () { let yamatoDummy: YamatoDummy; let pool: Pool; let priorityRegistry: PriorityRegistry; + let ScoreRegistry: ScoreRegistry; + let YmtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let ScoreWeightController: ScoreWeightController; let PRICE: BigNumber; let MCR: BigNumber; let accounts: Signer[]; @@ -283,7 +324,7 @@ describe("contract Yamato", function () { mockCurrencyOS.feePool.returns(mockFeePool.address); mockCurrencyOS.currency.returns(mockCJPY.address); - yamato = await getLinkedProxy( + yamato = await getLinkedProxy( contractVersion["Yamato"], [mockCurrencyOS.address], ["PledgeLib"] @@ -332,6 +373,27 @@ describe("contract Yamato", function () { contractVersion["PriorityRegistry"] ); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + ScoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + YmtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, ScoreWeightController.address] + ); + + ScoreRegistry = await getProxy( + contractVersion["ScoreRegistry"], + [YmtMinter.address, yamato.address] + ); + await ( await yamato.setDeps( yamatoDepositor.address, @@ -344,6 +406,7 @@ describe("contract Yamato", function () { mockPriorityRegistry.address ) ).wait(); + await (await yamato.setScoreRegistory(ScoreRegistry.address)).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec pool = await getProxy(contractVersion["Pool"], [ From a7a222bc85ff6ed7e947414c346ef3286a3f693d Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 21 Nov 2023 16:38:55 +0900 Subject: [PATCH 055/412] Change type of decimals to uint8 --- contracts/veYMT.sol | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/contracts/veYMT.sol b/contracts/veYMT.sol index 8d93e1a8..fe5adc7b 100644 --- a/contracts/veYMT.sol +++ b/contracts/veYMT.sol @@ -85,7 +85,7 @@ contract veYMT is ReentrancyGuard { string public name; string public symbol; - uint256 public decimals; + uint8 public decimals; // Checker for whitelisted (smart contract) wallets which are allowed to deposit // The goal is to prevent tokenizing the escrow @@ -107,12 +107,7 @@ contract veYMT is ReentrancyGuard { controller = msg.sender; transfersEnabled = true; - uint256 decimalsFromToken = IYMT(tokenAddr_).decimals(); - require( - decimalsFromToken <= 255, - "Decimals should be less than or equal to 255" - ); - decimals = decimalsFromToken; + decimals = IYMT(tokenAddr_).decimals(); name = "Voting-escrowed Yamato"; symbol = "veYMT"; From 5d23c8dfff45639783baf43a23177576ac18e058 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 21 Nov 2023 17:03:41 +0900 Subject: [PATCH 056/412] typo --- contracts/YamatoV4.sol | 2 +- test/unit/BurnCJPY.test.ts | 2 +- test/unit/Events.test.ts | 2 +- test/unit/FlashLock.test.ts | 2 +- test/unit/MintCJPY.test.ts | 2 +- test/unit/OracleMock.test.ts | 2 +- test/unit/PriceChangeAndRedemption.test.ts | 2 +- test/unit/Yamato.test.ts | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/YamatoV4.sol b/contracts/YamatoV4.sol index e66b7abd..6d0c3db0 100644 --- a/contracts/YamatoV4.sol +++ b/contracts/YamatoV4.sol @@ -195,7 +195,7 @@ contract YamatoV4 is } } - function setScoreRegistory(address _scoreRegistry) external onlyGovernance { + function setScoreRegistry(address _scoreRegistry) external onlyGovernance { bytes32 SCORE_REGISTRY_KEY = bytes32( keccak256(abi.encode(SCORE_REGISTRY_SLOT_ID)) ); diff --git a/test/unit/BurnCJPY.test.ts b/test/unit/BurnCJPY.test.ts index 39e99c0f..f4ae5a93 100644 --- a/test/unit/BurnCJPY.test.ts +++ b/test/unit/BurnCJPY.test.ts @@ -211,7 +211,7 @@ describe("burnCurrency :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); - await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); + await (await Yamato.setScoreRegistry(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); }); diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index 23ec4446..c73b1daa 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -173,7 +173,7 @@ describe("story Events", function () { mockPriorityRegistry.address ) ).wait(); - await (await yamato.setScoreRegistory(ScoreRegistry.address)).wait(); + await (await yamato.setScoreRegistry(ScoreRegistry.address)).wait(); PRICE = BigNumber.from(260000).mul(1e18 + ""); MCR = BigNumber.from(130); diff --git a/test/unit/FlashLock.test.ts b/test/unit/FlashLock.test.ts index facfc54d..f2b1e517 100644 --- a/test/unit/FlashLock.test.ts +++ b/test/unit/FlashLock.test.ts @@ -214,7 +214,7 @@ describe("FlashLock :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); - await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); + await (await Yamato.setScoreRegistry(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); diff --git a/test/unit/MintCJPY.test.ts b/test/unit/MintCJPY.test.ts index 2927659a..0b97a447 100644 --- a/test/unit/MintCJPY.test.ts +++ b/test/unit/MintCJPY.test.ts @@ -210,7 +210,7 @@ describe("MintCJPY :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); - await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); + await (await Yamato.setScoreRegistry(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); }); diff --git a/test/unit/OracleMock.test.ts b/test/unit/OracleMock.test.ts index 845592c6..ae01242f 100644 --- a/test/unit/OracleMock.test.ts +++ b/test/unit/OracleMock.test.ts @@ -14,7 +14,7 @@ let chainlinkInitialRoundId = BigNumber.from("30000000000000000001").add(1); let priceDeviationRange = 0.01; let accounts; -describe.only("OracleMockBase", function () { +describe("OracleMockBase", function () { beforeEach(async () => { const spec1 = ( await ethers.getContractFactory("ChainLinkMock") diff --git a/test/unit/PriceChangeAndRedemption.test.ts b/test/unit/PriceChangeAndRedemption.test.ts index 7685c343..d7af5f48 100644 --- a/test/unit/PriceChangeAndRedemption.test.ts +++ b/test/unit/PriceChangeAndRedemption.test.ts @@ -226,7 +226,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { PriorityRegistry.address ) ).wait(); - await (await Yamato.setScoreRegistory(ScoreRegistry.address)).wait(); + await (await Yamato.setScoreRegistry(ScoreRegistry.address)).wait(); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index 592345ca..c9ce5aa6 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -180,7 +180,7 @@ describe("contract Yamato - pure func quickier tests", function () { mockPriorityRegistry.address ) ).wait(); - await (await yamato.setScoreRegistory(ScoreRegistry.address)).wait(); + await (await yamato.setScoreRegistry(ScoreRegistry.address)).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec pool = await getProxy(contractVersion["Pool"], [ @@ -406,7 +406,7 @@ describe.only("contract Yamato", function () { mockPriorityRegistry.address ) ).wait(); - await (await yamato.setScoreRegistory(ScoreRegistry.address)).wait(); + await (await yamato.setScoreRegistry(ScoreRegistry.address)).wait(); // Note: Will use later for mintCurrency mockery test in borrow spec pool = await getProxy(contractVersion["Pool"], [ From 9ccb512bde2c1a0255aeb55348f55128f0f5e85e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 22 Nov 2023 10:00:44 +0900 Subject: [PATCH 057/412] Delete test code .only --- test/unit/Yamato.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index c9ce5aa6..60a8def6 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -276,7 +276,7 @@ describe("contract Yamato - pure func quickier tests", function () { }); }); -describe.only("contract Yamato", function () { +describe("contract Yamato", function () { let mockPool: FakeContract; let mockFeePool: FakeContract; let mockFeed: FakeContract; From 2134554479e70b74aea9a56f6be7ff66e53a0994 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 22 Nov 2023 10:25:30 +0900 Subject: [PATCH 058/412] Delete ERC-20 variable in ScoreRegist. --- contracts/Interfaces/IYamatoV4.sol | 2 ++ contracts/ScoreRegistry.sol | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/Interfaces/IYamatoV4.sol b/contracts/Interfaces/IYamatoV4.sol index 5715a718..7220404e 100644 --- a/contracts/Interfaces/IYamatoV4.sol +++ b/contracts/Interfaces/IYamatoV4.sol @@ -20,4 +20,6 @@ interface IYamatoV4 { function CHECKPOINT_BUFFER() external view returns (uint256); function scoreRegistry() external view returns (address); + + function totalDebt() external view returns (uint256); } diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 496ba590..4a697731 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -14,6 +14,7 @@ import "./Interfaces/IScoreController.sol"; import "./Interfaces/IYMT.sol"; import "./Interfaces/IYmtMinter.sol"; import "./Interfaces/IveYMT.sol"; +import "./Interfaces/IYamatoV4.sol"; contract ScoreRegistry is YamatoAction { event UpdateLiquidityLimit( @@ -43,10 +44,6 @@ contract ScoreRegistry is YamatoAction { uint256 public constant TOKENLESS_PRODUCTION = 40; uint256 public constant WEEK = 604800; - // ERC20 - mapping(address => uint256) public balanceOf; - uint256 public totalSupply; - // Score address public admin; @@ -218,7 +215,9 @@ contract ScoreRegistry is YamatoAction { "dev: unauthorized" ); checkpoint(addr_); - updateScoreLimit(addr_, balanceOf[addr_], totalSupply); + uint256 _balance = IYamato(yamato()).getPledge(addr_).debt; + uint256 _totalSupply = IYamatoV4(yamato()).totalDebt(); + updateScoreLimit(addr_, _balance, _totalSupply); return true; } @@ -228,7 +227,7 @@ contract ScoreRegistry is YamatoAction { addr_, IveYMT(votingEscrow).userPointEpoch(addr_) ); - uint256 _balance = balanceOf[addr_]; + uint256 _balance = IYamato(yamato()).getPledge(addr_).debt; require( IveYMT(votingEscrow).balanceOf(addr_) == 0 || _tVe > _tLast, @@ -240,7 +239,8 @@ contract ScoreRegistry is YamatoAction { ); checkpoint(addr_); - updateScoreLimit(addr_, balanceOf[addr_], totalSupply); + uint256 _totalSupply = IYamatoV4(yamato()).totalDebt(); + updateScoreLimit(addr_, _balance, _totalSupply); } function setKilled(bool isKilled_) external onlyAdmin { From 5af4da4ca6c1f703fae8549804c827f74593aeac Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 22 Nov 2023 10:34:27 +0900 Subject: [PATCH 059/412] getTotalDebt function added. --- contracts/Interfaces/IYamatoV4.sol | 2 +- contracts/ScoreRegistry.sol | 4 ++-- contracts/YamatoV4.sol | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/Interfaces/IYamatoV4.sol b/contracts/Interfaces/IYamatoV4.sol index 7220404e..8007a1de 100644 --- a/contracts/Interfaces/IYamatoV4.sol +++ b/contracts/Interfaces/IYamatoV4.sol @@ -21,5 +21,5 @@ interface IYamatoV4 { function scoreRegistry() external view returns (address); - function totalDebt() external view returns (uint256); + function getTotalDebt() external view returns (uint256); } diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 4a697731..4fdb9bfc 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -216,7 +216,7 @@ contract ScoreRegistry is YamatoAction { ); checkpoint(addr_); uint256 _balance = IYamato(yamato()).getPledge(addr_).debt; - uint256 _totalSupply = IYamatoV4(yamato()).totalDebt(); + uint256 _totalSupply = IYamatoV4(yamato()).getTotalDebt(); updateScoreLimit(addr_, _balance, _totalSupply); return true; } @@ -239,7 +239,7 @@ contract ScoreRegistry is YamatoAction { ); checkpoint(addr_); - uint256 _totalSupply = IYamatoV4(yamato()).totalDebt(); + uint256 _totalSupply = IYamatoV4(yamato()).getTotalDebt(); updateScoreLimit(addr_, _balance, _totalSupply); } diff --git a/contracts/YamatoV4.sol b/contracts/YamatoV4.sol index 6d0c3db0..fffdee70 100644 --- a/contracts/YamatoV4.sol +++ b/contracts/YamatoV4.sol @@ -449,6 +449,10 @@ contract YamatoV4 is return (pledge.coll, pledge.debt, pledge.isCreated, flashlocks[owner]); } + function getTotalDebt() public view override returns (uint256) { + return totalDebt; + } + // @dev Yamato.sol must override it with correct logic. function yamato() public view override returns (address) { return address(this); From 9a42b58d0dc1e9fb0bfa5014cfb4832b57790513 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 22 Nov 2023 10:41:17 +0900 Subject: [PATCH 060/412] ScoreRegistry array as a mapping. --- contracts/ScoreRegistry.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 4fdb9bfc..19d952aa 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -72,8 +72,8 @@ contract ScoreRegistry is YamatoAction { int128 public period; // Using dynamic array instead of fixed 100000000000000000000000000000 array to avoid warning about collisions - uint256[100000000000000000000000000000] public periodTimestamp; - uint256[100000000000000000000000000000] public integrateInvSupply; + mapping(int128=>uint256) public periodTimestamp; + mapping(int128=>uint256) public integrateInvSupply; function initialize(address minter_, address yamato_) public initializer { __YamatoAction_init(yamato_); @@ -82,7 +82,7 @@ contract ScoreRegistry is YamatoAction { scoreController = IYmtMinter(minter).controller(); votingEscrow = IScoreController(scoreController).votingEscrow(); - periodTimestamp[0] = block.timestamp; + periodTimestamp[int128(0)] = block.timestamp; admin = msg.sender; // Assuming you have the YMT interface defined somewhere for the following line @@ -94,9 +94,9 @@ contract ScoreRegistry is YamatoAction { CheckPointParameters memory _st; _st.period = period; - _st.periodTime = periodTimestamp[uint256(uint128(_st.period))]; + _st.periodTime = periodTimestamp[_st.period]; _st.integrateInvSupply = integrateInvSupply[ - uint256(uint128(_st.period)) + _st.period ]; _st.rate = inflationRate; @@ -165,8 +165,8 @@ contract ScoreRegistry is YamatoAction { _st.period += 1; period = _st.period; - periodTimestamp[uint256(uint128(_st.period))] = block.timestamp; - integrateInvSupply[uint256(uint128(_st.period))] = _st + periodTimestamp[_st.period] = block.timestamp; + integrateInvSupply[_st.period] = _st .integrateInvSupply; uint256 _workingBalance = workingBalances[addr]; @@ -248,7 +248,7 @@ contract ScoreRegistry is YamatoAction { } function integrateCheckpoint() external view returns (uint256) { - return periodTimestamp[uint256(uint128(period))]; + return periodTimestamp[period]; } function min(uint256 a, uint256 b) internal pure returns (uint256) { From db302eea60efe8a1cdfb86f1c6b6056a36ee158d Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 22 Nov 2023 12:03:07 +0900 Subject: [PATCH 061/412] The admin function for each contractor has been removed. --- contracts/ScoreRegistry.sol | 12 +-------- contracts/ScoreWeightController.sol | 39 +++-------------------------- contracts/veYMT.sol | 20 --------------- 3 files changed, 5 insertions(+), 66 deletions(-) diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 19d952aa..6ca08f4d 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -24,8 +24,6 @@ contract ScoreRegistry is YamatoAction { uint256 workingBalance, uint256 workingSupply ); - event CommitOwnership(address indexed admin); - event ApplyOwnership(address indexed admin); // to avoid "stack too deep" struct CheckPointParameters { @@ -44,9 +42,6 @@ contract ScoreRegistry is YamatoAction { uint256 public constant TOKENLESS_PRODUCTION = 40; uint256 public constant WEEK = 604800; - // Score - address public admin; - address public token; address public votingEscrow; address public minter; @@ -83,7 +78,6 @@ contract ScoreRegistry is YamatoAction { votingEscrow = IScoreController(scoreController).votingEscrow(); periodTimestamp[int128(0)] = block.timestamp; - admin = msg.sender; // Assuming you have the YMT interface defined somewhere for the following line inflationRate = IYMT(token).rate(); @@ -243,7 +237,7 @@ contract ScoreRegistry is YamatoAction { updateScoreLimit(addr_, _balance, _totalSupply); } - function setKilled(bool isKilled_) external onlyAdmin { + function setKilled(bool isKilled_) external onlyGovernance { isKilled = isKilled_; } @@ -255,8 +249,4 @@ contract ScoreRegistry is YamatoAction { return a < b ? a : b; } - modifier onlyAdmin() { - require(msg.sender == admin, "only owner"); - _; - } } diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 1650cdb7..bfc576bc 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -33,8 +33,6 @@ contract ScoreWeightController is UUPSBase { uint256 end; } - event CommitOwnership(address admin); - event ApplyOwnership(address admin); event AddType(string name, int128 typeId); event NewTypeWeight( int128 typeId, @@ -59,10 +57,6 @@ contract ScoreWeightController is UUPSBase { uint256 constant MULTIPLIER = 10 ** 18; - // Can and will be a smart contract - address public admin; - // Can and will be a smart contract - address public futureAdmin; // YMT token address public token; // Voting escrow @@ -117,31 +111,11 @@ contract ScoreWeightController is UUPSBase { require(votingEscrow_ != address(0)); __UUPSBase_init(); - admin = msg.sender; token = token_; votingEscrow = votingEscrow_; timeTotal = (block.timestamp / WEEK) * WEEK; } - /*** - * @notice Transfer ownership of ScoreController to `addr` - * @param addr_ Address to have ownership transferred to - */ - function commitTransferOwnership(address addr_) external onlyAdmin { - futureAdmin = addr_; - emit CommitOwnership(addr_); - } - - /*** - * @notice Apply pending ownership transfer - */ - function applyTransferOwnership() external onlyAdmin { - address _admin = futureAdmin; - require(_admin != address(0), "admin not set"); - admin = _admin; - emit ApplyOwnership(_admin); - } - /*** *@notice Get score type for address *@param addr_ Score address @@ -324,7 +298,7 @@ contract ScoreWeightController is UUPSBase { address addr_, int128 scoreType_, uint256 weight_ - ) external onlyAdmin { + ) external onlyGovernance { require( (scoreType_ >= 0) && (scoreType_ < nScoreTypes), "score_type 0 means unset" @@ -470,7 +444,7 @@ contract ScoreWeightController is UUPSBase { *@param name_ Name of score type *@param weight_ Weight of score type */ - function addType(string memory name_, uint256 weight_) external onlyAdmin { + function addType(string memory name_, uint256 weight_) external onlyGovernance { int128 _typeId = nScoreTypes; scoreTypeNames[_typeId] = name_; unchecked { @@ -490,7 +464,7 @@ contract ScoreWeightController is UUPSBase { function changeTypeWeight( int128 typeId_, uint256 weight_ - ) external onlyAdmin { + ) external onlyGovernance { _changeTypeWeight(typeId_, weight_); } @@ -532,7 +506,7 @@ contract ScoreWeightController is UUPSBase { function changeScoreWeight( address addr_, uint256 weight_ - ) external onlyAdmin { + ) external onlyGovernance { _changeScoreWeight(addr_, weight_); } @@ -690,9 +664,4 @@ contract ScoreWeightController is UUPSBase { function max(uint256 _a, uint256 _b) internal pure returns (uint256) { return _a >= _b ? _a : _b; } - - modifier onlyAdmin() { - require(admin == msg.sender, "admin only"); - _; - } } diff --git a/contracts/veYMT.sol b/contracts/veYMT.sol index fe5adc7b..8f9f855b 100644 --- a/contracts/veYMT.sol +++ b/contracts/veYMT.sol @@ -87,11 +87,6 @@ contract veYMT is ReentrancyGuard { string public symbol; uint8 public decimals; - // Checker for whitelisted (smart contract) wallets which are allowed to deposit - // The goal is to prevent tokenizing the escrow - address public futureSmartWalletChecker; - address public smartWalletChecker; - address public admin; address public futureAdmin; @@ -132,21 +127,6 @@ contract veYMT is ReentrancyGuard { emit ApplyOwnership(_admin); } - /*** - * @notice Set an external contract to check for approved smart contract wallets - * @param addr_ Address of Smart contract checker - */ - function commitSmartWalletChecker(address addr_) external onlyAdmin { - futureSmartWalletChecker = addr_; - } - - /*** - * @notice Apply setting external contract to check approved smart contract wallets - */ - function applySmartWalletChecker() external onlyAdmin { - smartWalletChecker = futureSmartWalletChecker; - } - /*** * @notice Get the most recently recorded rate of voting power decrease for `addr` * @param addr_ Address of the user wallet From 004d2833cc8f064385743af078eb50b1232100a5 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 22 Nov 2023 14:42:54 +0900 Subject: [PATCH 062/412] Change event name --- contracts/ScoreRegistry.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 6ca08f4d..22c7398b 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -17,7 +17,7 @@ import "./Interfaces/IveYMT.sol"; import "./Interfaces/IYamatoV4.sol"; contract ScoreRegistry is YamatoAction { - event UpdateLiquidityLimit( + event UpdateScoreLimit( address user, uint256 originalBalance, uint256 originalSupply, @@ -194,7 +194,7 @@ contract ScoreRegistry is YamatoAction { uint256 _workingSupply = workingSupply + _lim - _oldBal; workingSupply = _workingSupply; - emit UpdateLiquidityLimit( + emit UpdateScoreLimit( addr_, debt_, totaldebt_, From 13e1d137110dfa10671388d6619af49419992564 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 22 Nov 2023 14:44:48 +0900 Subject: [PATCH 063/412] ScoreWeightController array changed to mapping. --- contracts/ScoreWeightController.sol | 36 ++++++++++++----------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index bfc576bc..e7cc09ef 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -67,7 +67,7 @@ contract ScoreWeightController is UUPSBase { mapping(int128 => string) public scoreTypeNames; // Needed for enumeration - address[1000000000] public scores; + mapping(int128 => address) public scores; // we increment values by 1 prior to storing them here so we can rely on a value // of zero as meaning the score has not been set mapping(address => int128) scoreTypes; @@ -90,13 +90,13 @@ contract ScoreWeightController is UUPSBase { mapping(int128 => mapping(uint256 => Point)) public pointsSum; // type_id -> time -> Point mapping(int128 => mapping(uint256 => uint256)) public changesSum; // type_id -> time -> slope - uint256[1000000000] public timeSum; // type_id -> last scheduled time (next week) + mapping(int128 => uint256) public timeSum; // type_id -> last scheduled time (next week) mapping(uint256 => uint256) public pointsTotal; // time -> total weight uint256 public timeTotal; // last scheduled time mapping(int128 => mapping(uint256 => uint256)) public pointsTypeWeight; // type_id -> time -> type weight - uint256[1000000000] public timeTypeWeight; // type_id -> last scheduled time (next week) + mapping(int128 => uint256) public timeTypeWeight; // type_id -> last scheduled time (next week) /*** *@notice Contract constructor @@ -135,8 +135,7 @@ contract ScoreWeightController is UUPSBase { *@return Type weight */ function _getTypeWeight(int128 scoreType_) internal returns (uint256) { - uint256 _scoreType = uint256(uint128(scoreType_)); - uint256 _t = timeTypeWeight[_scoreType]; + uint256 _t = timeTypeWeight[scoreType_]; if (_t > 0) { uint256 _w = pointsTypeWeight[scoreType_][_t]; for (uint256 i; i < 500; ) { @@ -146,7 +145,7 @@ contract ScoreWeightController is UUPSBase { _t += WEEK; pointsTypeWeight[scoreType_][_t] = _w; if (_t > block.timestamp) { - timeTypeWeight[_scoreType] = _t; + timeTypeWeight[scoreType_] = _t; } unchecked { ++i; @@ -165,8 +164,7 @@ contract ScoreWeightController is UUPSBase { *@return Sum of weights */ function _getSum(int128 scoreType_) internal returns (uint256) { - uint256 _scoreType = uint256(uint128(scoreType_)); - uint256 _t = timeSum[_scoreType]; + uint256 _t = timeSum[scoreType_]; if (_t > 0) { Point memory _pt = pointsSum[scoreType_][_t]; for (uint256 i; i < 500; ) { @@ -185,7 +183,7 @@ contract ScoreWeightController is UUPSBase { } pointsSum[scoreType_][_t] = _pt; if (_t > block.timestamp) { - timeSum[_scoreType] = _t; + timeSum[scoreType_] = _t; } unchecked { ++i; @@ -308,8 +306,7 @@ contract ScoreWeightController is UUPSBase { unchecked { nScores = _n + 1; } - scores[uint256(uint128(_n))] = addr_; - uint256 _scoreType = uint256(uint128(scoreType_)); + scores[_n] = addr_; scoreTypes_[addr_] = scoreType_ + 1; uint256 _nextTime; unchecked { @@ -322,14 +319,14 @@ contract ScoreWeightController is UUPSBase { uint256 _oldTotal = _getTotal(); pointsSum[scoreType_][_nextTime].bias = weight_ + _oldSum; - timeSum[_scoreType] = _nextTime; + timeSum[scoreType_] = _nextTime; pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); timeTotal = _nextTime; pointsWeight[addr_][_nextTime].bias = weight_; } - if (timeSum[_scoreType] == 0) { - timeSum[_scoreType] = _nextTime; + if (timeSum[scoreType_] == 0) { + timeSum[scoreType_] = _nextTime; } timeWeight[addr_] = _nextTime; @@ -425,7 +422,6 @@ contract ScoreWeightController is UUPSBase { unchecked { _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; } - uint256 _typeId = uint256(uint128(typeId_)); _totalWeight = _totalWeight + @@ -434,7 +430,7 @@ contract ScoreWeightController is UUPSBase { pointsTotal[_nextTime] = _totalWeight; pointsTypeWeight[typeId_][_nextTime] = weight_; timeTotal = _nextTime; - timeTypeWeight[_typeId] = _nextTime; + timeTypeWeight[typeId_] = _nextTime; emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); } @@ -486,7 +482,7 @@ contract ScoreWeightController is UUPSBase { uint256 newSum = _oldSum + weight_ - _oldScoreWeight; pointsSum[_scoreType][_nextTime].bias = newSum; - timeSum[uint256(uint128(_scoreType))] = _nextTime; + timeSum[_scoreType] = _nextTime; _totalWeight = _totalWeight + @@ -637,8 +633,7 @@ contract ScoreWeightController is UUPSBase { *@return Type weight */ function getTypeWeight(int128 typeId_) external view returns (uint256) { - uint256 _typeId = uint256(uint128(typeId_)); - return pointsTypeWeight[typeId_][timeTypeWeight[_typeId]]; + return pointsTypeWeight[typeId_][timeTypeWeight[typeId_]]; } /*** @@ -657,8 +652,7 @@ contract ScoreWeightController is UUPSBase { function getWeightsSumPerType( int128 typeId_ ) external view returns (uint256) { - uint256 _typeId = uint256(uint128(typeId_)); - return pointsSum[typeId_][timeSum[_typeId]].bias; + return pointsSum[typeId_][timeSum[typeId_]].bias; } function max(uint256 _a, uint256 _b) internal pure returns (uint256) { From 499eec674396a24036739f9b0e15fd4aeb8bde93 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 24 Nov 2023 15:32:03 +0900 Subject: [PATCH 064/412] Added collateral rate boosting logic --- contracts/Interfaces/IScoreRegistry.sol | 3 +- contracts/ScoreRegistry.sol | 56 ++++++++++++++++++++----- contracts/YamatoBorrowerV2.sol | 3 +- contracts/YamatoRedeemerV5.sol | 8 +--- contracts/YamatoRepayerV3.sol | 5 ++- contracts/YamatoSweeperV3.sol | 3 +- 6 files changed, 57 insertions(+), 21 deletions(-) diff --git a/contracts/Interfaces/IScoreRegistry.sol b/contracts/Interfaces/IScoreRegistry.sol index dfc40d81..6ea52fa9 100644 --- a/contracts/Interfaces/IScoreRegistry.sol +++ b/contracts/Interfaces/IScoreRegistry.sol @@ -14,7 +14,8 @@ interface IScoreRegistry { function updateScoreLimit( address addr_, uint256 debt_, - uint256 totaldebt_ + uint256 totalDebt_, + uint256 collateralRatio_ ) external; function userCheckpoint(address addr_) external returns (bool); diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 22c7398b..0215391a 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -10,6 +10,7 @@ pragma solidity 0.8.4; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "./Dependencies/YamatoAction.sol"; +import "./Dependencies/PledgeLib.sol"; import "./Interfaces/IScoreController.sol"; import "./Interfaces/IYMT.sol"; import "./Interfaces/IYmtMinter.sol"; @@ -17,10 +18,13 @@ import "./Interfaces/IveYMT.sol"; import "./Interfaces/IYamatoV4.sol"; contract ScoreRegistry is YamatoAction { + using PledgeLib for IYamato.Pledge; + event UpdateScoreLimit( address user, uint256 originalBalance, uint256 originalSupply, + uint256 collateralRatio, uint256 workingBalance, uint256 workingSupply ); @@ -175,7 +179,8 @@ contract ScoreRegistry is YamatoAction { function updateScoreLimit( address addr_, uint256 debt_, - uint256 totaldebt_ + uint256 totalDebt_, + uint256 collateralRatio_ ) public onlyYamato { uint256 _votingBalance = IveYMT(votingEscrow).balanceOf(addr_); uint256 _votingTotal = IveYMT(votingEscrow).totalSupply(); @@ -183,35 +188,61 @@ contract ScoreRegistry is YamatoAction { uint256 _lim = (debt_ * TOKENLESS_PRODUCTION) / 100; if (_votingTotal > 0) { _lim += - (((totaldebt_ * _votingBalance) / _votingTotal) * + (((totalDebt_ * _votingBalance) / _votingTotal) * (100 - TOKENLESS_PRODUCTION)) / 100; } _lim = min(debt_, _lim); uint256 _oldBal = workingBalances[addr_]; - workingBalances[addr_] = _lim; - uint256 _workingSupply = workingSupply + _lim - _oldBal; + + // Apply the coefficient based on the collateral ratio provided + uint256 coefficient = calculateCoefficient(collateralRatio_); + + // Adjust the limit based on the coefficient + uint256 _limit = (_lim * coefficient) / 1e18; + + workingBalances[addr_] = _limit; + uint256 _workingSupply = workingSupply + _limit - _oldBal; workingSupply = _workingSupply; emit UpdateScoreLimit( addr_, debt_, - totaldebt_, - _lim, + totalDebt_, + collateralRatio_, + _limit, _workingSupply ); } + function calculateCoefficient(uint256 collateralRatio_) internal pure returns (uint256) { + uint256 _collateralRatio = collateralRatio_; + if (_collateralRatio >= 25000) { + return 2.5e18; + } else if (_collateralRatio >= 20000) { + return 2e18; + } else if (_collateralRatio >= 15000) { + return 1.5e18; + } else if (_collateralRatio >= 13000) { + return 1e18; + } else { + return 0; + } + } + + function userCheckpoint(address addr_) external onlyYamato returns (bool) { require( msg.sender == addr_ || msg.sender == minter, "dev: unauthorized" ); checkpoint(addr_); - uint256 _balance = IYamato(yamato()).getPledge(addr_).debt; + IYamato.Pledge memory _pledge = IYamato(yamato()).getPledge(addr_); + uint256 _collateralRatio = _pledge.getICR(priceFeed()); + uint256 _balance = _pledge.debt; uint256 _totalSupply = IYamatoV4(yamato()).getTotalDebt(); - updateScoreLimit(addr_, _balance, _totalSupply); + updateScoreLimit(addr_, _balance, _totalSupply, _collateralRatio); return true; } @@ -221,20 +252,23 @@ contract ScoreRegistry is YamatoAction { addr_, IveYMT(votingEscrow).userPointEpoch(addr_) ); - uint256 _balance = IYamato(yamato()).getPledge(addr_).debt; + IYamato.Pledge memory _pledge = IYamato(yamato()).getPledge(addr_); + uint256 _balance = _pledge.debt; + uint256 _collateralRatio = _pledge.getICR(priceFeed()); + uint256 coefficient = calculateCoefficient(_collateralRatio); require( IveYMT(votingEscrow).balanceOf(addr_) == 0 || _tVe > _tLast, "Not allowed" ); require( - workingBalances[addr_] > (_balance * TOKENLESS_PRODUCTION) / 100, + workingBalances[addr_] > (((_balance * TOKENLESS_PRODUCTION) / 100) * coefficient) / 1e18, "Not needed" ); checkpoint(addr_); uint256 _totalSupply = IYamatoV4(yamato()).getTotalDebt(); - updateScoreLimit(addr_, _balance, _totalSupply); + updateScoreLimit(addr_, _balance, _totalSupply, _collateralRatio); } function setKilled(bool isKilled_) external onlyGovernance { diff --git a/contracts/YamatoBorrowerV2.sol b/contracts/YamatoBorrowerV2.sol index 3af9ea9f..ac07b828 100644 --- a/contracts/YamatoBorrowerV2.sol +++ b/contracts/YamatoBorrowerV2.sol @@ -105,7 +105,8 @@ contract YamatoBorrowerV2 is IYamatoBorrower, YamatoAction { _scoreRegistry.updateScoreLimit( _sender, pledge.debt, - totalDebt + _borrowAmountInCurrency + totalDebt + _borrowAmountInCurrency, + pledge.getICR(priceFeed()) ); /* diff --git a/contracts/YamatoRedeemerV5.sol b/contracts/YamatoRedeemerV5.sol index 32a4ca34..58666a05 100644 --- a/contracts/YamatoRedeemerV5.sol +++ b/contracts/YamatoRedeemerV5.sol @@ -244,15 +244,11 @@ contract YamatoRedeemerV5 is IYamatoRedeemerV4, YamatoAction { _scoreRegistry.updateScoreLimit( _pledge.owner, _pledge.debt, - totalDebt - vars._toBeRedeemed + totalDebt - vars._toBeRedeemed, + _pledge.getICR(priceFeed()) ); } - /* - 8. Update score - */ - // _scoreRegistry.updateScoreLimit(_args.sender, _pledge.debt, totalDebt - vars._toBeRedeemed); - /* Handle compensations */ diff --git a/contracts/YamatoRepayerV3.sol b/contracts/YamatoRepayerV3.sol index a604b509..176b1bc2 100644 --- a/contracts/YamatoRepayerV3.sol +++ b/contracts/YamatoRepayerV3.sol @@ -29,6 +29,8 @@ import "hardhat/console.sol"; /// @title Yamato Repayer Contract contract YamatoRepayerV3 is IYamatoRepayer, YamatoAction { + using PledgeLib for IYamato.Pledge; + function initialize(address _yamato) public initializer { __YamatoAction_init(_yamato); } @@ -94,7 +96,8 @@ contract YamatoRepayerV3 is IYamatoRepayer, YamatoAction { _scoreRegistry.updateScoreLimit( _sender, pledge.debt, - totalDebt - _repayAmountInCurrency + totalDebt - _repayAmountInCurrency, + pledge.getICR(priceFeed()) ); /* diff --git a/contracts/YamatoSweeperV3.sol b/contracts/YamatoSweeperV3.sol index 718d8b05..900ccbcb 100644 --- a/contracts/YamatoSweeperV3.sol +++ b/contracts/YamatoSweeperV3.sol @@ -161,7 +161,8 @@ contract YamatoSweeperV3 is IYamatoSweeper, YamatoAction { _scoreRegistry.updateScoreLimit( _pledge.owner, _pledge.debt, - totalDebt - vars._toBeSwept + totalDebt - vars._toBeSwept, + _pledge.getICR(priceFeed()) ); } From 9ec4258d4a8d89a568e0d384f365482d5cbab707 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 27 Nov 2023 14:05:45 +0900 Subject: [PATCH 065/412] Add FeePoolV2 --- contracts/FeePoolV2.sol | 585 ++++++++++++++++++++++++++++ contracts/Interfaces/IFeePoolV2.sol | 14 + contracts/Interfaces/IveYMT.sol | 19 + 3 files changed, 618 insertions(+) create mode 100644 contracts/FeePoolV2.sol create mode 100644 contracts/Interfaces/IFeePoolV2.sol diff --git a/contracts/FeePoolV2.sol b/contracts/FeePoolV2.sol new file mode 100644 index 00000000..f736d84e --- /dev/null +++ b/contracts/FeePoolV2.sol @@ -0,0 +1,585 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Interfaces/IveYMT.sol"; +import "./Interfaces/IFeePoolV2.sol"; +import "./Dependencies/UUPSBase.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + + +contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { + uint256 public constant WEEK = 7 * 86400; + uint256 public constant TOKEN_CHECKPOINT_DEADLINE = 1 hours; + + string constant VEYMT_SLOT_ID = "deps.veYMT"; + + mapping(address => bool) protocolWhitelist; + + uint256 public startTime; + uint256 public timeCursor; + mapping(address => uint256) public timeCursorOf; + mapping(address => uint256) public userEpochOf; + + uint256 public lastTokenTime; + mapping(uint256 => uint256) public tokensPerWeek; + + uint256 public tokenLastBalance; + + mapping(uint256 => uint256) public veSupply; // VE total supply at week bounds + + bool public canCheckpointToken; + bool public isKilled; + + event ToggleAllowCheckpointToken(bool toggleFlag); + event CheckpointToken(uint256 time, uint256 tokens); + event Claimed( + address indexed recipient, + uint256 amount, + uint256 claimEpoch, + uint256 maxEpoch + ); + event Received(address sender, uint256 value); + event VeYMTSet(address sender, address veYMT); + + /*** + * @param startTime_ Epoch time for fee distribution to start + */ + function initialize(uint256 startTime_) public initializer { + __UUPSBase_init(); + __ReentrancyGuard_init(); + + uint256 t = (startTime_ / WEEK) * WEEK; + startTime = t; + lastTokenTime = t; + timeCursor = t; + } + + /** + * @dev Calculates and distributes tokens for the current week based on the balance changes. + * It updates the `lastTokenTime` to the current block timestamp and adjusts the + * `tokensPerWeek` mapping to reflect the tokens distributed for this checkpoint. + */ + function _checkpointToken() internal { + uint256 _tokenBalance = address(this).balance; + uint256 _toDistribute = _tokenBalance - tokenLastBalance; + tokenLastBalance = _tokenBalance; + + uint256 _t = lastTokenTime; + uint256 _sinceLast = block.timestamp - _t; + lastTokenTime = block.timestamp; + uint256 _thisWeek = (_t / WEEK) * WEEK; + uint256 _nextWeek = 0; + + for (uint256 i; i < 20; ) { + _nextWeek = _thisWeek + WEEK; + if (block.timestamp < _nextWeek) { + if (_sinceLast == 0 && block.timestamp == _t) { + tokensPerWeek[_thisWeek] += _toDistribute; + } else { + tokensPerWeek[_thisWeek] += + (_toDistribute * (block.timestamp - _t)) / + _sinceLast; + } + break; + } else { + if (_sinceLast == 0 && _nextWeek == _t) { + tokensPerWeek[_thisWeek] += _toDistribute; + } else { + tokensPerWeek[_thisWeek] += + (_toDistribute * (_nextWeek - _t)) / + _sinceLast; + } + } + _t = _nextWeek; + _thisWeek = _nextWeek; + unchecked { + ++i; + } + } + + emit CheckpointToken(block.timestamp, _toDistribute); + } + + /*** + * @notice Update the token checkpoint + * @dev Calculates the total number of tokens to be distributed in a given week. + During setup for the initial distribution this function is only callable + by the contract owner. Beyond initial distro, it can be enabled for anyone + to call. + */ + function checkpointToken() external { + require( + msg.sender == governance || + (canCheckpointToken && + block.timestamp > lastTokenTime + TOKEN_CHECKPOINT_DEADLINE), + "Unauthorized" + ); + _checkpointToken(); + } + + /** + * @dev Performs a binary search to find the applicable epoch for the given timestamp. + * @param ve_ Address of the voting escrow contract. + * @param timestamp_ The timestamp to find the epoch for. + * @return The epoch index that corresponds to the given timestamp. + */ + function _findTimestampEpoch( + address ve_, + uint256 timestamp_ + ) internal view returns (uint256) { + uint256 _min = 0; + uint256 _max = IveYMT(ve_).epoch(); + + unchecked { + for (uint256 i; i < 128; i++) { + if (_min >= _max) { + break; + } + uint256 _mid = (_min + _max + 2) / 2; + IveYMT.Point memory _pt = IveYMT(ve_).pointHistory(_mid); + if (_pt.ts <= timestamp_) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + } + return _min; + } + + function _findTimestampUserEpoch( + address ve_, + address user_, + uint256 timestamp_, + uint256 maxUserEpoch_ + ) internal view returns (uint256) { + uint256 _min = 0; + uint256 _max = maxUserEpoch_; + + unchecked { + for (uint256 i; i < 128; i++) { + if (_min >= _max) { + break; + } + uint256 _mid = (_min + _max + 2) / 2; + IveYMT.Point memory _pt = IveYMT(ve_).userPointHistory( + user_, + _mid + ); + if (_pt.ts <= timestamp_) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + } + return _min; + } + + /*** + * @notice Get the veYNWK balance for `user_` at `timestamp_` + * @param user_ Address to query balance for + * @param timestamp_ Epoch time + * @return uint256 veYNWK balance + */ + function veForAt( + address user_, + uint256 timestamp_ + ) external view returns (uint256) { + address _ve = veYMT(); + uint256 _maxUserEpoch = IveYMT(_ve).userPointEpoch(user_); + uint256 _epoch = _findTimestampUserEpoch( + _ve, + user_, + timestamp_, + _maxUserEpoch + ); + IveYMT.Point memory _pt = IveYMT(_ve).userPointHistory(user_, _epoch); + int128 _balance = _pt.bias - + _pt.slope * + int128(int256(timestamp_ - _pt.ts)); + if (_balance < 0) { + return 0; + } else { + return uint256(uint128(_balance)); + } + } + + function _checkpointTotalSupply() internal { + address _ve = veYMT(); + uint256 _t = timeCursor; + uint256 _roundedTimestamp = (block.timestamp / WEEK) * WEEK; + IveYMT(_ve).checkpoint(); + + for (uint256 i; i < 20; ) { + if (_t > _roundedTimestamp) { + break; + } else { + uint256 _epoch = _findTimestampEpoch(_ve, _t); + IveYMT.Point memory _pt = IveYMT(_ve).pointHistory(_epoch); + int128 _dt = 0; + if (_t > _pt.ts) { + _dt = int128(int256(_t) - int256(_pt.ts)); + } + veSupply[_t] = uint256(int256(_pt.bias - _pt.slope * _dt)); + _t += WEEK; + } + unchecked { + ++i; + } + } + + timeCursor = _t; + } + + /*** + * @notice Update the veCRV total supply checkpoint + * @dev The checkpoint is also updated by the first claimant each new epoch week. This function may be called independently of a claim, to reduce claiming gas costs. + */ + function checkpointTotalSupply() external { + address _ve = veYMT(); + uint256 _t = timeCursor; + uint256 _roundedTimestamp = (block.timestamp / WEEK) * WEEK; + IveYMT(_ve).checkpoint(); + + for (uint256 i; i < 20; ) { + if (_t > _roundedTimestamp) { + break; + } else { + uint256 _epoch = _findTimestampEpoch(_ve, _t); + IveYMT.Point memory _pt = IveYMT(_ve).pointHistory(_epoch); + uint256 _dt = 0; + if (_t > _pt.ts) { + _dt = uint256(int256(_t) - int256(_pt.ts)); + } + + int128 _balance = _pt.bias - _pt.slope * int128(int256(_dt)); + if (_balance < 0) { + veSupply[_t] = 0; + } else { + veSupply[_t] = uint256(uint128(_balance)); + } + } + _t += WEEK; + unchecked { + ++i; + } + } + + timeCursor = _t; + } + + function _claim( + address addr_, + address ve_, + uint256 lastTokenTime_ + ) internal returns (uint256) { + // Minimal user_epoch is 0 (if user had no point) + uint256 _userEpoch = 0; + uint256 _toDistribute = 0; + + uint256 _maxUserEpoch = IveYMT(ve_).userPointEpoch(addr_); + uint256 _startTime = startTime; + + if (_maxUserEpoch == 0) { + // No lock = no fees + return 0; + } + + uint256 _weekCursor = timeCursorOf[addr_]; + if (_weekCursor == 0) { + // Need to do the initial binary search + _userEpoch = _findTimestampUserEpoch( + ve_, + addr_, + _startTime, + _maxUserEpoch + ); + } else { + _userEpoch = userEpochOf[addr_]; + } + + if (_userEpoch == 0) { + _userEpoch = 1; + } + + IveYMT.Point memory _userPoint = IveYMT(ve_).userPointHistory( + addr_, + _userEpoch + ); + + if (_weekCursor == 0) { + _weekCursor = ((_userPoint.ts + WEEK - 1) / WEEK) * WEEK; + } + + if (_weekCursor >= lastTokenTime_) { + return 0; + } + + if (_weekCursor < _startTime) { + _weekCursor = _startTime; + } + + IveYMT.Point memory _oldUserPoint = IveYMT.Point({bias: 0, slope: 0, ts: 0, blk: 0}); + + // Iterate over weeks + for (uint256 i; i < 50; ) { + if (_weekCursor >= lastTokenTime_) { + break; + } else if ( + _weekCursor >= _userPoint.ts && _userEpoch <= _maxUserEpoch + ) { + _userEpoch += 1; + _oldUserPoint = IveYMT.Point({ + bias: _userPoint.bias, + slope: _userPoint.slope, + ts: _userPoint.ts, + blk: _userPoint.blk + }); + if (_userEpoch > _maxUserEpoch) { + _userPoint = IveYMT.Point({bias: 0, slope: 0, ts: 0, blk: 0}); + } else { + _userPoint = IveYMT(ve_).userPointHistory( + addr_, + _userEpoch + ); + } + } else { + int256 _dt = int256(_weekCursor) - int256(_oldUserPoint.ts); + int256 _balanceOf = int256(_oldUserPoint.bias) - + _dt * + int256(_oldUserPoint.slope); + if ( + int256(_oldUserPoint.bias) - + _dt * + int256(_oldUserPoint.slope) < + 0 + ) { + _balanceOf = 0; + } + + if (_balanceOf == 0 && _userEpoch > _maxUserEpoch) { + break; + } + if (_balanceOf > 0) { + _toDistribute += + (uint256(_balanceOf) * tokensPerWeek[_weekCursor]) / + veSupply[_weekCursor]; + } + _weekCursor += WEEK; + } + unchecked { + ++i; + } + } + + _userEpoch = Math.min(_maxUserEpoch, _userEpoch - 1); + userEpochOf[addr_] = _userEpoch; + timeCursorOf[addr_] = _weekCursor; + + emit Claimed(addr_, _toDistribute, _userEpoch, _maxUserEpoch); + + return _toDistribute; + } + + /*** + * @notice Claim fees for `msg.sender` + * @dev Each call to claim look at a maximum of 50 user veCRV points. + For accounts with many veCRV related actions, this function + may need to be called more than once to claim all available + fees. In the `Claimed` event that fires, if `claim_epoch` is + less than `max_epoch`, the account may claim again. + * @return uint256 Amount of fees claimed in the call + */ + function claim() external nonReentrant returns (uint256) { + require(!isKilled, "Contract is killed"); + address _addr = msg.sender; + if (block.timestamp >= timeCursor) { + _checkpointTotalSupply(); + } + + uint256 _lastTokenTime = lastTokenTime; + + if ( + canCheckpointToken && (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) + ) { + _checkpointToken(); + _lastTokenTime = block.timestamp; + } + + unchecked { + _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; + } + + uint256 _amount = _claim(_addr, veYMT(), _lastTokenTime); + if (_amount != 0) { + (bool success, ) = payable(_addr).call{value: _amount}(""); + require(success, "Transfer failed"); + tokenLastBalance -= _amount; + } + + return _amount; + } + + /*** + * @notice Claim fees for `addr_` + * @dev Each call to claim look at a maximum of 50 user veCRV points. + For accounts with many veCRV related actions, this function + may need to be called more than once to claim all available + fees. In the `Claimed` event that fires, if `claim_epoch` is + less than `max_epoch`, the account may claim again. + * @param addr_ Address to claim fees for + * @return uint256 Amount of fees claimed in the call + */ + function claim(address addr_) external nonReentrant returns (uint256) { + require(!isKilled, "Contract is killed"); + + if (block.timestamp >= timeCursor) { + _checkpointTotalSupply(); + } + + uint256 _lastTokenTime = lastTokenTime; + + if ( + canCheckpointToken && (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) + ) { + _checkpointToken(); + _lastTokenTime = block.timestamp; + } + + unchecked { + _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; + } + + uint256 amount = _claim(addr_, veYMT(), _lastTokenTime); + if (amount != 0) { + (bool success, ) = payable(addr_).call{value: amount}(""); + require(success, "Transfer failed"); + tokenLastBalance -= amount; + } + + return amount; + } + + /** + * @notice Makes multiple fee claims in a single transaction. + * @dev Can be used to claim fees for multiple accounts in one call. Processing stops + * when an address zero is encountered in the `receivers_` array. + * @param receivers_ List of addresses to claim fees for. + * @return True if the operation was successful. + */ + function claimMany( + address[] memory receivers_ + ) external nonReentrant returns (bool) { + require(!isKilled, "Contract is killed"); + + if (block.timestamp >= timeCursor) { + _checkpointTotalSupply(); + } + + uint256 _lastTokenTime = lastTokenTime; + + if ( + canCheckpointToken && (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) + ) { + _checkpointToken(); + _lastTokenTime = block.timestamp; + } + + _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; + uint256 _total = 0; + uint256 _l = receivers_.length; + for (uint256 i; i < _l; ) { + address _addr = receivers_[i]; + if (_addr == address(0)) { + break; + } + + uint256 _amount = _claim(_addr, veYMT(), _lastTokenTime); + if (_amount != 0) { + (bool success, ) = payable(_addr).call{value: _amount}(""); + require( + success, + "Transfer failed" + ); + _total += _amount; + } + unchecked { + ++i; + } + } + + if (_total != 0) { + tokenLastBalance -= _total; + } + + return true; + } + + /*** + * @notice Toggle permission for checkpointing by any account + */ + function toggleAllowCheckpointToken() external onlyGovernance { + canCheckpointToken = !canCheckpointToken; + emit ToggleAllowCheckpointToken(canCheckpointToken); + } + + /** + * @notice Kills the contract and sends the remaining balance to the governance address. + * @dev Once killed, the contract's claim and distribution functions are disabled permanently. + */ + function killMe() external onlyGovernance { + isKilled = true; + (bool success, ) = payable(governance).call{value: address(this).balance}(""); + require( + success, + "Transfer failed" + ); + } + + /*** + * @notice Recover ERC20 tokens from this contract + * @dev Tokens are sent to the emergency return address. + * @return bool success + */ + function recoverBalance() external onlyGovernance returns (bool) { + (bool success, ) = payable(governance).call{value: address(this).balance}(""); + require( + success, + "Transfer failed" + ); + return true; + } + + function setVeYMT(address _veymt) public onlyGovernance { + bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); + assembly { + sstore(VEYMT_KEY, _veymt) + } + emit VeYMTSet(msg.sender, _veymt); + } + + function veYMT() public view override returns (address _veYMT) { + bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); + assembly { + _veYMT := sload(VEYMT_KEY) + } + } + + modifier onlyProtocols() { + require(protocolWhitelist[msg.sender], "You are not in the whitelist"); + _; + } + + receive() external payable { + emit Received(msg.sender, msg.value); + } +} diff --git a/contracts/Interfaces/IFeePoolV2.sol b/contracts/Interfaces/IFeePoolV2.sol new file mode 100644 index 00000000..5b427c4d --- /dev/null +++ b/contracts/Interfaces/IFeePoolV2.sol @@ -0,0 +1,14 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Written by 0xMotoko (0xmotoko@pm.me) + * Copyright (C) 2021 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly +interface IFeePoolV2 { + function veYMT() external view returns (address); + +} diff --git a/contracts/Interfaces/IveYMT.sol b/contracts/Interfaces/IveYMT.sol index 0ea97927..d3b7ec8c 100644 --- a/contracts/Interfaces/IveYMT.sol +++ b/contracts/Interfaces/IveYMT.sol @@ -9,6 +9,14 @@ pragma solidity 0.8.4; //solhint-disable no-inline-assembly interface IveYMT { + + struct Point { + int128 bias; + int128 slope; + uint256 ts; + uint256 blk; +} + function totalSupply() external view returns (uint256); function getLastUserSlope(address addr_) external view returns (int128); @@ -30,4 +38,15 @@ interface IveYMT { address addr, uint256 epoch ) external view returns (uint256); + + function epoch() external view returns (uint256); + + function userPointHistory( + address addr, + uint256 loc + ) external view returns (Point memory); + + function pointHistory(uint256 loc) external view returns (Point memory); + + function checkpoint() external; } From 7b900e6acbd46c04839201de5a5ef86dd4aafaf9 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 27 Nov 2023 14:06:16 +0900 Subject: [PATCH 066/412] overflow avoidance --- contracts/veYMT.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/veYMT.sol b/contracts/veYMT.sol index 8f9f855b..59d6ae25 100644 --- a/contracts/veYMT.sol +++ b/contracts/veYMT.sol @@ -773,6 +773,10 @@ contract veYMT is ReentrancyGuard { uint256 _targetEpoch = findBlockEpoch(block_, _epoch); Point memory _point = pointHistory[_targetEpoch]; + // to avoid underflow revert + if (_point.blk > block_) { + return 0; + } uint256 _dt = 0; if (_targetEpoch < _epoch) { Point memory _pointNext = pointHistory[_targetEpoch + 1]; From e746d17536dfec6bb7868d45dedc5718b6f5602e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 27 Nov 2023 14:07:29 +0900 Subject: [PATCH 067/412] code format --- contracts/FeePoolV2.sol | 50 +++++++++++++++++------------ contracts/Interfaces/IFeePoolV2.sol | 1 - contracts/Interfaces/IveYMT.sol | 3 +- contracts/ScoreRegistry.sol | 21 ++++++------ contracts/ScoreWeightController.sol | 5 ++- 5 files changed, 44 insertions(+), 36 deletions(-) diff --git a/contracts/FeePoolV2.sol b/contracts/FeePoolV2.sol index f736d84e..920c5d26 100644 --- a/contracts/FeePoolV2.sol +++ b/contracts/FeePoolV2.sol @@ -14,7 +14,6 @@ import "./Dependencies/UUPSBase.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; - contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { uint256 public constant WEEK = 7 * 86400; uint256 public constant TOKEN_CHECKPOINT_DEADLINE = 1 hours; @@ -119,7 +118,8 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { require( msg.sender == governance || (canCheckpointToken && - block.timestamp > lastTokenTime + TOKEN_CHECKPOINT_DEADLINE), + block.timestamp > + lastTokenTime + TOKEN_CHECKPOINT_DEADLINE), "Unauthorized" ); _checkpointToken(); @@ -328,7 +328,12 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { _weekCursor = _startTime; } - IveYMT.Point memory _oldUserPoint = IveYMT.Point({bias: 0, slope: 0, ts: 0, blk: 0}); + IveYMT.Point memory _oldUserPoint = IveYMT.Point({ + bias: 0, + slope: 0, + ts: 0, + blk: 0 + }); // Iterate over weeks for (uint256 i; i < 50; ) { @@ -345,7 +350,12 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { blk: _userPoint.blk }); if (_userEpoch > _maxUserEpoch) { - _userPoint = IveYMT.Point({bias: 0, slope: 0, ts: 0, blk: 0}); + _userPoint = IveYMT.Point({ + bias: 0, + slope: 0, + ts: 0, + blk: 0 + }); } else { _userPoint = IveYMT(ve_).userPointHistory( addr_, @@ -409,7 +419,8 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { uint256 _lastTokenTime = lastTokenTime; if ( - canCheckpointToken && (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) + canCheckpointToken && + (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) ) { _checkpointToken(); _lastTokenTime = block.timestamp; @@ -449,7 +460,8 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { uint256 _lastTokenTime = lastTokenTime; if ( - canCheckpointToken && (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) + canCheckpointToken && + (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) ) { _checkpointToken(); _lastTokenTime = block.timestamp; @@ -488,7 +500,8 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { uint256 _lastTokenTime = lastTokenTime; if ( - canCheckpointToken && (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) + canCheckpointToken && + (block.timestamp > _lastTokenTime + TOKEN_CHECKPOINT_DEADLINE) ) { _checkpointToken(); _lastTokenTime = block.timestamp; @@ -506,10 +519,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { uint256 _amount = _claim(_addr, veYMT(), _lastTokenTime); if (_amount != 0) { (bool success, ) = payable(_addr).call{value: _amount}(""); - require( - success, - "Transfer failed" - ); + require(success, "Transfer failed"); _total += _amount; } unchecked { @@ -538,11 +548,10 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { */ function killMe() external onlyGovernance { isKilled = true; - (bool success, ) = payable(governance).call{value: address(this).balance}(""); - require( - success, - "Transfer failed" - ); + (bool success, ) = payable(governance).call{ + value: address(this).balance + }(""); + require(success, "Transfer failed"); } /*** @@ -551,11 +560,10 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { * @return bool success */ function recoverBalance() external onlyGovernance returns (bool) { - (bool success, ) = payable(governance).call{value: address(this).balance}(""); - require( - success, - "Transfer failed" - ); + (bool success, ) = payable(governance).call{ + value: address(this).balance + }(""); + require(success, "Transfer failed"); return true; } diff --git a/contracts/Interfaces/IFeePoolV2.sol b/contracts/Interfaces/IFeePoolV2.sol index 5b427c4d..31a8c62a 100644 --- a/contracts/Interfaces/IFeePoolV2.sol +++ b/contracts/Interfaces/IFeePoolV2.sol @@ -10,5 +10,4 @@ pragma solidity 0.8.4; //solhint-disable no-inline-assembly interface IFeePoolV2 { function veYMT() external view returns (address); - } diff --git a/contracts/Interfaces/IveYMT.sol b/contracts/Interfaces/IveYMT.sol index d3b7ec8c..9159564b 100644 --- a/contracts/Interfaces/IveYMT.sol +++ b/contracts/Interfaces/IveYMT.sol @@ -9,13 +9,12 @@ pragma solidity 0.8.4; //solhint-disable no-inline-assembly interface IveYMT { - struct Point { int128 bias; int128 slope; uint256 ts; uint256 blk; -} + } function totalSupply() external view returns (uint256); diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 0215391a..ceb1649e 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -71,8 +71,8 @@ contract ScoreRegistry is YamatoAction { int128 public period; // Using dynamic array instead of fixed 100000000000000000000000000000 array to avoid warning about collisions - mapping(int128=>uint256) public periodTimestamp; - mapping(int128=>uint256) public integrateInvSupply; + mapping(int128 => uint256) public periodTimestamp; + mapping(int128 => uint256) public integrateInvSupply; function initialize(address minter_, address yamato_) public initializer { __YamatoAction_init(yamato_); @@ -93,9 +93,7 @@ contract ScoreRegistry is YamatoAction { _st.period = period; _st.periodTime = periodTimestamp[_st.period]; - _st.integrateInvSupply = integrateInvSupply[ - _st.period - ]; + _st.integrateInvSupply = integrateInvSupply[_st.period]; _st.rate = inflationRate; _st.prevFutureEpoch = futureEpochTime; @@ -164,8 +162,7 @@ contract ScoreRegistry is YamatoAction { _st.period += 1; period = _st.period; periodTimestamp[_st.period] = block.timestamp; - integrateInvSupply[_st.period] = _st - .integrateInvSupply; + integrateInvSupply[_st.period] = _st.integrateInvSupply; uint256 _workingBalance = workingBalances[addr]; integrateFraction[addr] += @@ -216,7 +213,9 @@ contract ScoreRegistry is YamatoAction { ); } - function calculateCoefficient(uint256 collateralRatio_) internal pure returns (uint256) { + function calculateCoefficient( + uint256 collateralRatio_ + ) internal pure returns (uint256) { uint256 _collateralRatio = collateralRatio_; if (_collateralRatio >= 25000) { return 2.5e18; @@ -231,7 +230,6 @@ contract ScoreRegistry is YamatoAction { } } - function userCheckpoint(address addr_) external onlyYamato returns (bool) { require( msg.sender == addr_ || msg.sender == minter, @@ -262,7 +260,9 @@ contract ScoreRegistry is YamatoAction { "Not allowed" ); require( - workingBalances[addr_] > (((_balance * TOKENLESS_PRODUCTION) / 100) * coefficient) / 1e18, + workingBalances[addr_] > + (((_balance * TOKENLESS_PRODUCTION) / 100) * coefficient) / + 1e18, "Not needed" ); @@ -282,5 +282,4 @@ contract ScoreRegistry is YamatoAction { function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } - } diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index e7cc09ef..62d8ac83 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -440,7 +440,10 @@ contract ScoreWeightController is UUPSBase { *@param name_ Name of score type *@param weight_ Weight of score type */ - function addType(string memory name_, uint256 weight_) external onlyGovernance { + function addType( + string memory name_, + uint256 weight_ + ) external onlyGovernance { int128 _typeId = nScoreTypes; scoreTypeNames[_typeId] = name_; unchecked { From 20108fb29c95a61553fdab878b41a8c52e529fa5 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 27 Nov 2023 14:36:03 +0900 Subject: [PATCH 068/412] Edited comment on FeePoolV2. --- contracts/FeePoolV2.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/FeePoolV2.sol b/contracts/FeePoolV2.sol index 920c5d26..65d76112 100644 --- a/contracts/FeePoolV2.sol +++ b/contracts/FeePoolV2.sol @@ -185,10 +185,10 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { } /*** - * @notice Get the veYNWK balance for `user_` at `timestamp_` + * @notice Get the veYMT balance for `user_` at `timestamp_` * @param user_ Address to query balance for * @param timestamp_ Epoch time - * @return uint256 veYNWK balance + * @return uint256 veYMT balance */ function veForAt( address user_, @@ -241,7 +241,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { } /*** - * @notice Update the veCRV total supply checkpoint + * @notice Update the veYMT total supply checkpoint * @dev The checkpoint is also updated by the first claimant each new epoch week. This function may be called independently of a claim, to reduce claiming gas costs. */ function checkpointTotalSupply() external { @@ -402,8 +402,8 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { /*** * @notice Claim fees for `msg.sender` - * @dev Each call to claim look at a maximum of 50 user veCRV points. - For accounts with many veCRV related actions, this function + * @dev Each call to claim look at a maximum of 50 user veYMT points. + For accounts with many veYMT related actions, this function may need to be called more than once to claim all available fees. In the `Claimed` event that fires, if `claim_epoch` is less than `max_epoch`, the account may claim again. @@ -442,8 +442,8 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { /*** * @notice Claim fees for `addr_` - * @dev Each call to claim look at a maximum of 50 user veCRV points. - For accounts with many veCRV related actions, this function + * @dev Each call to claim look at a maximum of 50 user veYMT points. + For accounts with many veYMT related actions, this function may need to be called more than once to claim all available fees. In the `Claimed` event that fires, if `claim_epoch` is less than `max_epoch`, the account may claim again. @@ -555,7 +555,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { } /*** - * @notice Recover ERC20 tokens from this contract + * @notice Recover native token from this contract * @dev Tokens are sent to the emergency return address. * @return bool success */ From ce38fb79e530427e3cf57fd2489966679464614e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 27 Nov 2023 17:12:07 +0900 Subject: [PATCH 069/412] Refactored ScoreRegistry. --- contracts/ScoreRegistry.sol | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index ceb1649e..4abcc907 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -43,7 +43,7 @@ contract ScoreRegistry is YamatoAction { } // Constants - uint256 public constant TOKENLESS_PRODUCTION = 40; + uint256 public constant TOKENLESS_PRODUCTION = 4; uint256 public constant WEEK = 604800; address public token; @@ -182,12 +182,12 @@ contract ScoreRegistry is YamatoAction { uint256 _votingBalance = IveYMT(votingEscrow).balanceOf(addr_); uint256 _votingTotal = IveYMT(votingEscrow).totalSupply(); - uint256 _lim = (debt_ * TOKENLESS_PRODUCTION) / 100; + uint256 _lim = (debt_ * TOKENLESS_PRODUCTION) / 10; if (_votingTotal > 0) { _lim += (((totalDebt_ * _votingBalance) / _votingTotal) * - (100 - TOKENLESS_PRODUCTION)) / - 100; + (10 - TOKENLESS_PRODUCTION)) / + 10; } _lim = min(debt_, _lim); @@ -197,7 +197,7 @@ contract ScoreRegistry is YamatoAction { uint256 coefficient = calculateCoefficient(collateralRatio_); // Adjust the limit based on the coefficient - uint256 _limit = (_lim * coefficient) / 1e18; + uint256 _limit = (_lim * coefficient) / 10; workingBalances[addr_] = _limit; uint256 _workingSupply = workingSupply + _limit - _oldBal; @@ -217,17 +217,11 @@ contract ScoreRegistry is YamatoAction { uint256 collateralRatio_ ) internal pure returns (uint256) { uint256 _collateralRatio = collateralRatio_; - if (_collateralRatio >= 25000) { - return 2.5e18; - } else if (_collateralRatio >= 20000) { - return 2e18; - } else if (_collateralRatio >= 15000) { - return 1.5e18; - } else if (_collateralRatio >= 13000) { - return 1e18; - } else { - return 0; - } + if (_collateralRatio >= 25000) return 25; + if (_collateralRatio >= 20000) return 20; + if (_collateralRatio >= 15000) return 15; + if (_collateralRatio >= 13000) return 10; + return 0; } function userCheckpoint(address addr_) external onlyYamato returns (bool) { @@ -261,8 +255,7 @@ contract ScoreRegistry is YamatoAction { ); require( workingBalances[addr_] > - (((_balance * TOKENLESS_PRODUCTION) / 100) * coefficient) / - 1e18, + (((_balance * TOKENLESS_PRODUCTION) / 10) * coefficient) / 10, "Not needed" ); From 81f3564c1ab397ce7657f49a79d552713121fcbb Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 27 Nov 2023 17:13:55 +0900 Subject: [PATCH 070/412] Version control files organised. --- test/param/version.ts | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/test/param/version.ts b/test/param/version.ts index 850ab3e7..54558933 100644 --- a/test/param/version.ts +++ b/test/param/version.ts @@ -1,23 +1,12 @@ export interface Version { - CJPY: string; - ChainLinkMock: string; - Currency: string; CurrencyOS: string; - Dependencies: string; FeePool: string; - Import: string; - Interfaces: string; - YmtMinter: string; - OracleMockBase: string; Pool: string; PriceFeed: string; PriorityRegistry: string; ScoreRegistry: string; ScoreWeightController: string; - TellorCallerMock: string; - Vester: string; - VesterFactory: string; - YMT: string; + veYMT: string; Yamato: string; YamatoBorrower: string; YamatoDepositor: string; @@ -25,31 +14,20 @@ export interface Version { YamatoRepayer: string; YamatoSweeper: string; YamatoWithdrawer: string; + YMT: string; + YmtMinter: string; YmtOS: string; - curveFork: string; - veYMT: string; } export const contractVersion: Version = { - CJPY: "CJPY", - ChainLinkMock: "ChainLinkMock", - Currency: "Currency", CurrencyOS: "CurrencyOSV3", - Dependencies: "Dependencies", - FeePool: "FeePool", - Import: "Import", - Interfaces: "Interfaces", - YmtMinter: "YmtMinter", - OracleMockBase: "OracleMockBase", + FeePool: "FeePoolV2", Pool: "PoolV2", PriceFeed: "PriceFeedV3", PriorityRegistry: "PriorityRegistryV6", ScoreRegistry: "ScoreRegistry", ScoreWeightController: "ScoreWeightController", - TellorCallerMock: "TellorCallerMock", - Vester: "Vester", - VesterFactory: "VesterFactory", - YMT: "YMT", + veYMT: "veYMT", Yamato: "YamatoV4", YamatoBorrower: "YamatoBorrowerV2", YamatoDepositor: "YamatoDepositorV2", @@ -57,7 +35,7 @@ export const contractVersion: Version = { YamatoRepayer: "YamatoRepayerV3", YamatoSweeper: "YamatoSweeperV3", YamatoWithdrawer: "YamatoWithdrawerV2", + YMT: "YMT", + YmtMinter: "YmtMinter", YmtOS: "YmtOS", - curveFork: "curveFork", - veYMT: "veYMT", }; From ddd2b171036db9283ab465f4c8d43baf2eca78f6 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 28 Nov 2023 17:10:11 +0900 Subject: [PATCH 071/412] Add FeePool integration test code. --- .../integration/FeePool/checkpointTs.test.ts | 120 ++++ .../FeePool/distributefFeesStateful.test.ts | 536 ++++++++++++++++++ 2 files changed, 656 insertions(+) create mode 100644 test/v15/integration/FeePool/checkpointTs.test.ts create mode 100644 test/v15/integration/FeePool/distributefFeesStateful.test.ts diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts new file mode 100644 index 00000000..4bf0af86 --- /dev/null +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -0,0 +1,120 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + FeePool, + FeePool__factory, +} from "../../../../typechain"; +import { getProxy } from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +const DAY = 86400; +const WEEK = DAY * 7; +const MAX_EXAMPLES = 10; + +describe("FeePoolV2", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let coinA: Contract; + let veYMT: Contract; + let feePool: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + + const FeePool = await ethers.getContractFactory("FeePoolV2"); + const YMT = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + token = await YMT.deploy(); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + feePool = await getProxy( + contractVersion["FeePool"], + [now] + ); + await feePool.setVeYMT(veYMT.address); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + function generateUniqueRandomNumbers( + count: number, + min: number, + max: number + ): number[] { + const set = new Set(); + while (set.size < count) { + const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; + set.add(randomValue); + } + return Array.from(set); + } + + it("test checkpoint total supply", async function () { + const stAmount = generateUniqueRandomNumbers(MAX_EXAMPLES, 1e4, 100 * 1e4); + const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); + const stSleep = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 30); + for (let i = 0; i < MAX_EXAMPLES; i++) { + await token + .connect(accounts[i]) + .approve(veYMT.address, ethers.constants.MaxUint256); + await token + .connect(accounts[0]) + .transfer(await accounts[i].address, ethers.utils.parseEther("1000")); + } + + let finalLock = 0; + for (let i = 0; i < MAX_EXAMPLES; i++) { + const sleepTime = Math.floor(stSleep[i] * 86400); + await ethers.provider.send("evm_increaseTime", [sleepTime]); + const lockTime = (await time.latest()) + sleepTime + WEEK * stLocktime[i]; + finalLock = Math.max(finalLock, lockTime); + + await veYMT + .connect(accounts[i]) + .createLock( + BigNumber.from(stAmount[i].toFixed(0)).mul((1e14).toString()), + Math.floor(lockTime) + ); + } + + while ((await time.latest()) < finalLock) { + const weekEpoch = + Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; // WEEK * WEEK; + + await time.increaseTo(weekEpoch); + + const weekBlock = await time.latestBlock(); + + await ethers.provider.send("evm_increaseTime", [1]); + + // Max: 42 weeks + // doing checkpoint 3 times is enough + for (let i = 0; i < 3; i++) { + await feePool.connect(accounts[0]).checkpointTotalSupply(); + } + + const expected = await veYMT.totalSupplyAt(weekBlock); + const actual = await feePool.veSupply(weekEpoch); + console.log(`expected: ${expected} actual: ${actual}`); + expect(actual).to.equal(expected); + } + }); +}); diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts new file mode 100644 index 00000000..9c2d12b6 --- /dev/null +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -0,0 +1,536 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + FeePool, + FeePool__factory, +} from "../../../../typechain"; +import { getProxy } from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +// 参考) brownie Stateful Tests +// https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html + +const ACCOUNT_NUM = 5; +const MAX_EXAMPLES = 50; +const STATEFUL_STEP_COUNT = 30; +const WEEK = 86400 * 7; +const YEAR = 86400 * 365; +const two_to_the_256_minus_1 = BigNumber.from("2") + .pow(BigNumber.from("256")) + .sub(BigNumber.from("1")); +const MOUNT_DECIMALS = 3; + +// Helper functions to generate random variables -----> +function randomBigValue(min: number, max: number): BigNumber { + return BigNumber.from( + Math.floor(Math.random() * (max - min) + min).toString() + ); +} +function randomValue(min: number, max: number): number { + return Math.floor(Math.random() * (max - min) + min); +} +function getRandomAccountNum(): number { + // Corresponds strategy("address", length=5) + let rdm = Math.floor(Math.random() * ACCOUNT_NUM); //0~9 integer + return rdm; +} +function getRandomWeeks(): BigNumber { + // Corresponds strategy("uint256", min_value=1, max_value=12) + return randomBigValue(1, 12); +} +function getRandomAmounts(): BigNumber { + // Corresponds strategy("decimal", min_value=1, max_value=100, places=3) + return randomBigValue( + 1 * 10 ** MOUNT_DECIMALS, + 100 * 10 ** MOUNT_DECIMALS + ).mul(BigNumber.from(10).pow(18 - MOUNT_DECIMALS)); +} +function getRandomsTime(): BigNumber { + // Corresponds strategy("uint256", min_value=0, max_value=86400 * 3) + return randomBigValue(0, 86400 * 3); +} +// ------------------------------------------------ + +describe("FeePoolV2", function () { + let accounts: SignerWithAddress[]; + let veYMT: Contract; + let feePool: Contract; + let token: Contract; + + let lockedUntil: { [key: string]: number } = {}; + let fees: { [key: number]: BigNumber } = {}; // timestamp -> amount + let userClaims: { [key: string]: { [key: number]: BigNumber[] } } = {}; // address -> timestamp -> [claimed, timeCursor] + let claimAmount = []; + let totalFees: BigNumber = ethers.utils.parseEther("1"); + + let snapshot: SnapshotRestorer; + + beforeEach(async () => { + snapshot = await takeSnapshot(); + accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); + + lockedUntil = {}; + fees = {}; + userClaims = {}; + totalFees = ethers.utils.parseEther("1"); + + const YMT = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + const FeePool = await ethers.getContractFactory("FeePoolV2"); + + token = await YMT.deploy(); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + for (let i = 0; i < ACCOUNT_NUM; i++) { + // ensure accounts[:5] all have tokens that may be locked + await token + .connect(accounts[0]) + .transfer(accounts[i].address, ethers.utils.parseEther("10000000")); + await token + .connect(accounts[i]) + .approve(veYMT.address, two_to_the_256_minus_1); + + userClaims[accounts[i].address] = []; + } + + // accounts[0] locks 10,000,000 tokens for 2 years - longer than the maximum duration of the test + await veYMT + .connect(accounts[0]) + .createLock( + ethers.utils.parseEther("10000000"), + (await time.latest()) + YEAR * 2 + ); + + lockedUntil = { + [accounts[0].address]: ( + await veYMT.lockedEnd(accounts[0].address) + ).toNumber(), + }; + + // a week later we deploy the fee feePool + await time.increase(WEEK); + + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + + feePool = await getProxy( + contractVersion["FeePool"], + [now] + ); + await feePool.setVeYMT(veYMT.address); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + //--------------------------------------------- functions -----------------------------------------------------------// + async function _checkActiveLock(stAcct: SignerWithAddress) { + // check if `st_acct` has an active lock + if (!lockedUntil[stAcct.address]) { + return false; + } + + const currentTime = await time.latest(); + + if (lockedUntil[stAcct.address] < currentTime) { + await veYMT.connect(stAcct).withdraw(); + delete lockedUntil[stAcct.address]; + return false; + } + + return true; + } + //--------------------------------------------- randomly excuted functions -----------------------------------------------------------// + async function ruleNewLock( + stAcct?: SignerWithAddress, + stAmount?: BigNumber, + stWeeks?: BigNumber, + stTime?: BigNumber + ) { + /* + Add a new user lock. + + Arguments + --------- + stAcct : SignerWithAddress + Account to lock tokens for. If this account already has an active + lock, the rule is skipped. + stAmount: BigNumber + Amount of tokens to lock. + stWeeks: BigNumber + Duration of lock, given in weeks. + stTime: BigNumber + Duration to sleep before action, in seconds. + */ + stAcct = stAcct || accounts[getRandomAccountNum()]; + stAmount = stAmount || getRandomAmounts(); + stWeeks = stWeeks || getRandomWeeks(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleNewLock --- + stAcct: ${ + stAcct.address + }, stAmount: ${stAmount.toString()}, stWeeks: ${stWeeks.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + if (!(await _checkActiveLock(stAcct))) { + const until = + (Math.floor((await time.latest()) / WEEK) + stWeeks.toNumber()) * WEEK; + await veYMT.connect(stAcct).createLock(stAmount, until); + lockedUntil[stAcct.address] = until; + } + } + + async function ruleExtendLock( + stAcct?: SignerWithAddress, + stWeeks?: BigNumber, + stTime?: BigNumber + ) { + /* + Extend an existing user lock. + + Arguments + --------- + stAcct: SignerWithAddress + Account to extend lock for. If this account does not have an active + lock, the rule is skipped. + stWeeks: BigNumber + Duration to extend the lock, given in weeks. + stTime: BigNumber + Duration to sleep before action, in seconds. + */ + stAcct = stAcct || accounts[getRandomAccountNum()]; + stWeeks = stWeeks || getRandomWeeks(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleExtendLock --- stAmount ${ + stAcct.address + }, stAmount: ${stWeeks.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + if (await _checkActiveLock(stAcct)) { + const until = + (Math.floor((await veYMT.lockedEnd(stAcct.address)).toNumber() / WEEK) + + stWeeks.toNumber()) * + WEEK; + const newUntil = Math.min( + until, + Math.floor(((await time.latest()) + YEAR * 4) / WEEK) * WEEK + ); + await veYMT.connect(stAcct).increaseUnlockTime(newUntil); + lockedUntil[stAcct.address] = newUntil; + } + } + + async function ruleIncreaseLockAmount( + stAcct?: SignerWithAddress, + stAmount?: BigNumber, + stTime?: BigNumber + ) { + console.log("ruleIncreaseLockAmount") + + /* + Increase the amount of an existing user lock. + + Arguments + --------- + stAcct : SignerWithAddress + Account to increase lock amount for. If this account does not have an + active lock, the rule is skipped. + stAmount : BigNumber + Amount of tokens to add to lock. + stTime : number + Duration to sleep before action, in seconds. + */ + stAcct = accounts[getRandomAccountNum()]; + stAmount = getRandomAmounts(); + stTime = getRandomsTime(); + + console.log(` + ruleIncreaseLockAmount --- stAmount ${ + stAcct.address + }, stAmount: ${stAmount.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + if (await _checkActiveLock(stAcct)) { + await veYMT.connect(stAcct).increaseAmount(stAmount); + } + } + + async function ruleClaimFees(stAcct?: SignerWithAddress, stTime?: BigNumber) { + console.log("ruleClaimFees") + /* + Claim fees for a user. + + Arguments + --------- + stAcct : SignerWithAddress + Account to claim fees for. + stTime : number + Duration to sleep before action, in seconds. + */ + stAcct = accounts[getRandomAccountNum()]; + stTime = getRandomsTime(); + + console.log(` + ruleClaimFees --- stAmount ${stAcct.address}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + // For debug --- + // const t0: number = (await feePool.startTime()).toNumber(); + // const ue = await feePool.userEpochOf(stAcct.address); + // const up = await veYMT.userPointHistory(stAcct.address, ue); + // console.log(`Week: + // ${Math.floor(((await time.latest()) - t0) / WEEK)} + + // Point: ${up} + // `); + // --- + + const claimed = await ethers.provider.getBalance(stAcct.address); + const tx = await feePool.connect(stAcct)["claim()"](); + const newClaimed = (await ethers.provider.getBalance(stAcct.address)).sub(claimed); + userClaims[stAcct.address][tx.blockNumber] = [ + newClaimed, + await feePool.timeCursorOf(stAcct.address), + ]; + } + + async function ruleTransferFees(stAmount?: BigNumber, stTime?: BigNumber) { + console.log("ruleTransferFees") + /* + Transfer fees into the feePool and make a checkpoint. + + If this is the first checkpoint, `can_checkpoint_token` is also + enabled. + + Arguments + --------- + stAmount : BigNumber + Amount of fee tokens to add to the feePool. + stTime : number + Duration to sleep before action, in seconds. + */ + stAmount = stAmount || getRandomAmounts(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleTransferFees --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + const tx = await accounts[0].sendTransaction({ + to: feePool.address, + value: stAmount, + }); + // const tx = await feeCoin._mintForTesting(feePool.address, stAmount); + + if (!(await feePool.canCheckpointToken())) { + await feePool.toggleAllowCheckpointToken(); + await feePool.checkpointToken(); + } + + fees[tx.blockNumber] = stAmount; + totalFees = totalFees.add(stAmount); + } + + async function ruleTransferFeesWithoutCheckpoint( + stAmount?: BigNumber, + stTime?: BigNumber + ) { + console.log("ruleTransferFeesWithoutCheckpoint") + + /* + Transfer fees into the feePool without checkpointing. + + Arguments + --------- + stAmount : BigNumber + Amount of fee tokens to add to the feePool. + stTime : number + Duration to sleep before action, in seconds. + */ + stAmount = stAmount || getRandomAmounts(); + stTime = stTime || getRandomsTime(); + + console.log(` + ruleTransferFeesWithoutCheckpoint --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} + `); + + stTime.gt(0) && (await time.increase(stTime)); + + const tx = await accounts[0].sendTransaction({ + to: feePool.address, + value: stAmount, + }); + // const tx = await feeCoin._mintForTesting(feePool.address, stAmount); + + fees[tx.blockNumber] = stAmount; + totalFees = totalFees.add(stAmount); + } + + async function teardown() { + /* + Claim fees for all accounts and verify that only dust remains. + */ + console.log("teardown----"); + if (!(await feePool.canCheckpointToken())) { + //if no token checkpoint occured, add 100,000 tokens prior to teardown + await ruleTransferFees( + ethers.utils.parseEther("100000"), + BigNumber.from("0") + ); + } + + // Need two checkpoints to get tokens fully distributed + // Because tokens for current week are obtained in the next week + // And that is by design + await feePool.checkpointToken(); + await ethers.provider.send("evm_increaseTime", [WEEK * 2]); + await feePool.checkpointToken(); + const balanceList = []; + for (const acct of accounts) { + // For debug ---> + // const t0: number = (await feePool.startTime()).toNumber(); + // const ue = await feePool.userEpochOf(acct.address); + // const up = await veYMT.userPointHistory(acct.address, ue); + // console.log(`Week: + // ${Math.floor(((await time.latest()) - t0) / WEEK)} + + // Point: ${up} + // `); + // <---- + balanceList[acct.address] = await ethers.provider.getBalance(acct.address); + const claimTx = await feePool.connect(acct)["claim()"](); + const receipt = await claimTx.wait() + const gasCost = receipt.gasUsed.mul(receipt.effectiveGasPrice) + balanceList[acct.address] = balanceList[acct.address].sub(gasCost); + } + + const t0: number = (await feePool.startTime()).toNumber(); + const t1: number = Math.floor((await time.latest()) / WEEK) * WEEK; + + const tokensPerUserPerWeek = []; + for (const acct of accounts) { + tokensPerUserPerWeek[acct.address] = []; + for (let w = t0; w < t1 + WEEK; w += WEEK) { + const tokens = (await feePool.tokensPerWeek(w)) + .mul(await feePool.veForAt(acct.address, w)) + .div(await feePool.veSupply(w)); + tokensPerUserPerWeek[acct.address].push(tokens); + } + } + + // Display results-------------------------------------------------- + // console.log(`Results: ${tokensPerUserPerWeek}`); + // console.log(`TokensPerUserPerWeek------------`); + // Object.entries(tokensPerUserPerWeek).forEach(([key, val]) => { + // console.log(`${key}: ${val}`); + // }); + // console.log(``); + // console.log(`Fees-----------`); + // Object.entries(fees).forEach(([key, val]) => { + // console.log(`${key}: ${val}`); + // }); + // console.log(``); + // console.log(`Total Fee-----------`); + // console.log(totalFees.toString()); + // console.log(``); + // console.log(`User claims---------`); + // Object.entries(userClaims).forEach(([key, val]) => { + // console.log(`${key}:`); + // Object.entries(val).forEach(([k, v]) => { + // console.log(`${k}: ${v}`); + // }); + // }); + // console.log(``); + // console.log(`User balances---------`); + // for (const acct of accounts) { + // console.log( + // acct.address, + // (await feeCoin.balanceOf(acct.address)).toString() + // ); + // } + // console.log(`feeCoin balance of Distributor--------`); + // console.log((await feeCoin.balanceOf(feePool.address)).toString()); + // ------------------------------------------- + + for (const acct of accounts) { + expect((await ethers.provider.getBalance(acct.address)).sub(BigNumber.from(balanceList[acct.address]))).to.equal( + tokensPerUserPerWeek[acct.address].reduce( + (a: BigNumber, b: BigNumber) => a.add(b), + BigNumber.from("0") + ) + ); + } + + // Check if all fees are distributed + expect(await ethers.provider.getBalance(feePool.address)).to.be.lt(100); + } + + let func = [ + "ruleNewLock", + "ruleExtendLock", + "ruleIncreaseLockAmount", + "ruleTransferFees", + "ruleTransferFeesWithoutCheckpoint", + ]; + + describe("test_deposit_withdraw_voting", function () { + for (let i = 0; i < MAX_EXAMPLES; i++) { + it(`should distributes fee ${i}`, async () => { + // Corresponds initializer initialize_new_lock and initialize_transfer_fees + // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html#initializers + // initialize_new_lock: This is equivalent to `rule_new_lock` to make it more likely we have at least 2 accounts locked at the start of the test run. + // initialize_transfer_fees: This is equivalent to `rule_transfer_fees` to make it more likely that claimable fees are available from the start of the test. + const initializerSeed = Math.random(); + if (initializerSeed < 0.2) { + console.log("0.2"); + await ruleNewLock(); + await ruleTransferFees(); + } else if (initializerSeed < 0.4) { + console.log("0.4"); + await ruleTransferFees(); + await ruleNewLock(); + } else if (initializerSeed < 0.6) { + console.log("0.6"); + await ruleNewLock(); + } else if (initializerSeed < 0.8) { + console.log("0.8"); + await ruleTransferFees(); + } + + const steps = randomValue(1, STATEFUL_STEP_COUNT); + for (let x = 0; x < steps; x++) { + let n = randomValue(0, func.length); + await eval(func[n])(); + } + + await teardown(); + }); + } + }); +}); From 0d052362603cad339a8ed1a80651a7ad1a9d5df1 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 29 Nov 2023 15:17:50 +0900 Subject: [PATCH 072/412] Minter added to OnlyYamato. --- contracts/YamatoV4.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/YamatoV4.sol b/contracts/YamatoV4.sol index fffdee70..2169319b 100644 --- a/contracts/YamatoV4.sol +++ b/contracts/YamatoV4.sol @@ -23,7 +23,7 @@ import "./Dependencies/SafeMath.sol"; import "./Interfaces/IYamato.sol"; import "./Interfaces/IYamatoV4.sol"; import "./Interfaces/IFeePool.sol"; -import "./Interfaces/ICurrencyOS.sol"; +import "./Interfaces/ICurrencyOSV3.sol"; import "hardhat/console.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; @@ -573,7 +573,7 @@ contract YamatoV4 is /// @dev Yamato.sol must override it with correct logic. function feePool() public view override returns (address) { - return ICurrencyOS(currencyOS()).feePool(); + return ICurrencyOSV3(currencyOS()).feePool(); } /// @dev Yamato.sol must override it with correct logic. @@ -583,7 +583,7 @@ contract YamatoV4 is override(IYamato, YamatoBase) returns (address) { - return ICurrencyOS(currencyOS()).priceFeed(); + return ICurrencyOSV3(currencyOS()).priceFeed(); } /// @dev All YamatoStores and YamatoActions except Yamato.sol are NOT needed to modify these funcs. Just write the same signature and don't fill inside. Yamato.sol must override it with correct logic. @@ -591,7 +591,7 @@ contract YamatoV4 is address _sender ) public view override(IYamato, YamatoBase) returns (bool) { bool permit; - address[10] memory deps = getDeps(); + address[11] memory deps = getDeps(); for (uint256 i = 0; i < deps.length; i++) { if (_sender == deps[i]) permit = true; } @@ -599,7 +599,7 @@ contract YamatoV4 is } /// @dev Get package-deps to check onlyYamato-permitDeps logic - function getDeps() public view returns (address[10] memory) { + function getDeps() public view returns (address[11] memory) { return [ address(this), depositor(), @@ -610,7 +610,8 @@ contract YamatoV4 is sweeper(), pool(), priorityRegistry(), - scoreRegistry() + scoreRegistry(), + ICurrencyOSV3(currencyOS()).minter() ]; } From 6e1b8d1cbabe1ad7266b986f5be487e2d2b9e783 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 29 Nov 2023 15:21:28 +0900 Subject: [PATCH 073/412] If the debt is 0, the collateral rate is not calculated --- contracts/ScoreRegistry.sol | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 4abcc907..561ee35f 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -182,22 +182,23 @@ contract ScoreRegistry is YamatoAction { uint256 _votingBalance = IveYMT(votingEscrow).balanceOf(addr_); uint256 _votingTotal = IveYMT(votingEscrow).totalSupply(); - uint256 _lim = (debt_ * TOKENLESS_PRODUCTION) / 10; + uint256 _limit = (debt_ * TOKENLESS_PRODUCTION) / 10; if (_votingTotal > 0) { - _lim += + _limit += (((totalDebt_ * _votingBalance) / _votingTotal) * (10 - TOKENLESS_PRODUCTION)) / 10; } - _lim = min(debt_, _lim); + _limit = min(debt_, _limit); uint256 _oldBal = workingBalances[addr_]; - // Apply the coefficient based on the collateral ratio provided - uint256 coefficient = calculateCoefficient(collateralRatio_); - - // Adjust the limit based on the coefficient - uint256 _limit = (_lim * coefficient) / 10; + if(debt_ > 0){ + // Apply the coefficient based on the collateral ratio provided + uint256 coefficient = calculateCoefficient(collateralRatio_); + // Adjust the limit based on the coefficient + _limit = (_limit * coefficient) / 10; + } workingBalances[addr_] = _limit; uint256 _workingSupply = workingSupply + _limit - _oldBal; From f44fe670812fdb9d246bf63584282c6c2f2b0e70 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 29 Nov 2023 16:40:30 +0900 Subject: [PATCH 074/412] Removed type_id on controllers to make it only currency --- contracts/ScoreWeightController.sol | 266 +--------------------------- 1 file changed, 6 insertions(+), 260 deletions(-) diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 62d8ac83..573e080c 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -33,14 +33,6 @@ contract ScoreWeightController is UUPSBase { uint256 end; } - event AddType(string name, int128 typeId); - event NewTypeWeight( - int128 typeId, - uint256 time, - uint256 weight, - uint256 totalWeight - ); - event NewScoreWeight( address scoreAddress, uint256 time, @@ -53,7 +45,7 @@ contract ScoreWeightController is UUPSBase { address scoreAddr, uint256 weight ); - event NewScore(address addr, int128 scoreType, uint256 weight); + event NewScore(address addr, uint256 weight); uint256 constant MULTIPLIER = 10 ** 18; @@ -62,17 +54,11 @@ contract ScoreWeightController is UUPSBase { // Voting escrow address public votingEscrow; - int128 public nScoreTypes; int128 public nScores; //number of scores - mapping(int128 => string) public scoreTypeNames; // Needed for enumeration mapping(int128 => address) public scores; - // we increment values by 1 prior to storing them here so we can rely on a value - // of zero as meaning the score has not been set mapping(address => int128) scoreTypes; - mapping(address => int128) public scoreTypes_; - mapping(address => mapping(address => VotedSlope)) public voteUserSlopes; // user -> score_addr -> VotedSlope mapping(address => uint256) public voteUserPower; // Total vote power used by user @@ -88,16 +74,9 @@ contract ScoreWeightController is UUPSBase { mapping(address => mapping(uint256 => uint256)) public changesWeight; // score_addr -> time -> slope mapping(address => uint256) public timeWeight; // score_addr -> last scheduled time (next week) - mapping(int128 => mapping(uint256 => Point)) public pointsSum; // type_id -> time -> Point - mapping(int128 => mapping(uint256 => uint256)) public changesSum; // type_id -> time -> slope - mapping(int128 => uint256) public timeSum; // type_id -> last scheduled time (next week) - mapping(uint256 => uint256) public pointsTotal; // time -> total weight uint256 public timeTotal; // last scheduled time - mapping(int128 => mapping(uint256 => uint256)) public pointsTypeWeight; // type_id -> time -> type weight - mapping(int128 => uint256) public timeTypeWeight; // type_id -> last scheduled time (next week) - /*** *@notice Contract constructor *@param _token `Token` contract address @@ -116,85 +95,6 @@ contract ScoreWeightController is UUPSBase { timeTotal = (block.timestamp / WEEK) * WEEK; } - /*** - *@notice Get score type for address - *@param addr_ Score address - *@return Score type id - */ - function scoreTypes(address addr_) external view returns (int128) { - int128 _scoreType = scoreTypes_[addr_]; - require(_scoreType != 0, "dev: score is not added"); - - return _scoreType - 1; - } - - /*** - *@notice Fill historic type weights week-over-week for missed checkins - * and return the type weight for the future week - *@param _score_type Score type id - *@return Type weight - */ - function _getTypeWeight(int128 scoreType_) internal returns (uint256) { - uint256 _t = timeTypeWeight[scoreType_]; - if (_t > 0) { - uint256 _w = pointsTypeWeight[scoreType_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - pointsTypeWeight[scoreType_][_t] = _w; - if (_t > block.timestamp) { - timeTypeWeight[scoreType_] = _t; - } - unchecked { - ++i; - } - } - return _w; - } else { - return 0; - } - } - - /*** - *@notice Fill sum of score weights for the same type week-over-week for - * missed checkins and return the sum for the future week - *@param scoreType_ Score type id - *@return Sum of weights - */ - function _getSum(int128 scoreType_) internal returns (uint256) { - uint256 _t = timeSum[scoreType_]; - if (_t > 0) { - Point memory _pt = pointsSum[scoreType_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - uint256 _dBias = _pt.slope * WEEK; - if (_pt.bias > _dBias) { - _pt.bias -= _dBias; - uint256 _dSlope = changesSum[scoreType_][_t]; - _pt.slope -= _dSlope; - } else { - _pt.bias = 0; - _pt.slope = 0; - } - pointsSum[scoreType_][_t] = _pt; - if (_t > block.timestamp) { - timeSum[scoreType_] = _t; - } - unchecked { - ++i; - } - } - return _pt.bias; - } else { - return 0; - } - } - /*** *@notice Fill historic total weights week-over-week for missed checkins * and return the total for the future week @@ -202,50 +102,13 @@ contract ScoreWeightController is UUPSBase { */ function _getTotal() internal returns (uint256) { uint256 _t = timeTotal; - int128 _nScoreTypes = nScoreTypes; if (_t > block.timestamp) { // If we have already checkpointed - still need to change the value _t -= WEEK; } uint256 _pt = pointsTotal[_t]; - for (int128 _scoreType; _scoreType < 100; ) { - if (_scoreType == _nScoreTypes) { - break; - } - _getSum(_scoreType); - _getTypeWeight(_scoreType); - unchecked { - ++_scoreType; - } - } - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - _pt = 0; - // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) - for (int128 _scoreType; _scoreType < 100; ) { - if (_scoreType == _nScoreTypes) { - break; - } - uint256 _typeSum = pointsSum[_scoreType][_t].bias; - uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; - _pt += _typeSum * _typeWeight; - unchecked { - ++_scoreType; - } - } - pointsTotal[_t] = _pt; - if (_t > block.timestamp) { - timeTotal = _t; - } - unchecked { - ++i; - } - } return _pt; } @@ -290,47 +153,33 @@ contract ScoreWeightController is UUPSBase { /*** *@notice Add Currency `addr` of type `score_type` with weight `weight` *@param addr_ Score address - *@param scoreType_ Score type + *@param weight_ Score weight */ function addCurrency( address addr_, - int128 scoreType_, uint256 weight_ ) external onlyGovernance { - require( - (scoreType_ >= 0) && (scoreType_ < nScoreTypes), - "score_type 0 means unset" - ); //score_type 0 means unset - require(scoreTypes_[addr_] == 0, "cannot add the same score twice"); int128 _n = nScores; unchecked { nScores = _n + 1; } scores[_n] = addr_; - scoreTypes_[addr_] = scoreType_ + 1; uint256 _nextTime; unchecked { _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; } if (weight_ > 0) { - uint256 _typeWeight = _getTypeWeight(scoreType_); - uint256 _oldSum = _getSum(scoreType_); uint256 _oldTotal = _getTotal(); - pointsSum[scoreType_][_nextTime].bias = weight_ + _oldSum; - timeSum[scoreType_] = _nextTime; - pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); + pointsTotal[_nextTime] = _oldTotal + weight_; timeTotal = _nextTime; pointsWeight[addr_][_nextTime].bias = weight_; } - if (timeSum[scoreType_] == 0) { - timeSum[scoreType_] = _nextTime; - } timeWeight[addr_] = _nextTime; - emit NewScore(addr_, scoreType_, weight_); + emit NewScore(addr_, weight_); } /*** @@ -365,11 +214,9 @@ contract ScoreWeightController is UUPSBase { uint256 _totalWeight = pointsTotal[_t]; if (_totalWeight > 0) { - int128 _scoreType = scoreTypes_[addr_] - 1; - uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; uint256 _scoreWeight = pointsWeight[addr_][_t].bias; - return (MULTIPLIER * _typeWeight * _scoreWeight) / _totalWeight; + return (MULTIPLIER * _scoreWeight) / _totalWeight; } else { return 0; } @@ -409,71 +256,10 @@ contract ScoreWeightController is UUPSBase { return _scoreRelativeWeight(addr_, time_); } - /*** - *@notice Change type weight - *@param typeId_ Type id - *@param weight_ New type weight - */ - function _changeTypeWeight(int128 typeId_, uint256 weight_) internal { - uint256 _oldWeight = _getTypeWeight(typeId_); - uint256 _oldSum = _getSum(typeId_); - uint256 _totalWeight = _getTotal(); - uint256 _nextTime; - unchecked { - _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - - _totalWeight = - _totalWeight + - (_oldSum * weight_) - - (_oldSum * _oldWeight); - pointsTotal[_nextTime] = _totalWeight; - pointsTypeWeight[typeId_][_nextTime] = weight_; - timeTotal = _nextTime; - timeTypeWeight[typeId_] = _nextTime; - - emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); - } - - /*** - *@notice Add score type with name `name_` and weight `weight` //ex. type=1, Liquidity, 1*1e18 - *@param name_ Name of score type - *@param weight_ Weight of score type - */ - function addType( - string memory name_, - uint256 weight_ - ) external onlyGovernance { - int128 _typeId = nScoreTypes; - scoreTypeNames[_typeId] = name_; - unchecked { - nScoreTypes = _typeId + 1; - } - if (weight_ != 0) { - _changeTypeWeight(_typeId, weight_); - emit AddType(name_, _typeId); - } - } - - /*** - *@notice Change score type `type_id` weight to `weight` - *@param typeId_ Score type id - *@param weight_ New Score weight - */ - function changeTypeWeight( - int128 typeId_, - uint256 weight_ - ) external onlyGovernance { - _changeTypeWeight(typeId_, weight_); - } - function _changeScoreWeight(address addr_, uint256 weight_) internal { // Change score weight // Only needed when testing in reality - int128 _scoreType = scoreTypes_[addr_] - 1; uint256 _oldScoreWeight = _getWeight(addr_); - uint256 _typeWeight = _getTypeWeight(_scoreType); - uint256 _oldSum = _getSum(_scoreType); uint256 _totalWeight = _getTotal(); uint256 _nextTime; unchecked { @@ -483,14 +269,8 @@ contract ScoreWeightController is UUPSBase { pointsWeight[addr_][_nextTime].bias = weight_; timeWeight[addr_] = _nextTime; - uint256 newSum = _oldSum + weight_ - _oldScoreWeight; - pointsSum[_scoreType][_nextTime].bias = newSum; - timeSum[_scoreType] = _nextTime; - _totalWeight = - _totalWeight + - (newSum * _typeWeight) - - (_oldSum * _typeWeight); + _totalWeight + weight_ - _oldScoreWeight; pointsTotal[_nextTime] = _totalWeight; timeTotal = _nextTime; @@ -515,7 +295,6 @@ contract ScoreWeightController is UUPSBase { uint256 lockEnd; uint256 _nScores; uint256 nextTime; - int128 scoreType; uint256 oldDt; uint256 oldBias; } @@ -551,8 +330,6 @@ contract ScoreWeightController is UUPSBase { ); } - _vp.scoreType = scoreTypes_[scoreAddr_] - 1; - require(_vp.scoreType >= 0, "Score not added"); // Prepare slopes and biases in memory VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][scoreAddr_]; _vp.oldDt = 0; @@ -582,34 +359,23 @@ contract ScoreWeightController is UUPSBase { // Schedule recording of initial slope for nextTime uint256 _oldWeightBias = _getWeight(scoreAddr_); uint256 _oldWeightSlope = pointsWeight[scoreAddr_][_vp.nextTime].slope; - uint256 _oldSumBias = _getSum(_vp.scoreType); - uint256 _oldSumSlope = pointsSum[_vp.scoreType][_vp.nextTime].slope; pointsWeight[scoreAddr_][_vp.nextTime].bias = max(_oldWeightBias + _newBias, _vp.oldBias) - _vp.oldBias; - pointsSum[_vp.scoreType][_vp.nextTime].bias = - max(_oldSumBias + _newBias, _vp.oldBias) - - _vp.oldBias; if (_oldSlope.end > _vp.nextTime) { pointsWeight[scoreAddr_][_vp.nextTime].slope = max(_oldWeightSlope + _newSlope.slope, _oldSlope.slope) - _oldSlope.slope; - pointsSum[_vp.scoreType][_vp.nextTime].slope = - max(_oldSumSlope + _newSlope.slope, _oldSlope.slope) - - _oldSlope.slope; } else { pointsWeight[scoreAddr_][_vp.nextTime].slope += _newSlope.slope; - pointsSum[_vp.scoreType][_vp.nextTime].slope += _newSlope.slope; } if (_oldSlope.end > block.timestamp) { // Cancel old slope changes if they still didn't happen changesWeight[scoreAddr_][_oldSlope.end] -= _oldSlope.slope; - changesSum[_vp.scoreType][_oldSlope.end] -= _oldSlope.slope; } // Add slope changes for new slopes changesWeight[scoreAddr_][_newSlope.end] += _newSlope.slope; - changesSum[_vp.scoreType][_newSlope.end] += _newSlope.slope; _getTotal(); @@ -630,15 +396,6 @@ contract ScoreWeightController is UUPSBase { return pointsWeight[addr_][timeWeight[addr_]].bias; } - /*** - *@notice Get current type weight - *@param typeId_ Type id - *@return Type weight - */ - function getTypeWeight(int128 typeId_) external view returns (uint256) { - return pointsTypeWeight[typeId_][timeTypeWeight[typeId_]]; - } - /*** *@notice Get current total (type-weighted) weight *@return Total weight @@ -647,17 +404,6 @@ contract ScoreWeightController is UUPSBase { return pointsTotal[timeTotal]; } - /*** - *@notice Get sum of score weights per type - *@param typeId_ Type id - *@return Sum of score weights - */ - function getWeightsSumPerType( - int128 typeId_ - ) external view returns (uint256) { - return pointsSum[typeId_][timeSum[typeId_]].bias; - } - function max(uint256 _a, uint256 _b) internal pure returns (uint256) { return _a >= _b ? _a : _b; } From fa7a972aaa06f206c736ffb96b31581b46593d91 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 29 Nov 2023 17:34:12 +0900 Subject: [PATCH 075/412] Modified IScoreWeightController. Dependencies sorted out. --- .../{IScoreController.sol => IScoreWeightController.sol} | 7 +++---- contracts/ScoreRegistry.sol | 8 ++++---- contracts/ScoreWeightController.sol | 8 ++++---- contracts/YmtMinter.sol | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) rename contracts/Interfaces/{IScoreController.sol => IScoreWeightController.sol} (82%) diff --git a/contracts/Interfaces/IScoreController.sol b/contracts/Interfaces/IScoreWeightController.sol similarity index 82% rename from contracts/Interfaces/IScoreController.sol rename to contracts/Interfaces/IScoreWeightController.sol index 87e3d46f..b6a68bf8 100644 --- a/contracts/Interfaces/IScoreController.sol +++ b/contracts/Interfaces/IScoreWeightController.sol @@ -8,8 +8,7 @@ pragma solidity 0.8.4; //solhint-disable max-line-length //solhint-disable no-inline-assembly -interface IScoreController { - function scoreTypes(address addr_) external view returns (uint256); +interface IScoreWeightController { function votingEscrow() external view returns (address); @@ -21,8 +20,6 @@ interface IScoreController { function userCheckpoint(address addr_) external returns (bool); - function addType(string memory name_, uint256 weight_) external; - function addCurrency( address addr_, int128 scoreType_, @@ -33,4 +30,6 @@ interface IScoreController { address addr, uint256 time ) external view returns (uint256); + + function timeWeight(address addr_) external view returns (uint256); } diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 561ee35f..89ca9c67 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -11,7 +11,7 @@ pragma solidity 0.8.4; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "./Dependencies/YamatoAction.sol"; import "./Dependencies/PledgeLib.sol"; -import "./Interfaces/IScoreController.sol"; +import "./Interfaces/IScoreWeightController.sol"; import "./Interfaces/IYMT.sol"; import "./Interfaces/IYmtMinter.sol"; import "./Interfaces/IveYMT.sol"; @@ -79,7 +79,7 @@ contract ScoreRegistry is YamatoAction { minter = minter_; token = IYmtMinter(minter).token(); scoreController = IYmtMinter(minter).controller(); - votingEscrow = IScoreController(scoreController).votingEscrow(); + votingEscrow = IScoreWeightController(scoreController).votingEscrow(); periodTimestamp[int128(0)] = block.timestamp; @@ -112,7 +112,7 @@ contract ScoreRegistry is YamatoAction { if (block.timestamp > _st.periodTime) { uint256 _workingSupply = workingSupply; - IScoreController(scoreController).checkpointScore(address(this)); + IScoreWeightController(scoreController).checkpointScore(address(this)); uint256 _prevWeekTime = _st.periodTime; uint256 _weekTime = min( ((_st.periodTime + WEEK) / WEEK) * WEEK, @@ -121,7 +121,7 @@ contract ScoreRegistry is YamatoAction { for (uint256 i = 0; i < 500; ) { uint256 dt = _weekTime - _prevWeekTime; - uint256 w = IScoreController(scoreController) + uint256 w = IScoreWeightController(scoreController) .scoreRelativeWeight( address(this), (_prevWeekTime / WEEK) * WEEK diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 573e080c..07b5555e 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -64,7 +64,7 @@ contract ScoreWeightController is UUPSBase { mapping(address => uint256) public voteUserPower; // Total vote power used by user mapping(address => mapping(address => uint256)) public lastUserVote; // Last user vote's timestamp for each score address - // Past and scheduled points for score weight, sum of weights per type, total weight + // Past and scheduled points for score weight, total weight // Point is for bias+slope // changes_* are for changes in slope // time_* are for the last change timestamp @@ -151,7 +151,7 @@ contract ScoreWeightController is UUPSBase { } /*** - *@notice Add Currency `addr` of type `score_type` with weight `weight` + *@notice Add Currency `addr` of with weight `weight` *@param addr_ Score address *@param weight_ Score weight */ @@ -279,7 +279,7 @@ contract ScoreWeightController is UUPSBase { /*** *@notice Change weight of score `addr` to `weight` - *@param addr_ `ScoreController` contract address + *@param addr_ `ScoreWeightController` contract address *@param weight_ New Score weight */ function changeScoreWeight( @@ -397,7 +397,7 @@ contract ScoreWeightController is UUPSBase { } /*** - *@notice Get current total (type-weighted) weight + *@notice Get current total (weighted) weight *@return Total weight */ function getTotalWeight() external view returns (uint256) { diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index d8b967bd..668490c8 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -10,7 +10,7 @@ pragma solidity 0.8.4; import "./Interfaces/IYMT.sol"; import "./Interfaces/IScoreRegistry.sol"; -import "./Interfaces/IScoreController.sol"; +import "./Interfaces/IScoreWeightController.sol"; import "./Dependencies/UUPSBase.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; @@ -47,7 +47,7 @@ contract YmtMinter is function _mintFor(address scoreAddr_, address for_) internal { require( - IScoreController(controller).scoreTypes(scoreAddr_) >= 0, + IScoreWeightController(controller).timeWeight(scoreAddr_) != 0, "dev: score is not added" ); From e3a558d4aea6a2582e171e339a3ed4b1c432fbe2 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 29 Nov 2023 18:41:53 +0900 Subject: [PATCH 076/412] Revert "Modified IScoreWeightController. Dependencies sorted out." This reverts commit fa7a972aaa06f206c736ffb96b31581b46593d91. --- .../{IScoreWeightController.sol => IScoreController.sol} | 7 ++++--- contracts/ScoreRegistry.sol | 8 ++++---- contracts/ScoreWeightController.sol | 8 ++++---- contracts/YmtMinter.sol | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) rename contracts/Interfaces/{IScoreWeightController.sol => IScoreController.sol} (82%) diff --git a/contracts/Interfaces/IScoreWeightController.sol b/contracts/Interfaces/IScoreController.sol similarity index 82% rename from contracts/Interfaces/IScoreWeightController.sol rename to contracts/Interfaces/IScoreController.sol index b6a68bf8..87e3d46f 100644 --- a/contracts/Interfaces/IScoreWeightController.sol +++ b/contracts/Interfaces/IScoreController.sol @@ -8,7 +8,8 @@ pragma solidity 0.8.4; //solhint-disable max-line-length //solhint-disable no-inline-assembly -interface IScoreWeightController { +interface IScoreController { + function scoreTypes(address addr_) external view returns (uint256); function votingEscrow() external view returns (address); @@ -20,6 +21,8 @@ interface IScoreWeightController { function userCheckpoint(address addr_) external returns (bool); + function addType(string memory name_, uint256 weight_) external; + function addCurrency( address addr_, int128 scoreType_, @@ -30,6 +33,4 @@ interface IScoreWeightController { address addr, uint256 time ) external view returns (uint256); - - function timeWeight(address addr_) external view returns (uint256); } diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 89ca9c67..561ee35f 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -11,7 +11,7 @@ pragma solidity 0.8.4; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "./Dependencies/YamatoAction.sol"; import "./Dependencies/PledgeLib.sol"; -import "./Interfaces/IScoreWeightController.sol"; +import "./Interfaces/IScoreController.sol"; import "./Interfaces/IYMT.sol"; import "./Interfaces/IYmtMinter.sol"; import "./Interfaces/IveYMT.sol"; @@ -79,7 +79,7 @@ contract ScoreRegistry is YamatoAction { minter = minter_; token = IYmtMinter(minter).token(); scoreController = IYmtMinter(minter).controller(); - votingEscrow = IScoreWeightController(scoreController).votingEscrow(); + votingEscrow = IScoreController(scoreController).votingEscrow(); periodTimestamp[int128(0)] = block.timestamp; @@ -112,7 +112,7 @@ contract ScoreRegistry is YamatoAction { if (block.timestamp > _st.periodTime) { uint256 _workingSupply = workingSupply; - IScoreWeightController(scoreController).checkpointScore(address(this)); + IScoreController(scoreController).checkpointScore(address(this)); uint256 _prevWeekTime = _st.periodTime; uint256 _weekTime = min( ((_st.periodTime + WEEK) / WEEK) * WEEK, @@ -121,7 +121,7 @@ contract ScoreRegistry is YamatoAction { for (uint256 i = 0; i < 500; ) { uint256 dt = _weekTime - _prevWeekTime; - uint256 w = IScoreWeightController(scoreController) + uint256 w = IScoreController(scoreController) .scoreRelativeWeight( address(this), (_prevWeekTime / WEEK) * WEEK diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 07b5555e..573e080c 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -64,7 +64,7 @@ contract ScoreWeightController is UUPSBase { mapping(address => uint256) public voteUserPower; // Total vote power used by user mapping(address => mapping(address => uint256)) public lastUserVote; // Last user vote's timestamp for each score address - // Past and scheduled points for score weight, total weight + // Past and scheduled points for score weight, sum of weights per type, total weight // Point is for bias+slope // changes_* are for changes in slope // time_* are for the last change timestamp @@ -151,7 +151,7 @@ contract ScoreWeightController is UUPSBase { } /*** - *@notice Add Currency `addr` of with weight `weight` + *@notice Add Currency `addr` of type `score_type` with weight `weight` *@param addr_ Score address *@param weight_ Score weight */ @@ -279,7 +279,7 @@ contract ScoreWeightController is UUPSBase { /*** *@notice Change weight of score `addr` to `weight` - *@param addr_ `ScoreWeightController` contract address + *@param addr_ `ScoreController` contract address *@param weight_ New Score weight */ function changeScoreWeight( @@ -397,7 +397,7 @@ contract ScoreWeightController is UUPSBase { } /*** - *@notice Get current total (weighted) weight + *@notice Get current total (type-weighted) weight *@return Total weight */ function getTotalWeight() external view returns (uint256) { diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index 668490c8..d8b967bd 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -10,7 +10,7 @@ pragma solidity 0.8.4; import "./Interfaces/IYMT.sol"; import "./Interfaces/IScoreRegistry.sol"; -import "./Interfaces/IScoreWeightController.sol"; +import "./Interfaces/IScoreController.sol"; import "./Dependencies/UUPSBase.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; @@ -47,7 +47,7 @@ contract YmtMinter is function _mintFor(address scoreAddr_, address for_) internal { require( - IScoreWeightController(controller).timeWeight(scoreAddr_) != 0, + IScoreController(controller).scoreTypes(scoreAddr_) >= 0, "dev: score is not added" ); From 6272800f76093494366c7b8bc639757f602727d0 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 29 Nov 2023 18:41:58 +0900 Subject: [PATCH 077/412] Revert "Removed type_id on controllers to make it only currency" This reverts commit f44fe670812fdb9d246bf63584282c6c2f2b0e70. --- contracts/ScoreWeightController.sol | 266 +++++++++++++++++++++++++++- 1 file changed, 260 insertions(+), 6 deletions(-) diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 573e080c..62d8ac83 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -33,6 +33,14 @@ contract ScoreWeightController is UUPSBase { uint256 end; } + event AddType(string name, int128 typeId); + event NewTypeWeight( + int128 typeId, + uint256 time, + uint256 weight, + uint256 totalWeight + ); + event NewScoreWeight( address scoreAddress, uint256 time, @@ -45,7 +53,7 @@ contract ScoreWeightController is UUPSBase { address scoreAddr, uint256 weight ); - event NewScore(address addr, uint256 weight); + event NewScore(address addr, int128 scoreType, uint256 weight); uint256 constant MULTIPLIER = 10 ** 18; @@ -54,11 +62,17 @@ contract ScoreWeightController is UUPSBase { // Voting escrow address public votingEscrow; + int128 public nScoreTypes; int128 public nScores; //number of scores + mapping(int128 => string) public scoreTypeNames; // Needed for enumeration mapping(int128 => address) public scores; + // we increment values by 1 prior to storing them here so we can rely on a value + // of zero as meaning the score has not been set mapping(address => int128) scoreTypes; + mapping(address => int128) public scoreTypes_; + mapping(address => mapping(address => VotedSlope)) public voteUserSlopes; // user -> score_addr -> VotedSlope mapping(address => uint256) public voteUserPower; // Total vote power used by user @@ -74,9 +88,16 @@ contract ScoreWeightController is UUPSBase { mapping(address => mapping(uint256 => uint256)) public changesWeight; // score_addr -> time -> slope mapping(address => uint256) public timeWeight; // score_addr -> last scheduled time (next week) + mapping(int128 => mapping(uint256 => Point)) public pointsSum; // type_id -> time -> Point + mapping(int128 => mapping(uint256 => uint256)) public changesSum; // type_id -> time -> slope + mapping(int128 => uint256) public timeSum; // type_id -> last scheduled time (next week) + mapping(uint256 => uint256) public pointsTotal; // time -> total weight uint256 public timeTotal; // last scheduled time + mapping(int128 => mapping(uint256 => uint256)) public pointsTypeWeight; // type_id -> time -> type weight + mapping(int128 => uint256) public timeTypeWeight; // type_id -> last scheduled time (next week) + /*** *@notice Contract constructor *@param _token `Token` contract address @@ -95,6 +116,85 @@ contract ScoreWeightController is UUPSBase { timeTotal = (block.timestamp / WEEK) * WEEK; } + /*** + *@notice Get score type for address + *@param addr_ Score address + *@return Score type id + */ + function scoreTypes(address addr_) external view returns (int128) { + int128 _scoreType = scoreTypes_[addr_]; + require(_scoreType != 0, "dev: score is not added"); + + return _scoreType - 1; + } + + /*** + *@notice Fill historic type weights week-over-week for missed checkins + * and return the type weight for the future week + *@param _score_type Score type id + *@return Type weight + */ + function _getTypeWeight(int128 scoreType_) internal returns (uint256) { + uint256 _t = timeTypeWeight[scoreType_]; + if (_t > 0) { + uint256 _w = pointsTypeWeight[scoreType_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + pointsTypeWeight[scoreType_][_t] = _w; + if (_t > block.timestamp) { + timeTypeWeight[scoreType_] = _t; + } + unchecked { + ++i; + } + } + return _w; + } else { + return 0; + } + } + + /*** + *@notice Fill sum of score weights for the same type week-over-week for + * missed checkins and return the sum for the future week + *@param scoreType_ Score type id + *@return Sum of weights + */ + function _getSum(int128 scoreType_) internal returns (uint256) { + uint256 _t = timeSum[scoreType_]; + if (_t > 0) { + Point memory _pt = pointsSum[scoreType_][_t]; + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + uint256 _dBias = _pt.slope * WEEK; + if (_pt.bias > _dBias) { + _pt.bias -= _dBias; + uint256 _dSlope = changesSum[scoreType_][_t]; + _pt.slope -= _dSlope; + } else { + _pt.bias = 0; + _pt.slope = 0; + } + pointsSum[scoreType_][_t] = _pt; + if (_t > block.timestamp) { + timeSum[scoreType_] = _t; + } + unchecked { + ++i; + } + } + return _pt.bias; + } else { + return 0; + } + } + /*** *@notice Fill historic total weights week-over-week for missed checkins * and return the total for the future week @@ -102,13 +202,50 @@ contract ScoreWeightController is UUPSBase { */ function _getTotal() internal returns (uint256) { uint256 _t = timeTotal; + int128 _nScoreTypes = nScoreTypes; if (_t > block.timestamp) { // If we have already checkpointed - still need to change the value _t -= WEEK; } uint256 _pt = pointsTotal[_t]; + for (int128 _scoreType; _scoreType < 100; ) { + if (_scoreType == _nScoreTypes) { + break; + } + _getSum(_scoreType); + _getTypeWeight(_scoreType); + unchecked { + ++_scoreType; + } + } + for (uint256 i; i < 500; ) { + if (_t > block.timestamp) { + break; + } + _t += WEEK; + _pt = 0; + // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) + for (int128 _scoreType; _scoreType < 100; ) { + if (_scoreType == _nScoreTypes) { + break; + } + uint256 _typeSum = pointsSum[_scoreType][_t].bias; + uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; + _pt += _typeSum * _typeWeight; + unchecked { + ++_scoreType; + } + } + pointsTotal[_t] = _pt; + if (_t > block.timestamp) { + timeTotal = _t; + } + unchecked { + ++i; + } + } return _pt; } @@ -153,33 +290,47 @@ contract ScoreWeightController is UUPSBase { /*** *@notice Add Currency `addr` of type `score_type` with weight `weight` *@param addr_ Score address - *@param weight_ Score weight + *@param scoreType_ Score type */ function addCurrency( address addr_, + int128 scoreType_, uint256 weight_ ) external onlyGovernance { + require( + (scoreType_ >= 0) && (scoreType_ < nScoreTypes), + "score_type 0 means unset" + ); //score_type 0 means unset + require(scoreTypes_[addr_] == 0, "cannot add the same score twice"); int128 _n = nScores; unchecked { nScores = _n + 1; } scores[_n] = addr_; + scoreTypes_[addr_] = scoreType_ + 1; uint256 _nextTime; unchecked { _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; } if (weight_ > 0) { + uint256 _typeWeight = _getTypeWeight(scoreType_); + uint256 _oldSum = _getSum(scoreType_); uint256 _oldTotal = _getTotal(); - pointsTotal[_nextTime] = _oldTotal + weight_; + pointsSum[scoreType_][_nextTime].bias = weight_ + _oldSum; + timeSum[scoreType_] = _nextTime; + pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); timeTotal = _nextTime; pointsWeight[addr_][_nextTime].bias = weight_; } + if (timeSum[scoreType_] == 0) { + timeSum[scoreType_] = _nextTime; + } timeWeight[addr_] = _nextTime; - emit NewScore(addr_, weight_); + emit NewScore(addr_, scoreType_, weight_); } /*** @@ -214,9 +365,11 @@ contract ScoreWeightController is UUPSBase { uint256 _totalWeight = pointsTotal[_t]; if (_totalWeight > 0) { + int128 _scoreType = scoreTypes_[addr_] - 1; + uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; uint256 _scoreWeight = pointsWeight[addr_][_t].bias; - return (MULTIPLIER * _scoreWeight) / _totalWeight; + return (MULTIPLIER * _typeWeight * _scoreWeight) / _totalWeight; } else { return 0; } @@ -256,10 +409,71 @@ contract ScoreWeightController is UUPSBase { return _scoreRelativeWeight(addr_, time_); } + /*** + *@notice Change type weight + *@param typeId_ Type id + *@param weight_ New type weight + */ + function _changeTypeWeight(int128 typeId_, uint256 weight_) internal { + uint256 _oldWeight = _getTypeWeight(typeId_); + uint256 _oldSum = _getSum(typeId_); + uint256 _totalWeight = _getTotal(); + uint256 _nextTime; + unchecked { + _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + } + + _totalWeight = + _totalWeight + + (_oldSum * weight_) - + (_oldSum * _oldWeight); + pointsTotal[_nextTime] = _totalWeight; + pointsTypeWeight[typeId_][_nextTime] = weight_; + timeTotal = _nextTime; + timeTypeWeight[typeId_] = _nextTime; + + emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); + } + + /*** + *@notice Add score type with name `name_` and weight `weight` //ex. type=1, Liquidity, 1*1e18 + *@param name_ Name of score type + *@param weight_ Weight of score type + */ + function addType( + string memory name_, + uint256 weight_ + ) external onlyGovernance { + int128 _typeId = nScoreTypes; + scoreTypeNames[_typeId] = name_; + unchecked { + nScoreTypes = _typeId + 1; + } + if (weight_ != 0) { + _changeTypeWeight(_typeId, weight_); + emit AddType(name_, _typeId); + } + } + + /*** + *@notice Change score type `type_id` weight to `weight` + *@param typeId_ Score type id + *@param weight_ New Score weight + */ + function changeTypeWeight( + int128 typeId_, + uint256 weight_ + ) external onlyGovernance { + _changeTypeWeight(typeId_, weight_); + } + function _changeScoreWeight(address addr_, uint256 weight_) internal { // Change score weight // Only needed when testing in reality + int128 _scoreType = scoreTypes_[addr_] - 1; uint256 _oldScoreWeight = _getWeight(addr_); + uint256 _typeWeight = _getTypeWeight(_scoreType); + uint256 _oldSum = _getSum(_scoreType); uint256 _totalWeight = _getTotal(); uint256 _nextTime; unchecked { @@ -269,8 +483,14 @@ contract ScoreWeightController is UUPSBase { pointsWeight[addr_][_nextTime].bias = weight_; timeWeight[addr_] = _nextTime; + uint256 newSum = _oldSum + weight_ - _oldScoreWeight; + pointsSum[_scoreType][_nextTime].bias = newSum; + timeSum[_scoreType] = _nextTime; + _totalWeight = - _totalWeight + weight_ - _oldScoreWeight; + _totalWeight + + (newSum * _typeWeight) - + (_oldSum * _typeWeight); pointsTotal[_nextTime] = _totalWeight; timeTotal = _nextTime; @@ -295,6 +515,7 @@ contract ScoreWeightController is UUPSBase { uint256 lockEnd; uint256 _nScores; uint256 nextTime; + int128 scoreType; uint256 oldDt; uint256 oldBias; } @@ -330,6 +551,8 @@ contract ScoreWeightController is UUPSBase { ); } + _vp.scoreType = scoreTypes_[scoreAddr_] - 1; + require(_vp.scoreType >= 0, "Score not added"); // Prepare slopes and biases in memory VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][scoreAddr_]; _vp.oldDt = 0; @@ -359,23 +582,34 @@ contract ScoreWeightController is UUPSBase { // Schedule recording of initial slope for nextTime uint256 _oldWeightBias = _getWeight(scoreAddr_); uint256 _oldWeightSlope = pointsWeight[scoreAddr_][_vp.nextTime].slope; + uint256 _oldSumBias = _getSum(_vp.scoreType); + uint256 _oldSumSlope = pointsSum[_vp.scoreType][_vp.nextTime].slope; pointsWeight[scoreAddr_][_vp.nextTime].bias = max(_oldWeightBias + _newBias, _vp.oldBias) - _vp.oldBias; + pointsSum[_vp.scoreType][_vp.nextTime].bias = + max(_oldSumBias + _newBias, _vp.oldBias) - + _vp.oldBias; if (_oldSlope.end > _vp.nextTime) { pointsWeight[scoreAddr_][_vp.nextTime].slope = max(_oldWeightSlope + _newSlope.slope, _oldSlope.slope) - _oldSlope.slope; + pointsSum[_vp.scoreType][_vp.nextTime].slope = + max(_oldSumSlope + _newSlope.slope, _oldSlope.slope) - + _oldSlope.slope; } else { pointsWeight[scoreAddr_][_vp.nextTime].slope += _newSlope.slope; + pointsSum[_vp.scoreType][_vp.nextTime].slope += _newSlope.slope; } if (_oldSlope.end > block.timestamp) { // Cancel old slope changes if they still didn't happen changesWeight[scoreAddr_][_oldSlope.end] -= _oldSlope.slope; + changesSum[_vp.scoreType][_oldSlope.end] -= _oldSlope.slope; } // Add slope changes for new slopes changesWeight[scoreAddr_][_newSlope.end] += _newSlope.slope; + changesSum[_vp.scoreType][_newSlope.end] += _newSlope.slope; _getTotal(); @@ -396,6 +630,15 @@ contract ScoreWeightController is UUPSBase { return pointsWeight[addr_][timeWeight[addr_]].bias; } + /*** + *@notice Get current type weight + *@param typeId_ Type id + *@return Type weight + */ + function getTypeWeight(int128 typeId_) external view returns (uint256) { + return pointsTypeWeight[typeId_][timeTypeWeight[typeId_]]; + } + /*** *@notice Get current total (type-weighted) weight *@return Total weight @@ -404,6 +647,17 @@ contract ScoreWeightController is UUPSBase { return pointsTotal[timeTotal]; } + /*** + *@notice Get sum of score weights per type + *@param typeId_ Type id + *@return Sum of score weights + */ + function getWeightsSumPerType( + int128 typeId_ + ) external view returns (uint256) { + return pointsSum[typeId_][timeSum[typeId_]].bias; + } + function max(uint256 _a, uint256 _b) internal pure returns (uint256) { return _a >= _b ? _a : _b; } From c44c8789acb6ea0114ebd6e0a3d4ba721996abd1 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 09:54:49 +0900 Subject: [PATCH 078/412] ScoreWeightController to return fixed values. --- ...troller.sol => IScoreWeightController.sol} | 14 +- contracts/ScoreRegistry.sol | 17 +- contracts/ScoreWeightController.sol | 600 +----------------- contracts/YmtMinter.sol | 5 +- 4 files changed, 49 insertions(+), 587 deletions(-) rename contracts/Interfaces/{IScoreController.sol => IScoreWeightController.sol} (56%) diff --git a/contracts/Interfaces/IScoreController.sol b/contracts/Interfaces/IScoreWeightController.sol similarity index 56% rename from contracts/Interfaces/IScoreController.sol rename to contracts/Interfaces/IScoreWeightController.sol index 87e3d46f..5c655c7c 100644 --- a/contracts/Interfaces/IScoreController.sol +++ b/contracts/Interfaces/IScoreWeightController.sol @@ -8,24 +8,16 @@ pragma solidity 0.8.4; //solhint-disable max-line-length //solhint-disable no-inline-assembly -interface IScoreController { - function scoreTypes(address addr_) external view returns (uint256); +interface IScoreWeightController { - function votingEscrow() external view returns (address); + function veYMT() external view returns (address); function checkpointScore(address addr) external; function checkpoint(address addr_) external; - function updateScoreLimit(address addr_, uint256 l_, uint256 L_) external; - - function userCheckpoint(address addr_) external returns (bool); - - function addType(string memory name_, uint256 weight_) external; - function addCurrency( address addr_, - int128 scoreType_, uint256 weight_ ) external; @@ -33,4 +25,6 @@ interface IScoreController { address addr, uint256 time ) external view returns (uint256); + + function scores(address addr) external view returns (int128); } diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 561ee35f..f0e9732c 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -11,7 +11,7 @@ pragma solidity 0.8.4; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "./Dependencies/YamatoAction.sol"; import "./Dependencies/PledgeLib.sol"; -import "./Interfaces/IScoreController.sol"; +import "./Interfaces/IScoreWeightController.sol"; import "./Interfaces/IYMT.sol"; import "./Interfaces/IYmtMinter.sol"; import "./Interfaces/IveYMT.sol"; @@ -79,7 +79,7 @@ contract ScoreRegistry is YamatoAction { minter = minter_; token = IYmtMinter(minter).token(); scoreController = IYmtMinter(minter).controller(); - votingEscrow = IScoreController(scoreController).votingEscrow(); + votingEscrow = IScoreWeightController(scoreController).veYMT(); periodTimestamp[int128(0)] = block.timestamp; @@ -112,7 +112,7 @@ contract ScoreRegistry is YamatoAction { if (block.timestamp > _st.periodTime) { uint256 _workingSupply = workingSupply; - IScoreController(scoreController).checkpointScore(address(this)); + IScoreWeightController(scoreController).checkpointScore(address(this)); uint256 _prevWeekTime = _st.periodTime; uint256 _weekTime = min( ((_st.periodTime + WEEK) / WEEK) * WEEK, @@ -121,7 +121,7 @@ contract ScoreRegistry is YamatoAction { for (uint256 i = 0; i < 500; ) { uint256 dt = _weekTime - _prevWeekTime; - uint256 w = IScoreController(scoreController) + uint256 w = IScoreWeightController(scoreController) .scoreRelativeWeight( address(this), (_prevWeekTime / WEEK) * WEEK @@ -217,11 +217,10 @@ contract ScoreRegistry is YamatoAction { function calculateCoefficient( uint256 collateralRatio_ ) internal pure returns (uint256) { - uint256 _collateralRatio = collateralRatio_; - if (_collateralRatio >= 25000) return 25; - if (_collateralRatio >= 20000) return 20; - if (_collateralRatio >= 15000) return 15; - if (_collateralRatio >= 13000) return 10; + if (collateralRatio_ >= 25000) return 25; + if (collateralRatio_ >= 20000) return 20; + if (collateralRatio_ >= 15000) return 15; + if (collateralRatio_ >= 13000) return 10; return 0; } diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 62d8ac83..24891ab0 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -15,88 +15,17 @@ import "./Interfaces/IveYMT.sol"; import "./Dependencies/UUPSBase.sol"; contract ScoreWeightController is UUPSBase { - // 7 * 86400 seconds - all future times are rounded by week - // uint256 constant WEEK = 604800; - uint256 constant WEEK = 7 days; - - // Cannot change weight votes more often than once in 10 days. - uint256 constant WEIGHT_VOTE_DELAY = 10 days; - - struct Point { - uint256 bias; - uint256 slope; - } - - struct VotedSlope { - uint256 slope; - uint256 power; - uint256 end; - } - - event AddType(string name, int128 typeId); - event NewTypeWeight( - int128 typeId, - uint256 time, - uint256 weight, - uint256 totalWeight - ); - - event NewScoreWeight( - address scoreAddress, - uint256 time, - uint256 weight, - uint256 totalWeight - ); - event VoteForScore( - uint256 time, - address user, - address scoreAddr, - uint256 weight - ); - event NewScore(address addr, int128 scoreType, uint256 weight); + string constant YMT_SLOT_ID = "deps.YMT"; + string constant VEYMT_SLOT_ID = "deps.veYMT"; uint256 constant MULTIPLIER = 10 ** 18; - // YMT token - address public token; - // Voting escrow - address public votingEscrow; + event NewScore(address addr, uint256 weight); - int128 public nScoreTypes; int128 public nScores; //number of scores - mapping(int128 => string) public scoreTypeNames; // Needed for enumeration - mapping(int128 => address) public scores; - - // we increment values by 1 prior to storing them here so we can rely on a value - // of zero as meaning the score has not been set mapping(address => int128) scoreTypes; - mapping(address => int128) public scoreTypes_; - - mapping(address => mapping(address => VotedSlope)) public voteUserSlopes; // user -> score_addr -> VotedSlope - - mapping(address => uint256) public voteUserPower; // Total vote power used by user - mapping(address => mapping(address => uint256)) public lastUserVote; // Last user vote's timestamp for each score address - - // Past and scheduled points for score weight, sum of weights per type, total weight - // Point is for bias+slope - // changes_* are for changes in slope - // time_* are for the last change timestamp - // timestamps are rounded to whole weeks - - mapping(address => mapping(uint256 => Point)) public pointsWeight; // score_addr -> time -> Point - mapping(address => mapping(uint256 => uint256)) public changesWeight; // score_addr -> time -> slope - mapping(address => uint256) public timeWeight; // score_addr -> last scheduled time (next week) - - mapping(int128 => mapping(uint256 => Point)) public pointsSum; // type_id -> time -> Point - mapping(int128 => mapping(uint256 => uint256)) public changesSum; // type_id -> time -> slope - mapping(int128 => uint256) public timeSum; // type_id -> last scheduled time (next week) - - mapping(uint256 => uint256) public pointsTotal; // time -> total weight - uint256 public timeTotal; // last scheduled time - - mapping(int128 => mapping(uint256 => uint256)) public pointsTypeWeight; // type_id -> time -> type weight - mapping(int128 => uint256) public timeTypeWeight; // type_id -> last scheduled time (next week) + mapping(address => int128) public scores; /*** *@notice Contract constructor @@ -104,186 +33,22 @@ contract ScoreWeightController is UUPSBase { *@param _votingEscrow `VotingEscrow` contract address */ function initialize( - address token_, - address votingEscrow_ + address ymtAddr, + address veYmtAddr ) public initializer { - require(token_ != address(0)); - require(votingEscrow_ != address(0)); + require(ymtAddr != address(0)); + require(veYmtAddr != address(0)); __UUPSBase_init(); - token = token_; - votingEscrow = votingEscrow_; - timeTotal = (block.timestamp / WEEK) * WEEK; - } - - /*** - *@notice Get score type for address - *@param addr_ Score address - *@return Score type id - */ - function scoreTypes(address addr_) external view returns (int128) { - int128 _scoreType = scoreTypes_[addr_]; - require(_scoreType != 0, "dev: score is not added"); - - return _scoreType - 1; - } - - /*** - *@notice Fill historic type weights week-over-week for missed checkins - * and return the type weight for the future week - *@param _score_type Score type id - *@return Type weight - */ - function _getTypeWeight(int128 scoreType_) internal returns (uint256) { - uint256 _t = timeTypeWeight[scoreType_]; - if (_t > 0) { - uint256 _w = pointsTypeWeight[scoreType_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - pointsTypeWeight[scoreType_][_t] = _w; - if (_t > block.timestamp) { - timeTypeWeight[scoreType_] = _t; - } - unchecked { - ++i; - } - } - return _w; - } else { - return 0; - } - } - - /*** - *@notice Fill sum of score weights for the same type week-over-week for - * missed checkins and return the sum for the future week - *@param scoreType_ Score type id - *@return Sum of weights - */ - function _getSum(int128 scoreType_) internal returns (uint256) { - uint256 _t = timeSum[scoreType_]; - if (_t > 0) { - Point memory _pt = pointsSum[scoreType_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - uint256 _dBias = _pt.slope * WEEK; - if (_pt.bias > _dBias) { - _pt.bias -= _dBias; - uint256 _dSlope = changesSum[scoreType_][_t]; - _pt.slope -= _dSlope; - } else { - _pt.bias = 0; - _pt.slope = 0; - } - pointsSum[scoreType_][_t] = _pt; - if (_t > block.timestamp) { - timeSum[scoreType_] = _t; - } - unchecked { - ++i; - } - } - return _pt.bias; - } else { - return 0; - } - } - - /*** - *@notice Fill historic total weights week-over-week for missed checkins - * and return the total for the future week - *@return Total weight - */ - function _getTotal() internal returns (uint256) { - uint256 _t = timeTotal; - int128 _nScoreTypes = nScoreTypes; - if (_t > block.timestamp) { - // If we have already checkpointed - still need to change the value - _t -= WEEK; - } - uint256 _pt = pointsTotal[_t]; - - for (int128 _scoreType; _scoreType < 100; ) { - if (_scoreType == _nScoreTypes) { - break; - } - _getSum(_scoreType); - _getTypeWeight(_scoreType); - unchecked { - ++_scoreType; - } - } - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - _pt = 0; - // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) - for (int128 _scoreType; _scoreType < 100; ) { - if (_scoreType == _nScoreTypes) { - break; - } - uint256 _typeSum = pointsSum[_scoreType][_t].bias; - uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; - _pt += _typeSum * _typeWeight; - unchecked { - ++_scoreType; - } - } - pointsTotal[_t] = _pt; - - if (_t > block.timestamp) { - timeTotal = _t; - } - unchecked { - ++i; - } - } - return _pt; - } - - /*** - *@notice Fill historic score weights week-over-week for missed checkins - * and return the total for the future week - *@param scoreAddr_ Address of the score - *@return Score weight - */ - function _getWeight(address scoreAddr_) internal returns (uint256) { - uint256 _t = timeWeight[scoreAddr_]; - if (_t > 0) { - Point memory _pt = pointsWeight[scoreAddr_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - uint256 _dBias = _pt.slope * WEEK; - if (_pt.bias > _dBias) { - _pt.bias -= _dBias; - uint256 _dSlope = changesWeight[scoreAddr_][_t]; - _pt.slope -= _dSlope; - } else { - _pt.bias = 0; - _pt.slope = 0; - } - pointsWeight[scoreAddr_][_t] = _pt; - if (_t > block.timestamp) { - timeWeight[scoreAddr_] = _t; - } - unchecked { - ++i; - } - } - return _pt.bias; - } else { - return 0; + bytes32 YMT_LEY = bytes32( + keccak256(abi.encode(YMT_SLOT_ID)) + ); + bytes32 VEYMT_LEY = bytes32( + keccak256(abi.encode(VEYMT_SLOT_ID)) + ); + assembly { + sstore(YMT_LEY, ymtAddr) + sstore(VEYMT_LEY, veYmtAddr) } } @@ -294,50 +59,22 @@ contract ScoreWeightController is UUPSBase { */ function addCurrency( address addr_, - int128 scoreType_, uint256 weight_ ) external onlyGovernance { - require( - (scoreType_ >= 0) && (scoreType_ < nScoreTypes), - "score_type 0 means unset" - ); //score_type 0 means unset - require(scoreTypes_[addr_] == 0, "cannot add the same score twice"); int128 _n = nScores; unchecked { nScores = _n + 1; } - scores[_n] = addr_; - scoreTypes_[addr_] = scoreType_ + 1; - uint256 _nextTime; - unchecked { - _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - - if (weight_ > 0) { - uint256 _typeWeight = _getTypeWeight(scoreType_); - uint256 _oldSum = _getSum(scoreType_); - uint256 _oldTotal = _getTotal(); - - pointsSum[scoreType_][_nextTime].bias = weight_ + _oldSum; - timeSum[scoreType_] = _nextTime; - pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); - timeTotal = _nextTime; + scores[addr_] = nScores; - pointsWeight[addr_][_nextTime].bias = weight_; - } - if (timeSum[scoreType_] == 0) { - timeSum[scoreType_] = _nextTime; - } - timeWeight[addr_] = _nextTime; - - emit NewScore(addr_, scoreType_, weight_); + emit NewScore(addr_, weight_); } /*** * @notice Checkpoint to fill data common for all scores */ function checkpoint() external { - _getTotal(); + // Add to V2.0 } /*** @@ -345,34 +82,7 @@ contract ScoreWeightController is UUPSBase { *@param addr_ Score address */ function checkpointScore(address addr_) external { - _getWeight(addr_); - _getTotal(); - } - - /*** - *@notice Get Score relative weight (not more than 1.0) normalized to 1e18 - * (e.g. 1.0 == 1e18). Inflation which will be received by it is - * inflation_rate * relative_weight / 1e18 - *@param addr_ Score address - *@param time_ Relative weight at the specified timestamp in the past or present - *@return Value of relative weight normalized to 1e18 - */ - function _scoreRelativeWeight( - address addr_, - uint256 time_ - ) internal view returns (uint256) { - uint256 _t = (time_ / WEEK) * WEEK; - uint256 _totalWeight = pointsTotal[_t]; - - if (_totalWeight > 0) { - int128 _scoreType = scoreTypes_[addr_] - 1; - uint256 _typeWeight = pointsTypeWeight[_scoreType][_t]; - uint256 _scoreWeight = pointsWeight[addr_][_t].bias; - - return (MULTIPLIER * _typeWeight * _scoreWeight) / _totalWeight; - } else { - return 0; - } + // Add to V2.0 } /*** @@ -387,114 +97,7 @@ contract ScoreWeightController is UUPSBase { address addr_, uint256 time_ ) external view returns (uint256) { - //default value - if (time_ == 0) { - time_ = block.timestamp; - } - - return _scoreRelativeWeight(addr_, time_); - } - - function scoreRelativeWeightWrite( - address addr_, - uint256 time_ - ) external returns (uint256) { - //default value - if (time_ == 0) { - time_ = block.timestamp; - } - - _getWeight(addr_); - _getTotal(); // Also calculates get_sum - return _scoreRelativeWeight(addr_, time_); - } - - /*** - *@notice Change type weight - *@param typeId_ Type id - *@param weight_ New type weight - */ - function _changeTypeWeight(int128 typeId_, uint256 weight_) internal { - uint256 _oldWeight = _getTypeWeight(typeId_); - uint256 _oldSum = _getSum(typeId_); - uint256 _totalWeight = _getTotal(); - uint256 _nextTime; - unchecked { - _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - - _totalWeight = - _totalWeight + - (_oldSum * weight_) - - (_oldSum * _oldWeight); - pointsTotal[_nextTime] = _totalWeight; - pointsTypeWeight[typeId_][_nextTime] = weight_; - timeTotal = _nextTime; - timeTypeWeight[typeId_] = _nextTime; - - emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); - } - - /*** - *@notice Add score type with name `name_` and weight `weight` //ex. type=1, Liquidity, 1*1e18 - *@param name_ Name of score type - *@param weight_ Weight of score type - */ - function addType( - string memory name_, - uint256 weight_ - ) external onlyGovernance { - int128 _typeId = nScoreTypes; - scoreTypeNames[_typeId] = name_; - unchecked { - nScoreTypes = _typeId + 1; - } - if (weight_ != 0) { - _changeTypeWeight(_typeId, weight_); - emit AddType(name_, _typeId); - } - } - - /*** - *@notice Change score type `type_id` weight to `weight` - *@param typeId_ Score type id - *@param weight_ New Score weight - */ - function changeTypeWeight( - int128 typeId_, - uint256 weight_ - ) external onlyGovernance { - _changeTypeWeight(typeId_, weight_); - } - - function _changeScoreWeight(address addr_, uint256 weight_) internal { - // Change score weight - // Only needed when testing in reality - int128 _scoreType = scoreTypes_[addr_] - 1; - uint256 _oldScoreWeight = _getWeight(addr_); - uint256 _typeWeight = _getTypeWeight(_scoreType); - uint256 _oldSum = _getSum(_scoreType); - uint256 _totalWeight = _getTotal(); - uint256 _nextTime; - unchecked { - _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - - pointsWeight[addr_][_nextTime].bias = weight_; - timeWeight[addr_] = _nextTime; - - uint256 newSum = _oldSum + weight_ - _oldScoreWeight; - pointsSum[_scoreType][_nextTime].bias = newSum; - timeSum[_scoreType] = _nextTime; - - _totalWeight = - _totalWeight + - (newSum * _typeWeight) - - (_oldSum * _typeWeight); - pointsTotal[_nextTime] = _totalWeight; - timeTotal = _nextTime; - - emit NewScoreWeight(addr_, block.timestamp, weight_, _totalWeight); + return MULTIPLIER; } /*** @@ -506,159 +109,24 @@ contract ScoreWeightController is UUPSBase { address addr_, uint256 weight_ ) external onlyGovernance { - _changeScoreWeight(addr_, weight_); - } - - struct VotingParameter { - //to avoid "Stack too deep" issue - uint256 slope; - uint256 lockEnd; - uint256 _nScores; - uint256 nextTime; - int128 scoreType; - uint256 oldDt; - uint256 oldBias; + // Add to V2.0 } - /**** - *@notice Allocate voting power for changing pool weights - *@param scoreAddr_ Score which `msg.sender` votes for - *@param userWeight_ Weight for a score in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0. bps = basis points - */ - function voteForScoreWeights( - address scoreAddr_, - uint256 userWeight_ - ) external { - VotingParameter memory _vp; - _vp.slope = uint256( - uint128(IveYMT(votingEscrow).getLastUserSlope(msg.sender)) + function YMT() public view returns (address ymtAddr) { + bytes32 YMT_LEY = bytes32( + keccak256(abi.encode(YMT_SLOT_ID)) ); - _vp.lockEnd = IveYMT(votingEscrow).lockedEnd(msg.sender); - _vp._nScores = uint256(uint128(nScores)); - unchecked { - _vp.nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; + assembly { + ymtAddr := sload(YMT_LEY) } - require(_vp.lockEnd > _vp.nextTime, "Your token lock expires too soon"); - require( - (userWeight_ >= 0) && (userWeight_ <= 10000), - "You used all your voting power" - ); - unchecked { - require( - block.timestamp >= - lastUserVote[msg.sender][scoreAddr_] + WEIGHT_VOTE_DELAY, - "Cannot vote so often" - ); - } - - _vp.scoreType = scoreTypes_[scoreAddr_] - 1; - require(_vp.scoreType >= 0, "Score not added"); - // Prepare slopes and biases in memory - VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][scoreAddr_]; - _vp.oldDt = 0; - if (_oldSlope.end > _vp.nextTime) { - _vp.oldDt = _oldSlope.end - _vp.nextTime; - } - _vp.oldBias = _oldSlope.slope * _vp.oldDt; - VotedSlope memory _newSlope = VotedSlope({ - slope: (_vp.slope * userWeight_) / 10000, - end: _vp.lockEnd, - power: userWeight_ - }); - uint256 _newDt = _vp.lockEnd - _vp.nextTime; // dev: raises when expired - uint256 _newBias = _newSlope.slope * _newDt; + } - // Check and update powers (weights) used - uint256 _powerUsed = voteUserPower[msg.sender]; - _powerUsed = _powerUsed + _newSlope.power - _oldSlope.power; - voteUserPower[msg.sender] = _powerUsed; - require( - (_powerUsed >= 0) && (_powerUsed <= 10000), - "Used too much power" + function veYMT() public view returns (address ymtAddr) { + bytes32 VEYMT_LEY = bytes32( + keccak256(abi.encode(VEYMT_SLOT_ID)) ); - - //// Remove old and schedule new slope changes - // Remove slope changes for old slopes - // Schedule recording of initial slope for nextTime - uint256 _oldWeightBias = _getWeight(scoreAddr_); - uint256 _oldWeightSlope = pointsWeight[scoreAddr_][_vp.nextTime].slope; - uint256 _oldSumBias = _getSum(_vp.scoreType); - uint256 _oldSumSlope = pointsSum[_vp.scoreType][_vp.nextTime].slope; - - pointsWeight[scoreAddr_][_vp.nextTime].bias = - max(_oldWeightBias + _newBias, _vp.oldBias) - - _vp.oldBias; - pointsSum[_vp.scoreType][_vp.nextTime].bias = - max(_oldSumBias + _newBias, _vp.oldBias) - - _vp.oldBias; - if (_oldSlope.end > _vp.nextTime) { - pointsWeight[scoreAddr_][_vp.nextTime].slope = - max(_oldWeightSlope + _newSlope.slope, _oldSlope.slope) - - _oldSlope.slope; - pointsSum[_vp.scoreType][_vp.nextTime].slope = - max(_oldSumSlope + _newSlope.slope, _oldSlope.slope) - - _oldSlope.slope; - } else { - pointsWeight[scoreAddr_][_vp.nextTime].slope += _newSlope.slope; - pointsSum[_vp.scoreType][_vp.nextTime].slope += _newSlope.slope; - } - if (_oldSlope.end > block.timestamp) { - // Cancel old slope changes if they still didn't happen - changesWeight[scoreAddr_][_oldSlope.end] -= _oldSlope.slope; - changesSum[_vp.scoreType][_oldSlope.end] -= _oldSlope.slope; + assembly { + ymtAddr := sload(VEYMT_LEY) } - // Add slope changes for new slopes - changesWeight[scoreAddr_][_newSlope.end] += _newSlope.slope; - changesSum[_vp.scoreType][_newSlope.end] += _newSlope.slope; - - _getTotal(); - - voteUserSlopes[msg.sender][scoreAddr_] = _newSlope; - - // Record last action time - lastUserVote[msg.sender][scoreAddr_] = block.timestamp; - - emit VoteForScore(block.timestamp, msg.sender, scoreAddr_, userWeight_); - } - - /*** - *@notice Get current score weight - *@param addr_ Score address - *@return Score weight - */ - function getScoreWeight(address addr_) external view returns (uint256) { - return pointsWeight[addr_][timeWeight[addr_]].bias; - } - - /*** - *@notice Get current type weight - *@param typeId_ Type id - *@return Type weight - */ - function getTypeWeight(int128 typeId_) external view returns (uint256) { - return pointsTypeWeight[typeId_][timeTypeWeight[typeId_]]; - } - - /*** - *@notice Get current total (type-weighted) weight - *@return Total weight - */ - function getTotalWeight() external view returns (uint256) { - return pointsTotal[timeTotal]; - } - - /*** - *@notice Get sum of score weights per type - *@param typeId_ Type id - *@return Sum of score weights - */ - function getWeightsSumPerType( - int128 typeId_ - ) external view returns (uint256) { - return pointsSum[typeId_][timeSum[typeId_]].bias; - } - - function max(uint256 _a, uint256 _b) internal pure returns (uint256) { - return _a >= _b ? _a : _b; } } diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index d8b967bd..cfaf2314 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -10,7 +10,7 @@ pragma solidity 0.8.4; import "./Interfaces/IYMT.sol"; import "./Interfaces/IScoreRegistry.sol"; -import "./Interfaces/IScoreController.sol"; +import "./Interfaces/IScoreWeightController.sol"; import "./Dependencies/UUPSBase.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; @@ -41,13 +41,14 @@ contract YmtMinter is ) public initializer { token = _token; controller = _controller; + __UUPSBase_init(); __ReentrancyGuard_init(); __Pausable_init(); } function _mintFor(address scoreAddr_, address for_) internal { require( - IScoreController(controller).scoreTypes(scoreAddr_) >= 0, + IScoreWeightController(controller).scores(scoreAddr_) > 0, "dev: score is not added" ); From 713e606c204fc603e63343d7577ce9d50fd40e3a Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 09:55:53 +0900 Subject: [PATCH 079/412] Add YmtMinter integration test code. --- .../v15/integration/Minter/components.test.ts | 425 ++++++++++++++++++ .../Minter/minterIntegration.test.ts | 283 ++++++++++++ 2 files changed, 708 insertions(+) create mode 100644 test/v15/integration/Minter/components.test.ts create mode 100644 test/v15/integration/Minter/minterIntegration.test.ts diff --git a/test/v15/integration/Minter/components.test.ts b/test/v15/integration/Minter/components.test.ts new file mode 100644 index 00000000..f0950abc --- /dev/null +++ b/test/v15/integration/Minter/components.test.ts @@ -0,0 +1,425 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import chai, { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + CJPY, + CurrencyOSV3, + Pool, + FeePool, + PriceFeedV3, + PriorityRegistry, + PriorityRegistry__factory, + YamatoV4, + YamatoDepositor, + YamatoBorrower, + YamatoRepayer, + YamatoWithdrawer, + YamatoRedeemer, + YamatoSweeper, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + CJPY__factory, + CurrencyOSV3__factory, + YamatoV4__factory, + YamatoDepositor__factory, + YamatoBorrower__factory, + YamatoRepayer__factory, + YamatoWithdrawer__factory, + YamatoRedeemer__factory, + YamatoSweeper__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, + Pool__factory, +} from "../../../../typechain"; +import { + getFakeProxy, + getProxy, + getLinkedProxy, +} from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +chai.use(smock.matchers); + +const ACCOUNT_NUM = 4; +const NUMBER_OF_ATTEMPTS = 20; +const SCALE = BigNumber.from((1e20).toString()); +const WEEK = 86400 * 7; +const MONTH = 86400 * 30; + +describe("YmtMinter components", function () { + let mockFeePool: FakeContract; + let mockFeed: FakeContract; + let CJPY: CJPY; + let currencyOS: CurrencyOSV3; + let yamato: YamatoV4; + let yamatoDepositor: YamatoDepositor; + let yamatoBorrower: YamatoBorrower; + let yamatoRepayer: YamatoRepayer; + let yamatoWithdrawer: YamatoWithdrawer; + let yamatoRedeemer: YamatoRedeemer; + let yamatoSweeper: YamatoSweeper; + let scoreRegistry: ScoreRegistry; + let ymtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let scoreWeightController: ScoreWeightController; + let pool: Pool; + let priorityRegistry: PriorityRegistry; + let PRICE: BigNumber; + let accounts: SignerWithAddress[]; + let ownerAddress: string; + let snapshot: SnapshotRestorer; + + before(async function () { + accounts = await ethers.getSigners(); + ownerAddress = await accounts[0].getAddress(); + + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); + CJPY = await (( + await ethers.getContractFactory("CJPY") + )).deploy(); + currencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, mockFeed.address, mockFeePool.address] + ); + + yamato = await getLinkedProxy( + contractVersion["Yamato"], + [currencyOS.address], + ["PledgeLib"] + ); + + await currencyOS.addYamato(yamato.address); + await CJPY.setCurrencyOS(currencyOS.address); + + yamatoDepositor = await getLinkedProxy< + YamatoDepositor, + YamatoDepositor__factory + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); + + yamatoBorrower = await getLinkedProxy< + YamatoBorrower, + YamatoBorrower__factory + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); + + yamatoRepayer = await getLinkedProxy( + contractVersion["YamatoRepayer"], + [yamato.address], + ["PledgeLib"] + ); + + yamatoWithdrawer = await getLinkedProxy< + YamatoWithdrawer, + YamatoWithdrawer__factory + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); + + yamatoRedeemer = await getLinkedProxy< + YamatoRedeemer, + YamatoRedeemer__factory + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); + + yamatoSweeper = await getLinkedProxy( + contractVersion["YamatoSweeper"], + [yamato.address], + ["PledgeLib"] + ); + + priorityRegistry = await getLinkedProxy< + PriorityRegistry, + PriorityRegistry__factory + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + scoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + ymtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, scoreWeightController.address] + ); + + scoreRegistry = await getLinkedProxy( + contractVersion["ScoreRegistry"], + [ymtMinter.address, yamato.address], + ["PledgeLib"] + ); + + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); + + await ( + await yamato.setDeps( + yamatoDepositor.address, + yamatoBorrower.address, + yamatoRepayer.address, + yamatoWithdrawer.address, + yamatoRedeemer.address, + yamatoSweeper.address, + pool.address, + priorityRegistry.address + ) + ).wait(); + await (await yamato.setScoreRegistry(scoreRegistry.address)).wait(); + + await currencyOS.setYmtMinter(ymtMinter.address); + await YMT.setMinter(ymtMinter.address); + + PRICE = BigNumber.from(260000).mul(1e18 + ""); + + // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); + mockFeed.fetchPrice.returns(PRICE); + mockFeed.getPrice.returns(PRICE); + mockFeed.lastGoodPrice.returns(PRICE); + + + // await scoreWeightController.addType( + // "Currency", + // ethers.utils.parseEther("1") + // ); + await scoreWeightController.addCurrency( + scoreRegistry.address, + ethers.utils.parseEther("10") + ); + for (let i = 0; i < 4; i++) { + await yamato + .connect(accounts[i]) + .deposit({ value: ethers.utils.parseEther("100") }); + } + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + function generateUniqueRandomNumbers( + count: number, + min: number, + max: number + ): number[] { + const set = new Set(); + while (set.size < count) { + const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; + set.add(randomValue); + } + return Array.from(set); + } + + function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { + if (value.isZero() && target.isZero()) { + return true; + } + + const diff = value.sub(target).abs(); + const sum = value.add(target); + const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); + + // console.log( + // `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` + // ); + // console.log( + // `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` + // ); + + return ratio.lte(tol); + } + + for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { + it(`tests duration ${i}`, async function () { + const stDuration = generateUniqueRandomNumbers(3, WEEK, MONTH); + const depositTime: number[] = []; + + await time.increase(WEEK); + + for (let i = 0; i < 3; i++) { + await yamato + .connect(accounts[i + 1]) + .borrow(ethers.utils.parseEther("1")); + depositTime.push(await time.latest()); + + // await showGaugeInfo(); + } + const durations: number[] = []; + const balances: BigNumber[] = []; + for (let i = 0; i < 3; i++) { + await time.increase(stDuration[i]); + await yamato + .connect(accounts[i + 1]) + .repay(ethers.utils.parseEther("0.999")); + const duration = (await time.latest()) - depositTime[i]; + durations.push(duration); + await ymtMinter.connect(accounts[i + 1]).mint(scoreRegistry.address); + const balance = await YMT.balanceOf(accounts[i + 1].address); + balances.push(balance); + + // await showGaugeInfo(); + } + + const totalMinted: BigNumber = balances.reduce( + (a: BigNumber, b: BigNumber) => a.add(b), + ethers.BigNumber.from(0) + ); + const weight1 = Math.floor(durations[0]); + const weight2 = Math.floor(weight1 + (durations[1] - durations[0]) * 1.5); + const weight3 = Math.floor(weight2 + (durations[2] - durations[1]) * 3); + const totalWeight = weight1 + weight2 + weight3; + + console.log( + `Total minted: ${totalMinted.toString()}, Total Weight: ${totalWeight.toString()}` + ); + console.log( + `Balance 1: ${balances[0]} (${balances[0] + .mul(SCALE) + .div(totalMinted)}) Weight 1: ${weight1.toString()} (${ + (100 * weight1) / totalWeight + }%)` + ); + console.log( + `Balance 2: ${balances[1]} (${balances[1] + .mul(SCALE) + .div(totalMinted)}) Weight 2: ${weight2.toString()} (${ + (100 * weight2) / totalWeight + }%)` + ); + console.log( + `Balance 3: ${balances[2]} (${balances[2] + .mul(SCALE) + .div(totalMinted)}) Weight 3: ${weight3.toString()} (${ + (100 * weight3) / totalWeight + }%)` + ); + + expect( + approx( + balances[0].mul(SCALE).div(totalMinted), + BigNumber.from(weight1).mul(SCALE).div(totalWeight), + BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[1].mul(SCALE).div(totalMinted), + BigNumber.from(weight2).mul(SCALE).div(totalWeight), + BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[2].mul(SCALE).div(totalMinted), + BigNumber.from(weight3).mul(SCALE).div(totalWeight), + BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ) + ).to.be.true; + }); + } + + for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { + it(`tests amounts ${i}`, async function () { + const stAmounts = generateUniqueRandomNumbers(3, 1e17, 1e18); + const depositTime: number[] = []; + + for (let i = 0; i < 3; i++) { + await yamato.connect(accounts[i + 1]).borrow(stAmounts[i].toString()); + depositTime.push(await time.latest()); + } + + await time.increase(MONTH); + + const balances: BigNumber[] = []; + for (let i = 0; i < 3; i++) { + yamato.connect(accounts[i + 1]).repay(stAmounts[i].toString()); + } + + for (let i = 0; i < 3; i++) { + await ymtMinter.connect(accounts[i + 1]).mint(scoreRegistry.address); + balances.push(await YMT.balanceOf(accounts[i + 1].address)); + } + const totalDeposited: number = stAmounts.reduce( + (a: number, b: number) => a + b, + 0 + ); + const totalMinted: BigNumber = balances.reduce( + (a: BigNumber, b: BigNumber) => a.add(b), + ethers.BigNumber.from(0) + ); + + console.log( + `Total deposited: ${totalDeposited.toString()}, Total minted: ${totalMinted.toString()}` + ); + console.log( + `Balance 1: ${balances[0]} (${balances[0] + .mul(SCALE) + .div(totalMinted)}) Deposited 1: ${stAmounts[0].toString()} (${ + (100 * stAmounts[0]) / totalDeposited + }%)` + ); + console.log( + `Balance 2: ${balances[1]} (${balances[1] + .mul(SCALE) + .div(totalMinted)}) Deposited 2: ${stAmounts[1].toString()} (${ + (100 * stAmounts[1]) / totalDeposited + }%)` + ); + console.log( + `Balance 3: ${balances[2]} (${balances[2] + .mul(SCALE) + .div(totalMinted)}) Deposited 3: ${stAmounts[2].toString()} (${ + (100 * stAmounts[2]) / totalDeposited + }%)` + ); + + expect( + approx( + balances[0].mul(SCALE).div(totalMinted), + BigNumber.from(stAmounts[0].toString()) + .mul(SCALE) + .div(totalDeposited.toString()), + BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[1].mul(SCALE).div(totalMinted), + BigNumber.from(stAmounts[1].toString()) + .mul(SCALE) + .div(totalDeposited.toString()), + BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ) + ).to.be.true; + expect( + approx( + balances[2].mul(SCALE).div(totalMinted), + BigNumber.from(stAmounts[2].toString()) + .mul(SCALE) + .div(totalDeposited.toString()), + BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ) + ).to.be.true; + }); + } +}); diff --git a/test/v15/integration/Minter/minterIntegration.test.ts b/test/v15/integration/Minter/minterIntegration.test.ts new file mode 100644 index 00000000..6002c686 --- /dev/null +++ b/test/v15/integration/Minter/minterIntegration.test.ts @@ -0,0 +1,283 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { FakeContract } from "@defi-wonderland/smock"; +import { + CJPY, + CurrencyOSV3, + Pool, + FeePool, + PriceFeedV3, + PriorityRegistry, + PriorityRegistry__factory, + YamatoV4, + YamatoDepositor, + YamatoBorrower, + YamatoRepayer, + YamatoWithdrawer, + YamatoRedeemer, + YamatoSweeper, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + CJPY__factory, + CurrencyOSV3__factory, + YamatoV4__factory, + YamatoDepositor__factory, + YamatoBorrower__factory, + YamatoRepayer__factory, + YamatoWithdrawer__factory, + YamatoRedeemer__factory, + YamatoSweeper__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, + Pool__factory, +} from "../../../../typechain"; +import { + getFakeProxy, + getProxy, + getLinkedProxy, +} from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +const SCALE = BigNumber.from(10).pow(20); +const WEEK = 86400 * 7; +const MONTH = 86400 * 30; +const W = BigNumber.from(10).pow(18); + +describe.only("YmtMinter integration", function () { + let accounts: SignerWithAddress[]; + let mockFeePool: FakeContract; + let mockFeed: FakeContract; + let CJPY: CJPY; + let currencyOS: CurrencyOSV3; + let yamato: YamatoV4; + let yamatoDepositor: YamatoDepositor; + let yamatoBorrower: YamatoBorrower; + let yamatoRepayer: YamatoRepayer; + let yamatoWithdrawer: YamatoWithdrawer; + let yamatoRedeemer: YamatoRedeemer; + let yamatoSweeper: YamatoSweeper; + let scoreRegistry: ScoreRegistry; + let ymtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let scoreWeightController: ScoreWeightController; + let pool: Pool; + let priorityRegistry: PriorityRegistry; + let PRICE: BigNumber; + let snapshot: SnapshotRestorer; + + before(async function () { + accounts = await ethers.getSigners(); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); + CJPY = await (( + await ethers.getContractFactory("CJPY") + )).deploy(); + currencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, mockFeed.address, mockFeePool.address] + ); + + yamato = await getLinkedProxy( + contractVersion["Yamato"], + [currencyOS.address], + ["PledgeLib"] + ); + + await currencyOS.addYamato(yamato.address); + await CJPY.setCurrencyOS(currencyOS.address); + + yamatoDepositor = await getLinkedProxy< + YamatoDepositor, + YamatoDepositor__factory + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); + + yamatoBorrower = await getLinkedProxy< + YamatoBorrower, + YamatoBorrower__factory + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); + + yamatoRepayer = await getLinkedProxy( + contractVersion["YamatoRepayer"], + [yamato.address], + ["PledgeLib"] + ); + + yamatoWithdrawer = await getLinkedProxy< + YamatoWithdrawer, + YamatoWithdrawer__factory + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); + + yamatoRedeemer = await getLinkedProxy< + YamatoRedeemer, + YamatoRedeemer__factory + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); + + yamatoSweeper = await getLinkedProxy( + contractVersion["YamatoSweeper"], + [yamato.address], + ["PledgeLib"] + ); + + priorityRegistry = await getLinkedProxy< + PriorityRegistry, + PriorityRegistry__factory + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + scoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + ymtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, scoreWeightController.address] + ); + + scoreRegistry = await getLinkedProxy( + contractVersion["ScoreRegistry"], + [ymtMinter.address, yamato.address], + ["PledgeLib"] + ); + + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); + + await ( + await yamato.setDeps( + yamatoDepositor.address, + yamatoBorrower.address, + yamatoRepayer.address, + yamatoWithdrawer.address, + yamatoRedeemer.address, + yamatoSweeper.address, + pool.address, + priorityRegistry.address + ) + ).wait(); + await (await yamato.setScoreRegistry(scoreRegistry.address)).wait(); + + await currencyOS.setYmtMinter(ymtMinter.address); + await YMT.setMinter(ymtMinter.address); + + PRICE = BigNumber.from(200000).mul(1e18 + ""); + + // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); + mockFeed.fetchPrice.returns(PRICE); + mockFeed.getPrice.returns(PRICE); + mockFeed.lastGoodPrice.returns(PRICE); + + await yamato.deposit({ value: ethers.utils.parseEther("100") }); + await yamato.borrow(ethers.utils.parseEther("100000")); + for(let i = 0; i < 5; i++){ + await CJPY.transfer(accounts[i + 1].address, ethers.utils.parseEther("10000")); + } + await scoreWeightController + .addCurrency( + scoreRegistry.address, + W + ); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + function getICR(pledge, price){ + const {coll, debt} = pledge; + const collInCurrency = (coll.mul(price)).div(W); + return (BigNumber.from("10000").mul(collInCurrency)).div(debt); + } + + function calculateCoefficient(collateralRatio) { + if (collateralRatio >= 25000) return BigNumber.from("25"); + if (collateralRatio >= 20000) return BigNumber.from("20"); + if (collateralRatio >= 15000) return BigNumber.from("15"); + if (collateralRatio >= 13000) return BigNumber.from("10"); + return BigNumber.from("0"); +} + + function approxEqual(actual, expected, tolerance) { + // 差の絶対値を計算 + const diff = actual.sub(expected).abs(); + + // 差が許容誤差以下であるかどうかを確認 + return diff.lte(tolerance); + } + + it(`tests mint collateral ratio`, async function () { + const amount = "100000"; + await YMT.setMinter(ymtMinter.address); + const ethAmounts = ["1.5", "1.25", "1", "0.75", "0.65"]; + for(let i = 0; i < 5; i++){ + await yamato + .connect(accounts[i + 1]) + .deposit({ value: ethers.utils.parseEther(ethAmounts[i]) }); + } + + // For weights to activate + await time.increase(WEEK); + + // Bob and Charlie deposit to gauges with different weights + for(let i = 0; i < 5; i++){ + await yamato + .connect(accounts[i + 1]) + .borrow(ethers.utils.parseEther(amount)); + } + // mockFeed.fetchPrice.returns(PRICE = BigNumber.from(180000).mul(1e18 + "")); + await time.increase(MONTH); + + const ICRs = []; + for(let i = 0; i < 5; i++){ + const pledge = await yamato.getPledge(accounts[i + 1].address); + ICRs.push(Number(getICR(pledge, PRICE))); + await yamato.connect(accounts[i + 1]).repay(ethers.utils.parseEther(amount)); + } + + // Claim for Bob now + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + const bobTokens = await YMT.balanceOf(accounts[1].address); + await time.increase(MONTH); + + // This won't give anything + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(bobTokens); + + let tokens = []; + for(let i = 0; i < 5; i++) { + await ymtMinter.connect(accounts[i + 1]).mint(scoreRegistry.address); + const token = await YMT.balanceOf(accounts[i + 1].address); + console.log("tokenBalance:", Number(token)); + tokens.push(token); + } + for(let i = 0; i < 5; i++) { + const ratio = calculateCoefficient(ICRs[i]) + expect(approxEqual(tokens[i], (tokens[4].mul(ratio)).div(BigNumber.from("10")), BigNumber.from(10).pow(19))).to.be.true; + } + }); +}); + From 5b4e29c61572e3ff72c778bac56196c96ee39112 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 09:59:23 +0900 Subject: [PATCH 080/412] only removed. --- test/v15/integration/Minter/minterIntegration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/v15/integration/Minter/minterIntegration.test.ts b/test/v15/integration/Minter/minterIntegration.test.ts index 6002c686..d88aa89e 100644 --- a/test/v15/integration/Minter/minterIntegration.test.ts +++ b/test/v15/integration/Minter/minterIntegration.test.ts @@ -56,7 +56,7 @@ const WEEK = 86400 * 7; const MONTH = 86400 * 30; const W = BigNumber.from(10).pow(18); -describe.only("YmtMinter integration", function () { +describe("YmtMinter integration", function () { let accounts: SignerWithAddress[]; let mockFeePool: FakeContract; let mockFeed: FakeContract; From bea91cfc35d11f2b182e9321fe6949420e2eb4fb Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 10:03:53 +0900 Subject: [PATCH 081/412] Add YMT Integration test code. --- .../integration/YMT/mintIntegration.test.ts | 105 ++++++++++ .../YMT/mintableInTimeframe.test.ts | 182 ++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 test/v15/integration/YMT/mintIntegration.test.ts create mode 100644 test/v15/integration/YMT/mintableInTimeframe.test.ts diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts new file mode 100644 index 00000000..5a2913c2 --- /dev/null +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -0,0 +1,105 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { BigNumber } from "ethers"; + +// Assuming you have a helper function to increase blockchain time +async function increaseTime(duration: number) { + await ethers.provider.send("evm_increaseTime", [duration]); + await ethers.provider.send("evm_mine", []); +} + +const YEAR = 365 * 24 * 60 * 60; // seconds in a year + +describe("YMT", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("YMT"); + token = await Token.deploy(); + + await increaseTime(86401); + await token.updateMiningParameters(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YMT MintIntegration", function () { + it("should mint the correct amount", async function () { + const duration = YEAR; // Replace with dynamic value as needed + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await increaseTime(duration); + + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + const amount = currentTime.sub(creationTime).mul(rate); + await token.mint(accounts[1].address, amount); + + expect(await token.balanceOf(accounts[1].address)).to.equal(amount); + expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); + }); + + it("should revert on overmint", async function () { + const duration = YEAR; // Replace with dynamic value as needed + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const rate = await token.rate(); + + await increaseTime(duration); + + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + const amount = currentTime.sub(creationTime).add(2).mul(rate); + await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( + "dev: exceeds allowable mint amount" + ); + }); + + it("should mint multiple times correctly", async function () { + await token.setMinter(accounts[0].address); + let totalSupply = await token.totalSupply(); + let balance = BigNumber.from(0); + let epochStart = await token.startEpochTime(); + + const durations = [YEAR * 0.33, YEAR * 0.5, YEAR * 0.7]; // Replace with dynamic values as needed + + for (const time of durations) { + await increaseTime(time); + + if ( + (await ethers.provider.getBlock("latest")).timestamp - epochStart > + YEAR + ) { + await token.updateMiningParameters(); + epochStart = await token.startEpochTime(); + } + + const amount = (await token.availableSupply()).sub(totalSupply); + await token.mint(accounts[1].address, amount); + + balance = balance.add(amount); + totalSupply = totalSupply.add(amount); + + expect(await token.balanceOf(accounts[1].address)).to.equal(balance); + expect(await token.totalSupply()).to.equal(totalSupply); + } + }); + }); +}); diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts new file mode 100644 index 00000000..c4c9f435 --- /dev/null +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -0,0 +1,182 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { BigNumber, Contract } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import Constants from "../../Constants"; + +async function increaseTime(duration: BigNumber) { + await ethers.provider.send("evm_increaseTime", [duration.toNumber()]); + await ethers.provider.send("evm_mine", []); +} + +// Constants +const YEAR = Constants.YEAR; +const INITIAL_RATE = BigNumber.from(55000000); +const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)) + .div(YEAR) + .mul(YEAR); +const INITIAL_SUPPLY = BigNumber.from(450000000); + +describe("YMT", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let snapshot: SnapshotRestorer; + const year = Constants.year; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("YMT"); + token = await Token.deploy(); + + await increaseTime(BigNumber.from(86401)); + await token.updateMiningParameters(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YMT MintableInTimeframe", function () { + // Helper function for approximate equality + function approx(a: BigNumber, b: BigNumber, precision): boolean { + if (a.isZero() && b.isZero()) { + return true; + } + + // Adjust precision for BigNumber + const precisionAdjusted = BigNumber.from(10).pow(18).mul(precision); + + return a.sub(b).abs().lte(a.add(b).div(precisionAdjusted)); + } + + // Helper function for theoretical supply calculation + async function theoreticalSupply(token: Contract): Promise { + const epoch = await token.miningEpoch(); + const q = BigNumber.from(10).pow(18).div(BigNumber.from(2).pow(2)); // Equivalent to 1/2**0.25 + let S = INITIAL_SUPPLY.mul(BigNumber.from(10).pow(18)); + + if (epoch.gt(0)) { + S = S.add( + YEAR_1_SUPPLY.mul(BigNumber.from(10).pow(18)) + .mul(BigNumber.from(10).pow(18).sub(q.pow(epoch))) + .div(BigNumber.from(10).pow(18).sub(q)) + ); + } + + S = S.add( + YEAR_1_SUPPLY.div(YEAR) + .mul(q.pow(epoch)) + .mul( + (await ethers.provider.getBlock("latest")).timestamp - + (await token.startEpochTime()) + ) + ); + + return S; + } + + it("test_mintable_in_timeframe", async function () { + const t0 = Number(await token.startEpochTime()); + + // Ensure the exponentiation stays within safe integer limits + const exponent = BigNumber.from(10).pow(1); // Adjust the exponent as necessary + await increaseTime(exponent); + + let t1 = (await ethers.provider.getBlock("latest")).timestamp; + if (t1 - t0 >= year) { + await token.updateMiningParameters(); + } + t1 = (await ethers.provider.getBlock("latest")).timestamp; + + const availableSupply = await token.availableSupply(); + const mintable = await token.mintableInTimeframe(t0, t1); + expect( + availableSupply + .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) + .gte(mintable) + ).to.equal(true); + if (t1 == t0) { + expect(mintable).to.equal(BigNumber.from(0)); + } else { + const tolerance = BigNumber.from("10000000"); // Adjust as needed for precision + expect( + availableSupply + .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) + .div(mintable) + .sub(1) + ).to.be.lt(tolerance); + } + + // Replace this with the actual theoretical supply calculation + // const theoreticalSupply = BigNumber.from("EXPECTED_SUPPLY_CALCULATION"); + expect( + approx( + await theoreticalSupply(token), + availableSupply, + Constants.ten_to_the_16 + ) + ).to.equal(true); + }); + + it("test_random_range_year_one", async function () { + const creationTime = await token.startEpochTime(); + const time1 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); + const time2 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); + const [start, end] = [creationTime.add(time1), creationTime.add(time2)]; + const sortedTimes = start.lt(end) ? [start, end] : [end, start]; + const rate = YEAR_1_SUPPLY.div(YEAR); + + expect( + await token.mintableInTimeframe(sortedTimes[0], sortedTimes[1]) + ).to.equal(rate.mul(sortedTimes[1].sub(sortedTimes[0]))); + }); + + it("test_random_range_multiple_epochs", async function () { + const creationTime = await token.startEpochTime(); + const start = creationTime.add(YEAR.mul(2)); + const duration = YEAR.mul(2); + const end = start.add(duration); + + const startEpoch = start.sub(creationTime).div(YEAR); + const endEpoch = end.sub(creationTime).div(YEAR); + const exponent = startEpoch.mul(25); + const rate = YEAR_1_SUPPLY.div(YEAR).div( + BigNumber.from(2).pow(exponent.div(100)) + ); + + for (let i = startEpoch.toNumber(); i < endEpoch.toNumber(); i++) { + await increaseTime(YEAR); + await token.updateMiningParameters(); + } + + const mintable = await token.mintableInTimeframe(start, end); + if (startEpoch.eq(endEpoch)) { + const expectedMintable = rate.mul(end.sub(start)); + expect(approx(mintable, expectedMintable, Constants.ten_to_the_16)).to + .be.true; + } else { + expect(mintable.lt(rate.mul(end))).to.be.true; + } + }); + + it("test_available_supply", async function () { + const duration = BigNumber.from(100000); + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await increaseTime(duration); + + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + const expected = initialSupply.add(now.sub(creationTime).mul(rate)); + expect(await token.availableSupply()).to.equal(expected); + }); + }); +}); From bcc43b39b6954c46610b6d7343b2013b60b8be6c Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 10:55:51 +0900 Subject: [PATCH 082/412] Variable changed from minter to ymtMinter. --- contracts/CurrencyOSV3.sol | 12 ++++++------ contracts/Interfaces/ICurrencyOSV3.sol | 2 +- contracts/ScoreRegistry.sol | 12 ++++++------ contracts/YMT.sol | 24 ++++++++++++------------ contracts/YamatoV4.sol | 2 +- contracts/YmtMinter.sol | 2 +- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/contracts/CurrencyOSV3.sol b/contracts/CurrencyOSV3.sol index 00e4cf6d..7363d758 100644 --- a/contracts/CurrencyOSV3.sol +++ b/contracts/CurrencyOSV3.sol @@ -75,17 +75,17 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { } } - function setVeYMT(address veymtAddr) external onlyGovernance { + function setVeYMT(address veYmtAddr) external onlyGovernance { bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); assembly { - sstore(VEYMT_KEY, veymtAddr) + sstore(VEYMT_KEY, veYmtAddr) } } - function setYmtMinter(address minterAddr) external onlyGovernance { + function setYmtMinter(address ymtMinterAddr) external onlyGovernance { bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); assembly { - sstore(MINTER_KEY, minterAddr) + sstore(MINTER_KEY, ymtMinterAddr) } } @@ -176,10 +176,10 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { } } - function minter() public view override returns (address _minter) { + function ymtMinter() public view override returns (address _ymtMinter) { bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); assembly { - _minter := sload(MINTER_KEY) + _ymtMinter := sload(MINTER_KEY) } } diff --git a/contracts/Interfaces/ICurrencyOSV3.sol b/contracts/Interfaces/ICurrencyOSV3.sol index dbf73af1..e4b358ac 100644 --- a/contracts/Interfaces/ICurrencyOSV3.sol +++ b/contracts/Interfaces/ICurrencyOSV3.sol @@ -22,7 +22,7 @@ interface ICurrencyOSV3 { function veYMT() external view returns (address); - function minter() external view returns (address); + function ymtMinter() external view returns (address); function scoreWeightController() external view returns (address); } diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index f0e9732c..6e2a028c 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -48,7 +48,7 @@ contract ScoreRegistry is YamatoAction { address public token; address public votingEscrow; - address public minter; + address public ymtMinter; address public scoreController; bool public isKilled; @@ -74,11 +74,11 @@ contract ScoreRegistry is YamatoAction { mapping(int128 => uint256) public periodTimestamp; mapping(int128 => uint256) public integrateInvSupply; - function initialize(address minter_, address yamato_) public initializer { + function initialize(address ymtMinter_, address yamato_) public initializer { __YamatoAction_init(yamato_); - minter = minter_; - token = IYmtMinter(minter).token(); - scoreController = IYmtMinter(minter).controller(); + ymtMinter = ymtMinter_; + token = IYmtMinter(ymtMinter).token(); + scoreController = IYmtMinter(ymtMinter).controller(); votingEscrow = IScoreWeightController(scoreController).veYMT(); periodTimestamp[int128(0)] = block.timestamp; @@ -226,7 +226,7 @@ contract ScoreRegistry is YamatoAction { function userCheckpoint(address addr_) external onlyYamato returns (bool) { require( - msg.sender == addr_ || msg.sender == minter, + msg.sender == addr_ || msg.sender == ymtMinter, "dev: unauthorized" ); checkpoint(addr_); diff --git a/contracts/YMT.sol b/contracts/YMT.sol index 57b8570a..033d07bb 100644 --- a/contracts/YMT.sol +++ b/contracts/YMT.sol @@ -16,10 +16,10 @@ import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; */ contract YMT is ERC20Permit { event UpdateMiningParameters(uint256 time, uint256 rate, uint256 supply); - event SetMinter(address minter); + event SetMinter(address ymtMinter); event SetAdmin(address admin); - address public minter; + address public ymtMinter; address public admin; // General constants @@ -183,16 +183,16 @@ contract YMT is ERC20Permit { return to_mint; } - // @notice Set the minter address - // @dev Only callable once, when minter has not yet been set - // @param _minter Address of the minter - function setMinter(address _minter) external onlyAdmin { + // @notice Set the ymtMinter address + // @dev Only callable once, when ymtMinter has not yet been set + // @param _ymtMinter Address of the ymtMinter + function setMinter(address _ymtMinter) external onlyAdmin { require( - _minter != address(0), - "dev: can set the minter only once, at creation" - ); // dev: can set the minter only once, at creation - minter = _minter; - emit SetMinter(_minter); + _ymtMinter != address(0), + "dev: can set the ymtMinter only once, at creation" + ); // dev: can set the ymtMinter only once, at creation + ymtMinter = _ymtMinter; + emit SetMinter(_ymtMinter); } // @notice Set the new admin. @@ -209,7 +209,7 @@ contract YMT is ERC20Permit { // @param _value The amount that will be created // @return bool success function mint(address _to, uint256 _value) external returns (bool) { - require(msg.sender == minter, "dev: minter only"); // dev: minter only + require(msg.sender == ymtMinter, "dev: ymtMinter only"); // dev: ymtMinter only require(_to != address(0), "dev: zero address"); // dev: zero address if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { diff --git a/contracts/YamatoV4.sol b/contracts/YamatoV4.sol index 2169319b..117f1207 100644 --- a/contracts/YamatoV4.sol +++ b/contracts/YamatoV4.sol @@ -611,7 +611,7 @@ contract YamatoV4 is pool(), priorityRegistry(), scoreRegistry(), - ICurrencyOSV3(currencyOS()).minter() + ICurrencyOSV3(currencyOS()).ymtMinter() ]; } diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index cfaf2314..4e433089 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -32,7 +32,7 @@ contract YmtMinter is // user -> score -> value mapping(address => mapping(address => uint256)) public minted; // minted amount of user from specific score. - // minter -> user -> can mint? + // ymtMinter -> user -> can mint? mapping(address => mapping(address => bool)) public allowedToMintFor; // A can mint for B if [A => B => true]. function initialize( From 8d5fc457fa0b8f767971f2c8aa651393c017d525 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 12:01:49 +0900 Subject: [PATCH 083/412] Address storage changed to slot ID. --- contracts/CurrencyOSV3.sol | 10 +-- contracts/Interfaces/IYmtMinter.sol | 4 +- contracts/ScoreRegistry.sol | 99 ++++++++++++++++++++++------- contracts/ScoreWeightController.sol | 5 ++ contracts/YmtMinter.sol | 47 +++++++++++--- 5 files changed, 126 insertions(+), 39 deletions(-) diff --git a/contracts/CurrencyOSV3.sol b/contracts/CurrencyOSV3.sol index 7363d758..523d0f0f 100644 --- a/contracts/CurrencyOSV3.sol +++ b/contracts/CurrencyOSV3.sol @@ -22,7 +22,7 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { string constant FEEPOOL_SLOT_ID = "deps.FeePool"; string constant YMT_SLOT_ID = "deps.YMT"; string constant VEYMT_SLOT_ID = "deps.veYMT"; - string constant MINTER_SLOT_ID = "deps.Minter"; + string constant YMT_MINTER_SLOT_ID = "deps.ymtMinter"; string constant WEIGHT_CONTROLLER_SLOT_ID = "deps.ScoreWeightController"; /* @@ -83,9 +83,9 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { } function setYmtMinter(address ymtMinterAddr) external onlyGovernance { - bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); + bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); assembly { - sstore(MINTER_KEY, ymtMinterAddr) + sstore(YMT_MINTER_KEY, ymtMinterAddr) } } @@ -177,9 +177,9 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { } function ymtMinter() public view override returns (address _ymtMinter) { - bytes32 MINTER_KEY = bytes32(keccak256(abi.encode(MINTER_SLOT_ID))); + bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); assembly { - _ymtMinter := sload(MINTER_KEY) + _ymtMinter := sload(YMT_MINTER_KEY) } } diff --git a/contracts/Interfaces/IYmtMinter.sol b/contracts/Interfaces/IYmtMinter.sol index a6c2c6b7..67b51f14 100644 --- a/contracts/Interfaces/IYmtMinter.sol +++ b/contracts/Interfaces/IYmtMinter.sol @@ -9,9 +9,9 @@ pragma solidity 0.8.4; //solhint-disable no-inline-assembly interface IYmtMinter { - function token() external view returns (address); + function YMT() external view returns (address); - function controller() external view returns (address); + function scoreWeightController() external view returns (address); function minted( address user_, diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 6e2a028c..3872658e 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -42,15 +42,15 @@ contract ScoreRegistry is YamatoAction { uint256 workingSupply; } + string constant YMT_SLOT_ID = "deps.YMT"; + string constant VEYMT_SLOT_ID = "deps.veYMT"; + string constant YMT_MINTER_SLOT_ID = "deps.ymtMinter"; + string constant WEIGHT_CONTROLLER_SLOT_ID = "deps.ScoreWeightController"; + // Constants uint256 public constant TOKENLESS_PRODUCTION = 4; uint256 public constant WEEK = 604800; - address public token; - address public votingEscrow; - address public ymtMinter; - address public scoreController; - bool public isKilled; // [future_epoch_time uint40][inflation_rate uint216] @@ -70,22 +70,34 @@ contract ScoreRegistry is YamatoAction { // The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint int128 public period; - // Using dynamic array instead of fixed 100000000000000000000000000000 array to avoid warning about collisions mapping(int128 => uint256) public periodTimestamp; mapping(int128 => uint256) public integrateInvSupply; - function initialize(address ymtMinter_, address yamato_) public initializer { - __YamatoAction_init(yamato_); - ymtMinter = ymtMinter_; - token = IYmtMinter(ymtMinter).token(); - scoreController = IYmtMinter(ymtMinter).controller(); - votingEscrow = IScoreWeightController(scoreController).veYMT(); + function initialize(address ymtMinterAddr, address yamatoAddr) public initializer { + __YamatoAction_init(yamatoAddr); + + bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); + bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); + bytes32 WEIGHT_CONTROLLER_KEY = bytes32( + keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) + ); + address ymtAddr = IYmtMinter(ymtMinterAddr).YMT(); + address scoreWeightControllerAddr = IYmtMinter(ymtMinterAddr).scoreWeightController(); + address veYmtAddr = IScoreWeightController(scoreWeightControllerAddr).veYMT(); + + assembly { + sstore(YMT_KEY, ymtAddr) + sstore(VEYMT_KEY, veYmtAddr) + sstore(YMT_MINTER_KEY, ymtMinterAddr) + sstore(WEIGHT_CONTROLLER_KEY, scoreWeightControllerAddr) + } periodTimestamp[int128(0)] = block.timestamp; // Assuming you have the YMT interface defined somewhere for the following line - inflationRate = IYMT(token).rate(); - futureEpochTime = IYMT(token).futureEpochTimeWrite(); + inflationRate = IYMT(ymtAddr).rate(); + futureEpochTime = IYMT(ymtAddr).futureEpochTimeWrite(); } function checkpoint(address addr) public onlyYamato { @@ -100,8 +112,8 @@ contract ScoreRegistry is YamatoAction { _st.newRate = _st.rate; if (_st.prevFutureEpoch >= _st.periodTime) { - futureEpochTime = IYMT(token).futureEpochTimeWrite(); - _st.newRate = IYMT(token).rate(); + futureEpochTime = IYMT(YMT()).futureEpochTimeWrite(); + _st.newRate = IYMT(YMT()).rate(); inflationRate = _st.newRate; } @@ -112,7 +124,7 @@ contract ScoreRegistry is YamatoAction { if (block.timestamp > _st.periodTime) { uint256 _workingSupply = workingSupply; - IScoreWeightController(scoreController).checkpointScore(address(this)); + IScoreWeightController(scoreWeightController()).checkpointScore(address(this)); uint256 _prevWeekTime = _st.periodTime; uint256 _weekTime = min( ((_st.periodTime + WEEK) / WEEK) * WEEK, @@ -121,7 +133,7 @@ contract ScoreRegistry is YamatoAction { for (uint256 i = 0; i < 500; ) { uint256 dt = _weekTime - _prevWeekTime; - uint256 w = IScoreWeightController(scoreController) + uint256 w = IScoreWeightController(scoreWeightController()) .scoreRelativeWeight( address(this), (_prevWeekTime / WEEK) * WEEK @@ -179,8 +191,8 @@ contract ScoreRegistry is YamatoAction { uint256 totalDebt_, uint256 collateralRatio_ ) public onlyYamato { - uint256 _votingBalance = IveYMT(votingEscrow).balanceOf(addr_); - uint256 _votingTotal = IveYMT(votingEscrow).totalSupply(); + uint256 _votingBalance = IveYMT(veYMT()).balanceOf(addr_); + uint256 _votingTotal = IveYMT(veYMT()).totalSupply(); uint256 _limit = (debt_ * TOKENLESS_PRODUCTION) / 10; if (_votingTotal > 0) { @@ -226,7 +238,7 @@ contract ScoreRegistry is YamatoAction { function userCheckpoint(address addr_) external onlyYamato returns (bool) { require( - msg.sender == addr_ || msg.sender == ymtMinter, + msg.sender == addr_ || msg.sender == ymtMinter(), "dev: unauthorized" ); checkpoint(addr_); @@ -240,9 +252,9 @@ contract ScoreRegistry is YamatoAction { function kick(address addr_) external { uint256 _tLast = integrateCheckpointOf[addr_]; - uint256 _tVe = IveYMT(votingEscrow).userPointHistoryTs( + uint256 _tVe = IveYMT(veYMT()).userPointHistoryTs( addr_, - IveYMT(votingEscrow).userPointEpoch(addr_) + IveYMT(veYMT()).userPointEpoch(addr_) ); IYamato.Pledge memory _pledge = IYamato(yamato()).getPledge(addr_); uint256 _balance = _pledge.debt; @@ -250,7 +262,7 @@ contract ScoreRegistry is YamatoAction { uint256 coefficient = calculateCoefficient(_collateralRatio); require( - IveYMT(votingEscrow).balanceOf(addr_) == 0 || _tVe > _tLast, + IveYMT(veYMT()).balanceOf(addr_) == 0 || _tVe > _tLast, "Not allowed" ); require( @@ -275,4 +287,43 @@ contract ScoreRegistry is YamatoAction { function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } + + /* + ===================== + Getter Functions + ===================== + */ + function YMT() public view returns (address _YMT) { + bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + assembly { + _YMT := sload(YMT_KEY) + } + } + + function veYMT() public view returns (address _veYMT) { + bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); + assembly { + _veYMT := sload(VEYMT_KEY) + } + } + + function ymtMinter() public view returns (address _ymtMinter) { + bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); + assembly { + _ymtMinter := sload(YMT_MINTER_KEY) + } + } + + function scoreWeightController() + public + view + returns (address _scoreWeightController) + { + bytes32 WEIGHT_CONTROLLER_KEY = bytes32( + keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) + ); + assembly { + _scoreWeightController := sload(WEIGHT_CONTROLLER_KEY) + } + } } diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 24891ab0..fb3ed899 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -112,6 +112,11 @@ contract ScoreWeightController is UUPSBase { // Add to V2.0 } + /* + ===================== + Getter Functions + ===================== + */ function YMT() public view returns (address ymtAddr) { bytes32 YMT_LEY = bytes32( keccak256(abi.encode(YMT_SLOT_ID)) diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index 4e433089..c0a62dd9 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -26,8 +26,8 @@ contract YmtMinter is { event Minted(address indexed recipient, address score, uint256 minted); - address public token; - address public controller; + string constant YMT_SLOT_ID = "deps.YMT"; + string constant WEIGHT_CONTROLLER_SLOT_ID = "deps.ScoreWeightController"; // user -> score -> value mapping(address => mapping(address => uint256)) public minted; // minted amount of user from specific score. @@ -36,19 +36,25 @@ contract YmtMinter is mapping(address => mapping(address => bool)) public allowedToMintFor; // A can mint for B if [A => B => true]. function initialize( - address _token, - address _controller + address ymtAddr, + address scoreWeightControllerAddr ) public initializer { - token = _token; - controller = _controller; __UUPSBase_init(); __ReentrancyGuard_init(); __Pausable_init(); + bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + bytes32 WEIGHT_CONTROLLER_KEY = bytes32( + keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) + ); + assembly { + sstore(YMT_KEY, ymtAddr) + sstore(WEIGHT_CONTROLLER_KEY, scoreWeightControllerAddr) + } } function _mintFor(address scoreAddr_, address for_) internal { require( - IScoreWeightController(controller).scores(scoreAddr_) > 0, + IScoreWeightController(scoreWeightController()).scores(scoreAddr_) > 0, "dev: score is not added" ); @@ -57,7 +63,7 @@ contract YmtMinter is uint256 _toMint = totalMint - minted[for_][scoreAddr_]; if (_toMint != 0) { - IYMT(token).mint(for_, _toMint); + IYMT(YMT()).mint(for_, _toMint); minted[for_][scoreAddr_] = totalMint; emit Minted(for_, scoreAddr_, totalMint); @@ -126,4 +132,29 @@ contract YmtMinter is _pause(); } } + + /* + ===================== + Getter Functions + ===================== + */ + function YMT() public view returns (address _YMT) { + bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + assembly { + _YMT := sload(YMT_KEY) + } + } + + function scoreWeightController() + public + view + returns (address _scoreWeightController) + { + bytes32 WEIGHT_CONTROLLER_KEY = bytes32( + keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) + ); + assembly { + _scoreWeightController := sload(WEIGHT_CONTROLLER_KEY) + } + } } From 358dd244e002af1b8854824b61d5c12238c96c2c Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 12:21:32 +0900 Subject: [PATCH 084/412] Faster YMT integration test. --- test/v15/integration/YMT/mintIntegration.test.ts | 7 +++++-- test/v15/integration/YMT/mintableInTimeframe.test.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index 5a2913c2..c7160722 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -21,8 +21,7 @@ describe("YMT", function () { let token: Contract; let snapshot: SnapshotRestorer; - beforeEach(async function () { - snapshot = await takeSnapshot(); + before(async function () { accounts = await ethers.getSigners(); const Token = await ethers.getContractFactory("YMT"); token = await Token.deploy(); @@ -31,6 +30,10 @@ describe("YMT", function () { await token.updateMiningParameters(); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + afterEach(async () => { await snapshot.restore(); }); diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index c4c9f435..97c6d292 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -27,8 +27,7 @@ describe("YMT", function () { let snapshot: SnapshotRestorer; const year = Constants.year; - beforeEach(async function () { - snapshot = await takeSnapshot(); + before(async function () { accounts = await ethers.getSigners(); const Token = await ethers.getContractFactory("YMT"); token = await Token.deploy(); @@ -37,6 +36,10 @@ describe("YMT", function () { await token.updateMiningParameters(); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + afterEach(async () => { await snapshot.restore(); }); From a55052c425848603205d77c4d2f53b4aa0c57da1 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 12:22:53 +0900 Subject: [PATCH 085/412] Add YMT unitary test --- test/v15/unitary/YMT/burn.test.ts | 71 +++++++++++ test/v15/unitary/YMT/epochTimeSupply.test.ts | 121 +++++++++++++++++++ test/v15/unitary/YMT/inflationDelay.test.ts | 80 ++++++++++++ test/v15/unitary/YMT/mint.test.ts | 103 ++++++++++++++++ test/v15/unitary/YMT/setters.test.ts | 49 ++++++++ 5 files changed, 424 insertions(+) create mode 100644 test/v15/unitary/YMT/burn.test.ts create mode 100644 test/v15/unitary/YMT/epochTimeSupply.test.ts create mode 100644 test/v15/unitary/YMT/inflationDelay.test.ts create mode 100644 test/v15/unitary/YMT/mint.test.ts create mode 100644 test/v15/unitary/YMT/setters.test.ts diff --git a/test/v15/unitary/YMT/burn.test.ts b/test/v15/unitary/YMT/burn.test.ts new file mode 100644 index 00000000..82ccb75b --- /dev/null +++ b/test/v15/unitary/YMT/burn.test.ts @@ -0,0 +1,71 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +describe("YMT", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + before(async function () { + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("YMT"); + token = await Token.deploy(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YMT EpochTimeSupply", function () { + it("test_burn", async function () { + const balance: BigNumber = await token.balanceOf(accounts[0].address); + const initialSupply: BigNumber = await token.totalSupply(); + + await token.connect(accounts[0]).burn(31337); + + expect(await token.balanceOf(accounts[0].address)).to.equal( + balance.sub(31337) + ); + expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); + }); + + it("test_burn_not_admin", async function () { + const initialSupply: BigNumber = await token.totalSupply(); + + await token.transfer(accounts[1].address, 1000000); + await token.connect(accounts[1]).burn(31337); + + expect(await token.balanceOf(accounts[1].address)).to.equal( + 1000000 - 31337 + ); + expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); + }); + + it("test_burn_all", async function () { + const initialSupply: BigNumber = await token.totalSupply(); + + await token.connect(accounts[0]).burn(initialSupply); + + expect(await token.balanceOf(accounts[0].address)).to.equal(0); + expect(await token.totalSupply()).to.equal(0); + }); + + it("test_overburn", async function () { + const initialSupply: BigNumber = await token.totalSupply(); + + await expect( + token.connect(accounts[0]).burn(initialSupply.add(1)) + ).to.be.revertedWith("ERC20: burn amount exceeds balance"); + }); + }); +}); diff --git a/test/v15/unitary/YMT/epochTimeSupply.test.ts b/test/v15/unitary/YMT/epochTimeSupply.test.ts new file mode 100644 index 00000000..7723c686 --- /dev/null +++ b/test/v15/unitary/YMT/epochTimeSupply.test.ts @@ -0,0 +1,121 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import Constants from "../../Constants"; + +describe("YMT", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + const week = Constants.week; + const YEAR = Constants.YEAR; + const year = Constants.year; + + before(async function () { + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("YMT"); + token = await Token.deploy(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YMT EpochTimeSupply", function () { + it("test_start_epoch_time_write", async function () { + const creationTime: BigNumber = await token.startEpochTime(); + await ethers.provider.send("evm_increaseTime", [year]); + await ethers.provider.send("evm_mine", []); + + expect(await token.startEpochTime()).to.equal(creationTime); + + await token.startEpochTimeWrite(); + + expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); + }); + + it("test_start_epoch_time_write_same_epoch", async function () { + await token.startEpochTimeWrite(); + await token.startEpochTimeWrite(); + }); + + it("test_update_mining_parameters", async function () { + const creationTime = await token.startEpochTime(); + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + const newEpoch = creationTime.add(YEAR).sub(now); + await ethers.provider.send("evm_increaseTime", [newEpoch.toNumber()]); + await token.updateMiningParameters(); + }); + + it("test_update_mining_parameters_same_epoch", async function () { + const creationTime = await token.startEpochTime(); + const now = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + const newEpoch = creationTime.add(YEAR).sub(now); + await ethers.provider.send("evm_increaseTime", [ + newEpoch.sub(BigNumber.from("3")).toNumber(), + ]); + await expect(token.updateMiningParameters()).to.be.revertedWith( + "dev: too soon!" + ); + }); + + it("test_mintable_in_timeframe_end_before_start", async function () { + const creationTime = await token.startEpochTime(); + await expect( + token.mintableInTimeframe(creationTime.add(1), creationTime) + ).to.be.revertedWith("dev: start > end"); + }); + + it("test_mintable_in_timeframe_multiple_epochs", async function () { + const creationTime = await token.startEpochTime(); + + // Two epochs should not raise + const mintable = BigNumber.from("19").div(BigNumber.from("10")); + await token.mintableInTimeframe( + creationTime, + creationTime + .add(YEAR) + .mul(BigNumber.from("19").div(BigNumber.from("10"))) + ); + + // Three epochs should raise + await expect( + token.mintableInTimeframe( + creationTime, + creationTime + .add(YEAR) + .mul(BigNumber.from("21").div(BigNumber.from("10"))) + ) + ).to.be.revertedWith("dev: too far in future"); + }); + + it("test_available_supply", async function () { + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + await ethers.provider.send("evm_increaseTime", [week]); + + const latestBlock = await ethers.provider.getBlock("latest"); + const currentTime = BigNumber.from(latestBlock.timestamp); + + const timeElapsed = currentTime.sub(creationTime); + const expected = initialSupply.add(timeElapsed.mul(rate)); + + expect(await token.availableSupply()).to.equal(expected); + }); + }); +}); diff --git a/test/v15/unitary/YMT/inflationDelay.test.ts b/test/v15/unitary/YMT/inflationDelay.test.ts new file mode 100644 index 00000000..8977e4f7 --- /dev/null +++ b/test/v15/unitary/YMT/inflationDelay.test.ts @@ -0,0 +1,80 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { Contract } from "ethers"; +import Constants from "../../Constants"; + +describe("YMT", function () { + let token: Contract; + let snapshot: SnapshotRestorer; + + const YEAR = Constants.YEAR; + + before(async function () { + const Token = await ethers.getContractFactory("YMT"); + token = await Token.deploy(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YMT InflationDelay", function () { + it("test_rate", async function () { + expect(await token.rate()).to.equal(0); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.rate()).to.be.gt(0); + }); + + it("test_start_epoch_time", async function () { + const creationTime = await token.startEpochTime(); + // const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); + // expect(creationTime).to.equal(now.add("86392").sub(YEAR)); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); + }); + + it("test_mining_epoch", async function () { + expect(await token.miningEpoch()).to.equal(-1); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.miningEpoch()).to.equal(0); + }); + + it("test_available_supply", async function () { + expect(await token.availableSupply()).to.equal( + ethers.utils.parseEther("450000000") + ); + + await ethers.provider.send("evm_increaseTime", [86401]); + await ethers.provider.send("evm_mine", []); + + await token.updateMiningParameters(); + + expect(await token.availableSupply()).to.be.gt( + ethers.utils.parseEther("450000000") + ); + }); + }); +}); diff --git a/test/v15/unitary/YMT/mint.test.ts b/test/v15/unitary/YMT/mint.test.ts new file mode 100644 index 00000000..fe5badd7 --- /dev/null +++ b/test/v15/unitary/YMT/mint.test.ts @@ -0,0 +1,103 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import Constants from "../../Constants"; + +describe("YMT", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + const week = Constants.week; + const ZERO_ADDRESS = Constants.ZERO_ADDRESS; + + before(async function () { + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("YMT"); + token = await Token.deploy(); + await ethers.provider.send("evm_increaseTime", [86401]); + await token.updateMiningParameters(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YMT Mint", function () { + it("test_available_supply", async function () { + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await ethers.provider.send("evm_increaseTime", [week]); + await ethers.provider.send("evm_mine", []); + + const currentBlock = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + // const currentBlock = await ethers.provider.getBlock('latest'); + const expected = initialSupply.add( + currentBlock.sub(creationTime).mul(rate) + ); + expect(await token.availableSupply()).to.equal(expected); + }); + + it("test_mint", async function () { + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const initialSupply = await token.totalSupply(); + const rate = await token.rate(); + + await ethers.provider.send("evm_increaseTime", [week]); + + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + const amount = currentTime.sub(creationTime).mul(rate); + await token.mint(accounts[1].address, amount); + + expect(await token.balanceOf(accounts[1].address)).to.equal(amount); + expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); + }); + + it("test_overmint", async function () { + await token.setMinter(accounts[0].address); + const creationTime = await token.startEpochTime(); + const rate = await token.rate(); + + await ethers.provider.send("evm_increaseTime", [week]); + await ethers.provider.send("evm_mine", []); + + const currentTime = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + const amount = currentTime.sub(creationTime).add(2).mul(rate); + await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( + "dev: exceeds allowable mint amount" + ); + }); + + it("test_ymtMinter_only", async function () { + await token.setMinter(accounts[0].address); + await expect( + token.connect(accounts[1]).mint(accounts[1].address, 0) + ).to.be.revertedWith("dev: ymtMinter only"); + }); + + it("test_zero_address", async function () { + await token.setMinter(accounts[0].address); + await expect(token.mint(ZERO_ADDRESS, 0)).to.be.revertedWith( + "dev: zero address" + ); + }); + }); +}); diff --git a/test/v15/unitary/YMT/setters.test.ts b/test/v15/unitary/YMT/setters.test.ts new file mode 100644 index 00000000..f461dd43 --- /dev/null +++ b/test/v15/unitary/YMT/setters.test.ts @@ -0,0 +1,49 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +describe("YMT", function () { + let accounts: SignerWithAddress[]; + let token: Contract; + let snapshot: SnapshotRestorer; + + beforeEach(async function () { + snapshot = await takeSnapshot(); + accounts = await ethers.getSigners(); + const Token = await ethers.getContractFactory("YMT"); + token = await Token.deploy(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YMT Setters", function () { + it("should revert when non-admin tries to set ymtMinter", async function () { + await expect( + token.connect(accounts[1]).setMinter(accounts[2].address) + ).to.be.revertedWith("dev: admin only"); + }); + + it("should revert when non-admin tries to set admin", async function () { + await expect( + token.connect(accounts[1]).setAdmin(accounts[2].address) + ).to.be.revertedWith("dev: admin only"); + }); + + it("should allow admin to set ymtMinter", async function () { + await token.connect(accounts[0]).setMinter(accounts[1].address); + expect(await token.ymtMinter()).to.equal(accounts[1].address); + }); + + it("should allow admin to set admin", async function () { + await token.connect(accounts[0]).setAdmin(accounts[1].address); + expect(await token.admin()).to.equal(accounts[1].address); + }); + }); +}); From bbf7daacd1988e5941ae36717636ee39d2c3f37e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 12:25:48 +0900 Subject: [PATCH 086/412] Add veYMT unitary test code. --- .../unitary/veYMT/votingescrowAdmin.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 test/v15/unitary/veYMT/votingescrowAdmin.test.ts diff --git a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts new file mode 100644 index 00000000..3f196a92 --- /dev/null +++ b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts @@ -0,0 +1,73 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +describe.only("veYMT", () => { + let veYMT: Contract; + let token: Contract; + let accounts: SignerWithAddress[]; + let snapshot: SnapshotRestorer; + + before(async function () { + snapshot = await takeSnapshot(); + const YMT = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + token = await YMT.deploy(); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + accounts = await ethers.getSigners(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + it("test_commit_admin_only", async function () { + await expect( + veYMT.connect(accounts[1]).commitTransferOwnership(accounts[1].address) + ).to.be.revertedWith("admin only"); + }); + + it("test_apply_admin_only", async function () { + await expect( + veYMT.connect(accounts[1]).applyTransferOwnership() + ).to.be.revertedWith("admin only"); + }); + + it("test_commit_transfer_ownership", async function () { + await veYMT + .connect(accounts[0]) + .commitTransferOwnership(accounts[1].address); + + expect(await veYMT.admin()).to.equal(accounts[0].address); + expect(await veYMT.futureAdmin()).to.equal(accounts[1].address); + }); + + it("test_apply_transfer_ownership", async function () { + await veYMT + .connect(accounts[0]) + .commitTransferOwnership(accounts[1].address); + await veYMT.connect(accounts[0]).applyTransferOwnership(); + + expect(await veYMT.admin()).to.equal(accounts[1].address); + }); + + it("test_apply_without_commit", async function () { + await expect( + veYMT.connect(accounts[0]).applyTransferOwnership() + ).to.be.revertedWith("admin not set"); + }); +}); From aae0351a94a6dfacab60350da5e659f5588d1cbc Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 14:58:14 +0900 Subject: [PATCH 087/412] Add veYMT integration test code. --- .../veYMT/depositWithdrawVoting.test.ts | 326 +++++++++++ .../integration/veYMT/votingEscrow.test.ts | 540 ++++++++++++++++++ .../veYMT/zeroBalanceAtUnlockTime.test.ts | 114 ++++ 3 files changed, 980 insertions(+) create mode 100644 test/v15/integration/veYMT/depositWithdrawVoting.test.ts create mode 100644 test/v15/integration/veYMT/votingEscrow.test.ts create mode 100644 test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts diff --git a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts new file mode 100644 index 00000000..471dbf1c --- /dev/null +++ b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts @@ -0,0 +1,326 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +const ACCOUNT_NUM = 10; +const MAX_EXAMPLES = 30; // テストの試行回数 +const STATEFUL_STEP_COUNT = 20; // ruleの実行回数 +const YEAR = BigNumber.from(86400 * 365); +const WEEK = BigNumber.from(86400 * 7); +const two_to_the_256_minus_1 = BigNumber.from("2") + .pow(BigNumber.from("256")) + .sub(BigNumber.from("1")); +const ten_to_the_40 = BigNumber.from( + "10000000000000000000000000000000000000000" +); + +describe("veYMT", function () { + let accounts: SignerWithAddress[]; + let veYMT: Contract; + let token: Contract; + + let stAccountN: number; + let stAccount: SignerWithAddress; + let stValue: BigNumber = BigNumber.from("0"); + let stLockDuration: BigNumber; + let votingBalances: { [key: string]: BigNumber }[]; + let unlockTime: BigNumber; + + let snapshot: SnapshotRestorer; + + beforeEach(async () => { + snapshot = await takeSnapshot(); + accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); + + const Token = await ethers.getContractFactory("MockToken"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + token = await Token.deploy("Test Token", "TST", 18); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + //init + for (let i = 0; i < ACCOUNT_NUM; i++) { + await token + .connect(accounts[i]) + ._mintForTesting(accounts[i].address, ten_to_the_40); + await token + .connect(accounts[i]) + .approve(veYMT.address, two_to_the_256_minus_1); + } + + //setup + votingBalances = new Array(ACCOUNT_NUM).fill({ + value: BigNumber.from("0"), + unlockTime: BigNumber.from("0"), + }); + }); + + //--------------------------------------------- functions -----------------------------------------------------------// + + function rdmValue(a): BigNumber { + let rdm = BigNumber.from(Math.floor(Math.random() * a).toString()); + return rdm; + } + + //--------------------------------------------- randomly excuted functions -----------------------------------------------------------// + async function ruleCreateLock() { + console.log("ruleCreateLock"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + //stValue + stValue = rdmValue(9007199254740991); + + //number of weeks to lock a deposit + stLockDuration = rdmValue(255); //uint8.max + + let timestamp = BigNumber.from(await time.latest()); + unlockTime = timestamp.add(WEEK.mul(stLockDuration)).div(WEEK).mul(WEEK); + + if (stValue.eq(0)) { + // console.log("--revert: 1"); + await expect( + veYMT.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("need non-zero value"); + } else if (votingBalances[stAccountN]["value"].gt("0")) { + // console.log("--revert: 2"); + await expect( + veYMT.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("Withdraw old tokens first"); + } else if (unlockTime.lte(timestamp)) { + // console.log("--revert: 3"); + await expect( + veYMT.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("Can only lock until time in the future"); + } else if (unlockTime.gte(timestamp.add(YEAR.mul("4")))) { + // console.log("--revert: 4"); + await expect( + veYMT.connect(stAccount).createLock(stValue, unlockTime) + ).to.revertedWith("Voting lock can be 4 years max"); + } else { + // console.log("--success, account:", stAccountN); + const tx = await veYMT.connect(stAccount).createLock(stValue, unlockTime); + const receipt = await tx.wait(); + votingBalances[stAccountN] = { + value: stValue, + unlockTime: receipt.events[1]["args"]["locktime"], + }; + } + } + + async function ruleIncreaseAmount() { + console.log("ruleIncreaseAmount"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + //stValue + stValue = rdmValue(9007199254740991); + + let timestamp = BigNumber.from(await time.latest()); + + if (stValue.eq(0)) { + // console.log("--revert: 1"); + await expect( + veYMT.connect(stAccount).increaseAmount(stValue) + ).to.revertedWith("dev: need non-zero value"); + } else if (votingBalances[stAccountN]["value"].eq("0")) { + // console.log("--revert: 2"); + await expect( + veYMT.connect(stAccount).increaseAmount(stValue) + ).to.revertedWith("No existing lock found"); + } else if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { + // console.log("--revert: 3"); + await expect( + veYMT.connect(stAccount).increaseAmount(stValue) + ).to.revertedWith("Cannot add to expired lock. Withdraw"); + } else { + await veYMT.connect(stAccount).increaseAmount(stValue); + votingBalances[stAccountN]["value"] = + votingBalances[stAccountN]["value"].add(stValue); + + // console.log( + // "--success, account:", + // stAccountN, + // "new balance:", + // votingBalances[stAccountN]["value"].toString() + // ); + } + } + + async function ruleIncreaseUnlockTime() { + console.log("ruleIncreaseUnlockTime"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + //unlockTime + let timestamp = BigNumber.from(await time.latest()); + stLockDuration = rdmValue(255); //number of weeks + let unlockTime = timestamp + .add(stLockDuration.mul(WEEK)) + .div(WEEK) + .mul(WEEK); + + if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { + // console.log("--revert: 1"); + await expect( + veYMT.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Lock expired"); + } else if (votingBalances[stAccountN]["value"].eq("0")) { + // console.log("--revert: 2"); + await expect( + veYMT.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Nothing is locked"); + } else if (votingBalances[stAccountN]["unlockTime"].gte(unlockTime)) { + // console.log("--revert: 3"); + await expect( + veYMT.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Can only increase lock duration"); + } else if (unlockTime.gt(timestamp.add(YEAR.mul("4")))) { + // console.log("--revert: 4"); + await expect( + veYMT.connect(stAccount).increaseUnlockTime(unlockTime) + ).to.revertedWith("Voting lock can be 4 years max"); + } else { + // console.log("--success, account:", stAccountN); + const tx = await veYMT.connect(stAccount).increaseUnlockTime(unlockTime); + const receipt = await tx.wait(); + votingBalances[stAccountN]["unlockTime"] = + receipt.events[0]["args"]["locktime"]; + } + } + + async function ruleWithdraw() { + console.log("ruleWithdraw"); + // Withdraw tokens from the voting escrow. + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + let timestamp = BigNumber.from(await time.latest()); + + if (votingBalances[stAccountN]["unlockTime"].gt(timestamp)) { + // console.log("--reverted"); + await expect(veYMT.connect(stAccount).withdraw()).to.revertedWith( + "The lock didn't expire" + ); + } else { + // console.log("--success, account:", stAccountN); + await veYMT.connect(stAccount).withdraw(); + votingBalances[stAccountN]["value"] = BigNumber.from("0"); + } + } + + async function ruleCheckpoint() { + console.log("ruleCheckpoint"); + + //stAccount + let rdm = Math.floor(Math.random() * 10); //0~9 integer + stAccountN = rdm; + stAccount = accounts[stAccountN]; + + await veYMT.connect(stAccount).checkpoint(); + } + + async function ruleAdvanceTime() { + console.log("ruleAdvanceTime"); + + let stSleepDuration = Math.floor(Math.random() * 3) + 1; //1~4 + + await time.increase(WEEK.mul(stSleepDuration).toNumber()); + + if (stSleepDuration == 1) { + console.log("Time advanced"); + } else { + console.log("Time advanced"); + } + } + + async function checkInvariants() { + // console.log("=====Invariant checks====="); + + // console.log("invariant_token_balances"); + for (let i = 0; i < ACCOUNT_NUM; i++) { + expect(await token.balanceOf(accounts[i].address)).to.equal( + ten_to_the_40.sub(votingBalances[i]["value"]) + ); + } + + // console.log("invariant_escrow_current_balances"); + let total_supply = BigNumber.from("0"); + let timestamp = BigNumber.from(await time.latest()); + + for (let i = 0; i < ACCOUNT_NUM; i++) { + let data = votingBalances[i]; + + let balance = await veYMT["balanceOf(address)"](accounts[i].address); + total_supply = total_supply.add(balance); + + if (data["unlockTime"].gt(timestamp) && data["value"].div(YEAR).gt("0")) { + expect(balance.isZero()).to.equal(false); + } else if (data["value"].isZero() || data["unlockTime"].lte(timestamp)) { + expect(balance.isZero()).to.equal(true); + } + } + expect(await veYMT["totalSupply()"]()).to.equal(total_supply); + + // console.log("invariant_historic_balances"); + total_supply = BigNumber.from("0"); + let blocknumber = (await time.latestBlock()) - 4; + // console.log(blocknumber); + + for (let i = 0; i < ACCOUNT_NUM; i++) { + total_supply = total_supply.add( + await veYMT.balanceOfAt(accounts[i].address, blocknumber) + ); + } + + expect(await veYMT.totalSupplyAt(blocknumber)).to.equal(total_supply); + } + + let func = [ + "ruleCreateLock", + "ruleIncreaseAmount", + "ruleIncreaseUnlockTime", + "ruleWithdraw", + "ruleCheckpoint", + "ruleAdvanceTime", + ]; + + describe("test_deposit_withdraw_voting", function () { + //set arbitral number of repeats + for (let x = 0; x < MAX_EXAMPLES; x++) { + it(`Try ${x}`, async () => { + let steps = Math.floor(Math.random() * (STATEFUL_STEP_COUNT - 1)) + 1; + for (let i = 0; i < steps; i++) { + let n = (await rdmValue(func.length)).toNumber(); + await eval(func[n])(); + await checkInvariants(); + } + }); + } + }); + + afterEach(async () => { + await snapshot.restore(); + }); +}); diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v15/integration/veYMT/votingEscrow.test.ts new file mode 100644 index 00000000..fb037a06 --- /dev/null +++ b/test/v15/integration/veYMT/votingEscrow.test.ts @@ -0,0 +1,540 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +type Stage = { + blockNumber: number; + timestamp: number; + bias?: string; +}; + +const H = 3600; +const DAY = 86400; +const WEEK = 7 * DAY; +const MAXTIME = DAY * 365 * 4; // 126144000 +const SCALE = 1e20; +const TOL = (120 / WEEK) * SCALE; + +describe("Voting Powers Test", function () { + // Test voting power in the following scenario. + // Alice: + // ~~~~~~~ + // ^ + // | * * + // | | \ | \ + // | | \ | \ + // +-+---+---+------+---> t + + // Bob: + // ~~~~~~~ + // ^ + // | * + // | | \ + // | | \ + // +-+---+---+---+--+---> t + + // Alice has 100% of voting power in the first period. + // She has 2/3 power at the start of 2nd period, with Bob having 1/2 power + // (due to smaller locktime). + // Alice's power grows to 100% by Bob's unlock. + + // Checking that totalSupply is appropriate. + + // After the test is done, check all over again with balanceOfAt / totalSupplyAt + let alice, bob: SignerWithAddress; + let veYMT: Contract; + let token: Contract; + let t0: number; + let wTotal, wAlice, wBob: BigNumber; + let snapshot: SnapshotRestorer; + + before(async function () { + snapshot = await takeSnapshot(); + const YMT = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + token = await YMT.deploy(); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + [alice, bob] = await ethers.getSigners(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + it("test voting powers", async function () { + const amount: BigNumber = ethers.utils.parseEther("1000"); + await token.connect(alice).transfer(bob.address, amount); + const stages: { [key: string]: Stage | Stage[] } = {}; + + await token.connect(alice).approve(veYMT.address, amount.mul(10)); + await token.connect(bob).approve(veYMT.address, amount.mul(10)); + + expect(await veYMT["totalSupply()"]()).to.equal(0); + expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + + const timeToNextWeek = + (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - + (await time.latest()); + + // Move to timing which is good for testing - beginning of a UTC week + await time.increase(timeToNextWeek); + + await ethers.provider.send("evm_increaseTime", [H]); + + stages["before_deposits"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + await veYMT.connect(alice).createLock(amount, (await time.latest()) + WEEK); + + stages["alice_deposit"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("alice_deposit", stages["alice_deposit"]); + + await time.increase(H); + + const totalSupply = await veYMT["totalSupply()"](); + const aliceBalance = await veYMT["balanceOf(address)"](alice.address); + + expect(approx(totalSupply, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to + .be.true; + expect(approx(aliceBalance, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to + .be.true; + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + + t0 = await time.latest(); + + stages["alice_in_0"] = []; + stages["alice_in_0"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }); + + // console.log("alice_in_0", stages["alice_in_0"]); + + // Simulating the passage of time with 7 days and 24 hours per day + for (let i = 0; i < 7; i++) { + for (let _ = 0; _ < 24; _++) { + await time.increase(H); + } + + const dt = (await time.latest()) - t0; + const totalSupply = await veYMT["totalSupply()"](); + const aliceBalance = await veYMT["balanceOf(address)"](alice.address); + + expect( + approx( + totalSupply, + amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), + TOL + ) + ).to.be.true; + expect( + approx( + aliceBalance, + amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), + TOL + ) + ).to.be.true; + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + + stages["alice_in_0"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + bias: aliceBalance.toString(), + }); + } + // console.log("alice_in_0", stages["alice_in_0"]); + await ethers.provider.send("evm_increaseTime", [H]); + + expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); + await veYMT.connect(alice).withdraw(); + stages["alice_withdraw"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("alice_withdraw", stages["alice_withdraw"]); + + expect(await veYMT["totalSupply()"]()).to.equal(0); + expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + + await time.increase(H); + + // Calculate the next week for round counting + const nextWeek = + (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - + (await time.latest()); + + await time.increase(nextWeek); + + await veYMT + .connect(alice) + .createLock(amount, (await time.latest()) + 2 * WEEK); + + stages["alice_deposit_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("alice_deposit_2", stages["alice_deposit_2"]); + + expect( + approx( + await veYMT["totalSupply()"](), + amount.div(MAXTIME).mul(2 * WEEK), + TOL + ) + ).to.be.true; + expect( + approx( + await veYMT["balanceOf(address)"](alice.address), + amount.div(MAXTIME).mul(2 * WEEK), + TOL + ) + ).to.be.true; + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + + await veYMT.connect(bob).createLock(amount, (await time.latest()) + WEEK); + + stages["bob_deposit_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + // console.log("bob_deposit_2", stages["bob_deposit_2"]); + + expect( + approx( + await veYMT["totalSupply()"](), + amount.div(MAXTIME).mul(3 * WEEK), + TOL + ) + ).to.be.true; + expect( + approx( + await veYMT["balanceOf(address)"](alice.address), + amount.div(MAXTIME).mul(2 * WEEK), + TOL + ) + ).to.be.true; + expect( + approx( + await veYMT["balanceOf(address)"](bob.address), + amount.div(MAXTIME).mul(WEEK), + TOL + ) + ).to.be.true; + + t0 = await time.latest(); + await time.increase(H); + + stages["alice_bob_in_2"] = []; + // Beginning of week: weight 3 + // End of week: weight 1 + for (let i = 0; i < 7; i++) { + for (let _ = 0; _ < 24; _++) { + await time.increase(H); + } + const dt = (await time.latest()) - t0; + wTotal = await veYMT["totalSupply()"](); + wAlice = await veYMT["balanceOf(address)"](alice.address); + wBob = await veYMT["balanceOf(address)"](bob.address); + expect(wTotal).to.equal(wAlice.add(wBob)); + expect( + approx(wAlice, amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), TOL) + ).to.be.true; + expect(approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), TOL)) + .to.be.true; + + stages["alice_bob_in_2"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + bias: `${wAlice.toString()}, ${wBob.toString()}`, + }); + } + // console.log("alice_bob_in_2", stages["alice_bob_in_2"]); + + await time.increase(H); + + await veYMT.connect(bob).withdraw(); + t0 = await time.latest(); + stages["bob_withdraw_1"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + const wTotal1 = await veYMT["totalSupply()"](); + const wAlice1 = await veYMT["balanceOf(address)"](alice.address); + expect(wAlice1).to.equal(wTotal1); + expect(approx(wTotal1, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be + .true; + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + + await time.increase(H); + + stages["alice_in_2"] = []; + for (let i = 0; i < 7; i++) { + for (let _ = 0; _ < 24; _++) { + await time.increase(H); + } + const dt = (await time.latest()) - t0; + wTotal = await veYMT["totalSupply()"](); + wAlice = await veYMT["balanceOf(address)"](alice.address); + expect(wTotal).to.equal(wAlice); + expect( + approx( + wTotal, + amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), + TOL + ) + ).to.be.true; + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + stages["alice_in_2"].push({ + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }); + } + // console.log("alice_in_2", stages["alice_in_2"]); + + await veYMT.connect(alice).withdraw(); + stages["alice_withdraw_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + + await time.increase(H); + + await veYMT.connect(bob).withdraw(); + stages["bob_withdraw_2"] = { + blockNumber: await time.latestBlock(), + timestamp: await time.latest(), + }; + // console.log("bob_withdraw_2", stages["bob_withdraw_2"]); + + expect(await veYMT["totalSupply()"]()).to.equal(0); + expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); + expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); + + // Now test historical balanceOfAt and others + expect( + await veYMT.balanceOfAt( + alice.address, + stages["before_deposits"].blockNumber + ) + ).to.equal(0); + expect( + await veYMT.balanceOfAt( + bob.address, + stages["before_deposits"].blockNumber + ) + ).to.equal(0); + expect( + await veYMT.totalSupplyAt(stages["before_deposits"].blockNumber) + ).to.equal(0); + + wAlice = await veYMT.balanceOfAt( + alice.address, + stages["alice_deposit"].blockNumber + ); + + expect(approx(wAlice, amount.div(MAXTIME).mul(WEEK - H), TOL)).to.be.true; + expect( + await veYMT.balanceOfAt(bob.address, stages["alice_deposit"].blockNumber) + ).to.equal(0); + wTotal = await veYMT.totalSupplyAt(stages["alice_deposit"].blockNumber); + expect(wAlice).to.equal(wTotal); + + for (let i = 0; i < stages["alice_in_0"].length; i++) { + const block = stages["alice_in_0"][i].blockNumber; + wAlice = await veYMT.balanceOfAt(alice.address, block); + wBob = await veYMT.balanceOfAt(bob.address, block); + wTotal = await veYMT.totalSupplyAt(block); + expect(wBob).to.equal(0); + expect(wAlice).to.equal(wTotal); + const timeLeft = Math.floor((WEEK * (7 - i)) / 7) - 2 * H; + const error1h = (H / timeLeft) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + expect(approx(wAlice, amount.div(MAXTIME).mul(timeLeft), error1h)).to.be + .true; + } + + wTotal = await veYMT.totalSupplyAt(stages["alice_withdraw"].blockNumber); + wAlice = await veYMT.balanceOfAt( + alice.address, + stages["alice_withdraw"].blockNumber + ); + wBob = await veYMT.balanceOfAt( + bob.address, + stages["alice_withdraw"].blockNumber + ); + expect(wAlice).to.equal(wBob); + expect(wAlice).to.equal(wTotal); + expect(wTotal).to.equal(0); + + wTotal = await veYMT.totalSupplyAt(stages["alice_deposit_2"].blockNumber); + wAlice = await veYMT.balanceOfAt( + alice.address, + stages["alice_deposit_2"].blockNumber + ); + expect(approx(wTotal, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; + expect(wTotal).to.equal(wAlice); + expect( + await veYMT.balanceOfAt( + bob.address, + stages["alice_deposit_2"].blockNumber + ) + ).to.equal(0); + + wTotal = await veYMT.totalSupplyAt(stages["bob_deposit_2"].blockNumber); + wAlice = await veYMT.balanceOfAt( + alice.address, + stages["bob_deposit_2"].blockNumber + ); + wBob = await veYMT.balanceOfAt( + bob.address, + stages["bob_deposit_2"].blockNumber + ); + expect(wTotal).to.equal(wAlice.add(wBob)); + expect(approx(wTotal, amount.div(MAXTIME).mul(3 * WEEK), TOL)).to.be.true; + expect(approx(wAlice, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; + + t0 = stages["bob_deposit_2"].timestamp; + for (let i = 0; i < stages["alice_bob_in_2"].length; i++) { + const block = stages["alice_bob_in_2"][i].blockNumber; + wAlice = await veYMT.balanceOfAt(alice.address, block); + wBob = await veYMT.balanceOfAt(bob.address, block); + wTotal = await veYMT.totalSupplyAt(block); + expect(wTotal).to.equal(wAlice.add(wBob)); + const dt = stages["alice_bob_in_2"][i].timestamp - t0; + const error1h = (H / (2 * WEEK - i * DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + expect( + approx( + wAlice, + amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), + error1h + ) + ).to.be.true; + expect( + approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), error1h) + ).to.be.true; + } + + wTotal = await veYMT.totalSupplyAt(stages["bob_withdraw_1"].blockNumber); + wAlice = await veYMT.balanceOfAt( + alice.address, + stages["bob_withdraw_1"].blockNumber + ); + wBob = await veYMT.balanceOfAt( + bob.address, + stages["bob_withdraw_1"].blockNumber + ); + expect(wTotal).to.equal(wAlice); + expect(approx(wTotal, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be + .true; + expect(wBob).to.equal(0); + + t0 = stages["bob_withdraw_1"].timestamp; + for (let i = 0; i < stages["alice_in_2"].length; i++) { + const block = stages["alice_in_2"][i].blockNumber; + wAlice = await veYMT.balanceOfAt(alice.address, block); + const wBob = await veYMT.balanceOfAt(bob.address, block); + wTotal = await veYMT.totalSupplyAt(block); + expect(wTotal).to.equal(wAlice); + expect(wBob).to.equal(0); + const dt = stages["alice_in_2"][i].timestamp - t0; + const error1h = (H / (WEEK - i * DAY + DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + + expect( + approx( + wTotal, + amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), + error1h + ) + ).to.be.true; + } + + wTotal = await veYMT.totalSupplyAt(stages["bob_withdraw_2"].blockNumber); + wAlice = await veYMT.balanceOfAt( + alice.address, + stages["bob_withdraw_2"].blockNumber + ); + const wBob4 = await veYMT.balanceOfAt( + bob.address, + stages["bob_withdraw_2"].blockNumber + ); + expect(wTotal).to.equal(0); + expect(wAlice).to.equal(0); + expect(wBob4).to.equal(0); + + await showStats(); + }); + + function approx(value: BigNumber, target: BigNumber, tol: number) { + if (value.isZero() && target.isZero()) { + return true; + } + + const diff = value.sub(target).abs(); + const sum = value.add(target); + const ratio = diff.mul(2).mul(BigNumber.from(SCALE.toString())).div(sum); + + return ratio.lte(BigNumber.from(tol.toString())); + } + + async function showStats() { + const initialBlock = (await veYMT.pointHistory(0)).blk.toNumber(); + const latestBlock = await time.latestBlock(); + for (let i = initialBlock; i < latestBlock; i++) { + let a = await veYMT.balanceOfAt(alice.address, `${i}`); + let b = await veYMT.balanceOfAt(bob.address, `${i}`); + console.log(`Block ${i}: a: ${a.toString()} b: ${b.toString()}`); + } + const epoch = (await veYMT.epoch()).toNumber(); + const aliceEpoch = (await veYMT.userPointEpoch(alice.address)).toNumber(); + const bobEpoch = (await veYMT.userPointEpoch(bob.address)).toNumber(); + for (let i = 0; i <= epoch; i++) { + let p = await veYMT.pointHistory(i); + console.log( + `epoch: ${i}: bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + ); + } + for (let i = 0; i <= aliceEpoch; i++) { + let p = await veYMT.userPointHistory(alice.address, i); + let balanceAt = p.blk.isZero() + ? 0 + : await veYMT.balanceOfAt(alice.address, p.blk.toNumber()); + console.log( + `alice epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + ); + } + for (let i = 0; i < bobEpoch; i++) { + let p = await veYMT.userPointHistory(bob.address, i); + let balanceAt = p.blk.isZero() + ? 0 + : await veYMT.balanceOfAt(bob.address, p.blk.toNumber()); + console.log( + `bob epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + ); + } + } +}); diff --git a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts new file mode 100644 index 00000000..e8d85577 --- /dev/null +++ b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts @@ -0,0 +1,114 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; + +const WEEK = 86400 * 7; + +describe("Voting Escrow tests", function () { + let accounts: SignerWithAddress[]; + let veYMT: Contract; + let token: Contract; + let snapshot: SnapshotRestorer; + + before(async () => { + accounts = await ethers.getSigners(); + + const YMT = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + token = await YMT.deploy(); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + await token + .connect(accounts[0]) + .approve(veYMT.address, ethers.utils.parseEther("1")); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + for ( + let st_initial = WEEK * 2; + st_initial <= WEEK * 52; + st_initial += WEEK * 2 + ) { + it(`create lock with zero balance: Lock ${Math.floor( + st_initial / WEEK + )} Week`, async () => { + const expectedUnlock = (await time.latest()) + st_initial; + await veYMT + .connect(accounts[0]) + .createLock(ethers.utils.parseEther("1"), expectedUnlock); + + const actualUnlock = (await veYMT.locked(accounts[0].address))[1]; + + await time.increase(actualUnlock - (await time.latest()) - 5); + + expect( + await veYMT["balanceOf(address)"](accounts[0].address) + ).to.not.equal(0); + + await time.increase(10); + + expect(await veYMT["balanceOf(address)"](accounts[0].address)).to.equal( + 0 + ); + }); + } + + for ( + let st_initial = WEEK * 2; + st_initial <= WEEK * 52; + st_initial += WEEK * 2 + ) { + for (let st_extend = WEEK; st_extend <= WEEK * 2; st_extend += WEEK) { + it(`increase unlock with zero balance: Lock ${Math.floor( + st_initial / WEEK + )} Week, extend: ${Math.floor(st_extend / WEEK)} Week`, async () => { + await veYMT + .connect(accounts[0]) + .createLock( + ethers.utils.parseEther("1"), + (await time.latest()) + st_initial + ); + + const initialUnlock = ( + await veYMT.locked(accounts[0].address) + )[1].toNumber(); + const extendedExpectedUnlock = initialUnlock + st_extend; + + await veYMT.increaseUnlockTime(extendedExpectedUnlock); + + const extendedActualUnlock = ( + await veYMT.locked(accounts[0].address) + )[1]; + + await time.increase(extendedActualUnlock - (await time.latest()) - 5); + + expect( + await veYMT["balanceOf(address)"](accounts[0].address) + ).to.not.equal(0); + + await time.increase(10); + + expect(await veYMT["balanceOf(address)"](accounts[0].address)).to.equal( + 0 + ); + }); + } + } +}); From 19699a85852bb0ab4a2b85821b88376c1ebf82d7 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 16:37:52 +0900 Subject: [PATCH 088/412] Add YmtMinter unitary test code. --- test/v15/unitary/YmtMinter/Minter.test.ts | 505 ++++++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 test/v15/unitary/YmtMinter/Minter.test.ts diff --git a/test/v15/unitary/YmtMinter/Minter.test.ts b/test/v15/unitary/YmtMinter/Minter.test.ts new file mode 100644 index 00000000..3f4c4c6d --- /dev/null +++ b/test/v15/unitary/YmtMinter/Minter.test.ts @@ -0,0 +1,505 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import chai, { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + CJPY, + CurrencyOSV3, + Pool, + FeePool, + PriceFeedV3, + PriorityRegistry, + PriorityRegistry__factory, + YamatoV4, + YamatoDepositor, + YamatoBorrower, + YamatoRepayer, + YamatoWithdrawer, + YamatoRedeemer, + YamatoSweeper, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + CJPY__factory, + CurrencyOSV3__factory, + YamatoV4__factory, + YamatoDepositor__factory, + YamatoBorrower__factory, + YamatoRepayer__factory, + YamatoWithdrawer__factory, + YamatoRedeemer__factory, + YamatoSweeper__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, + Pool__factory, +} from "../../../../typechain"; +import { + getFakeProxy, + getProxy, + getLinkedProxy, +} from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; + +chai.use(smock.matchers); + +describe("YmtMinter", function () { + let mockFeePool: FakeContract; + let mockFeed: FakeContract; + let CJPY: CJPY; + let currencyOS: CurrencyOSV3; + let yamato: YamatoV4; + let yamatoDepositor: YamatoDepositor; + let yamatoBorrower: YamatoBorrower; + let yamatoRepayer: YamatoRepayer; + let yamatoWithdrawer: YamatoWithdrawer; + let yamatoRedeemer: YamatoRedeemer; + let yamatoSweeper: YamatoSweeper; + let scoreRegistry: ScoreRegistry; + let ymtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let scoreWeightController: ScoreWeightController; + let pool: Pool; + let priorityRegistry: PriorityRegistry; + let PRICE: BigNumber; + let accounts: SignerWithAddress[]; + let ownerAddress: string; + let snapshot: SnapshotRestorer; + + const ten_to_the_18 = Constants.ten_to_the_18; + const ten_to_the_17 = Constants.ten_to_the_17; + const zero = Constants.zero; + const WEEK = Constants.WEEK; + const month = Constants.month; + const week = Constants.week; + + before(async function () { + accounts = await ethers.getSigners(); + ownerAddress = await accounts[0].getAddress(); + + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); + CJPY = await (( + await ethers.getContractFactory("CJPY") + )).deploy(); + currencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, mockFeed.address, mockFeePool.address] + ); + + yamato = await getLinkedProxy( + contractVersion["Yamato"], + [currencyOS.address], + ["PledgeLib"] + ); + + await currencyOS.addYamato(yamato.address); + await CJPY.setCurrencyOS(currencyOS.address); + + yamatoDepositor = await getLinkedProxy< + YamatoDepositor, + YamatoDepositor__factory + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); + + yamatoBorrower = await getLinkedProxy< + YamatoBorrower, + YamatoBorrower__factory + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); + + yamatoRepayer = await getLinkedProxy( + contractVersion["YamatoRepayer"], + [yamato.address], + ["PledgeLib"] + ); + + yamatoWithdrawer = await getLinkedProxy< + YamatoWithdrawer, + YamatoWithdrawer__factory + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); + + yamatoRedeemer = await getLinkedProxy< + YamatoRedeemer, + YamatoRedeemer__factory + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); + + yamatoSweeper = await getLinkedProxy( + contractVersion["YamatoSweeper"], + [yamato.address], + ["PledgeLib"] + ); + + priorityRegistry = await getLinkedProxy< + PriorityRegistry, + PriorityRegistry__factory + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + scoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + ymtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, scoreWeightController.address] + ); + + scoreRegistry = await getLinkedProxy( + contractVersion["ScoreRegistry"], + [ymtMinter.address, yamato.address], + ["PledgeLib"] + ); + + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); + + await ( + await yamato.setDeps( + yamatoDepositor.address, + yamatoBorrower.address, + yamatoRepayer.address, + yamatoWithdrawer.address, + yamatoRedeemer.address, + yamatoSweeper.address, + pool.address, + priorityRegistry.address + ) + ).wait(); + await (await yamato.setScoreRegistry(scoreRegistry.address)).wait(); + + await currencyOS.setYmtMinter(ymtMinter.address); + await YMT.setMinter(ymtMinter.address); + + PRICE = BigNumber.from(260000).mul(1e18 + ""); + + // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); + mockFeed.fetchPrice.returns(PRICE); + mockFeed.getPrice.returns(PRICE); + mockFeed.lastGoodPrice.returns(PRICE); + + await scoreWeightController.addCurrency( + scoreRegistry.address, + ethers.utils.parseEther("10") + ); + for (let i = 0; i < 4; i++) { + await yamato + .connect(accounts[i]) + .deposit({ value: ethers.utils.parseEther("100") }); + } + await yamato.borrow(ethers.utils.parseEther("100000")); + + // for (const account of accounts) { + // mockLpToken.transfer(account.address, ten_to_the_18); + // } + // for (let i = 1; i <= 3; i++) { + // for (const gauge of threeScoreRegistry) { + // await mockLpToken.connect(accounts[i]).approve(gauge, ten_to_the_18); + // } + // } + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YmtMinter Behavior", function () { + // Test basic mint functionality + it("test_mint", async () => { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_17); + + await time.increase(month); + + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); //gauge_address, msg.sender, mint + let expected = await scoreRegistry.integrateFraction(accounts[1].address); + + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(expected); + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal(expected); + }); + + // Test minting immediately after setup + it("test_mint_immediate", async () => { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_18); + + let t0 = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + let moment = t0.add(WEEK).div(WEEK).mul(WEEK).add("5"); + await ethers.provider.send("evm_setNextBlockTimestamp", [ + moment.toNumber(), + ]); + + //mint + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal("0"); + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + + //check + let balance = await YMT.balanceOf(accounts[1].address); + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal(balance); + }); + + // Test multiple mint operations on the same gauge + it("test_mint_multiple_same_gauge", async () => { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_18); + await time.increase(month); + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + let balance = await YMT.balanceOf(accounts[1].address); + await time.increase(month); + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + let expected = await scoreRegistry.integrateFraction(accounts[1].address); + let final_balance = await YMT.balanceOf(accounts[1].address); + + expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success + expect(final_balance).to.equal(expected); //2nd mint works fine + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal(expected); //tracks fine + }); + + // Test minting across multiple gauges + it("test_mint_multiple_gauges", async () => { + //setup + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_17); + + await time.increase(month); + + + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + + let minted = await ymtMinter.minted(accounts[1].address, scoreRegistry.address); + expect(minted).to.equal( + await scoreRegistry.integrateFraction(accounts[1].address) + ); + }); + + // Test minting after withdrawing + it("test_mint_after_withdraw", async () => { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_18); + + await time.increase(week * 2); + const balance = await CJPY.connect(accounts[1]).balanceOf(accounts[1].address); + await CJPY.transfer(accounts[1].address, ten_to_the_18.sub(balance)); + await yamato.connect(accounts[1]).repay(ten_to_the_18); + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + + expect( + (await YMT.balanceOf(accounts[1].address)).gt(BigNumber.from("0")) + ).to.equal(true); + }); + + // Test multiple mints after withdrawing + it("test_mint_multiple_after_withdraw", async () => { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_18); + + await time.increase(10); + const CJPY_balance = await CJPY.connect(accounts[1]).balanceOf(accounts[1].address); + await CJPY.transfer(accounts[1].address, ten_to_the_18.sub(CJPY_balance)); + await yamato.connect(accounts[1]).repay(ten_to_the_18); + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + + let balance = await YMT.balanceOf(accounts[1].address); + + await time.increase(10); + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + + expect(await YMT.balanceOf(accounts[1].address)).to.equal(balance); + }); + + // Test mint without any deposit + it("test_no_deposit", async () => { + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(zero); + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal(zero); + }); + + // Test minting with an invalid gauge address + it("test_mint_not_a_gauge", async () => { + await expect(ymtMinter.mint(accounts[1].address)).to.revertedWith( + "dev: score is not added" + ); + }); + + // Test minting before inflation begins + it("test_mint_before_inflation_begins", async function () { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_18); + const startEpochTime = await YMT.startEpochTime(); + const currentTime = await time.latest(); + + const timeToSleep = startEpochTime.sub(currentTime).sub(5); + await ethers.provider.send("evm_increaseTime", [timeToSleep.toNumber()]); + + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); + + expect(await YMT.balanceOf(accounts[1].address)).to.equal( + BigNumber.from(0) + ); + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal(BigNumber.from(0)); + }); + + // Test mintMany function with multiple gauges + // it("test_mintMany_function_multiple_gauges", async () => { + // //setup + // await scoreRegistry + // .connect(accounts[1]) + // .deposit(ten_to_the_17, accounts[1].address, false); + // await gauges[1] + // .connect(accounts[1]) + // .deposit(ten_to_the_17, accounts[1].address, false); + // await gauges[2] + // .connect(accounts[1]) + // .deposit(ten_to_the_17, accounts[1].address, false); + + // await time.increase(month); + + // let addresses = [ + // scoreRegistry.address, + // threeScoreRegistry[1], + // threeScoreRegistry[2], + // ZERO_ADDRESS, + // ZERO_ADDRESS, + // ZERO_ADDRESS, + // ZERO_ADDRESS, + // ZERO_ADDRESS, + // ]; + // await ymtMinter.connect(accounts[1]).mintMany(addresses); + + // //check + // let total_minted = BigNumber.from("0"); + + // for (let i = 0; i < 3; i++) { + // let gauge = gauges[i]; + // let minted = await ymtMinter.minted(accounts[1].address, gauge.address); + // expect(minted).to.equal( + // await gauge.integrateFraction(accounts[1].address) + // ); + // total_minted = total_minted.add(minted); + // } + + // expect(await YMT.balanceOf(accounts[1].address)).to.equal(total_minted); + // }); + + // Test toggling of the mint approval function + it("test_toggleApproveMint_function", async () => { + await ymtMinter + .connect(accounts[1]) + .toggleApproveMint(accounts[2].address); + expect( + await ymtMinter.allowedToMintFor( + accounts[2].address, + accounts[1].address + ) + ).to.equal(true); + + await ymtMinter + .connect(accounts[1]) + .toggleApproveMint(accounts[2].address); + expect( + await ymtMinter.allowedToMintFor( + accounts[2].address, + accounts[1].address + ) + ).to.equal(false); + }); + + // Test minting on behalf of another user + it("test_mintFor_function", async () => { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_17); + + await time.increase(month); + + await ymtMinter + .connect(accounts[1]) + .toggleApproveMint(accounts[2].address); + expect( + await ymtMinter.allowedToMintFor( + accounts[2].address, + accounts[1].address + ) + ).to.equal(true); + + await ymtMinter + .connect(accounts[2]) + .mintFor(scoreRegistry.address, accounts[1].address); + + let expected = await scoreRegistry.integrateFraction(accounts[1].address); + expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(expected); + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal(expected); + }); + + // Test mintFor function when not approved + it("test_mintForFail_function", async () => { + await yamato + .connect(accounts[1]) + .borrow(ten_to_the_17); + + await time.increase(month); + + expect( + await ymtMinter.allowedToMintFor( + accounts[2].address, + accounts[1].address + ) + ).to.equal(false); + + await ymtMinter + .connect(accounts[2]) + .mintFor(scoreRegistry.address, accounts[1].address); + + expect(await YMT.balanceOf(accounts[1].address)).to.equal(0); + expect( + await ymtMinter.minted(accounts[1].address, scoreRegistry.address) + ).to.equal(0); + }); + }); +}); From de93ac85dfd17d35997af649d72ec2466cb3170a Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 16:39:22 +0900 Subject: [PATCH 089/412] Commented out unnecessary functions in YmtMinter V1.5. --- contracts/YmtMinter.sol | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index c0a62dd9..09c89e06 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -83,17 +83,17 @@ contract YmtMinter is *@param scoreAddrs_ List of `LiquidityScore` addresses *@dev address[8]: 8 has randomly decided and has no meaning. */ - function mintMany(address[8] memory scoreAddrs_) external nonReentrant { - for (uint256 i; i < 8; ) { - if (scoreAddrs_[i] == address(0)) { - break; - } - _mintFor(scoreAddrs_[i], msg.sender); - unchecked { - ++i; - } - } - } + // function mintMany(address[8] memory scoreAddrs_) external nonReentrant { + // for (uint256 i; i < 8; ) { + // if (scoreAddrs_[i] == address(0)) { + // break; + // } + // _mintFor(scoreAddrs_[i], msg.sender); + // unchecked { + // ++i; + // } + // } + // } /*** *@notice Mint tokens for `for_` From c534cafe06688a925773932f31d2e475f1a6e9ed Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 1 Dec 2023 16:39:48 +0900 Subject: [PATCH 090/412] Release ONLY. --- test/v15/unitary/veYMT/votingescrowAdmin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts index 3f196a92..3186eaa2 100644 --- a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts +++ b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts @@ -7,7 +7,7 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -describe.only("veYMT", () => { +describe("veYMT", () => { let veYMT: Contract; let token: Contract; let accounts: SignerWithAddress[]; From 87dc63e7019d17cf346d74c22f2bfa53a2749d53 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Sat, 2 Dec 2023 20:46:51 +0900 Subject: [PATCH 091/412] Unnecessary variables in FeePool integration test code removed and renamed. --- .../integration/FeePool/checkpointTs.test.ts | 21 +++++++--------- .../FeePool/distributefFeesStateful.test.ts | 24 +++++++------------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index 4bf0af86..96951c10 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -20,8 +20,7 @@ const MAX_EXAMPLES = 10; describe("FeePoolV2", function () { let accounts: SignerWithAddress[]; - let token: Contract; - let coinA: Contract; + let YMT: Contract; let veYMT: Contract; let feePool: Contract; let snapshot: SnapshotRestorer; @@ -30,22 +29,18 @@ describe("FeePoolV2", function () { snapshot = await takeSnapshot(); accounts = await ethers.getSigners(); - const FeePool = await ethers.getContractFactory("FeePoolV2"); - const YMT = await ethers.getContractFactory("YMT"); + const ymt = await ethers.getContractFactory("YMT"); const VeYMT = await ethers.getContractFactory("veYMT"); - token = await YMT.deploy(); - await token.deployed(); + YMT = await ymt.deploy(); + await YMT.deployed(); - veYMT = await VeYMT.deploy(token.address); + veYMT = await VeYMT.deploy(YMT.address); await veYMT.deployed(); - const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); feePool = await getProxy( contractVersion["FeePool"], - [now] + [await time.latest()] ); await feePool.setVeYMT(veYMT.address); }); @@ -72,10 +67,10 @@ describe("FeePoolV2", function () { const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); const stSleep = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 30); for (let i = 0; i < MAX_EXAMPLES; i++) { - await token + await YMT .connect(accounts[i]) .approve(veYMT.address, ethers.constants.MaxUint256); - await token + await YMT .connect(accounts[0]) .transfer(await accounts[i].address, ethers.utils.parseEther("1000")); } diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 9c2d12b6..aa709626 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -62,12 +62,11 @@ describe("FeePoolV2", function () { let accounts: SignerWithAddress[]; let veYMT: Contract; let feePool: Contract; - let token: Contract; + let YMT: Contract; let lockedUntil: { [key: string]: number } = {}; let fees: { [key: number]: BigNumber } = {}; // timestamp -> amount let userClaims: { [key: string]: { [key: number]: BigNumber[] } } = {}; // address -> timestamp -> [claimed, timeCursor] - let claimAmount = []; let totalFees: BigNumber = ethers.utils.parseEther("1"); let snapshot: SnapshotRestorer; @@ -81,22 +80,21 @@ describe("FeePoolV2", function () { userClaims = {}; totalFees = ethers.utils.parseEther("1"); - const YMT = await ethers.getContractFactory("YMT"); + const ymt = await ethers.getContractFactory("YMT"); const VeYMT = await ethers.getContractFactory("veYMT"); - const FeePool = await ethers.getContractFactory("FeePoolV2"); - token = await YMT.deploy(); - await token.deployed(); + YMT = await ymt.deploy(); + await YMT.deployed(); - veYMT = await VeYMT.deploy(token.address); + veYMT = await VeYMT.deploy(YMT.address); await veYMT.deployed(); for (let i = 0; i < ACCOUNT_NUM; i++) { - // ensure accounts[:5] all have tokens that may be locked - await token + // ensure accounts[:5] all have YMTs that may be locked + await YMT .connect(accounts[0]) .transfer(accounts[i].address, ethers.utils.parseEther("10000000")); - await token + await YMT .connect(accounts[i]) .approve(veYMT.address, two_to_the_256_minus_1); @@ -120,13 +118,9 @@ describe("FeePoolV2", function () { // a week later we deploy the fee feePool await time.increase(WEEK); - const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - feePool = await getProxy( contractVersion["FeePool"], - [now] + [await time.latest()] ); await feePool.setVeYMT(veYMT.address); }); From 39e141b6c26efb3bc9f570db99e688d22ae96dd7 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 4 Dec 2023 15:24:55 +0900 Subject: [PATCH 092/412] Add FeePool unitary test code. --- test/v15/unitary/FeePool/checkpoints.test.ts | 132 +++++++++ test/v15/unitary/FeePool/claimMany.test.ts | 124 ++++++++ test/v15/unitary/FeePool/feePool.test.ts | 270 ++++++++++++++++++ .../v15/unitary/FeePool/killFeeDistro.test.ts | 142 +++++++++ 4 files changed, 668 insertions(+) create mode 100644 test/v15/unitary/FeePool/checkpoints.test.ts create mode 100644 test/v15/unitary/FeePool/claimMany.test.ts create mode 100644 test/v15/unitary/FeePool/feePool.test.ts create mode 100644 test/v15/unitary/FeePool/killFeeDistro.test.ts diff --git a/test/v15/unitary/FeePool/checkpoints.test.ts b/test/v15/unitary/FeePool/checkpoints.test.ts new file mode 100644 index 00000000..e30e6cac --- /dev/null +++ b/test/v15/unitary/FeePool/checkpoints.test.ts @@ -0,0 +1,132 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + FeePool, + FeePool__factory, +} from "../../../../typechain"; +import { getProxy } from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +const DAY = 86400; +const WEEK = DAY * 7; +const YEAR = DAY * 365; + +describe("FeePoolV2", () => { + let alice, bob, charlie: SignerWithAddress; + + let feePool: Contract; + let veYMT: Contract; + let token: Contract; + let snapshot: SnapshotRestorer; + + before(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const YMT = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + token = await YMT.deploy(); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + feePool = await getProxy( + contractVersion["FeePool"], + [await time.latest()] + ); + await feePool.setVeYMT(veYMT.address); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + // TODO + // 下記の観点のテストを追加 + // https://discord.com/channels/729808684359876718/729812922649542758/1162241240360816730 + + describe("test_checkpoints", () => { + beforeEach(async function () { + await token.approve(veYMT.address, ethers.constants.MaxUint256); + await veYMT.createLock( + ethers.utils.parseEther("1000"), + (await time.latest()) + WEEK * 52 + ); + }); + it("test_checkpoint_total_supply", async function () { + const startTime = await feePool.timeCursor(); + const weekEpoch = + Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; + + await time.increaseTo(weekEpoch); + + const weekBlock = await time.latestBlock(); + + await feePool.checkpointTotalSupply(); + + expect(await feePool.veSupply(startTime)).to.equal(0); + expect(await feePool.veSupply(weekEpoch)).to.equal( + await veYMT.totalSupplyAt(weekBlock) + ); + }); + + it("test_advance_time_cursor", async function () { + const startTime = (await feePool.timeCursor()).toNumber(); + await time.increase(YEAR); + await feePool.checkpointTotalSupply(); + const newTimeCursor = (await feePool.timeCursor()).toNumber(); + expect(newTimeCursor).to.equal(startTime + WEEK * 20); + expect(await feePool.veSupply(startTime + WEEK * 19)).to.be.above(0); + expect(await feePool.veSupply(startTime + WEEK * 20)).to.equal(0); + + await feePool.checkpointTotalSupply(); + + expect(await feePool.timeCursor()).to.equal(startTime + WEEK * 40); + expect(await feePool.veSupply(startTime + WEEK * 20)).to.be.above(0); + expect(await feePool.veSupply(startTime + WEEK * 39)).to.be.above(0); + expect(await feePool.veSupply(startTime + WEEK * 40)).to.equal(0); + }); + + it("test_claim_checkpoints_total_supply", async function () { + const start_time = (await feePool.timeCursor()).toNumber(); + + await feePool.connect(alice)["claim()"](); + + expect((await feePool.timeCursor()).toNumber()).to.equal( + start_time + WEEK + ); + }); + + it("test_toggle_allow_checkpoint", async function () { + const lastTokenTime = (await feePool.lastTokenTime()).toNumber(); + + await time.increase(WEEK); + + await feePool.connect(alice)["claim()"](); + expect((await feePool.lastTokenTime()).toNumber()).to.equal( + lastTokenTime + ); + + await feePool.toggleAllowCheckpointToken(); + const tx = await feePool.connect(alice)["claim()"](); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockNumber); + + expect((await feePool.lastTokenTime()).toNumber()).to.equal( + block.timestamp + ); + }); + }); +}); diff --git a/test/v15/unitary/FeePool/claimMany.test.ts b/test/v15/unitary/FeePool/claimMany.test.ts new file mode 100644 index 00000000..3bde8323 --- /dev/null +++ b/test/v15/unitary/FeePool/claimMany.test.ts @@ -0,0 +1,124 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + FeePool, + FeePool__factory, +} from "../../../../typechain"; +import { getProxy } from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +const DAY = 86400; +const WEEK = DAY * 7; + +describe("FeePoolV2", () => { + let alice, bob, charlie: SignerWithAddress; + + let feePool: Contract; + let veYMT: Contract; + let YMT: Contract; + let snapshot: SnapshotRestorer; + + before(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const ymt = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + YMT = await ymt.deploy(); + await YMT.deployed(); + + veYMT = await VeYMT.deploy(YMT.address); + await veYMT.deployed(); + + feePool = await getProxy( + contractVersion["FeePool"], + [await time.latest()] + ); + await feePool.setVeYMT(veYMT.address); + const amount = ethers.utils.parseEther("1000"); + for (let acct of [alice, bob, charlie]) { + await YMT.connect(acct).approve(veYMT.address, amount.mul(10)); + await YMT.connect(alice).transfer(acct.address, amount); + await veYMT + .connect(acct) + .createLock(amount, (await time.latest()) + 8 * WEEK); + } + await time.increase(WEEK); + await alice.sendTransaction({ + to: feePool.address, + value: ethers.utils.parseEther("10"), + }); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + async function gasCostOf(tx) { + const receipt = await tx.wait() + return receipt.gasUsed.mul(receipt.effectiveGasPrice) + } + + describe("test_claim_many", () => { + it("test_claim_many", async function () { + await feePool.checkpointToken(); + await time.increase(WEEK); + await feePool.checkpointToken(); + + const snapshot = await takeSnapshot(); + + const tx1 = await feePool + .connect(alice) + .claimMany( + [alice.address, bob.address, charlie.address].concat( + Array(17).fill(ethers.constants.AddressZero) + ) + ); + let balances = [ + (await ethers.provider.getBalance(alice.address)).add(await gasCostOf(tx1)), + await ethers.provider.getBalance(bob.address), + await ethers.provider.getBalance(charlie.address), + ]; + + await snapshot.restore(); + + const gas = []; + for (let acct of [alice, bob, charlie]) { + const tx2 = await feePool.connect(acct)["claim()"](); + gas.push(await gasCostOf(tx2)) + } + + expect((await ethers.provider.getBalance(alice.address)).add(gas[0])).to.be.eq(balances[0]); + expect((await ethers.provider.getBalance(bob.address)).add(gas[1])).to.be.eq(balances[1]); + expect((await ethers.provider.getBalance(charlie.address)).add(gas[2])).to.be.eq(balances[2]); + + }); + it("test_claim_many_same_account", async function () { + await feePool.checkpointToken(); + await time.increase(WEEK); + await feePool.checkpointToken(); + + const expected = await feePool.connect(alice).callStatic["claim()"](); + + expect(expected).to.above(0); + + const balanceBefore = await ethers.provider.getBalance(alice.address); + const tx = await feePool.connect(alice).claimMany(Array(20).fill(alice.address)); + + const balanceAfter = await ethers.provider.getBalance(alice.address); + const gas = await gasCostOf(tx); + expect(balanceAfter.sub(balanceBefore).add(gas)).to.be.eq(expected); + }); + }); +}); diff --git a/test/v15/unitary/FeePool/feePool.test.ts b/test/v15/unitary/FeePool/feePool.test.ts new file mode 100644 index 00000000..4d1e13c8 --- /dev/null +++ b/test/v15/unitary/FeePool/feePool.test.ts @@ -0,0 +1,270 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + FeePool, + FeePool__factory, +} from "../../../../typechain"; +import { getProxy } from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +const DAY = 86400; +const WEEK = DAY * 7; + +describe("FeePoolV2", () => { + let alice, bob, charlie: SignerWithAddress; + + let feePool: Contract; + let veYMT: Contract; + let YMT: Contract; + let snapshot: SnapshotRestorer; + + before(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const ymt = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + YMT = await ymt.deploy(); + await YMT.deployed(); + + veYMT = await VeYMT.deploy(YMT.address); + await veYMT.deployed(); + + feePool = await getProxy( + contractVersion["FeePool"], + [await time.latest()] + ); + await feePool.setVeYMT(veYMT.address); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + async function gasCostOf(tx) { + const receipt = await tx.wait() + return receipt.gasUsed.mul(receipt.effectiveGasPrice) + } + + describe("test_fee_distribution", () => { + it("test_deposited_after", async function () { + const amount = ethers.utils.parseEther("1000"); + await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); + + for (let i = 0; i < 5; i++) { + for (let j = 0; j < 7; j++) { + await bob.sendTransaction({ + to: feePool.address, + value: ethers.utils.parseEther("1"), + }); + await feePool.checkpointToken(); + await feePool.checkpointTotalSupply(); + await time.increase(DAY); + } + } + + await time.increase(WEEK); + await veYMT + .connect(alice) + .createLock(amount, (await time.latest()) + 3 * WEEK); + await time.increase(2 * WEEK); + + await feePool.connect(alice)["claim()"](); + + const balanceBefore = await ethers.provider.getBalance(alice.address); + const tx = await feePool.connect(alice)["claim()"](); + const balanceAfter = await ethers.provider.getBalance(alice.address); + expect(balanceAfter.sub(balanceBefore).add(await gasCostOf(tx))).to.be.eq(0); + }); + + it("test_deposited_during", async function () { + const amount = ethers.utils.parseEther("1000"); + await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); + + await time.increase(WEEK); + await veYMT + .connect(alice) + .createLock(amount, (await time.latest()) + 8 * WEEK); + await time.increase(WEEK); + + feePool = await getProxy( + contractVersion["FeePool"], + [await time.latest()] + ); + await feePool.setVeYMT(veYMT.address); + + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 7; j++) { + await bob.sendTransaction({ + to: feePool.address, + value: ethers.utils.parseEther("1"), + }); + await feePool.checkpointToken(); + await feePool.checkpointTotalSupply(); + await time.increase(DAY); + } + } + + await time.increase(WEEK); + await feePool.checkpointToken(); + const balanceAlice = await ethers.provider.getBalance(alice.address); + const tx = await feePool.connect(alice)["claim()"](); + const gasCost = await gasCostOf(tx) + const afterBalanceAlice = await ethers.provider.getBalance(alice.address); + + expect(afterBalanceAlice.sub(balanceAlice).add(gasCost)).to.be.closeTo( + ethers.utils.parseEther("21"), + 20 + ); + }); + + it("test_deposited_before", async function () { + const [alice, bob] = await ethers.getSigners(); + const amount = ethers.utils.parseEther("1000"); + + await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); + + await veYMT + .connect(alice) + .createLock(amount, (await time.latest()) + 8 * WEEK); + await time.increase(WEEK); + const startTime = await time.latest(); + await time.increase(WEEK * 5); + + feePool = await getProxy( + contractVersion["FeePool"], + [await time.latest()] + ); + await feePool.setVeYMT(veYMT.address); + + await bob.sendTransaction({ + to: feePool.address, + value: ethers.utils.parseEther("10"), + }); + + await feePool.checkpointToken(); + await time.increase(WEEK); + await feePool.checkpointToken(); + let balanceAlice = await ethers.provider.getBalance(alice.address); + const tx = await feePool.connect(alice)["claim()"](); + balanceAlice = (await ethers.provider.getBalance(alice.address)).sub(balanceAlice).add(await gasCostOf(tx)); + expect( + Number(balanceAlice) - 10 ** 18 + ).to.be.gte(10 ** 18); + }); + + it("test_deposited_twice", async function () { + const amount = ethers.utils.parseEther("1000"); + + await YMT.approve(veYMT.address, amount.mul(10)); + + const currentTimestamp = await time.latest(); + await veYMT.createLock(amount, currentTimestamp + 4 * WEEK); + + await time.increase(WEEK); + + const startTime = await time.latest(); + + await time.increase(3 * WEEK); + + await veYMT.connect(alice).withdraw(); + const excludeTime = Math.floor((await time.latest()) / WEEK) * WEEK; + await veYMT + .connect(alice) + .createLock(amount, (await time.latest()) + 4 * WEEK); + + await time.increase(2 * WEEK); + + feePool = await getProxy( + contractVersion["FeePool"], + [startTime] + ); + await feePool.setVeYMT(veYMT.address); + + await bob.sendTransaction({ + to: feePool.address, + value: ethers.utils.parseEther("10"), + }); + + await feePool.checkpointToken(); + + await time.increase(WEEK); + + await feePool.checkpointToken(); + + await feePool.connect(alice)["claim()"](); + + const tokensToExclude = await feePool.tokensPerWeek(excludeTime); + + expect( + ethers.utils + .parseEther("10") + .sub(await ethers.provider.getBalance(alice.address)) + .sub(tokensToExclude) + ).to.be.lt(10); + }); + + it("test_deposited_parallel", async function () { + const amount = ethers.utils.parseEther("1000"); + + await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); + await YMT.connect(bob).approve(veYMT.address, amount.mul(10)); + await YMT.connect(alice).transfer(bob.address, amount); + + const currentTimestamp = await time.latest(); + await veYMT + .connect(alice) + .createLock(amount, currentTimestamp + 8 * WEEK); + await veYMT.connect(bob).createLock(amount, currentTimestamp + 8 * WEEK); + + await time.increase(WEEK); + + const startTime = await time.latest(); + + await time.increase(5 * WEEK); + + feePool = await getProxy( + contractVersion["FeePool"], + [startTime] + ); + await feePool.setVeYMT(veYMT.address); + + await bob.sendTransaction({ + to: feePool.address, + value: ethers.utils.parseEther("10"), + }); + + await feePool.checkpointToken(); + + await time.increase(WEEK); + + await feePool.checkpointToken(); + + let balanceAlice = await ethers.provider.getBalance(alice.address); + let balanceBob = await ethers.provider.getBalance(bob.address); + + const txAlice = await feePool.connect(alice)["claim()"](); + const txBob = await feePool.connect(bob)["claim()"](); + + balanceAlice = (await ethers.provider.getBalance(alice.address)).sub(balanceAlice).add(await gasCostOf(txAlice)); + balanceBob = (await ethers.provider.getBalance(bob.address)).sub(balanceBob).add(await gasCostOf(txBob)); + + expect(balanceAlice).to.equal(balanceBob); + expect(balanceAlice.add(balanceBob)).to.be.closeTo( + ethers.utils.parseEther("10"), + 20 + ); + }); + }); +}); diff --git a/test/v15/unitary/FeePool/killFeeDistro.test.ts b/test/v15/unitary/FeePool/killFeeDistro.test.ts new file mode 100644 index 00000000..8f246f6f --- /dev/null +++ b/test/v15/unitary/FeePool/killFeeDistro.test.ts @@ -0,0 +1,142 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { Contract } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + FeePool, + FeePool__factory, +} from "../../../../typechain"; +import { getProxy } from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; + +describe("FeePoolV2", () => { + let alice, bob, charlie: SignerWithAddress; + let feePool: Contract; + let veYMT: Contract; + let token: Contract; + let snapshot: SnapshotRestorer; + + before(async function () { + [alice, bob, charlie] = await ethers.getSigners(); + + const YMT = await ethers.getContractFactory("YMT"); + const VeYMT = await ethers.getContractFactory("veYMT"); + + token = await YMT.deploy(); + await token.deployed(); + + veYMT = await VeYMT.deploy(token.address); + await veYMT.deployed(); + + feePool = await getProxy( + contractVersion["FeePool"], + [await time.latest()] + ); + await feePool.setVeYMT(veYMT.address); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + async function gasCostOf(tx) { + const receipt = await tx.wait() + return receipt.gasUsed.mul(receipt.effectiveGasPrice) + } + + describe("test_kill_fee_distro", () => { + let accounts: SignerWithAddress[]; + beforeEach(async () => { + accounts = await ethers.getSigners(); + }); + + it("test_assumptions", async function () { + expect(await feePool.isKilled()).to.be.false; + expect(await feePool.governance()).to.equal(alice.address); + }); + + it("test_kill", async function () { + await feePool.connect(alice).killMe(); + expect(await feePool.isKilled()).to.be.true; + }); + + it("test_multi_kill", async function () { + await feePool.connect(alice).killMe(); + await feePool.connect(alice).killMe(); + expect(await feePool.isKilled()).to.be.true; + }); + + it("test_killing_transfers_tokens", async function () { + await alice.sendTransaction({ + to: feePool.address, + value: 31337, + }); + + let balance = await ethers.provider.getBalance(alice.address); + const tx = await feePool.connect(alice).killMe(); + balance = balance.sub(await gasCostOf(tx)); + + expect(await feePool.governance()).to.equal(alice.address); + expect(await ethers.provider.getBalance(alice.address)).to.equal(balance.add(31337)); + }); + + it("test_multi_kill_token_transfer", async function () { + await bob.sendTransaction({ + to: feePool.address, + value: 10000, + }); + let balance = await ethers.provider.getBalance(alice.address); + + const tx1 = await feePool.connect(alice).killMe(); + balance = balance.sub(await gasCostOf(tx1)); + + await bob.sendTransaction({ + to: feePool.address, + value: 30000, + }); + + const tx2 = await feePool.connect(alice).killMe(); + balance = balance.sub(await gasCostOf(tx2)); + + expect(await feePool.governance()).to.equal(alice.address); + expect(await ethers.provider.getBalance(alice.address)).to.equal(balance.add(40000)); + }); + + for (let idx = 1; idx <= 2; idx++) { + it(`test_only_admin_for_account_index_${idx}`, async function () { + await expect(feePool.connect(accounts[idx]).killMe()).to.be.reverted; + }); + + it(`test_cannot_claim_after_killed_for_account_index_${idx}`, async function () { + await feePool.connect(alice).killMe(); + await expect(feePool.connect(accounts[idx])["claim()"]()).to.be + .reverted; + }); + + it(`test_cannot_claim_for_after_killed_for_account_index_${idx}`, async function () { + await feePool.connect(alice).killMe(); + await expect( + feePool.connect(accounts[idx])["claim(address)"](alice.address) + ).to.be.reverted; + }); + + it(`test_cannot_claim_many_after_killed_for_account_index_${idx}`, async function () { + await feePool.connect(alice).killMe(); + await expect( + feePool + .connect(accounts[idx]) + .claimMany(new Array(20).fill(alice.address)) + ).to.be.reverted; + }); + } + }); +}); From 21e7e3b7ead9fd53995a15e0d851d4c022f92318 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 4 Dec 2023 16:45:33 +0900 Subject: [PATCH 093/412] Add ScoreRegistry unitary test code. --- .../unitary/ScoreRegistry/checkpoint.test.ts | 219 +++++++++++++++ test/v15/unitary/ScoreRegistry/kick.test.ts | 261 ++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 test/v15/unitary/ScoreRegistry/checkpoint.test.ts create mode 100644 test/v15/unitary/ScoreRegistry/kick.test.ts diff --git a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts new file mode 100644 index 00000000..47117743 --- /dev/null +++ b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts @@ -0,0 +1,219 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import chai, { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + CJPY, + CurrencyOSV3, + Pool, + FeePool, + PriceFeedV3, + PriorityRegistry, + PriorityRegistry__factory, + YamatoV4, + YamatoDepositor, + YamatoBorrower, + YamatoRepayer, + YamatoWithdrawer, + YamatoRedeemer, + YamatoSweeper, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + CJPY__factory, + CurrencyOSV3__factory, + YamatoV4__factory, + YamatoDepositor__factory, + YamatoBorrower__factory, + YamatoRepayer__factory, + YamatoWithdrawer__factory, + YamatoRedeemer__factory, + YamatoSweeper__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, + Pool__factory, +} from "../../../../typechain"; +import { + getFakeProxy, + getProxy, + getLinkedProxy, +} from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; + +chai.use(smock.matchers); + +describe("ScoreRegistry checkpoint", function () { + let mockFeePool: FakeContract; + let mockFeed: FakeContract; + let CJPY: CJPY; + let currencyOS: CurrencyOSV3; + let yamato: YamatoV4; + let yamatoDepositor: YamatoDepositor; + let yamatoBorrower: YamatoBorrower; + let yamatoRepayer: YamatoRepayer; + let yamatoWithdrawer: YamatoWithdrawer; + let yamatoRedeemer: YamatoRedeemer; + let yamatoSweeper: YamatoSweeper; + let scoreRegistry: ScoreRegistry; + let ymtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let scoreWeightController: ScoreWeightController; + let pool: Pool; + let priorityRegistry: PriorityRegistry; + let PRICE: BigNumber; + let accounts: SignerWithAddress[]; + let snapshot: SnapshotRestorer; + const year = Constants.year; + + before(async function () { + accounts = await ethers.getSigners(); + + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); + CJPY = await (( + await ethers.getContractFactory("CJPY") + )).deploy(); + currencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, mockFeed.address, mockFeePool.address] + ); + + yamato = await getLinkedProxy( + contractVersion["Yamato"], + [currencyOS.address], + ["PledgeLib"] + ); + + await currencyOS.addYamato(yamato.address); + await CJPY.setCurrencyOS(currencyOS.address); + + yamatoDepositor = await getLinkedProxy< + YamatoDepositor, + YamatoDepositor__factory + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); + + yamatoBorrower = await getLinkedProxy< + YamatoBorrower, + YamatoBorrower__factory + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); + + yamatoRepayer = await getLinkedProxy( + contractVersion["YamatoRepayer"], + [yamato.address], + ["PledgeLib"] + ); + + yamatoWithdrawer = await getLinkedProxy< + YamatoWithdrawer, + YamatoWithdrawer__factory + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); + + yamatoRedeemer = await getLinkedProxy< + YamatoRedeemer, + YamatoRedeemer__factory + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); + + yamatoSweeper = await getLinkedProxy( + contractVersion["YamatoSweeper"], + [yamato.address], + ["PledgeLib"] + ); + + priorityRegistry = await getLinkedProxy< + PriorityRegistry, + PriorityRegistry__factory + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + scoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + ymtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, scoreWeightController.address] + ); + + scoreRegistry = await getLinkedProxy( + contractVersion["ScoreRegistry"], + [ymtMinter.address, yamato.address], + ["PledgeLib"] + ); + + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); + + await ( + await yamato.setDeps( + yamatoDepositor.address, + yamatoBorrower.address, + yamatoRepayer.address, + yamatoWithdrawer.address, + yamatoRedeemer.address, + yamatoSweeper.address, + pool.address, + priorityRegistry.address + ) + ).wait(); + await (await yamato.setScoreRegistry(scoreRegistry.address)).wait(); + + await currencyOS.setYmtMinter(ymtMinter.address); + await YMT.setMinter(ymtMinter.address); + + PRICE = BigNumber.from(260000).mul(1e18 + ""); + + // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); + mockFeed.fetchPrice.returns(PRICE); + mockFeed.getPrice.returns(PRICE); + mockFeed.lastGoodPrice.returns(PRICE); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + it("test_user_checkpoint", async function () { + // Assuming `userCheckpoint` is a function on your contract + await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + }); + + it("test_user_checkpoint_new_period", async function () { + await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + + // Increase the time on the blockchain + await time.increase(year * 1.1); + + await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + }); + + it("test_user_checkpoint_wrong_account", async function () { + // Expect the transaction to be reverted with the specified error message + await expect( + scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[2].address) + ).to.be.revertedWith("dev: unauthorized"); + }); +}); diff --git a/test/v15/unitary/ScoreRegistry/kick.test.ts b/test/v15/unitary/ScoreRegistry/kick.test.ts new file mode 100644 index 00000000..2513fe7f --- /dev/null +++ b/test/v15/unitary/ScoreRegistry/kick.test.ts @@ -0,0 +1,261 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import chai, { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + CJPY, + CurrencyOSV3, + Pool, + FeePool, + PriceFeedV3, + PriorityRegistry, + PriorityRegistry__factory, + YamatoV4, + YamatoDepositor, + YamatoBorrower, + YamatoRepayer, + YamatoWithdrawer, + YamatoRedeemer, + YamatoSweeper, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + CJPY__factory, + CurrencyOSV3__factory, + YamatoV4__factory, + YamatoDepositor__factory, + YamatoBorrower__factory, + YamatoRepayer__factory, + YamatoWithdrawer__factory, + YamatoRedeemer__factory, + YamatoSweeper__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, + Pool__factory, +} from "../../../../typechain"; +import { + getFakeProxy, + getProxy, + getLinkedProxy, +} from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; + +chai.use(smock.matchers); + +describe.only("ScoreRegistry kick", function () { + let mockFeePool: FakeContract; + let mockFeed: FakeContract; + let CJPY: CJPY; + let currencyOS: CurrencyOSV3; + let yamato: YamatoV4; + let yamatoDepositor: YamatoDepositor; + let yamatoBorrower: YamatoBorrower; + let yamatoRepayer: YamatoRepayer; + let yamatoWithdrawer: YamatoWithdrawer; + let yamatoRedeemer: YamatoRedeemer; + let yamatoSweeper: YamatoSweeper; + let scoreRegistry: ScoreRegistry; + let ymtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let scoreWeightController: ScoreWeightController; + let pool: Pool; + let priorityRegistry: PriorityRegistry; + let PRICE: BigNumber; + let accounts: SignerWithAddress[]; + let snapshot: SnapshotRestorer; + const MAX_UINT256 = Constants.MAX_UINT256; + const week = Constants.week; + const DEPOSIT_AMOUNT = Constants.ten_to_the_21; // 10^21 + const LOCK_AMOUNT = Constants.ten_to_the_20; // 10^20 + + before(async function () { + accounts = await ethers.getSigners(); + + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); + CJPY = await (( + await ethers.getContractFactory("CJPY") + )).deploy(); + currencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, mockFeed.address, mockFeePool.address] + ); + + yamato = await getLinkedProxy( + contractVersion["Yamato"], + [currencyOS.address], + ["PledgeLib"] + ); + + await currencyOS.addYamato(yamato.address); + await CJPY.setCurrencyOS(currencyOS.address); + + yamatoDepositor = await getLinkedProxy< + YamatoDepositor, + YamatoDepositor__factory + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); + + yamatoBorrower = await getLinkedProxy< + YamatoBorrower, + YamatoBorrower__factory + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); + + yamatoRepayer = await getLinkedProxy( + contractVersion["YamatoRepayer"], + [yamato.address], + ["PledgeLib"] + ); + + yamatoWithdrawer = await getLinkedProxy< + YamatoWithdrawer, + YamatoWithdrawer__factory + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); + + yamatoRedeemer = await getLinkedProxy< + YamatoRedeemer, + YamatoRedeemer__factory + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); + + yamatoSweeper = await getLinkedProxy( + contractVersion["YamatoSweeper"], + [yamato.address], + ["PledgeLib"] + ); + + priorityRegistry = await getLinkedProxy< + PriorityRegistry, + PriorityRegistry__factory + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + scoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + ymtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, scoreWeightController.address] + ); + + scoreRegistry = await getLinkedProxy( + contractVersion["ScoreRegistry"], + [ymtMinter.address, yamato.address], + ["PledgeLib"] + ); + + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); + + await ( + await yamato.setDeps( + yamatoDepositor.address, + yamatoBorrower.address, + yamatoRepayer.address, + yamatoWithdrawer.address, + yamatoRedeemer.address, + yamatoSweeper.address, + pool.address, + priorityRegistry.address + ) + ).wait(); + await (await yamato.setScoreRegistry(scoreRegistry.address)).wait(); + + await currencyOS.setYmtMinter(ymtMinter.address); + await YMT.setMinter(ymtMinter.address); + + PRICE = BigNumber.from(260000).mul(1e18 + ""); + + // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); + mockFeed.fetchPrice.returns(PRICE); + mockFeed.getPrice.returns(PRICE); + mockFeed.lastGoodPrice.returns(PRICE); + + await scoreWeightController.addCurrency( + scoreRegistry.address, + ethers.utils.parseEther("10") + ); + for (let i = 0; i < 4; i++) { + await yamato + .connect(accounts[i]) + .deposit({ value: ethers.utils.parseEther("100") }); + } + await yamato.borrow(ethers.utils.parseEther("100000")); + + await YMT.transfer(accounts[1].address, LOCK_AMOUNT); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + it("test kick functionality", async function () { + // Forward time by 2 weeks + 5 seconds + await time.increase(2 * week + 5); + + // Alice approves tokens to voting escrow and creates a lock + await YMT.connect(accounts[1]).approve(veYMT.address, MAX_UINT256); + await veYMT + .connect(accounts[1]) + .createLock( + LOCK_AMOUNT, + (await ethers.provider.getBlock("latest")).timestamp + 4 * week + ); + + await yamato + .connect(accounts[1]) + .borrow(DEPOSIT_AMOUNT); + + // Check working balance of Alice in Gauge + expect(await scoreRegistry.workingBalances(accounts[1].address)).to.equal( + DEPOSIT_AMOUNT.mul("25").div("10") + ); + + // Forward time by 1 week + await time.increase(week); + + // Bob tries to kick Alice but should fail because it's not allowed yet + await expect( + scoreRegistry.connect(accounts[1]).kick(accounts[1].address) + ).to.be.revertedWith("Not allowed"); + + // Forward time by 4 weeks + await time.increase(4 * week); + + // Now Bob kicks Alice + await scoreRegistry.connect(accounts[1]).kick(accounts[1].address); + + // Check the working balance of Alice after kick + expect(await scoreRegistry.workingBalances(accounts[1].address)).to.equal( + LOCK_AMOUNT.mul(4).mul("25").div("10") + ); + + // Trying to kick again should fail as it's not needed + await expect( + scoreRegistry.connect(accounts[1]).kick(accounts[1].address) + ).to.be.revertedWith("Not needed"); + }); +}); From 322f87928e5d8d0472798d0ac2cca9b36ac828b0 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 4 Dec 2023 16:46:59 +0900 Subject: [PATCH 094/412] Modifier of ScoreRegistry was changed. --- contracts/ScoreRegistry.sol | 25 +++++++++++++++++++------ contracts/YamatoV4.sol | 7 +++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 3872658e..d4d8b1d1 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -101,6 +101,10 @@ contract ScoreRegistry is YamatoAction { } function checkpoint(address addr) public onlyYamato { + _checkpoint(addr); + } + + function _checkpoint(address addr) private { CheckPointParameters memory _st; _st.period = period; @@ -191,6 +195,15 @@ contract ScoreRegistry is YamatoAction { uint256 totalDebt_, uint256 collateralRatio_ ) public onlyYamato { + _updateScoreLimit(addr_, debt_, totalDebt_, collateralRatio_); + } + + function _updateScoreLimit( + address addr_, + uint256 debt_, + uint256 totalDebt_, + uint256 collateralRatio_ + ) private { uint256 _votingBalance = IveYMT(veYMT()).balanceOf(addr_); uint256 _votingTotal = IveYMT(veYMT()).totalSupply(); @@ -236,17 +249,17 @@ contract ScoreRegistry is YamatoAction { return 0; } - function userCheckpoint(address addr_) external onlyYamato returns (bool) { + function userCheckpoint(address addr_) external returns (bool) { require( msg.sender == addr_ || msg.sender == ymtMinter(), "dev: unauthorized" ); - checkpoint(addr_); + _checkpoint(addr_); IYamato.Pledge memory _pledge = IYamato(yamato()).getPledge(addr_); uint256 _collateralRatio = _pledge.getICR(priceFeed()); uint256 _balance = _pledge.debt; uint256 _totalSupply = IYamatoV4(yamato()).getTotalDebt(); - updateScoreLimit(addr_, _balance, _totalSupply, _collateralRatio); + _updateScoreLimit(addr_, _balance, _totalSupply, _collateralRatio); return true; } @@ -271,9 +284,9 @@ contract ScoreRegistry is YamatoAction { "Not needed" ); - checkpoint(addr_); + _checkpoint(addr_); uint256 _totalSupply = IYamatoV4(yamato()).getTotalDebt(); - updateScoreLimit(addr_, _balance, _totalSupply, _collateralRatio); + _updateScoreLimit(addr_, _balance, _totalSupply, _collateralRatio); } function setKilled(bool isKilled_) external onlyGovernance { @@ -290,7 +303,7 @@ contract ScoreRegistry is YamatoAction { /* ===================== - Getter Functions + Address Getter Functions ===================== */ function YMT() public view returns (address _YMT) { diff --git a/contracts/YamatoV4.sol b/contracts/YamatoV4.sol index 117f1207..dccc079d 100644 --- a/contracts/YamatoV4.sol +++ b/contracts/YamatoV4.sol @@ -591,7 +591,7 @@ contract YamatoV4 is address _sender ) public view override(IYamato, YamatoBase) returns (bool) { bool permit; - address[11] memory deps = getDeps(); + address[10] memory deps = getDeps(); for (uint256 i = 0; i < deps.length; i++) { if (_sender == deps[i]) permit = true; } @@ -599,7 +599,7 @@ contract YamatoV4 is } /// @dev Get package-deps to check onlyYamato-permitDeps logic - function getDeps() public view returns (address[11] memory) { + function getDeps() public view returns (address[10] memory) { return [ address(this), depositor(), @@ -610,8 +610,7 @@ contract YamatoV4 is sweeper(), pool(), priorityRegistry(), - scoreRegistry(), - ICurrencyOSV3(currencyOS()).ymtMinter() + scoreRegistry() ]; } From fbb880f7301afc675107a1d8a71abd572dd3b7a5 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 11:47:55 +0900 Subject: [PATCH 095/412] Upgrade Withdrawer and Depositor with additional checkpoint --- contracts/YamatoDepositorV3.sol | 115 ++++++++++++++++++++++++++ contracts/YamatoWithdrawerV3.sol | 137 +++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 contracts/YamatoDepositorV3.sol create mode 100644 contracts/YamatoWithdrawerV3.sol diff --git a/contracts/YamatoDepositorV3.sol b/contracts/YamatoDepositorV3.sol new file mode 100644 index 00000000..5fed1199 --- /dev/null +++ b/contracts/YamatoDepositorV3.sol @@ -0,0 +1,115 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./PoolV2.sol"; +import "./YMT.sol"; +import "./PriceFeedV3.sol"; +import "./Dependencies/YamatoAction.sol"; +import "./Dependencies/PledgeLib.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Interfaces/IYamato.sol"; +import "./Interfaces/IYamatoV4.sol"; +import "./Interfaces/IFeePool.sol"; +import "./Interfaces/ICurrencyOS.sol"; +import "./Interfaces/IYamatoDepositor.sol"; +import "./Interfaces/IPriorityRegistryV6.sol"; +import "./Interfaces/IScoreRegistry.sol"; +import "hardhat/console.sol"; + +/// @title Yamato Depositor Contract +contract YamatoDepositorV3 is IYamatoDepositor, YamatoAction { + using PledgeLib for IYamato.Pledge; + using PledgeLib for uint256; + + function initialize(address _yamato) public initializer { + __YamatoAction_init(_yamato); + } + + /// @dev no reentrancy guard because action funcs are protected by permitDeps() + function runDeposit(address _sender) public payable override onlyYamato { + IPriceFeedV3(priceFeed()).fetchPrice(); + uint256 _ethAmount = msg.value; + + /* + 1. Validate lock + 2. Compose a pledge in memory + */ + require( + !IYamato(yamato()).checkFlashLock(_sender), + "Those can't be called in the same block." + ); + + IYamato.Pledge memory pledge = IYamato(yamato()).getPledge(_sender); + (uint256 totalColl, uint256 totalDebt, , , , ) = IYamato(yamato()).getStates(); + + pledge.coll += _ethAmount; + + require( + pledge.coll >= IYamatoV4(yamato()).collFloor(), + "Deposit or Withdraw can't make pledge less than floor size." + ); + + if (!pledge.isCreated) { + // new pledge + pledge.isCreated = true; + pledge.owner = _sender; + } + + /* + scoreRegistry checkpoint + */ + IScoreRegistry _scoreRegistry = IScoreRegistry( + IYamatoV4(yamato()).scoreRegistry() + ); + if(pledge.debt > 0){ + _scoreRegistry.checkpoint(_sender); + } + + /* + 3. Update PriorityRegistry + */ + pledge.priority = IPriorityRegistryV6(priorityRegistry()).upsert( + pledge + ); + + /* + 4. Commit pledge modifications + */ + IYamato(yamato()).setPledge(pledge.owner, pledge); + + /* + 5. Set totalColl + */ + IYamato(yamato()).setTotalColl(totalColl + _ethAmount); + + /* + 6. Set FlashLock + */ + IYamato(yamato()).setFlashLock(_sender); + + /* + Update score + */ + if(pledge.debt > 0){ + _scoreRegistry.updateScoreLimit( + _sender, + pledge.debt, + totalDebt, + pledge.getICR(priceFeed()) + ); + } + + /* + 7. Send ETH to pool + */ + (bool success, ) = payable(pool()).call{value: _ethAmount}(""); + require(success, "transfer failed"); + } +} diff --git a/contracts/YamatoWithdrawerV3.sol b/contracts/YamatoWithdrawerV3.sol new file mode 100644 index 00000000..45e74169 --- /dev/null +++ b/contracts/YamatoWithdrawerV3.sol @@ -0,0 +1,137 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +//solhint-disable max-line-length +//solhint-disable no-inline-assembly + +import "./Pool.sol"; +import "./YMT.sol"; +import "./PriceFeedV3.sol"; +import "./Dependencies/YamatoAction.sol"; +import "./Dependencies/PledgeLib.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Interfaces/IYamato.sol"; +import "./Interfaces/IYamatoV4.sol"; +import "./Interfaces/IFeePool.sol"; +import "./Interfaces/ICurrencyOS.sol"; +import "./Interfaces/IYamatoWithdrawer.sol"; +import "./Interfaces/IPriorityRegistry.sol"; +import "./Interfaces/IScoreRegistry.sol"; +import "hardhat/console.sol"; + +/// @title Yamato Withdrawer Contract +contract YamatoWithdrawerV3 is IYamatoWithdrawer, YamatoAction { + using PledgeLib for IYamato.Pledge; + using PledgeLib for uint256; + + function initialize(address _yamato) public initializer { + __YamatoAction_init(_yamato); + } + + /// @dev no reentrancy guard because action funcs are protected by permitDeps() + function runWithdraw( + address _sender, + uint256 _ethAmount + ) public override onlyYamato { + /* + 1. Get feed and pledge + */ + IPriceFeedV3(priceFeed()).fetchPrice(); + IYamato.Pledge memory pledge = IYamato(yamato()).getPledge(_sender); + (uint256 totalColl, uint256 totalDebt, , , , ) = IYamato(yamato()).getStates(); + + /* + scoreRegistry checkpoint + */ + IScoreRegistry _scoreRegistry = IScoreRegistry( + IYamatoV4(yamato()).scoreRegistry() + ); + if(pledge.debt > 0){ + _scoreRegistry.checkpoint(_sender); + } + + /* + 2. Validate + */ + require( + _ethAmount <= pledge.coll, + "Withdrawal amount must be less than equal to the target coll amount." + ); + require( + _ethAmount <= totalColl, + "Withdrawal amount must be less than equal to the total coll amount." + ); + require( + !IYamato(yamato()).checkFlashLock(_sender), + "Those can't be called in the same block." + ); + require( + pledge.getICR(priceFeed()) > uint256(IYamato(yamato()).MCR()) * 100, + "Withdrawal failure: ICR is not more than MCR." + ); + + /* + 3. Set flashlock + */ + IYamato(yamato()).setFlashLock(_sender); + + /* + 4. Update pledge + */ + // Note: SafeMath unintentionally checks full withdrawal + pledge.coll -= _ethAmount; + + IYamato(yamato()).setPledge(pledge.owner, pledge); + + IYamato(yamato()).setTotalColl(totalColl - _ethAmount); + + /* + 5. Validate and update PriorityRegistry + */ + if (pledge.coll == 0 && pledge.debt == 0) { + /* + 5-a. Clean full withdrawal + */ + IPriorityRegistry(priorityRegistry()).remove(pledge); + } else { + /* + 5-b. Reasonable partial withdrawal + */ + require( + pledge.coll >= IYamatoV4(yamato()).collFloor(), + "Deposit or Withdraw can't make pledge less than floor size." + ); + require( + pledge.getICR(priceFeed()) >= + uint256(IYamato(yamato()).MCR()) * 100, + "Withdrawal failure: ICR can't be less than MCR after withdrawal." + ); + pledge.priority = IPriorityRegistry(priorityRegistry()).upsert( + pledge + ); + } + IYamato(yamato()).setPledge(pledge.owner, pledge); + + /* + Update score + */ + if(pledge.debt > 0){ + _scoreRegistry.updateScoreLimit( + _sender, + pledge.debt, + totalDebt, + pledge.getICR(priceFeed()) + ); + } + + /* + 6-1. Charge CJPY + 6-2. Return coll to the withdrawer + */ + IPool(pool()).sendETH(_sender, _ethAmount); + } +} From 6e03e0f4c401ca40808191adb546c2ef41863c25 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 11:48:38 +0900 Subject: [PATCH 096/412] only removed. --- test/v15/unitary/ScoreRegistry/kick.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/v15/unitary/ScoreRegistry/kick.test.ts b/test/v15/unitary/ScoreRegistry/kick.test.ts index 2513fe7f..de98e7a3 100644 --- a/test/v15/unitary/ScoreRegistry/kick.test.ts +++ b/test/v15/unitary/ScoreRegistry/kick.test.ts @@ -54,7 +54,7 @@ import Constants from "../../Constants"; chai.use(smock.matchers); -describe.only("ScoreRegistry kick", function () { +describe("ScoreRegistry kick", function () { let mockFeePool: FakeContract; let mockFeed: FakeContract; let CJPY: CJPY; From 703754c3e5e4aa8743f8c9d7b6a302d66ce179b5 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 11:49:58 +0900 Subject: [PATCH 097/412] Add ScoreRegistry integration test code. --- .../ScoreRegistry/scoreRegistry.test.ts | 532 ++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 test/v15/integration/ScoreRegistry/scoreRegistry.test.ts diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts new file mode 100644 index 00000000..a6e427ce --- /dev/null +++ b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts @@ -0,0 +1,532 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import chai, { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + CJPY, + CurrencyOSV3, + Pool, + FeePool, + PriceFeedV3, + PriorityRegistry, + PriorityRegistry__factory, + YamatoV4, + YamatoDepositor, + YamatoBorrower, + YamatoRepayer, + YamatoWithdrawer, + YamatoRedeemer, + YamatoSweeper, + YMT, + VeYMT, + ScoreWeightController, + YmtMinter, + ScoreRegistry, + CJPY__factory, + CurrencyOSV3__factory, + YamatoV4__factory, + YamatoDepositor__factory, + YamatoBorrower__factory, + YamatoRepayer__factory, + YamatoWithdrawer__factory, + YamatoRedeemer__factory, + YamatoSweeper__factory, + YMT__factory, + VeYMT__factory, + ScoreWeightController__factory, + YmtMinter__factory, + ScoreRegistry__factory, + Pool__factory, +} from "../../../../typechain"; +import { + getFakeProxy, + getProxy, + getLinkedProxy, +} from "../../../../src/testUtil"; +import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; + +chai.use(smock.matchers); + +describe("ScoreRegistry", function () { + let mockFeePool: FakeContract; + let mockFeed: FakeContract; + let CJPY: CJPY; + let currencyOS: CurrencyOSV3; + let yamato: YamatoV4; + let yamatoDepositor: YamatoDepositor; + let yamatoBorrower: YamatoBorrower; + let yamatoRepayer: YamatoRepayer; + let yamatoWithdrawer: YamatoWithdrawer; + let yamatoRedeemer: YamatoRedeemer; + let yamatoSweeper: YamatoSweeper; + let scoreRegistry: ScoreRegistry; + let ymtMinter: YmtMinter; + let veYMT: VeYMT; + let YMT: YMT; + let scoreWeightController: ScoreWeightController; + let pool: Pool; + let priorityRegistry: PriorityRegistry; + let PRICE: BigNumber; + let accounts: SignerWithAddress[]; + let snapshot: SnapshotRestorer; + const ten_to_the_18 = Constants.ten_to_the_18; + const ten_to_the_20 = Constants.ten_to_the_20; + const ten_to_the_21 = Constants.ten_to_the_21; + const MAX_UINT256 = Constants.MAX_UINT256; + const zero = Constants.zero; + const week = Constants.week; + const WEEK = Constants.WEEK; + + const DAY = 86400; + const YEAR = DAY * 365; + const MIN_AMOUNT = 10000; + const SCALE = BigNumber.from((1e20).toString()); + + beforeEach(async () => { + accounts = await ethers.getSigners(); + + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); + CJPY = await (( + await ethers.getContractFactory("CJPY") + )).deploy(); + currencyOS = await getProxy( + contractVersion["CurrencyOS"], + [CJPY.address, mockFeed.address, mockFeePool.address] + ); + + yamato = await getLinkedProxy( + contractVersion["Yamato"], + [currencyOS.address], + ["PledgeLib"] + ); + + await currencyOS.addYamato(yamato.address); + await CJPY.setCurrencyOS(currencyOS.address); + + yamatoDepositor = await getLinkedProxy< + YamatoDepositor, + YamatoDepositor__factory + >(contractVersion["YamatoDepositor"], [yamato.address], ["PledgeLib"]); + + yamatoBorrower = await getLinkedProxy< + YamatoBorrower, + YamatoBorrower__factory + >(contractVersion["YamatoBorrower"], [yamato.address], ["PledgeLib"]); + + yamatoRepayer = await getLinkedProxy( + contractVersion["YamatoRepayer"], + [yamato.address], + ["PledgeLib"] + ); + + yamatoWithdrawer = await getLinkedProxy< + YamatoWithdrawer, + YamatoWithdrawer__factory + >(contractVersion["YamatoWithdrawer"], [yamato.address], ["PledgeLib"]); + + yamatoRedeemer = await getLinkedProxy< + YamatoRedeemer, + YamatoRedeemer__factory + >(contractVersion["YamatoRedeemer"], [yamato.address], ["PledgeLib"]); + + yamatoSweeper = await getLinkedProxy( + contractVersion["YamatoSweeper"], + [yamato.address], + ["PledgeLib"] + ); + + priorityRegistry = await getLinkedProxy< + PriorityRegistry, + PriorityRegistry__factory + >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); + + scoreWeightController = await getProxy< + ScoreWeightController, + ScoreWeightController__factory + >(contractVersion["ScoreWeightController"], [YMT.address, veYMT.address]); + + ymtMinter = await getProxy( + contractVersion["YmtMinter"], + [YMT.address, scoreWeightController.address] + ); + + scoreRegistry = await getLinkedProxy( + contractVersion["ScoreRegistry"], + [ymtMinter.address, yamato.address], + ["PledgeLib"] + ); + + pool = await getProxy(contractVersion["Pool"], [ + yamato.address, + ]); + + await ( + await yamato.setDeps( + yamatoDepositor.address, + yamatoBorrower.address, + yamatoRepayer.address, + yamatoWithdrawer.address, + yamatoRedeemer.address, + yamatoSweeper.address, + pool.address, + priorityRegistry.address + ) + ).wait(); + await (await yamato.setScoreRegistry(scoreRegistry.address)).wait(); + + await currencyOS.setYmtMinter(ymtMinter.address); + await YMT.setMinter(ymtMinter.address); + + PRICE = BigNumber.from(260000).mul(1e18 + ""); + + // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); + mockFeed.fetchPrice.returns(PRICE); + mockFeed.getPrice.returns(PRICE); + mockFeed.lastGoodPrice.returns(PRICE); + + await scoreWeightController.addCurrency( + scoreRegistry.address, + ethers.utils.parseEther("10") + ); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + function randomBigValue(min: number, max: number): BigNumber { + return BigNumber.from( + Math.floor(Math.random() * (max - min) + min).toString() + ); + } + + function randomValue(min: number, max: number): number { + return Math.floor(Math.random() * (max - min) + min); + } + + function fee(amount : BigNumber): BigNumber { + return amount.sub((amount.div(1000))) + } + + function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { + if (value.isZero() && target.isZero()) { + return true; + } + + const diff = value.sub(target).abs(); + const sum = value.add(target); + const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); + + // console.log( + // `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` + // ); + // console.log( + // `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` + // ); + + return ratio.lte(tol); + } + + describe("Gauge Integral Calculations", function () { + /** + * テスト: test_gauge_integral + * 1. AliceとBobの保有量と積分の初期化を行う。 + * 2. 現在のブロックのタイムスタンプと初期レートを取得する。 + * 3. タイプを追加し、その重みを変更する。 + * 4. LPトークンをAliceとBobに均等に送信する。 + * 5. 積分の更新処理を行うための補助関数「update_integral」を定義する。 + * 6. Bobが預金または引き出しを繰り返し行い、Aliceがそれをランダムに行う10回のループを開始する。 + * a. ランダムな時間を経過させるシミュレーションを行う。 + * b. Bobがランダムに預金または引き出しを行う。 + * c. 20%の確率でAliceも預金または引き出しを行う。 + * d. 同じ秒数でのユーザーチェックポイントの更新が影響しないことを確認する。 + * e. AliceとBobの保有量が正しいことを確認する。 + * f. もう一度、ランダムな時間を経過させるシミュレーションを行う。 + * g. Aliceのユーザーチェックポイントを更新し、積分を更新する。 + * 7. テストが終了する。 + */ + it("should correctly calculate user integrals over randomized actions", async () => { + // AliceとBobの保有量と積分を初期化 + let alice_staked = BigNumber.from("0"); + let bob_staked = BigNumber.from("0"); + let integral = BigNumber.from("0"); + + // 最新のブロックのタイムスタンプを取得 + let checkpoint = BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + // 初期レートの取得 + let checkpoint_rate = await YMT.rate(); + let checkpoint_supply = BigNumber.from("0"); + let checkpoint_balance = BigNumber.from("0"); + + await scoreWeightController.addCurrency( + scoreRegistry.address, + ten_to_the_18 + ); + + // ETHをdeposit + for (let i = 0; i < 3; i++) { + await yamato + .connect(accounts[i]) + .deposit({ value: ethers.utils.parseEther("10000") }); + } + + // 積分を更新する関数 + async function update_integral() { + let t1 = BigNumber.from(await time.latest()); + let rate1 = await YMT.rate(); + let t_epoch = await YMT.startEpochTime(); + let rate_x_time = BigNumber.from("0"); + + // checkpoint >= t_epoch + if (checkpoint.gte(t_epoch)) { + // t1 - checkpoint * rate1 + rate_x_time = t1.sub(checkpoint).mul(rate1); + } else { + // t_epoch - checkpoint * checkpoint_rate ( t1 - t_epoch * rate1) + rate_x_time = t_epoch + .sub(checkpoint) + .mul(checkpoint_rate) + .add(t1.sub(t_epoch).mul(rate1)); + } + + // checkpoint_supply > 0 + if (checkpoint_supply.gt(BigNumber.from("0"))) { + // integral + rate_x_time * checkpoint_balance / checkpoint_supply + integral = integral.add( + rate_x_time.mul(checkpoint_balance).div(checkpoint_supply) + ); + } + + checkpoint_rate = rate1; + checkpoint = t1; + checkpoint_supply = await CJPY.totalSupply(); + checkpoint_balance = await CJPY.balanceOf(accounts[1].address); + } + + // Bobは常に預金または引き出しを行い、Aliceはそれをあまり行わない + for (let i = 0; i < 40; i++) { + let is_alice = Math.random() < 0.2; + + // ランダムな時間経過をシミュレート + let dt = randomValue(1, Math.floor(YEAR / 5)) + await ethers.provider.send("evm_increaseTime", [dt]); + + // Bobの処理 + let is_withdraw = i > 0 && Math.random() < 0.5; + const bob_balance = Number(await CJPY.balanceOf(accounts[2].address)) + if (is_withdraw) { + // 引き出し処理 + let amount = randomBigValue(1, bob_balance + 1) + await yamato.connect(accounts[2]).repay(amount); + await update_integral(); + bob_staked = bob_staked.sub(amount); + } else { + // 預金処理 + let amount = randomBigValue(1 + MIN_AMOUNT, Math.floor(bob_balance / 10) + 1 + MIN_AMOUNT); + await yamato + .connect(accounts[2]) + .borrow(amount); + await update_integral(); + + bob_staked = bob_staked.add(fee(amount)); + } + + // Aliceの処理 + if (is_alice) { + const alice_balance = Number(await CJPY.balanceOf(accounts[1].address)) + let is_withdraw_alice = alice_balance > 0 && Math.random() > 0.5; + if (is_withdraw_alice) { + // 引き出し処理 + let amount_alice = randomBigValue(1, Math.floor(alice_balance / 10) + 1) + await yamato.connect(accounts[1]).repay(amount_alice); + await update_integral(); + alice_staked = alice_staked.sub(amount_alice); + } else { + // 預金処理 + let amount_alice = randomBigValue(1 + MIN_AMOUNT, alice_balance + 1 + MIN_AMOUNT) + await yamato + .connect(accounts[1]) + .borrow(amount_alice); + await update_integral(); + + alice_staked = alice_staked.add(fee(amount_alice)); + } + } + + // 同じ秒数でのチェックポイントの更新は影響しないことの確認 + if (Math.random() < 0.5) { + await scoreRegistry + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); + } + if (Math.random() < 0.5) { + await scoreRegistry + .connect(accounts[2]) + .userCheckpoint(accounts[2].address); + } + + // 保有量の確認 + expect(await CJPY.balanceOf(accounts[1].address)).to.equal( + alice_staked + ); + expect(await CJPY.balanceOf(accounts[2].address)).to.equal( + bob_staked + ); + + // ランダムな時間経過をさらにシミュレート + dt = randomValue(1, Math.floor(YEAR / 20)) + await ethers.provider.send("evm_increaseTime", [dt]); + + await scoreRegistry + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); + await update_integral(); + const reward = await scoreRegistry.integrateFraction(accounts[1].address); + expect(approx(reward, integral, Constants.ten_to_the_18)).to.be.true; + } + }); + }); + + /** + * test_mining_with_votelock の全体的な流れ: + * + * 1. 2週間と5秒の時間を進める。 + * 2. ゲージとコントローラーをセットアップし、適切なレートを設定する。 + * 3. AliceとBobにトークンを転送し、それぞれのアドレスに関連する承認を設定する。 + * 4. Aliceは投票のエスクローにトークンをロックすることで、BOOSTを取得する。 + * 5. AliceとBobはそれぞれ流動性をデポジットする。 + * 6. Aliceの投票ロックの存在とBobの投票ロックの不在を確認する。 + * 7. 時間をさらに進め、両方のユーザーのチェックポイントを更新する。 + * 8. 4週間後、AliceとBobの投票エスクローのバランスが0であることを確認する。 + * 9. Aliceが投票ロックでTokenを獲得したため、AliceはBobの2.5倍のリワードを獲得することを確認する。 + * 10. さらに時間を進め、両方のユーザーのチェックポイントを更新する。 + * 11. 最終的に、AliceとBobが同じ量のリワードを獲得していることを確認する。 + */ + describe("Mining with Vote Locking", function () { + it("should distribute rewards according to vote lock status", async () => { + // 2週間と5秒時間を進める + await ethers.provider.send("evm_increaseTime", [ + week * 2 + 5, + ]); + + // ゲージをコントローラーに接続して適切なレートなどを設定する + await scoreWeightController.addCurrency( + scoreRegistry.address, + ten_to_the_18 + ); + + // トークンの準備 + await YMT.transfer(accounts[1].address, ten_to_the_20); + await YMT.transfer(accounts[2].address, ten_to_the_20); + + await YMT.connect(accounts[1]).approve(veYMT.address, MAX_UINT256); + await YMT.connect(accounts[2]).approve(veYMT.address, MAX_UINT256); + + // Aliceがescrowにデポジットする。AliceはBOOSTを持っていることになる + let t = await time.latest() + + await veYMT + .connect(accounts[1]) + .createLock(ten_to_the_20, t + (week * 2)); + + // ETHをdeposit + for (let i = 0; i < 3; i++) { + await yamato + .connect(accounts[i]) + .deposit({ value: ethers.utils.parseEther("100") }); + } + // AliceとBobが一部の流動性をデポジットする + await ethers.provider.send("evm_setAutomine", [false]); + await yamato.connect(accounts[1]).borrow(ten_to_the_21); + await yamato.connect(accounts[2]).borrow(ten_to_the_21); + await ethers.provider.send("evm_mine", []); + await ethers.provider.send("evm_setAutomine", [true]); + let now = await time.latest() + + // 現在、Aliceは投票ロックを持っているが、Bobは持っていないことを確認する + expect( + await veYMT["balanceOf(address,uint256)"](accounts[1].address, now) + ).to.not.equal(zero); + expect( + await veYMT["balanceOf(address,uint256)"](accounts[2].address, now) + ).to.equal(zero); + + // 時間を進めてチェックポイントを更新する + now = await time.latest(); + + await ethers.provider.send("evm_setNextBlockTimestamp", [ + now + week * 4, + ]); + + // チェックポイント更新 + await ethers.provider.send("evm_setAutomine", [false]); + await scoreRegistry.connect(accounts[2]).userCheckpoint(accounts[2].address); + await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + await ethers.provider.send("evm_mine", []); + await ethers.provider.send("evm_setAutomine", [true]); + + // 4週間後、balanceOfは0であるべき + now = await time.latest(); + expect( + await veYMT["balanceOf(address,uint256)"](accounts[1].address, now) + ).to.equal(zero); + expect( + await veYMT["balanceOf(address,uint256)"](accounts[2].address, now) + ).to.equal(zero); + + // AliceはTokenを投票ロックしたので、2.5倍のTokenを獲得 + let rewards_alice = await scoreRegistry.integrateFraction( + accounts[1].address + ); + let rewards_bob = await scoreRegistry.integrateFraction(accounts[2].address); + + expect( + rewards_alice.mul(BigNumber.from("10000000000000000")).div(rewards_bob) + ).to.equal(BigNumber.from("25000000000000000")); + + // 時間を進めてチェックポイントを更新: 今は誰もがTokenを投票ロックしていない + now = await time.latest(); + + await ethers.provider.send("evm_setNextBlockTimestamp", [ + now + week * 4, + ]); + + await ethers.provider.send("evm_setAutomine", [false]); + await scoreRegistry.connect(accounts[2]).userCheckpoint(accounts[2].address); + await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + await ethers.provider.send("evm_mine", []); + await ethers.provider.send("evm_setAutomine", [true]); + let old_rewards_alice = rewards_alice; + let old_rewards_bob = rewards_bob; + // 今、AliceはBobと同じ量を獲得した + rewards_alice = await scoreRegistry.integrateFraction(accounts[1].address); + rewards_bob = await scoreRegistry.integrateFraction(accounts[2].address); + + let d_alice = rewards_alice.sub(old_rewards_alice); + let d_bob = rewards_bob.sub(old_rewards_bob); + + expect(d_alice.sub(d_bob)).to.equal(zero); + }); + }); + +}); From 34832cbfe9396fdfbd4ed9a22cfbfbf400cce37b Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 12:46:26 +0900 Subject: [PATCH 098/412] Added Constants for test code. --- test/v15/Constants.ts | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/v15/Constants.ts diff --git a/test/v15/Constants.ts b/test/v15/Constants.ts new file mode 100644 index 00000000..961d8393 --- /dev/null +++ b/test/v15/Constants.ts @@ -0,0 +1,53 @@ +import { BigNumber } from "ethers"; + +class Constants { + static DAY = BigNumber.from(86400); + static WEEK = BigNumber.from(86400 * 7); + static MONTH = BigNumber.from(86400 * 30); + static YEAR = BigNumber.from(86400 * 365); + + static hour = 3600; + static day = 86400; + static week = 86400 * 7; + static month = 86400 * 30; + static year = 86400 * 365; + + static TYPE_WEIGHTS: BigNumber[] = [ + BigNumber.from(5).mul(BigNumber.from(10).pow(17)), + BigNumber.from(2).mul(BigNumber.from(10).pow(18)), + ]; + static GAUGE_WEIGHTS: BigNumber[] = [ + BigNumber.from(2).mul(BigNumber.from(10).pow(18)), + BigNumber.from(10).pow(18), + BigNumber.from(5).mul(BigNumber.from(10).pow(17)), + ]; + + static symbol = "Token"; + static decimal = 18; + static INITIAL_SUPPLY = BigNumber.from("1303030303000000000000000000"); + + static ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + static ten_to_the_24 = BigNumber.from("1000000000000000000000000"); + static ten_to_the_21 = BigNumber.from("1000000000000000000000"); + static ten_to_the_20 = BigNumber.from("100000000000000000000"); + static ten_to_the_19 = BigNumber.from("10000000000000000000"); + static ten_to_the_18 = BigNumber.from("1000000000000000000"); + static ten_to_the_17 = BigNumber.from("100000000000000000"); + static ten_to_the_16 = BigNumber.from("10000000000000000"); + static ten_to_the_9 = BigNumber.from("1000000000"); + static a = BigNumber.from("2"); + static b = BigNumber.from("5"); + static zero = BigNumber.from("0"); + static MAX_UINT256 = BigNumber.from( + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + ); + + static GAUGE_TYPES = [ + BigNumber.from("1"), + BigNumber.from("1"), + BigNumber.from("2"), + ]; +} + +export default Constants; From a5f6d478c4800c0662dd96ba7eaa6593763bab85 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 12:50:22 +0900 Subject: [PATCH 099/412] Refactoring the use of Constants for test code. --- .../integration/FeePool/checkpointTs.test.ts | 8 +- .../FeePool/distributefFeesStateful.test.ts | 33 ++++--- .../v15/integration/Minter/components.test.ts | 14 +-- .../Minter/minterIntegration.test.ts | 18 ++-- .../ScoreRegistry/scoreRegistry.test.ts | 22 ++--- .../integration/YMT/mintIntegration.test.ts | 13 +-- .../YMT/mintableInTimeframe.test.ts | 5 +- .../veYMT/depositWithdrawVoting.test.ts | 11 +-- .../integration/veYMT/votingEscrow.test.ts | 93 ++++++++++--------- .../veYMT/zeroBalanceAtUnlockTime.test.ts | 23 ++--- test/v15/unitary/FeePool/checkpoints.test.ts | 30 +++--- test/v15/unitary/FeePool/claimMany.test.ts | 12 +-- test/v15/unitary/FeePool/feePool.test.ts | 55 +++++------ .../unitary/ScoreRegistry/checkpoint.test.ts | 3 +- test/v15/unitary/ScoreRegistry/kick.test.ts | 9 +- test/v15/unitary/YMT/epochTimeSupply.test.ts | 8 +- test/v15/unitary/YMT/inflationDelay.test.ts | 4 +- test/v15/unitary/YMT/mint.test.ts | 6 +- test/v15/unitary/YmtMinter/Minter.test.ts | 14 +-- 19 files changed, 192 insertions(+), 189 deletions(-) diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index 96951c10..0503388d 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -13,9 +13,9 @@ import { } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; -const DAY = 86400; -const WEEK = DAY * 7; +const week = Constants.week; const MAX_EXAMPLES = 10; describe("FeePoolV2", function () { @@ -79,7 +79,7 @@ describe("FeePoolV2", function () { for (let i = 0; i < MAX_EXAMPLES; i++) { const sleepTime = Math.floor(stSleep[i] * 86400); await ethers.provider.send("evm_increaseTime", [sleepTime]); - const lockTime = (await time.latest()) + sleepTime + WEEK * stLocktime[i]; + const lockTime = (await time.latest()) + sleepTime + week * stLocktime[i]; finalLock = Math.max(finalLock, lockTime); await veYMT @@ -92,7 +92,7 @@ describe("FeePoolV2", function () { while ((await time.latest()) < finalLock) { const weekEpoch = - Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; // WEEK * WEEK; + Math.floor(((await time.latest()) + week) / week) * week; // week * week; await time.increaseTo(weekEpoch); diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index aa709626..77017087 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -13,6 +13,7 @@ import { } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; // 参考) brownie Stateful Tests // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html @@ -20,11 +21,9 @@ import { contractVersion } from "../../../param/version"; const ACCOUNT_NUM = 5; const MAX_EXAMPLES = 50; const STATEFUL_STEP_COUNT = 30; -const WEEK = 86400 * 7; -const YEAR = 86400 * 365; -const two_to_the_256_minus_1 = BigNumber.from("2") - .pow(BigNumber.from("256")) - .sub(BigNumber.from("1")); +const week = Constants.week; +const year = Constants.year; +const MAX_UINT256 = Constants.MAX_UINT256; const MOUNT_DECIMALS = 3; // Helper functions to generate random variables -----> @@ -96,7 +95,7 @@ describe("FeePoolV2", function () { .transfer(accounts[i].address, ethers.utils.parseEther("10000000")); await YMT .connect(accounts[i]) - .approve(veYMT.address, two_to_the_256_minus_1); + .approve(veYMT.address, MAX_UINT256); userClaims[accounts[i].address] = []; } @@ -106,7 +105,7 @@ describe("FeePoolV2", function () { .connect(accounts[0]) .createLock( ethers.utils.parseEther("10000000"), - (await time.latest()) + YEAR * 2 + (await time.latest()) + year * 2 ); lockedUntil = { @@ -116,7 +115,7 @@ describe("FeePoolV2", function () { }; // a week later we deploy the fee feePool - await time.increase(WEEK); + await time.increase(week); feePool = await getProxy( contractVersion["FeePool"], @@ -184,7 +183,7 @@ describe("FeePoolV2", function () { if (!(await _checkActiveLock(stAcct))) { const until = - (Math.floor((await time.latest()) / WEEK) + stWeeks.toNumber()) * WEEK; + (Math.floor((await time.latest()) / week) + stWeeks.toNumber()) * week; await veYMT.connect(stAcct).createLock(stAmount, until); lockedUntil[stAcct.address] = until; } @@ -222,12 +221,12 @@ describe("FeePoolV2", function () { if (await _checkActiveLock(stAcct)) { const until = - (Math.floor((await veYMT.lockedEnd(stAcct.address)).toNumber() / WEEK) + + (Math.floor((await veYMT.lockedEnd(stAcct.address)).toNumber() / week) + stWeeks.toNumber()) * - WEEK; + week; const newUntil = Math.min( until, - Math.floor(((await time.latest()) + YEAR * 4) / WEEK) * WEEK + Math.floor(((await time.latest()) + year * 4) / week) * week ); await veYMT.connect(stAcct).increaseUnlockTime(newUntil); lockedUntil[stAcct.address] = newUntil; @@ -297,7 +296,7 @@ describe("FeePoolV2", function () { // const ue = await feePool.userEpochOf(stAcct.address); // const up = await veYMT.userPointHistory(stAcct.address, ue); // console.log(`Week: - // ${Math.floor(((await time.latest()) - t0) / WEEK)} + // ${Math.floor(((await time.latest()) - t0) / week)} // Point: ${up} // `); @@ -403,7 +402,7 @@ describe("FeePoolV2", function () { // Because tokens for current week are obtained in the next week // And that is by design await feePool.checkpointToken(); - await ethers.provider.send("evm_increaseTime", [WEEK * 2]); + await ethers.provider.send("evm_increaseTime", [week * 2]); await feePool.checkpointToken(); const balanceList = []; for (const acct of accounts) { @@ -412,7 +411,7 @@ describe("FeePoolV2", function () { // const ue = await feePool.userEpochOf(acct.address); // const up = await veYMT.userPointHistory(acct.address, ue); // console.log(`Week: - // ${Math.floor(((await time.latest()) - t0) / WEEK)} + // ${Math.floor(((await time.latest()) - t0) / week)} // Point: ${up} // `); @@ -425,12 +424,12 @@ describe("FeePoolV2", function () { } const t0: number = (await feePool.startTime()).toNumber(); - const t1: number = Math.floor((await time.latest()) / WEEK) * WEEK; + const t1: number = Math.floor((await time.latest()) / week) * week; const tokensPerUserPerWeek = []; for (const acct of accounts) { tokensPerUserPerWeek[acct.address] = []; - for (let w = t0; w < t1 + WEEK; w += WEEK) { + for (let w = t0; w < t1 + week; w += week) { const tokens = (await feePool.tokensPerWeek(w)) .mul(await feePool.veForAt(acct.address, w)) .div(await feePool.veSupply(w)); diff --git a/test/v15/integration/Minter/components.test.ts b/test/v15/integration/Minter/components.test.ts index f0950abc..521765ae 100644 --- a/test/v15/integration/Minter/components.test.ts +++ b/test/v15/integration/Minter/components.test.ts @@ -50,14 +50,14 @@ import { getLinkedProxy, } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; chai.use(smock.matchers); -const ACCOUNT_NUM = 4; const NUMBER_OF_ATTEMPTS = 20; -const SCALE = BigNumber.from((1e20).toString()); -const WEEK = 86400 * 7; -const MONTH = 86400 * 30; +const SCALE = Constants.ten_to_the_20; +const week = Constants.week; +const month = Constants.month; describe("YmtMinter components", function () { let mockFeePool: FakeContract; @@ -251,10 +251,10 @@ describe("YmtMinter components", function () { for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { it(`tests duration ${i}`, async function () { - const stDuration = generateUniqueRandomNumbers(3, WEEK, MONTH); + const stDuration = generateUniqueRandomNumbers(3, week, month); const depositTime: number[] = []; - await time.increase(WEEK); + await time.increase(week); for (let i = 0; i < 3; i++) { await yamato @@ -348,7 +348,7 @@ describe("YmtMinter components", function () { depositTime.push(await time.latest()); } - await time.increase(MONTH); + await time.increase(month); const balances: BigNumber[] = []; for (let i = 0; i < 3; i++) { diff --git a/test/v15/integration/Minter/minterIntegration.test.ts b/test/v15/integration/Minter/minterIntegration.test.ts index d88aa89e..575bfa2b 100644 --- a/test/v15/integration/Minter/minterIntegration.test.ts +++ b/test/v15/integration/Minter/minterIntegration.test.ts @@ -50,11 +50,11 @@ import { getLinkedProxy, } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; -const SCALE = BigNumber.from(10).pow(20); -const WEEK = 86400 * 7; -const MONTH = 86400 * 30; -const W = BigNumber.from(10).pow(18); +const week = Constants.week; +const month = Constants.month; +const ten_to_the_18 = Constants.ten_to_the_18; describe("YmtMinter integration", function () { let accounts: SignerWithAddress[]; @@ -195,7 +195,7 @@ describe("YmtMinter integration", function () { await scoreWeightController .addCurrency( scoreRegistry.address, - W + ten_to_the_18 ); }); @@ -209,7 +209,7 @@ describe("YmtMinter integration", function () { function getICR(pledge, price){ const {coll, debt} = pledge; - const collInCurrency = (coll.mul(price)).div(W); + const collInCurrency = (coll.mul(price)).div(ten_to_the_18); return (BigNumber.from("10000").mul(collInCurrency)).div(debt); } @@ -240,7 +240,7 @@ describe("YmtMinter integration", function () { } // For weights to activate - await time.increase(WEEK); + await time.increase(week); // Bob and Charlie deposit to gauges with different weights for(let i = 0; i < 5; i++){ @@ -249,7 +249,7 @@ describe("YmtMinter integration", function () { .borrow(ethers.utils.parseEther(amount)); } // mockFeed.fetchPrice.returns(PRICE = BigNumber.from(180000).mul(1e18 + "")); - await time.increase(MONTH); + await time.increase(month); const ICRs = []; for(let i = 0; i < 5; i++){ @@ -261,7 +261,7 @@ describe("YmtMinter integration", function () { // Claim for Bob now await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); const bobTokens = await YMT.balanceOf(accounts[1].address); - await time.increase(MONTH); + await time.increase(month); // This won't give anything await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts index a6e427ce..f7bfb64c 100644 --- a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts +++ b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts @@ -54,6 +54,16 @@ import Constants from "../../Constants"; chai.use(smock.matchers); +const ten_to_the_18 = Constants.ten_to_the_18; +const ten_to_the_20 = Constants.ten_to_the_20; +const ten_to_the_21 = Constants.ten_to_the_21; +const MAX_UINT256 = Constants.MAX_UINT256; +const SCALE = ten_to_the_20; +const zero = Constants.zero; +const week = Constants.week; +const YEAR = Constants.year; +const MIN_AMOUNT = 10000; + describe("ScoreRegistry", function () { let mockFeePool: FakeContract; let mockFeed: FakeContract; @@ -76,18 +86,6 @@ describe("ScoreRegistry", function () { let PRICE: BigNumber; let accounts: SignerWithAddress[]; let snapshot: SnapshotRestorer; - const ten_to_the_18 = Constants.ten_to_the_18; - const ten_to_the_20 = Constants.ten_to_the_20; - const ten_to_the_21 = Constants.ten_to_the_21; - const MAX_UINT256 = Constants.MAX_UINT256; - const zero = Constants.zero; - const week = Constants.week; - const WEEK = Constants.WEEK; - - const DAY = 86400; - const YEAR = DAY * 365; - const MIN_AMOUNT = 10000; - const SCALE = BigNumber.from((1e20).toString()); beforeEach(async () => { accounts = await ethers.getSigners(); diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index c7160722..9d6beaf1 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -7,6 +7,9 @@ import { import { Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { BigNumber } from "ethers"; +import Constants from "../../Constants"; + +const year = Constants.year; // Assuming you have a helper function to increase blockchain time async function increaseTime(duration: number) { @@ -14,8 +17,6 @@ async function increaseTime(duration: number) { await ethers.provider.send("evm_mine", []); } -const YEAR = 365 * 24 * 60 * 60; // seconds in a year - describe("YMT", function () { let accounts: SignerWithAddress[]; let token: Contract; @@ -40,7 +41,7 @@ describe("YMT", function () { describe("YMT MintIntegration", function () { it("should mint the correct amount", async function () { - const duration = YEAR; // Replace with dynamic value as needed + const duration = year; // Replace with dynamic value as needed await token.setMinter(accounts[0].address); const creationTime = await token.startEpochTime(); const initialSupply = await token.totalSupply(); @@ -59,7 +60,7 @@ describe("YMT", function () { }); it("should revert on overmint", async function () { - const duration = YEAR; // Replace with dynamic value as needed + const duration = year; // Replace with dynamic value as needed await token.setMinter(accounts[0].address); const creationTime = await token.startEpochTime(); const rate = await token.rate(); @@ -81,14 +82,14 @@ describe("YMT", function () { let balance = BigNumber.from(0); let epochStart = await token.startEpochTime(); - const durations = [YEAR * 0.33, YEAR * 0.5, YEAR * 0.7]; // Replace with dynamic values as needed + const durations = [year * 0.33, year * 0.5, year * 0.7]; // Replace with dynamic values as needed for (const time of durations) { await increaseTime(time); if ( (await ethers.provider.getBlock("latest")).timestamp - epochStart > - YEAR + year ) { await token.updateMiningParameters(); epochStart = await token.startEpochTime(); diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index 97c6d292..111f3e7c 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -8,13 +8,15 @@ import { import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import Constants from "../../Constants"; +const year = Constants.year; +const YEAR = Constants.YEAR; + async function increaseTime(duration: BigNumber) { await ethers.provider.send("evm_increaseTime", [duration.toNumber()]); await ethers.provider.send("evm_mine", []); } // Constants -const YEAR = Constants.YEAR; const INITIAL_RATE = BigNumber.from(55000000); const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)) .div(YEAR) @@ -25,7 +27,6 @@ describe("YMT", function () { let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; - const year = Constants.year; before(async function () { accounts = await ethers.getSigners(); diff --git a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts index 471dbf1c..27019149 100644 --- a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts +++ b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts @@ -7,15 +7,14 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import Constants from "../../Constants"; const ACCOUNT_NUM = 10; const MAX_EXAMPLES = 30; // テストの試行回数 const STATEFUL_STEP_COUNT = 20; // ruleの実行回数 -const YEAR = BigNumber.from(86400 * 365); -const WEEK = BigNumber.from(86400 * 7); -const two_to_the_256_minus_1 = BigNumber.from("2") - .pow(BigNumber.from("256")) - .sub(BigNumber.from("1")); +const YEAR = Constants.YEAR; +const WEEK = Constants.WEEK; +const NAX_UINT256 = Constants.MAX_UINT256; const ten_to_the_40 = BigNumber.from( "10000000000000000000000000000000000000000" ); @@ -54,7 +53,7 @@ describe("veYMT", function () { ._mintForTesting(accounts[i].address, ten_to_the_40); await token .connect(accounts[i]) - .approve(veYMT.address, two_to_the_256_minus_1); + .approve(veYMT.address, NAX_UINT256); } //setup diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v15/integration/veYMT/votingEscrow.test.ts index fb037a06..edde5608 100644 --- a/test/v15/integration/veYMT/votingEscrow.test.ts +++ b/test/v15/integration/veYMT/votingEscrow.test.ts @@ -7,6 +7,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import Constants from "../../Constants"; type Stage = { blockNumber: number; @@ -14,12 +15,12 @@ type Stage = { bias?: string; }; -const H = 3600; -const DAY = 86400; -const WEEK = 7 * DAY; -const MAXTIME = DAY * 365 * 4; // 126144000 +const hour = Constants.hour; +const day = Constants.day; +const week = Constants.week; +const MAXTIME = Constants.year * 4; // 126144000 const SCALE = 1e20; -const TOL = (120 / WEEK) * SCALE; +const TOL = (120 / week) * SCALE; describe("Voting Powers Test", function () { // Test voting power in the following scenario. @@ -89,20 +90,20 @@ describe("Voting Powers Test", function () { expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); const timeToNextWeek = - (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - + (Math.floor((await time.latest()) / week) + 1) * week - (await time.latest()); // Move to timing which is good for testing - beginning of a UTC week await time.increase(timeToNextWeek); - await ethers.provider.send("evm_increaseTime", [H]); + await ethers.provider.send("evm_increaseTime", [hour]); stages["before_deposits"] = { blockNumber: await time.latestBlock(), timestamp: await time.latest(), }; - await veYMT.connect(alice).createLock(amount, (await time.latest()) + WEEK); + await veYMT.connect(alice).createLock(amount, (await time.latest()) + week); stages["alice_deposit"] = { blockNumber: await time.latestBlock(), @@ -111,14 +112,14 @@ describe("Voting Powers Test", function () { // console.log("alice_deposit", stages["alice_deposit"]); - await time.increase(H); + await time.increase(hour); const totalSupply = await veYMT["totalSupply()"](); const aliceBalance = await veYMT["balanceOf(address)"](alice.address); - expect(approx(totalSupply, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to + expect(approx(totalSupply, amount.div(MAXTIME).mul(week - 2 * hour), TOL)).to .be.true; - expect(approx(aliceBalance, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to + expect(approx(aliceBalance, amount.div(MAXTIME).mul(week - 2 * hour), TOL)).to .be.true; expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); @@ -135,7 +136,7 @@ describe("Voting Powers Test", function () { // Simulating the passage of time with 7 days and 24 hours per day for (let i = 0; i < 7; i++) { for (let _ = 0; _ < 24; _++) { - await time.increase(H); + await time.increase(hour); } const dt = (await time.latest()) - t0; @@ -145,14 +146,14 @@ describe("Voting Powers Test", function () { expect( approx( totalSupply, - amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), + amount.div(MAXTIME).mul(Math.max(week - 2 * hour - dt, 0)), TOL ) ).to.be.true; expect( approx( aliceBalance, - amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), + amount.div(MAXTIME).mul(Math.max(week - 2 * hour - dt, 0)), TOL ) ).to.be.true; @@ -165,7 +166,7 @@ describe("Voting Powers Test", function () { }); } // console.log("alice_in_0", stages["alice_in_0"]); - await ethers.provider.send("evm_increaseTime", [H]); + await ethers.provider.send("evm_increaseTime", [hour]); expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); await veYMT.connect(alice).withdraw(); @@ -180,18 +181,18 @@ describe("Voting Powers Test", function () { expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); - await time.increase(H); + await time.increase(hour); // Calculate the next week for round counting const nextWeek = - (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - + (Math.floor((await time.latest()) / week) + 1) * week - (await time.latest()); await time.increase(nextWeek); await veYMT .connect(alice) - .createLock(amount, (await time.latest()) + 2 * WEEK); + .createLock(amount, (await time.latest()) + 2 * week); stages["alice_deposit_2"] = { blockNumber: await time.latestBlock(), @@ -203,20 +204,20 @@ describe("Voting Powers Test", function () { expect( approx( await veYMT["totalSupply()"](), - amount.div(MAXTIME).mul(2 * WEEK), + amount.div(MAXTIME).mul(2 * week), TOL ) ).to.be.true; expect( approx( await veYMT["balanceOf(address)"](alice.address), - amount.div(MAXTIME).mul(2 * WEEK), + amount.div(MAXTIME).mul(2 * week), TOL ) ).to.be.true; expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); - await veYMT.connect(bob).createLock(amount, (await time.latest()) + WEEK); + await veYMT.connect(bob).createLock(amount, (await time.latest()) + week); stages["bob_deposit_2"] = { blockNumber: await time.latestBlock(), @@ -228,34 +229,34 @@ describe("Voting Powers Test", function () { expect( approx( await veYMT["totalSupply()"](), - amount.div(MAXTIME).mul(3 * WEEK), + amount.div(MAXTIME).mul(3 * week), TOL ) ).to.be.true; expect( approx( await veYMT["balanceOf(address)"](alice.address), - amount.div(MAXTIME).mul(2 * WEEK), + amount.div(MAXTIME).mul(2 * week), TOL ) ).to.be.true; expect( approx( await veYMT["balanceOf(address)"](bob.address), - amount.div(MAXTIME).mul(WEEK), + amount.div(MAXTIME).mul(week), TOL ) ).to.be.true; t0 = await time.latest(); - await time.increase(H); + await time.increase(hour); stages["alice_bob_in_2"] = []; // Beginning of week: weight 3 // End of week: weight 1 for (let i = 0; i < 7; i++) { for (let _ = 0; _ < 24; _++) { - await time.increase(H); + await time.increase(hour); } const dt = (await time.latest()) - t0; wTotal = await veYMT["totalSupply()"](); @@ -263,9 +264,9 @@ describe("Voting Powers Test", function () { wBob = await veYMT["balanceOf(address)"](bob.address); expect(wTotal).to.equal(wAlice.add(wBob)); expect( - approx(wAlice, amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), TOL) + approx(wAlice, amount.div(MAXTIME).mul(Math.max(2 * week - dt, 0)), TOL) ).to.be.true; - expect(approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), TOL)) + expect(approx(wBob, amount.div(MAXTIME).mul(Math.max(week - dt, 0)), TOL)) .to.be.true; stages["alice_bob_in_2"].push({ @@ -276,7 +277,7 @@ describe("Voting Powers Test", function () { } // console.log("alice_bob_in_2", stages["alice_bob_in_2"]); - await time.increase(H); + await time.increase(hour); await veYMT.connect(bob).withdraw(); t0 = await time.latest(); @@ -287,16 +288,16 @@ describe("Voting Powers Test", function () { const wTotal1 = await veYMT["totalSupply()"](); const wAlice1 = await veYMT["balanceOf(address)"](alice.address); expect(wAlice1).to.equal(wTotal1); - expect(approx(wTotal1, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be + expect(approx(wTotal1, amount.div(MAXTIME).mul(week - 2 * hour), TOL)).to.be .true; expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); - await time.increase(H); + await time.increase(hour); stages["alice_in_2"] = []; for (let i = 0; i < 7; i++) { for (let _ = 0; _ < 24; _++) { - await time.increase(H); + await time.increase(hour); } const dt = (await time.latest()) - t0; wTotal = await veYMT["totalSupply()"](); @@ -305,7 +306,7 @@ describe("Voting Powers Test", function () { expect( approx( wTotal, - amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), + amount.div(MAXTIME).mul(Math.max(week - dt - 2 * hour, 0)), TOL ) ).to.be.true; @@ -323,7 +324,7 @@ describe("Voting Powers Test", function () { timestamp: await time.latest(), }; - await time.increase(H); + await time.increase(hour); await veYMT.connect(bob).withdraw(); stages["bob_withdraw_2"] = { @@ -358,7 +359,7 @@ describe("Voting Powers Test", function () { stages["alice_deposit"].blockNumber ); - expect(approx(wAlice, amount.div(MAXTIME).mul(WEEK - H), TOL)).to.be.true; + expect(approx(wAlice, amount.div(MAXTIME).mul(week - hour), TOL)).to.be.true; expect( await veYMT.balanceOfAt(bob.address, stages["alice_deposit"].blockNumber) ).to.equal(0); @@ -372,8 +373,8 @@ describe("Voting Powers Test", function () { wTotal = await veYMT.totalSupplyAt(block); expect(wBob).to.equal(0); expect(wAlice).to.equal(wTotal); - const timeLeft = Math.floor((WEEK * (7 - i)) / 7) - 2 * H; - const error1h = (H / timeLeft) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + const timeLeft = Math.floor((week * (7 - i)) / 7) - 2 * hour; + const error1h = (hour / timeLeft) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks expect(approx(wAlice, amount.div(MAXTIME).mul(timeLeft), error1h)).to.be .true; } @@ -396,7 +397,7 @@ describe("Voting Powers Test", function () { alice.address, stages["alice_deposit_2"].blockNumber ); - expect(approx(wTotal, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; + expect(approx(wTotal, amount.div(MAXTIME).mul(2 * week), TOL)).to.be.true; expect(wTotal).to.equal(wAlice); expect( await veYMT.balanceOfAt( @@ -415,8 +416,8 @@ describe("Voting Powers Test", function () { stages["bob_deposit_2"].blockNumber ); expect(wTotal).to.equal(wAlice.add(wBob)); - expect(approx(wTotal, amount.div(MAXTIME).mul(3 * WEEK), TOL)).to.be.true; - expect(approx(wAlice, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; + expect(approx(wTotal, amount.div(MAXTIME).mul(3 * week), TOL)).to.be.true; + expect(approx(wAlice, amount.div(MAXTIME).mul(2 * week), TOL)).to.be.true; t0 = stages["bob_deposit_2"].timestamp; for (let i = 0; i < stages["alice_bob_in_2"].length; i++) { @@ -426,16 +427,16 @@ describe("Voting Powers Test", function () { wTotal = await veYMT.totalSupplyAt(block); expect(wTotal).to.equal(wAlice.add(wBob)); const dt = stages["alice_bob_in_2"][i].timestamp - t0; - const error1h = (H / (2 * WEEK - i * DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + const error1h = (hour / (2 * week - i * day)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks expect( approx( wAlice, - amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), + amount.div(MAXTIME).mul(Math.max(2 * week - dt, 0)), error1h ) ).to.be.true; expect( - approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), error1h) + approx(wBob, amount.div(MAXTIME).mul(Math.max(week - dt, 0)), error1h) ).to.be.true; } @@ -449,7 +450,7 @@ describe("Voting Powers Test", function () { stages["bob_withdraw_1"].blockNumber ); expect(wTotal).to.equal(wAlice); - expect(approx(wTotal, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be + expect(approx(wTotal, amount.div(MAXTIME).mul(week - 2 * hour), TOL)).to.be .true; expect(wBob).to.equal(0); @@ -462,12 +463,12 @@ describe("Voting Powers Test", function () { expect(wTotal).to.equal(wAlice); expect(wBob).to.equal(0); const dt = stages["alice_in_2"][i].timestamp - t0; - const error1h = (H / (WEEK - i * DAY + DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks + const error1h = (hour / (week - i * day + day)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks expect( approx( wTotal, - amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), + amount.div(MAXTIME).mul(Math.max(week - dt - 2 * hour, 0)), error1h ) ).to.be.true; diff --git a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts index e8d85577..dc38a38b 100644 --- a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts +++ b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts @@ -7,8 +7,9 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import Constants from "../../Constants"; -const WEEK = 86400 * 7; +const week = Constants.week; describe("Voting Escrow tests", function () { let accounts: SignerWithAddress[]; @@ -42,12 +43,12 @@ describe("Voting Escrow tests", function () { }); for ( - let st_initial = WEEK * 2; - st_initial <= WEEK * 52; - st_initial += WEEK * 2 + let st_initial = week * 2; + st_initial <= week * 52; + st_initial += week * 2 ) { it(`create lock with zero balance: Lock ${Math.floor( - st_initial / WEEK + st_initial / week )} Week`, async () => { const expectedUnlock = (await time.latest()) + st_initial; await veYMT @@ -71,14 +72,14 @@ describe("Voting Escrow tests", function () { } for ( - let st_initial = WEEK * 2; - st_initial <= WEEK * 52; - st_initial += WEEK * 2 + let st_initial = week * 2; + st_initial <= week * 52; + st_initial += week * 2 ) { - for (let st_extend = WEEK; st_extend <= WEEK * 2; st_extend += WEEK) { + for (let st_extend = week; st_extend <= week * 2; st_extend += week) { it(`increase unlock with zero balance: Lock ${Math.floor( - st_initial / WEEK - )} Week, extend: ${Math.floor(st_extend / WEEK)} Week`, async () => { + st_initial / week + )} Week, extend: ${Math.floor(st_extend / week)} Week`, async () => { await veYMT .connect(accounts[0]) .createLock( diff --git a/test/v15/unitary/FeePool/checkpoints.test.ts b/test/v15/unitary/FeePool/checkpoints.test.ts index e30e6cac..04b85c27 100644 --- a/test/v15/unitary/FeePool/checkpoints.test.ts +++ b/test/v15/unitary/FeePool/checkpoints.test.ts @@ -13,10 +13,10 @@ import { } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; -const DAY = 86400; -const WEEK = DAY * 7; -const YEAR = DAY * 365; +const week = Constants.week; +const year = Constants.year; describe("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; @@ -62,13 +62,13 @@ describe("FeePoolV2", () => { await token.approve(veYMT.address, ethers.constants.MaxUint256); await veYMT.createLock( ethers.utils.parseEther("1000"), - (await time.latest()) + WEEK * 52 + (await time.latest()) + week * 52 ); }); it("test_checkpoint_total_supply", async function () { const startTime = await feePool.timeCursor(); const weekEpoch = - Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; + Math.floor(((await time.latest()) + week) / week) * week; await time.increaseTo(weekEpoch); @@ -84,19 +84,19 @@ describe("FeePoolV2", () => { it("test_advance_time_cursor", async function () { const startTime = (await feePool.timeCursor()).toNumber(); - await time.increase(YEAR); + await time.increase(year); await feePool.checkpointTotalSupply(); const newTimeCursor = (await feePool.timeCursor()).toNumber(); - expect(newTimeCursor).to.equal(startTime + WEEK * 20); - expect(await feePool.veSupply(startTime + WEEK * 19)).to.be.above(0); - expect(await feePool.veSupply(startTime + WEEK * 20)).to.equal(0); + expect(newTimeCursor).to.equal(startTime + week * 20); + expect(await feePool.veSupply(startTime + week * 19)).to.be.above(0); + expect(await feePool.veSupply(startTime + week * 20)).to.equal(0); await feePool.checkpointTotalSupply(); - expect(await feePool.timeCursor()).to.equal(startTime + WEEK * 40); - expect(await feePool.veSupply(startTime + WEEK * 20)).to.be.above(0); - expect(await feePool.veSupply(startTime + WEEK * 39)).to.be.above(0); - expect(await feePool.veSupply(startTime + WEEK * 40)).to.equal(0); + expect(await feePool.timeCursor()).to.equal(startTime + week * 40); + expect(await feePool.veSupply(startTime + week * 20)).to.be.above(0); + expect(await feePool.veSupply(startTime + week * 39)).to.be.above(0); + expect(await feePool.veSupply(startTime + week * 40)).to.equal(0); }); it("test_claim_checkpoints_total_supply", async function () { @@ -105,14 +105,14 @@ describe("FeePoolV2", () => { await feePool.connect(alice)["claim()"](); expect((await feePool.timeCursor()).toNumber()).to.equal( - start_time + WEEK + start_time + week ); }); it("test_toggle_allow_checkpoint", async function () { const lastTokenTime = (await feePool.lastTokenTime()).toNumber(); - await time.increase(WEEK); + await time.increase(week); await feePool.connect(alice)["claim()"](); expect((await feePool.lastTokenTime()).toNumber()).to.equal( diff --git a/test/v15/unitary/FeePool/claimMany.test.ts b/test/v15/unitary/FeePool/claimMany.test.ts index 3bde8323..551d3123 100644 --- a/test/v15/unitary/FeePool/claimMany.test.ts +++ b/test/v15/unitary/FeePool/claimMany.test.ts @@ -13,9 +13,9 @@ import { } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; -const DAY = 86400; -const WEEK = DAY * 7; +const week = Constants.week; describe("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; @@ -48,9 +48,9 @@ describe("FeePoolV2", () => { await YMT.connect(alice).transfer(acct.address, amount); await veYMT .connect(acct) - .createLock(amount, (await time.latest()) + 8 * WEEK); + .createLock(amount, (await time.latest()) + 8 * week); } - await time.increase(WEEK); + await time.increase(week); await alice.sendTransaction({ to: feePool.address, value: ethers.utils.parseEther("10"), @@ -73,7 +73,7 @@ describe("FeePoolV2", () => { describe("test_claim_many", () => { it("test_claim_many", async function () { await feePool.checkpointToken(); - await time.increase(WEEK); + await time.increase(week); await feePool.checkpointToken(); const snapshot = await takeSnapshot(); @@ -106,7 +106,7 @@ describe("FeePoolV2", () => { }); it("test_claim_many_same_account", async function () { await feePool.checkpointToken(); - await time.increase(WEEK); + await time.increase(week); await feePool.checkpointToken(); const expected = await feePool.connect(alice).callStatic["claim()"](); diff --git a/test/v15/unitary/FeePool/feePool.test.ts b/test/v15/unitary/FeePool/feePool.test.ts index 4d1e13c8..4ed6de87 100644 --- a/test/v15/unitary/FeePool/feePool.test.ts +++ b/test/v15/unitary/FeePool/feePool.test.ts @@ -13,9 +13,10 @@ import { } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import Constants from "../../Constants"; -const DAY = 86400; -const WEEK = DAY * 7; +const day = Constants.day; +const week = Constants.week; describe("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; @@ -70,15 +71,15 @@ describe("FeePoolV2", () => { }); await feePool.checkpointToken(); await feePool.checkpointTotalSupply(); - await time.increase(DAY); + await time.increase(day); } } - await time.increase(WEEK); + await time.increase(week); await veYMT .connect(alice) - .createLock(amount, (await time.latest()) + 3 * WEEK); - await time.increase(2 * WEEK); + .createLock(amount, (await time.latest()) + 3 * week); + await time.increase(2 * week); await feePool.connect(alice)["claim()"](); @@ -92,11 +93,11 @@ describe("FeePoolV2", () => { const amount = ethers.utils.parseEther("1000"); await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); - await time.increase(WEEK); + await time.increase(week); await veYMT .connect(alice) - .createLock(amount, (await time.latest()) + 8 * WEEK); - await time.increase(WEEK); + .createLock(amount, (await time.latest()) + 8 * week); + await time.increase(week); feePool = await getProxy( contractVersion["FeePool"], @@ -112,11 +113,11 @@ describe("FeePoolV2", () => { }); await feePool.checkpointToken(); await feePool.checkpointTotalSupply(); - await time.increase(DAY); + await time.increase(day); } } - await time.increase(WEEK); + await time.increase(week); await feePool.checkpointToken(); const balanceAlice = await ethers.provider.getBalance(alice.address); const tx = await feePool.connect(alice)["claim()"](); @@ -137,10 +138,10 @@ describe("FeePoolV2", () => { await veYMT .connect(alice) - .createLock(amount, (await time.latest()) + 8 * WEEK); - await time.increase(WEEK); + .createLock(amount, (await time.latest()) + 8 * week); + await time.increase(week); const startTime = await time.latest(); - await time.increase(WEEK * 5); + await time.increase(week * 5); feePool = await getProxy( contractVersion["FeePool"], @@ -154,7 +155,7 @@ describe("FeePoolV2", () => { }); await feePool.checkpointToken(); - await time.increase(WEEK); + await time.increase(week); await feePool.checkpointToken(); let balanceAlice = await ethers.provider.getBalance(alice.address); const tx = await feePool.connect(alice)["claim()"](); @@ -170,21 +171,21 @@ describe("FeePoolV2", () => { await YMT.approve(veYMT.address, amount.mul(10)); const currentTimestamp = await time.latest(); - await veYMT.createLock(amount, currentTimestamp + 4 * WEEK); + await veYMT.createLock(amount, currentTimestamp + 4 * week); - await time.increase(WEEK); + await time.increase(week); const startTime = await time.latest(); - await time.increase(3 * WEEK); + await time.increase(3 * week); await veYMT.connect(alice).withdraw(); - const excludeTime = Math.floor((await time.latest()) / WEEK) * WEEK; + const excludeTime = Math.floor((await time.latest()) / week) * week; await veYMT .connect(alice) - .createLock(amount, (await time.latest()) + 4 * WEEK); + .createLock(amount, (await time.latest()) + 4 * week); - await time.increase(2 * WEEK); + await time.increase(2 * week); feePool = await getProxy( contractVersion["FeePool"], @@ -199,7 +200,7 @@ describe("FeePoolV2", () => { await feePool.checkpointToken(); - await time.increase(WEEK); + await time.increase(week); await feePool.checkpointToken(); @@ -225,14 +226,14 @@ describe("FeePoolV2", () => { const currentTimestamp = await time.latest(); await veYMT .connect(alice) - .createLock(amount, currentTimestamp + 8 * WEEK); - await veYMT.connect(bob).createLock(amount, currentTimestamp + 8 * WEEK); + .createLock(amount, currentTimestamp + 8 * week); + await veYMT.connect(bob).createLock(amount, currentTimestamp + 8 * week); - await time.increase(WEEK); + await time.increase(week); const startTime = await time.latest(); - await time.increase(5 * WEEK); + await time.increase(5 * week); feePool = await getProxy( contractVersion["FeePool"], @@ -247,7 +248,7 @@ describe("FeePoolV2", () => { await feePool.checkpointToken(); - await time.increase(WEEK); + await time.increase(week); await feePool.checkpointToken(); diff --git a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts index 47117743..32de2c42 100644 --- a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts +++ b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts @@ -54,6 +54,8 @@ import Constants from "../../Constants"; chai.use(smock.matchers); +const year = Constants.year; + describe("ScoreRegistry checkpoint", function () { let mockFeePool: FakeContract; let mockFeed: FakeContract; @@ -76,7 +78,6 @@ describe("ScoreRegistry checkpoint", function () { let PRICE: BigNumber; let accounts: SignerWithAddress[]; let snapshot: SnapshotRestorer; - const year = Constants.year; before(async function () { accounts = await ethers.getSigners(); diff --git a/test/v15/unitary/ScoreRegistry/kick.test.ts b/test/v15/unitary/ScoreRegistry/kick.test.ts index de98e7a3..3d8407dc 100644 --- a/test/v15/unitary/ScoreRegistry/kick.test.ts +++ b/test/v15/unitary/ScoreRegistry/kick.test.ts @@ -54,6 +54,11 @@ import Constants from "../../Constants"; chai.use(smock.matchers); +const MAX_UINT256 = Constants.MAX_UINT256; +const week = Constants.week; +const DEPOSIT_AMOUNT = Constants.ten_to_the_21; // 10^21 +const LOCK_AMOUNT = Constants.ten_to_the_20; // 10^20 + describe("ScoreRegistry kick", function () { let mockFeePool: FakeContract; let mockFeed: FakeContract; @@ -76,10 +81,6 @@ describe("ScoreRegistry kick", function () { let PRICE: BigNumber; let accounts: SignerWithAddress[]; let snapshot: SnapshotRestorer; - const MAX_UINT256 = Constants.MAX_UINT256; - const week = Constants.week; - const DEPOSIT_AMOUNT = Constants.ten_to_the_21; // 10^21 - const LOCK_AMOUNT = Constants.ten_to_the_20; // 10^20 before(async function () { accounts = await ethers.getSigners(); diff --git a/test/v15/unitary/YMT/epochTimeSupply.test.ts b/test/v15/unitary/YMT/epochTimeSupply.test.ts index 7723c686..a920b51a 100644 --- a/test/v15/unitary/YMT/epochTimeSupply.test.ts +++ b/test/v15/unitary/YMT/epochTimeSupply.test.ts @@ -8,15 +8,15 @@ import { BigNumber, Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import Constants from "../../Constants"; +const week = Constants.week; +const year = Constants.year; +const YEAR = Constants.YEAR; + describe("YMT", function () { let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; - const week = Constants.week; - const YEAR = Constants.YEAR; - const year = Constants.year; - before(async function () { accounts = await ethers.getSigners(); const Token = await ethers.getContractFactory("YMT"); diff --git a/test/v15/unitary/YMT/inflationDelay.test.ts b/test/v15/unitary/YMT/inflationDelay.test.ts index 8977e4f7..f7091f75 100644 --- a/test/v15/unitary/YMT/inflationDelay.test.ts +++ b/test/v15/unitary/YMT/inflationDelay.test.ts @@ -7,12 +7,12 @@ import { import { Contract } from "ethers"; import Constants from "../../Constants"; +const YEAR = Constants.YEAR; + describe("YMT", function () { let token: Contract; let snapshot: SnapshotRestorer; - const YEAR = Constants.YEAR; - before(async function () { const Token = await ethers.getContractFactory("YMT"); token = await Token.deploy(); diff --git a/test/v15/unitary/YMT/mint.test.ts b/test/v15/unitary/YMT/mint.test.ts index fe5badd7..317fb71d 100644 --- a/test/v15/unitary/YMT/mint.test.ts +++ b/test/v15/unitary/YMT/mint.test.ts @@ -8,14 +8,14 @@ import { BigNumber, Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import Constants from "../../Constants"; +const week = Constants.week; +const ZERO_ADDRESS = Constants.ZERO_ADDRESS; + describe("YMT", function () { let accounts: SignerWithAddress[]; let token: Contract; let snapshot: SnapshotRestorer; - const week = Constants.week; - const ZERO_ADDRESS = Constants.ZERO_ADDRESS; - before(async function () { accounts = await ethers.getSigners(); const Token = await ethers.getContractFactory("YMT"); diff --git a/test/v15/unitary/YmtMinter/Minter.test.ts b/test/v15/unitary/YmtMinter/Minter.test.ts index 3f4c4c6d..174c059b 100644 --- a/test/v15/unitary/YmtMinter/Minter.test.ts +++ b/test/v15/unitary/YmtMinter/Minter.test.ts @@ -54,6 +54,13 @@ import Constants from "../../Constants"; chai.use(smock.matchers); +const ten_to_the_18 = Constants.ten_to_the_18; +const ten_to_the_17 = Constants.ten_to_the_17; +const zero = Constants.zero; +const WEEK = Constants.WEEK; +const month = Constants.month; +const week = Constants.week; + describe("YmtMinter", function () { let mockFeePool: FakeContract; let mockFeed: FakeContract; @@ -78,13 +85,6 @@ describe("YmtMinter", function () { let ownerAddress: string; let snapshot: SnapshotRestorer; - const ten_to_the_18 = Constants.ten_to_the_18; - const ten_to_the_17 = Constants.ten_to_the_17; - const zero = Constants.zero; - const WEEK = Constants.WEEK; - const month = Constants.month; - const week = Constants.week; - before(async function () { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); From a5371381851e56f6ed8f6d5ae2caf24658783fc3 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 14:43:30 +0900 Subject: [PATCH 100/412] Refactoring the feePool integration test code. --- .../integration/FeePool/checkpointTs.test.ts | 29 +++++++------- .../FeePool/distributefFeesStateful.test.ts | 38 ++++++++++--------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index 0503388d..9e8cf81c 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -10,12 +10,15 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { FeePool, FeePool__factory, + YMT__factory, + VeYMT__factory, } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; const week = Constants.week; +const MAX_UINT256 = Constants.MAX_UINT256; const MAX_EXAMPLES = 10; describe("FeePoolV2", function () { @@ -25,18 +28,14 @@ describe("FeePoolV2", function () { let feePool: Contract; let snapshot: SnapshotRestorer; - beforeEach(async function () { - snapshot = await takeSnapshot(); + before(async function () { accounts = await ethers.getSigners(); - const ymt = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); - - YMT = await ymt.deploy(); - await YMT.deployed(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - veYMT = await VeYMT.deploy(YMT.address); - await veYMT.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); feePool = await getProxy( contractVersion["FeePool"], @@ -45,6 +44,10 @@ describe("FeePoolV2", function () { await feePool.setVeYMT(veYMT.address); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + afterEach(async () => { await snapshot.restore(); }); @@ -69,7 +72,7 @@ describe("FeePoolV2", function () { for (let i = 0; i < MAX_EXAMPLES; i++) { await YMT .connect(accounts[i]) - .approve(veYMT.address, ethers.constants.MaxUint256); + .approve(veYMT.address, MAX_UINT256); await YMT .connect(accounts[0]) .transfer(await accounts[i].address, ethers.utils.parseEther("1000")); @@ -78,7 +81,7 @@ describe("FeePoolV2", function () { let finalLock = 0; for (let i = 0; i < MAX_EXAMPLES; i++) { const sleepTime = Math.floor(stSleep[i] * 86400); - await ethers.provider.send("evm_increaseTime", [sleepTime]); + await time.increase(sleepTime); const lockTime = (await time.latest()) + sleepTime + week * stLocktime[i]; finalLock = Math.max(finalLock, lockTime); @@ -98,8 +101,6 @@ describe("FeePoolV2", function () { const weekBlock = await time.latestBlock(); - await ethers.provider.send("evm_increaseTime", [1]); - // Max: 42 weeks // doing checkpoint 3 times is enough for (let i = 0; i < 3; i++) { @@ -108,7 +109,7 @@ describe("FeePoolV2", function () { const expected = await veYMT.totalSupplyAt(weekBlock); const actual = await feePool.veSupply(weekEpoch); - console.log(`expected: ${expected} actual: ${actual}`); + // console.log(`expected: ${expected} actual: ${actual}`); expect(actual).to.equal(expected); } }); diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 77017087..2425d337 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -10,6 +10,8 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { FeePool, FeePool__factory, + YMT__factory, + VeYMT__factory, } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; @@ -57,7 +59,7 @@ function getRandomsTime(): BigNumber { } // ------------------------------------------------ -describe("FeePoolV2", function () { +describe.only("FeePoolV2", function () { let accounts: SignerWithAddress[]; let veYMT: Contract; let feePool: Contract; @@ -66,12 +68,11 @@ describe("FeePoolV2", function () { let lockedUntil: { [key: string]: number } = {}; let fees: { [key: number]: BigNumber } = {}; // timestamp -> amount let userClaims: { [key: string]: { [key: number]: BigNumber[] } } = {}; // address -> timestamp -> [claimed, timeCursor] - let totalFees: BigNumber = ethers.utils.parseEther("1"); + let totalFees: BigNumber; let snapshot: SnapshotRestorer; - beforeEach(async () => { - snapshot = await takeSnapshot(); + before(async () => { accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); lockedUntil = {}; @@ -79,14 +80,11 @@ describe("FeePoolV2", function () { userClaims = {}; totalFees = ethers.utils.parseEther("1"); - const ymt = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); - - YMT = await ymt.deploy(); - await YMT.deployed(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - veYMT = await VeYMT.deploy(YMT.address); - await veYMT.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); for (let i = 0; i < ACCOUNT_NUM; i++) { // ensure accounts[:5] all have YMTs that may be locked @@ -108,12 +106,6 @@ describe("FeePoolV2", function () { (await time.latest()) + year * 2 ); - lockedUntil = { - [accounts[0].address]: ( - await veYMT.lockedEnd(accounts[0].address) - ).toNumber(), - }; - // a week later we deploy the fee feePool await time.increase(week); @@ -124,6 +116,16 @@ describe("FeePoolV2", function () { await feePool.setVeYMT(veYMT.address); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + + lockedUntil = { + [accounts[0].address]: ( + await veYMT.lockedEnd(accounts[0].address) + ).toNumber(), + }; + }); + afterEach(async () => { await snapshot.restore(); }); @@ -402,7 +404,7 @@ describe("FeePoolV2", function () { // Because tokens for current week are obtained in the next week // And that is by design await feePool.checkpointToken(); - await ethers.provider.send("evm_increaseTime", [week * 2]); + await time.increase(week * 2); await feePool.checkpointToken(); const balanceList = []; for (const acct of accounts) { From d57b4fbe2b796b7658d1f6f8c3d1a5aad5cc063e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 15:43:48 +0900 Subject: [PATCH 101/412] Refactoring the Minter integration test code. --- .../FeePool/distributefFeesStateful.test.ts | 2 +- .../v15/integration/Minter/components.test.ts | 77 +++++++++---------- .../Minter/minterIntegration.test.ts | 3 +- 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 2425d337..5748fca8 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -59,7 +59,7 @@ function getRandomsTime(): BigNumber { } // ------------------------------------------------ -describe.only("FeePoolV2", function () { +describe("FeePoolV2", function () { let accounts: SignerWithAddress[]; let veYMT: Contract; let feePool: Contract; diff --git a/test/v15/integration/Minter/components.test.ts b/test/v15/integration/Minter/components.test.ts index 521765ae..49cd332e 100644 --- a/test/v15/integration/Minter/components.test.ts +++ b/test/v15/integration/Minter/components.test.ts @@ -55,9 +55,11 @@ import Constants from "../../Constants"; chai.use(smock.matchers); const NUMBER_OF_ATTEMPTS = 20; -const SCALE = Constants.ten_to_the_20; +const ten_to_the_20 = Constants.ten_to_the_20; +const ten_to_the_18 = Constants.ten_to_the_18; const week = Constants.week; const month = Constants.month; +const zero = Constants.zero; describe("YmtMinter components", function () { let mockFeePool: FakeContract; @@ -80,12 +82,10 @@ describe("YmtMinter components", function () { let priorityRegistry: PriorityRegistry; let PRICE: BigNumber; let accounts: SignerWithAddress[]; - let ownerAddress: string; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - ownerAddress = await accounts[0].getAddress(); mockFeePool = await getFakeProxy(contractVersion["FeePool"]); mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); @@ -193,19 +193,14 @@ describe("YmtMinter components", function () { mockFeed.getPrice.returns(PRICE); mockFeed.lastGoodPrice.returns(PRICE); - - // await scoreWeightController.addType( - // "Currency", - // ethers.utils.parseEther("1") - // ); await scoreWeightController.addCurrency( scoreRegistry.address, - ethers.utils.parseEther("10") + ten_to_the_18 ); for (let i = 0; i < 4; i++) { await yamato .connect(accounts[i]) - .deposit({ value: ethers.utils.parseEther("100") }); + .deposit({ value: ten_to_the_20 }); } }); @@ -237,7 +232,7 @@ describe("YmtMinter components", function () { const diff = value.sub(target).abs(); const sum = value.add(target); - const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); + const ratio = diff.mul(2).mul(ten_to_the_20).div(sum); // console.log( // `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` @@ -250,7 +245,8 @@ describe("YmtMinter components", function () { } for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { - it(`tests duration ${i}`, async function () { + // 持続時間に基づく報酬の分布をテストする + it(`should test reward distribution based on duration [Attempt ${i}]`, async function () { const stDuration = generateUniqueRandomNumbers(3, week, month); const depositTime: number[] = []; @@ -259,7 +255,7 @@ describe("YmtMinter components", function () { for (let i = 0; i < 3; i++) { await yamato .connect(accounts[i + 1]) - .borrow(ethers.utils.parseEther("1")); + .borrow(ten_to_the_18); depositTime.push(await time.latest()); // await showGaugeInfo(); @@ -282,7 +278,7 @@ describe("YmtMinter components", function () { const totalMinted: BigNumber = balances.reduce( (a: BigNumber, b: BigNumber) => a.add(b), - ethers.BigNumber.from(0) + zero ); const weight1 = Math.floor(durations[0]); const weight2 = Math.floor(weight1 + (durations[1] - durations[0]) * 1.5); @@ -294,21 +290,21 @@ describe("YmtMinter components", function () { ); console.log( `Balance 1: ${balances[0]} (${balances[0] - .mul(SCALE) + .mul(ten_to_the_20) .div(totalMinted)}) Weight 1: ${weight1.toString()} (${ (100 * weight1) / totalWeight }%)` ); console.log( `Balance 2: ${balances[1]} (${balances[1] - .mul(SCALE) + .mul(ten_to_the_20) .div(totalMinted)}) Weight 2: ${weight2.toString()} (${ (100 * weight2) / totalWeight }%)` ); console.log( `Balance 3: ${balances[2]} (${balances[2] - .mul(SCALE) + .mul(ten_to_the_20) .div(totalMinted)}) Weight 3: ${weight3.toString()} (${ (100 * weight3) / totalWeight }%)` @@ -316,30 +312,31 @@ describe("YmtMinter components", function () { expect( approx( - balances[0].mul(SCALE).div(totalMinted), - BigNumber.from(weight1).mul(SCALE).div(totalWeight), - BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + balances[0].mul(ten_to_the_20).div(totalMinted), + BigNumber.from(weight1).mul(ten_to_the_20).div(totalWeight), + ten_to_the_18 ) ).to.be.true; expect( approx( - balances[1].mul(SCALE).div(totalMinted), - BigNumber.from(weight2).mul(SCALE).div(totalWeight), - BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + balances[1].mul(ten_to_the_20).div(totalMinted), + BigNumber.from(weight2).mul(ten_to_the_20).div(totalWeight), + ten_to_the_18 ) ).to.be.true; expect( approx( - balances[2].mul(SCALE).div(totalMinted), - BigNumber.from(weight3).mul(SCALE).div(totalWeight), - BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + balances[2].mul(ten_to_the_20).div(totalMinted), + BigNumber.from(weight3).mul(ten_to_the_20).div(totalWeight), + ten_to_the_18 ) ).to.be.true; }); } for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { - it(`tests amounts ${i}`, async function () { + // 借入額に基づく報酬の分布をテストする + it(`should test reward distribution based on borrowed amounts [Attempt ${i}]`, async function () { const stAmounts = generateUniqueRandomNumbers(3, 1e17, 1e18); const depositTime: number[] = []; @@ -365,7 +362,7 @@ describe("YmtMinter components", function () { ); const totalMinted: BigNumber = balances.reduce( (a: BigNumber, b: BigNumber) => a.add(b), - ethers.BigNumber.from(0) + zero ); console.log( @@ -373,21 +370,21 @@ describe("YmtMinter components", function () { ); console.log( `Balance 1: ${balances[0]} (${balances[0] - .mul(SCALE) + .mul(ten_to_the_20) .div(totalMinted)}) Deposited 1: ${stAmounts[0].toString()} (${ (100 * stAmounts[0]) / totalDeposited }%)` ); console.log( `Balance 2: ${balances[1]} (${balances[1] - .mul(SCALE) + .mul(ten_to_the_20) .div(totalMinted)}) Deposited 2: ${stAmounts[1].toString()} (${ (100 * stAmounts[1]) / totalDeposited }%)` ); console.log( `Balance 3: ${balances[2]} (${balances[2] - .mul(SCALE) + .mul(ten_to_the_20) .div(totalMinted)}) Deposited 3: ${stAmounts[2].toString()} (${ (100 * stAmounts[2]) / totalDeposited }%)` @@ -395,29 +392,29 @@ describe("YmtMinter components", function () { expect( approx( - balances[0].mul(SCALE).div(totalMinted), + balances[0].mul(ten_to_the_20).div(totalMinted), BigNumber.from(stAmounts[0].toString()) - .mul(SCALE) + .mul(ten_to_the_20) .div(totalDeposited.toString()), - BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ten_to_the_18 ) ).to.be.true; expect( approx( - balances[1].mul(SCALE).div(totalMinted), + balances[1].mul(ten_to_the_20).div(totalMinted), BigNumber.from(stAmounts[1].toString()) - .mul(SCALE) + .mul(ten_to_the_20) .div(totalDeposited.toString()), - BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ten_to_the_18 ) ).to.be.true; expect( approx( - balances[2].mul(SCALE).div(totalMinted), + balances[2].mul(ten_to_the_20).div(totalMinted), BigNumber.from(stAmounts[2].toString()) - .mul(SCALE) + .mul(ten_to_the_20) .div(totalDeposited.toString()), - BigNumber.from(10).pow(18) // = (10 ** -4) * SCALE + ten_to_the_18 ) ).to.be.true; }); diff --git a/test/v15/integration/Minter/minterIntegration.test.ts b/test/v15/integration/Minter/minterIntegration.test.ts index 575bfa2b..67c4a4c0 100644 --- a/test/v15/integration/Minter/minterIntegration.test.ts +++ b/test/v15/integration/Minter/minterIntegration.test.ts @@ -229,7 +229,8 @@ describe("YmtMinter integration", function () { return diff.lte(tolerance); } - it(`tests mint collateral ratio`, async function () { + // 担保比率をテストする + it(`should test the mint collateral ratio`, async function () { const amount = "100000"; await YMT.setMinter(ymtMinter.address); const ethAmounts = ["1.5", "1.25", "1", "0.75", "0.65"]; From d365c904389f833274d382a6abcc49c7c05b51b0 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 15:44:35 +0900 Subject: [PATCH 102/412] Directory name of test code changed to YmtMinter. --- test/v15/integration/{Minter => YmtMinter}/components.test.ts | 0 .../integration/{Minter => YmtMinter}/minterIntegration.test.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/v15/integration/{Minter => YmtMinter}/components.test.ts (100%) rename test/v15/integration/{Minter => YmtMinter}/minterIntegration.test.ts (100%) diff --git a/test/v15/integration/Minter/components.test.ts b/test/v15/integration/YmtMinter/components.test.ts similarity index 100% rename from test/v15/integration/Minter/components.test.ts rename to test/v15/integration/YmtMinter/components.test.ts diff --git a/test/v15/integration/Minter/minterIntegration.test.ts b/test/v15/integration/YmtMinter/minterIntegration.test.ts similarity index 100% rename from test/v15/integration/Minter/minterIntegration.test.ts rename to test/v15/integration/YmtMinter/minterIntegration.test.ts From e52fe746b26addcfcc25c9649f8fc3674adea381 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 16:02:59 +0900 Subject: [PATCH 103/412] Refactoring the ScoreRegistry integration test code. --- .../ScoreRegistry/scoreRegistry.test.ts | 93 +++++++++---------- 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts index f7bfb64c..848c0ce7 100644 --- a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts +++ b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts @@ -58,10 +58,9 @@ const ten_to_the_18 = Constants.ten_to_the_18; const ten_to_the_20 = Constants.ten_to_the_20; const ten_to_the_21 = Constants.ten_to_the_21; const MAX_UINT256 = Constants.MAX_UINT256; -const SCALE = ten_to_the_20; const zero = Constants.zero; const week = Constants.week; -const YEAR = Constants.year; +const year = Constants.year; const MIN_AMOUNT = 10000; describe("ScoreRegistry", function () { @@ -198,7 +197,7 @@ describe("ScoreRegistry", function () { await scoreWeightController.addCurrency( scoreRegistry.address, - ethers.utils.parseEther("10") + ten_to_the_18 ); }); @@ -244,37 +243,35 @@ describe("ScoreRegistry", function () { } describe("Gauge Integral Calculations", function () { - /** - * テスト: test_gauge_integral - * 1. AliceとBobの保有量と積分の初期化を行う。 - * 2. 現在のブロックのタイムスタンプと初期レートを取得する。 - * 3. タイプを追加し、その重みを変更する。 - * 4. LPトークンをAliceとBobに均等に送信する。 - * 5. 積分の更新処理を行うための補助関数「update_integral」を定義する。 - * 6. Bobが預金または引き出しを繰り返し行い、Aliceがそれをランダムに行う10回のループを開始する。 - * a. ランダムな時間を経過させるシミュレーションを行う。 - * b. Bobがランダムに預金または引き出しを行う。 - * c. 20%の確率でAliceも預金または引き出しを行う。 - * d. 同じ秒数でのユーザーチェックポイントの更新が影響しないことを確認する。 - * e. AliceとBobの保有量が正しいことを確認する。 - * f. もう一度、ランダムな時間を経過させるシミュレーションを行う。 - * g. Aliceのユーザーチェックポイントを更新し、積分を更新する。 - * 7. テストが終了する。 - */ + /** + * テスト: Gauge Integral Calculations + * 1. AliceとBobの保有量と積分の初期化を行う。 + * 2. 現在のブロックのタイムスタンプと初期レートを取得する。 + * 3. scoreWeightControllerを使って、scoreRegistryにタイプを追加し、その重みを変更する。 + * 4. AliceとBobがそれぞれETHをデポジットし、流動性を提供する。 + * 5. 積分の更新処理を行うための補助関数「update_integral」を定義する。 + * 6. Bobが預金または引き出しを繰り返し行い、Aliceがそれをランダムに行う40回のループを開始する。 + * a. ランダムな時間を経過させるシミュレーションを行う。 + * b. Bobがランダムに預金または引き出しを行う。 + * c. 20%の確率でAliceも預金または引き出しを行う。 + * d. 同じ秒数でのユーザーチェックポイントの更新が影響しないことを確認する。 + * e. AliceとBobの保有量が正しいことを確認する。 + * f. もう一度、ランダムな時間を経過させるシミュレーションを行う。 + * g. Aliceのユーザーチェックポイントを更新し、積分を更新する。 + * 7. Aliceの報酬が正しいことを確認する。 + */ it("should correctly calculate user integrals over randomized actions", async () => { // AliceとBobの保有量と積分を初期化 - let alice_staked = BigNumber.from("0"); - let bob_staked = BigNumber.from("0"); - let integral = BigNumber.from("0"); + let alice_staked = zero; + let bob_staked = zero; + let integral = zero; // 最新のブロックのタイムスタンプを取得 - let checkpoint = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); + let checkpoint = BigNumber.from(await time.latest()); // 初期レートの取得 let checkpoint_rate = await YMT.rate(); - let checkpoint_supply = BigNumber.from("0"); - let checkpoint_balance = BigNumber.from("0"); + let checkpoint_supply = zero; + let checkpoint_balance = zero; await scoreWeightController.addCurrency( scoreRegistry.address, @@ -293,7 +290,7 @@ describe("ScoreRegistry", function () { let t1 = BigNumber.from(await time.latest()); let rate1 = await YMT.rate(); let t_epoch = await YMT.startEpochTime(); - let rate_x_time = BigNumber.from("0"); + let rate_x_time = zero; // checkpoint >= t_epoch if (checkpoint.gte(t_epoch)) { @@ -308,7 +305,7 @@ describe("ScoreRegistry", function () { } // checkpoint_supply > 0 - if (checkpoint_supply.gt(BigNumber.from("0"))) { + if (checkpoint_supply.gt(zero)) { // integral + rate_x_time * checkpoint_balance / checkpoint_supply integral = integral.add( rate_x_time.mul(checkpoint_balance).div(checkpoint_supply) @@ -326,8 +323,8 @@ describe("ScoreRegistry", function () { let is_alice = Math.random() < 0.2; // ランダムな時間経過をシミュレート - let dt = randomValue(1, Math.floor(YEAR / 5)) - await ethers.provider.send("evm_increaseTime", [dt]); + let dt = randomValue(1, Math.floor(year / 5)) + await time.increase(dt); // Bobの処理 let is_withdraw = i > 0 && Math.random() < 0.5; @@ -354,13 +351,13 @@ describe("ScoreRegistry", function () { const alice_balance = Number(await CJPY.balanceOf(accounts[1].address)) let is_withdraw_alice = alice_balance > 0 && Math.random() > 0.5; if (is_withdraw_alice) { - // 引き出し処理 + // 払い戻し処理 let amount_alice = randomBigValue(1, Math.floor(alice_balance / 10) + 1) await yamato.connect(accounts[1]).repay(amount_alice); await update_integral(); alice_staked = alice_staked.sub(amount_alice); } else { - // 預金処理 + // 借入処理 let amount_alice = randomBigValue(1 + MIN_AMOUNT, alice_balance + 1 + MIN_AMOUNT) await yamato .connect(accounts[1]) @@ -392,8 +389,8 @@ describe("ScoreRegistry", function () { ); // ランダムな時間経過をさらにシミュレート - dt = randomValue(1, Math.floor(YEAR / 20)) - await ethers.provider.send("evm_increaseTime", [dt]); + dt = randomValue(1, Math.floor(year / 20)) + await time.increase(dt); await scoreRegistry .connect(accounts[1]) @@ -406,26 +403,24 @@ describe("ScoreRegistry", function () { }); /** - * test_mining_with_votelock の全体的な流れ: + * "Mining with Vote Locking" テストの全体的な流れ: * * 1. 2週間と5秒の時間を進める。 * 2. ゲージとコントローラーをセットアップし、適切なレートを設定する。 - * 3. AliceとBobにトークンを転送し、それぞれのアドレスに関連する承認を設定する。 + * 3. AliceとBobにトークンを転送し、veYMTコントラクトに対する承認を設定する。 * 4. Aliceは投票のエスクローにトークンをロックすることで、BOOSTを取得する。 - * 5. AliceとBobはそれぞれ流動性をデポジットする。 + * 5. AliceとBobはそれぞれETHをデポジットし、流動性を提供する。 * 6. Aliceの投票ロックの存在とBobの投票ロックの不在を確認する。 - * 7. 時間をさらに進め、両方のユーザーのチェックポイントを更新する。 + * 7. 4週間の時間を進め、両方のユーザーのチェックポイントを更新する。 * 8. 4週間後、AliceとBobの投票エスクローのバランスが0であることを確認する。 * 9. Aliceが投票ロックでTokenを獲得したため、AliceはBobの2.5倍のリワードを獲得することを確認する。 - * 10. さらに時間を進め、両方のユーザーのチェックポイントを更新する。 + * 10. さらに4週間の時間を進め、両方のユーザーのチェックポイントを更新する。 * 11. 最終的に、AliceとBobが同じ量のリワードを獲得していることを確認する。 */ describe("Mining with Vote Locking", function () { it("should distribute rewards according to vote lock status", async () => { // 2週間と5秒時間を進める - await ethers.provider.send("evm_increaseTime", [ - week * 2 + 5, - ]); + await time.increase(week * 2 + 5); // ゲージをコントローラーに接続して適切なレートなどを設定する await scoreWeightController.addCurrency( @@ -451,7 +446,7 @@ describe("ScoreRegistry", function () { for (let i = 0; i < 3; i++) { await yamato .connect(accounts[i]) - .deposit({ value: ethers.utils.parseEther("100") }); + .deposit({ value: ten_to_the_20 }); } // AliceとBobが一部の流動性をデポジットする await ethers.provider.send("evm_setAutomine", [false]); @@ -472,9 +467,7 @@ describe("ScoreRegistry", function () { // 時間を進めてチェックポイントを更新する now = await time.latest(); - await ethers.provider.send("evm_setNextBlockTimestamp", [ - now + week * 4, - ]); + await time.setNextBlockTimestamp(now + week * 4); // チェックポイント更新 await ethers.provider.send("evm_setAutomine", [false]); @@ -505,9 +498,7 @@ describe("ScoreRegistry", function () { // 時間を進めてチェックポイントを更新: 今は誰もがTokenを投票ロックしていない now = await time.latest(); - await ethers.provider.send("evm_setNextBlockTimestamp", [ - now + week * 4, - ]); + await time.setNextBlockTimestamp(now + week * 4); await ethers.provider.send("evm_setAutomine", [false]); await scoreRegistry.connect(accounts[2]).userCheckpoint(accounts[2].address); From 850d3c4d3e3dfdc02040959562312523ab12bc66 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 16:49:28 +0900 Subject: [PATCH 104/412] Refactoring the FeePool unitary test code. --- test/v15/unitary/FeePool/checkpoints.test.ts | 31 +++++----- test/v15/unitary/FeePool/claimMany.test.ts | 44 +++++++------- test/v15/unitary/FeePool/feePool.test.ts | 58 +++++++++---------- .../v15/unitary/FeePool/killFeeDistro.test.ts | 33 +++++------ 4 files changed, 80 insertions(+), 86 deletions(-) diff --git a/test/v15/unitary/FeePool/checkpoints.test.ts b/test/v15/unitary/FeePool/checkpoints.test.ts index 04b85c27..cdb949a0 100644 --- a/test/v15/unitary/FeePool/checkpoints.test.ts +++ b/test/v15/unitary/FeePool/checkpoints.test.ts @@ -1,6 +1,5 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { Contract } from "ethers"; import { time, takeSnapshot, @@ -8,8 +7,12 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - FeePool, - FeePool__factory, + FeePoolV2, + FeePoolV2__factory, + YMT, + YMT__factory, + VeYMT, + VeYMT__factory, } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; @@ -17,28 +20,26 @@ import Constants from "../../Constants"; const week = Constants.week; const year = Constants.year; +const MAX_UINT256 = Constants.MAX_UINT256; describe("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; - let feePool: Contract; - let veYMT: Contract; - let token: Contract; + let feePool: FeePoolV2; + let veYMT: VeYMT; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { [alice, bob, charlie] = await ethers.getSigners(); - const YMT = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - token = await YMT.deploy(); - await token.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); - veYMT = await VeYMT.deploy(token.address); - await veYMT.deployed(); - - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [await time.latest()] ); @@ -59,7 +60,7 @@ describe("FeePoolV2", () => { describe("test_checkpoints", () => { beforeEach(async function () { - await token.approve(veYMT.address, ethers.constants.MaxUint256); + await YMT.approve(veYMT.address, MAX_UINT256); await veYMT.createLock( ethers.utils.parseEther("1000"), (await time.latest()) + week * 52 diff --git a/test/v15/unitary/FeePool/claimMany.test.ts b/test/v15/unitary/FeePool/claimMany.test.ts index 551d3123..bf056e51 100644 --- a/test/v15/unitary/FeePool/claimMany.test.ts +++ b/test/v15/unitary/FeePool/claimMany.test.ts @@ -1,6 +1,5 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { Contract } from "ethers"; import { time, takeSnapshot, @@ -8,36 +7,40 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - FeePool, - FeePool__factory, + FeePoolV2, + FeePoolV2__factory, + YMT, + YMT__factory, + VeYMT, + VeYMT__factory, } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; +import { gasCostOf } from "../../testHelpers"; const week = Constants.week; +const ten_to_the_19 = Constants.ten_to_the_19; +const ZERO_ADDRESS = Constants.ZERO_ADDRESS; describe("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; - let feePool: Contract; - let veYMT: Contract; - let YMT: Contract; + let feePool: FeePoolV2; + let veYMT: VeYMT; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { [alice, bob, charlie] = await ethers.getSigners(); - const ymt = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - YMT = await ymt.deploy(); - await YMT.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); - veYMT = await VeYMT.deploy(YMT.address); - await veYMT.deployed(); - - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [await time.latest()] ); @@ -53,7 +56,7 @@ describe("FeePoolV2", () => { await time.increase(week); await alice.sendTransaction({ to: feePool.address, - value: ethers.utils.parseEther("10"), + value: ten_to_the_19, }); }); @@ -65,11 +68,6 @@ describe("FeePoolV2", () => { await snapshot.restore(); }); - async function gasCostOf(tx) { - const receipt = await tx.wait() - return receipt.gasUsed.mul(receipt.effectiveGasPrice) - } - describe("test_claim_many", () => { it("test_claim_many", async function () { await feePool.checkpointToken(); @@ -82,7 +80,7 @@ describe("FeePoolV2", () => { .connect(alice) .claimMany( [alice.address, bob.address, charlie.address].concat( - Array(17).fill(ethers.constants.AddressZero) + Array(17).fill(ZERO_ADDRESS) ) ); let balances = [ @@ -114,10 +112,10 @@ describe("FeePoolV2", () => { expect(expected).to.above(0); const balanceBefore = await ethers.provider.getBalance(alice.address); - const tx = await feePool.connect(alice).claimMany(Array(20).fill(alice.address)); + const tx3 = await feePool.connect(alice).claimMany(Array(20).fill(alice.address)); const balanceAfter = await ethers.provider.getBalance(alice.address); - const gas = await gasCostOf(tx); + const gas = await gasCostOf(tx3); expect(balanceAfter.sub(balanceBefore).add(gas)).to.be.eq(expected); }); }); diff --git a/test/v15/unitary/FeePool/feePool.test.ts b/test/v15/unitary/FeePool/feePool.test.ts index 4ed6de87..605c62b2 100644 --- a/test/v15/unitary/FeePool/feePool.test.ts +++ b/test/v15/unitary/FeePool/feePool.test.ts @@ -1,6 +1,5 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { Contract } from "ethers"; import { time, takeSnapshot, @@ -8,37 +7,41 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - FeePool, - FeePool__factory, + FeePoolV2, + FeePoolV2__factory, + YMT, + YMT__factory, + VeYMT, + VeYMT__factory, } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; +import { gasCostOf } from "../../testHelpers"; const day = Constants.day; const week = Constants.week; +const ten_to_the_18 = Constants.ten_to_the_18; +const ten_to_the_19 = Constants.ten_to_the_19; -describe("FeePoolV2", () => { +describe.only("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; - let feePool: Contract; - let veYMT: Contract; - let YMT: Contract; + let feePool: FeePoolV2; + let veYMT: VeYMT; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { [alice, bob, charlie] = await ethers.getSigners(); - const ymt = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - YMT = await ymt.deploy(); - await YMT.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); - veYMT = await VeYMT.deploy(YMT.address); - await veYMT.deployed(); - - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [await time.latest()] ); @@ -53,11 +56,6 @@ describe("FeePoolV2", () => { await snapshot.restore(); }); - async function gasCostOf(tx) { - const receipt = await tx.wait() - return receipt.gasUsed.mul(receipt.effectiveGasPrice) - } - describe("test_fee_distribution", () => { it("test_deposited_after", async function () { const amount = ethers.utils.parseEther("1000"); @@ -67,7 +65,7 @@ describe("FeePoolV2", () => { for (let j = 0; j < 7; j++) { await bob.sendTransaction({ to: feePool.address, - value: ethers.utils.parseEther("1"), + value: ten_to_the_18, }); await feePool.checkpointToken(); await feePool.checkpointTotalSupply(); @@ -99,7 +97,7 @@ describe("FeePoolV2", () => { .createLock(amount, (await time.latest()) + 8 * week); await time.increase(week); - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [await time.latest()] ); @@ -109,7 +107,7 @@ describe("FeePoolV2", () => { for (let j = 0; j < 7; j++) { await bob.sendTransaction({ to: feePool.address, - value: ethers.utils.parseEther("1"), + value: ten_to_the_18, }); await feePool.checkpointToken(); await feePool.checkpointTotalSupply(); @@ -143,7 +141,7 @@ describe("FeePoolV2", () => { const startTime = await time.latest(); await time.increase(week * 5); - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [await time.latest()] ); @@ -151,7 +149,7 @@ describe("FeePoolV2", () => { await bob.sendTransaction({ to: feePool.address, - value: ethers.utils.parseEther("10"), + value: ten_to_the_19, }); await feePool.checkpointToken(); @@ -187,7 +185,7 @@ describe("FeePoolV2", () => { await time.increase(2 * week); - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [startTime] ); @@ -195,7 +193,7 @@ describe("FeePoolV2", () => { await bob.sendTransaction({ to: feePool.address, - value: ethers.utils.parseEther("10"), + value: ten_to_the_19, }); await feePool.checkpointToken(); @@ -235,7 +233,7 @@ describe("FeePoolV2", () => { await time.increase(5 * week); - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [startTime] ); @@ -243,7 +241,7 @@ describe("FeePoolV2", () => { await bob.sendTransaction({ to: feePool.address, - value: ethers.utils.parseEther("10"), + value: ten_to_the_19, }); await feePool.checkpointToken(); @@ -263,7 +261,7 @@ describe("FeePoolV2", () => { expect(balanceAlice).to.equal(balanceBob); expect(balanceAlice.add(balanceBob)).to.be.closeTo( - ethers.utils.parseEther("10"), + ten_to_the_19, 20 ); }); diff --git a/test/v15/unitary/FeePool/killFeeDistro.test.ts b/test/v15/unitary/FeePool/killFeeDistro.test.ts index 8f246f6f..177955e4 100644 --- a/test/v15/unitary/FeePool/killFeeDistro.test.ts +++ b/test/v15/unitary/FeePool/killFeeDistro.test.ts @@ -8,32 +8,34 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - FeePool, - FeePool__factory, + FeePoolV2, + FeePoolV2__factory, + YMT, + YMT__factory, + VeYMT, + VeYMT__factory, } from "../../../../typechain"; import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; +import { gasCostOf } from "../../testHelpers"; describe("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; - let feePool: Contract; - let veYMT: Contract; - let token: Contract; + let feePool: FeePoolV2; + let veYMT: VeYMT; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { [alice, bob, charlie] = await ethers.getSigners(); - const YMT = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - token = await YMT.deploy(); - await token.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); - veYMT = await VeYMT.deploy(token.address); - await veYMT.deployed(); - - feePool = await getProxy( + feePool = await getProxy( contractVersion["FeePool"], [await time.latest()] ); @@ -48,11 +50,6 @@ describe("FeePoolV2", () => { await snapshot.restore(); }); - async function gasCostOf(tx) { - const receipt = await tx.wait() - return receipt.gasUsed.mul(receipt.effectiveGasPrice) - } - describe("test_kill_fee_distro", () => { let accounts: SignerWithAddress[]; beforeEach(async () => { From c550ffce966f3d4d0f53680e62a4991c9aede8bb Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 6 Dec 2023 17:53:17 +0900 Subject: [PATCH 105/412] Organise common functions in test code into testHelpers. --- .../integration/FeePool/checkpointTs.test.ts | 14 +--- .../FeePool/distributefFeesStateful.test.ts | 33 +-------- .../ScoreRegistry/scoreRegistry.test.ts | 34 +--------- .../integration/YmtMinter/components.test.ts | 33 +-------- test/v15/testHelpers.ts | 67 +++++++++++++++++++ 5 files changed, 72 insertions(+), 109 deletions(-) create mode 100644 test/v15/testHelpers.ts diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index 9e8cf81c..af63026b 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -16,6 +16,7 @@ import { import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; +import { generateUniqueRandomNumbers } from "../../testHelpers"; const week = Constants.week; const MAX_UINT256 = Constants.MAX_UINT256; @@ -52,19 +53,6 @@ describe("FeePoolV2", function () { await snapshot.restore(); }); - function generateUniqueRandomNumbers( - count: number, - min: number, - max: number - ): number[] { - const set = new Set(); - while (set.size < count) { - const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; - set.add(randomValue); - } - return Array.from(set); - } - it("test checkpoint total supply", async function () { const stAmount = generateUniqueRandomNumbers(MAX_EXAMPLES, 1e4, 100 * 1e4); const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 5748fca8..07dc58fa 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -16,6 +16,7 @@ import { import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; +import { randomValue, getRandomAccountNum, getRandomWeeks, getRandomAmounts, getRandomsTime } from "../../testHelpers"; // 参考) brownie Stateful Tests // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html @@ -26,37 +27,7 @@ const STATEFUL_STEP_COUNT = 30; const week = Constants.week; const year = Constants.year; const MAX_UINT256 = Constants.MAX_UINT256; -const MOUNT_DECIMALS = 3; - -// Helper functions to generate random variables -----> -function randomBigValue(min: number, max: number): BigNumber { - return BigNumber.from( - Math.floor(Math.random() * (max - min) + min).toString() - ); -} -function randomValue(min: number, max: number): number { - return Math.floor(Math.random() * (max - min) + min); -} -function getRandomAccountNum(): number { - // Corresponds strategy("address", length=5) - let rdm = Math.floor(Math.random() * ACCOUNT_NUM); //0~9 integer - return rdm; -} -function getRandomWeeks(): BigNumber { - // Corresponds strategy("uint256", min_value=1, max_value=12) - return randomBigValue(1, 12); -} -function getRandomAmounts(): BigNumber { - // Corresponds strategy("decimal", min_value=1, max_value=100, places=3) - return randomBigValue( - 1 * 10 ** MOUNT_DECIMALS, - 100 * 10 ** MOUNT_DECIMALS - ).mul(BigNumber.from(10).pow(18 - MOUNT_DECIMALS)); -} -function getRandomsTime(): BigNumber { - // Corresponds strategy("uint256", min_value=0, max_value=86400 * 3) - return randomBigValue(0, 86400 * 3); -} + // ------------------------------------------------ describe("FeePoolV2", function () { diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts index 848c0ce7..53e2e3f9 100644 --- a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts +++ b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts @@ -51,6 +51,7 @@ import { } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; +import { randomBigValue, randomValue, fee, approx } from "../../testHelpers"; chai.use(smock.matchers); @@ -209,39 +210,6 @@ describe("ScoreRegistry", function () { await snapshot.restore(); }); - function randomBigValue(min: number, max: number): BigNumber { - return BigNumber.from( - Math.floor(Math.random() * (max - min) + min).toString() - ); - } - - function randomValue(min: number, max: number): number { - return Math.floor(Math.random() * (max - min) + min); - } - - function fee(amount : BigNumber): BigNumber { - return amount.sub((amount.div(1000))) - } - - function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { - if (value.isZero() && target.isZero()) { - return true; - } - - const diff = value.sub(target).abs(); - const sum = value.add(target); - const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); - - // console.log( - // `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` - // ); - // console.log( - // `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` - // ); - - return ratio.lte(tol); - } - describe("Gauge Integral Calculations", function () { /** * テスト: Gauge Integral Calculations diff --git a/test/v15/integration/YmtMinter/components.test.ts b/test/v15/integration/YmtMinter/components.test.ts index 49cd332e..94923702 100644 --- a/test/v15/integration/YmtMinter/components.test.ts +++ b/test/v15/integration/YmtMinter/components.test.ts @@ -51,6 +51,7 @@ import { } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; +import { generateUniqueRandomNumbers, approx } from "../../testHelpers"; chai.use(smock.matchers); @@ -212,38 +213,6 @@ describe("YmtMinter components", function () { await snapshot.restore(); }); - function generateUniqueRandomNumbers( - count: number, - min: number, - max: number - ): number[] { - const set = new Set(); - while (set.size < count) { - const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; - set.add(randomValue); - } - return Array.from(set); - } - - function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { - if (value.isZero() && target.isZero()) { - return true; - } - - const diff = value.sub(target).abs(); - const sum = value.add(target); - const ratio = diff.mul(2).mul(ten_to_the_20).div(sum); - - // console.log( - // `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` - // ); - // console.log( - // `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` - // ); - - return ratio.lte(tol); - } - for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { // 持続時間に基づく報酬の分布をテストする it(`should test reward distribution based on duration [Attempt ${i}]`, async function () { diff --git a/test/v15/testHelpers.ts b/test/v15/testHelpers.ts new file mode 100644 index 00000000..5d12f357 --- /dev/null +++ b/test/v15/testHelpers.ts @@ -0,0 +1,67 @@ +import { BigNumber } from "ethers"; + +const ACCOUNT_NUM = 5; +const MOUNT_DECIMALS = 3; + +export function randomBigValue(min: number, max: number): BigNumber { + return BigNumber.from( + Math.floor(Math.random() * (max - min) + min).toString() + ); +} + +export function randomValue(min: number, max: number): number { + return Math.floor(Math.random() * (max - min) + min); +} + +export function getRandomAccountNum(): number { + return Math.floor(Math.random() * ACCOUNT_NUM); //0~9 integer +} + +export function getRandomWeeks(): BigNumber { + return randomBigValue(1, 12); +} + +export function getRandomAmounts(): BigNumber { + return randomBigValue( + 1 * 10 ** MOUNT_DECIMALS, + 100 * 10 ** MOUNT_DECIMALS + ).mul(BigNumber.from(10).pow(18 - MOUNT_DECIMALS)); +} + +export function getRandomsTime(): BigNumber { + return randomBigValue(0, 86400 * 3); +} + +export function fee(amount: BigNumber): BigNumber { + return amount.sub(amount.div(1000)); +} + +export function approx(value: BigNumber, target: BigNumber, tol: BigNumber): boolean { + if (value.isZero() && target.isZero()) { + return true; + } + + const diff = value.sub(target).abs(); + const sum = value.add(target); + const ratio = diff.mul(2).mul(BigNumber.from(10).pow(20)).div(sum); + + return ratio.lte(tol); +} + +export function generateUniqueRandomNumbers( + count: number, + min: number, + max: number +): number[] { + const set = new Set(); + while (set.size < count) { + const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; + set.add(randomValue); + } + return Array.from(set); +} + +export async function gasCostOf(tx) { + const receipt = await tx.wait() + return receipt.gasUsed.mul(receipt.effectiveGasPrice) +} \ No newline at end of file From 35f776896cc20e3d062fca2a7fd99de486cae66a Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 11:38:12 +0900 Subject: [PATCH 106/412] Refactoring the YMT unitary test code. --- test/v15/unitary/YMT/burn.test.ts | 59 +++++----- test/v15/unitary/YMT/epochTimeSupply.test.ts | 115 ++++++++----------- test/v15/unitary/YMT/inflationDelay.test.ts | 66 ++++++----- test/v15/unitary/YMT/mint.test.ts | 88 +++++++------- test/v15/unitary/YMT/setters.test.ts | 43 ++++--- 5 files changed, 190 insertions(+), 181 deletions(-) diff --git a/test/v15/unitary/YMT/burn.test.ts b/test/v15/unitary/YMT/burn.test.ts index 82ccb75b..50c081e2 100644 --- a/test/v15/unitary/YMT/burn.test.ts +++ b/test/v15/unitary/YMT/burn.test.ts @@ -1,21 +1,24 @@ -import { ethers } from "hardhat"; import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, +} from "../../../../typechain"; describe("YMT", function () { let accounts: SignerWithAddress[]; - let token: Contract; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("YMT"); - token = await Token.deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); }); beforeEach(async () => { @@ -26,45 +29,49 @@ describe("YMT", function () { await snapshot.restore(); }); - describe("YMT EpochTimeSupply", function () { - it("test_burn", async function () { - const balance: BigNumber = await token.balanceOf(accounts[0].address); - const initialSupply: BigNumber = await token.totalSupply(); + describe("YMT Burn Function Tests", function () { + it("should correctly burn a specific amount of tokens", async function () { + // 特定の量のトークンを正しく燃焼させるテスト + const balance: BigNumber = await YMT.balanceOf(accounts[0].address); + const initialSupply: BigNumber = await YMT.totalSupply(); - await token.connect(accounts[0]).burn(31337); + await YMT.burn(31337); - expect(await token.balanceOf(accounts[0].address)).to.equal( + expect(await YMT.balanceOf(accounts[0].address)).to.equal( balance.sub(31337) ); - expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); + expect(await YMT.totalSupply()).to.equal(initialSupply.sub(31337)); }); - it("test_burn_not_admin", async function () { - const initialSupply: BigNumber = await token.totalSupply(); + it("should allow non-admin to burn their tokens", async function () { + // 非管理者が自分のトークンを燃焼させることを許可するテスト + const initialSupply: BigNumber = await YMT.totalSupply(); - await token.transfer(accounts[1].address, 1000000); - await token.connect(accounts[1]).burn(31337); + await YMT.transfer(accounts[1].address, 1000000); + await YMT.connect(accounts[1]).burn(31337); - expect(await token.balanceOf(accounts[1].address)).to.equal( + expect(await YMT.balanceOf(accounts[1].address)).to.equal( 1000000 - 31337 ); - expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); + expect(await YMT.totalSupply()).to.equal(initialSupply.sub(31337)); }); - it("test_burn_all", async function () { - const initialSupply: BigNumber = await token.totalSupply(); + it("should correctly burn the entire token supply", async function () { + // トークン供給の全量を正しく燃焼させるテスト + const initialSupply: BigNumber = await YMT.totalSupply(); - await token.connect(accounts[0]).burn(initialSupply); + await YMT.burn(initialSupply); - expect(await token.balanceOf(accounts[0].address)).to.equal(0); - expect(await token.totalSupply()).to.equal(0); + expect(await YMT.balanceOf(accounts[0].address)).to.equal(0); + expect(await YMT.totalSupply()).to.equal(0); }); - it("test_overburn", async function () { - const initialSupply: BigNumber = await token.totalSupply(); + it("should revert if burning more than the total supply", async function () { + // 総供給量を超える量を燃焼させようとした場合にリバートするテスト + const initialSupply: BigNumber = await YMT.totalSupply(); await expect( - token.connect(accounts[0]).burn(initialSupply.add(1)) + YMT.burn(initialSupply.add(1)) ).to.be.revertedWith("ERC20: burn amount exceeds balance"); }); }); diff --git a/test/v15/unitary/YMT/epochTimeSupply.test.ts b/test/v15/unitary/YMT/epochTimeSupply.test.ts index a920b51a..f17a708c 100644 --- a/test/v15/unitary/YMT/epochTimeSupply.test.ts +++ b/test/v15/unitary/YMT/epochTimeSupply.test.ts @@ -1,11 +1,15 @@ import { ethers } from "hardhat"; import { expect } from "chai"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { BigNumber } from "ethers"; +import { + YMT, + YMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -13,14 +17,11 @@ const year = Constants.year; const YEAR = Constants.YEAR; describe("YMT", function () { - let accounts: SignerWithAddress[]; - let token: Contract; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { - accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("YMT"); - token = await Token.deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); }); beforeEach(async () => { @@ -31,91 +32,73 @@ describe("YMT", function () { await snapshot.restore(); }); - describe("YMT EpochTimeSupply", function () { - it("test_start_epoch_time_write", async function () { - const creationTime: BigNumber = await token.startEpochTime(); - await ethers.provider.send("evm_increaseTime", [year]); - await ethers.provider.send("evm_mine", []); + describe("YMT Epoch Time and Supply Tests", function () { + it("should update start epoch time correctly", async function () { + // 開始エポック時間が正しく更新されるかを確認するテスト + const creationTime: BigNumber = await YMT.startEpochTime(); + await time.increase(year); - expect(await token.startEpochTime()).to.equal(creationTime); + expect(await YMT.startEpochTime()).to.equal(creationTime); - await token.startEpochTimeWrite(); + await YMT.startEpochTimeWrite(); - expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); + expect(await YMT.startEpochTime()).to.equal(creationTime.add(YEAR)); }); - it("test_start_epoch_time_write_same_epoch", async function () { - await token.startEpochTimeWrite(); - await token.startEpochTimeWrite(); + it("should not update epoch time in the same epoch", async function () { + // 同じエポック内で開始エポック時間が更新されないことを確認するテスト + await YMT.startEpochTimeWrite(); + await YMT.startEpochTimeWrite(); }); - it("test_update_mining_parameters", async function () { - const creationTime = await token.startEpochTime(); - const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); + it("should update mining parameters correctly", async function () { + // マイニングパラメータが正しく更新されるかを確認するテスト + const creationTime = await YMT.startEpochTime(); + const now = BigNumber.from(await time.latest()); const newEpoch = creationTime.add(YEAR).sub(now); - await ethers.provider.send("evm_increaseTime", [newEpoch.toNumber()]); - await token.updateMiningParameters(); + await time.increase(newEpoch); + await YMT.updateMiningParameters(); }); - it("test_update_mining_parameters_same_epoch", async function () { - const creationTime = await token.startEpochTime(); - const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); + it("should not update mining parameters too soon", async function () { + // マイニングパラメータが早すぎる場合に更新されないことを確認するテスト + const creationTime = await YMT.startEpochTime(); + const now = BigNumber.from(await time.latest()); const newEpoch = creationTime.add(YEAR).sub(now); - await ethers.provider.send("evm_increaseTime", [ - newEpoch.sub(BigNumber.from("3")).toNumber(), - ]); - await expect(token.updateMiningParameters()).to.be.revertedWith( - "dev: too soon!" - ); + await time.increase(newEpoch.sub(BigNumber.from("3"))); + await expect(YMT.updateMiningParameters()).to.be.revertedWith("dev: too soon!"); }); - it("test_mintable_in_timeframe_end_before_start", async function () { - const creationTime = await token.startEpochTime(); - await expect( - token.mintableInTimeframe(creationTime.add(1), creationTime) - ).to.be.revertedWith("dev: start > end"); + it("should revert mintable timeframe if end is before start", async function () { + // 終了時間が開始時間より前の場合にmintable timeframeがリバートされることを確認するテスト + const creationTime = await YMT.startEpochTime(); + await expect(YMT.mintableInTimeframe(creationTime.add(1), creationTime)).to.be.revertedWith("dev: start > end"); }); - it("test_mintable_in_timeframe_multiple_epochs", async function () { - const creationTime = await token.startEpochTime(); + it("should calculate mintable amount over multiple epochs", async function () { + // 複数のエポックにわたるmintableな量を計算するテスト + const creationTime = await YMT.startEpochTime(); // Two epochs should not raise - const mintable = BigNumber.from("19").div(BigNumber.from("10")); - await token.mintableInTimeframe( - creationTime, - creationTime - .add(YEAR) - .mul(BigNumber.from("19").div(BigNumber.from("10"))) - ); + await YMT.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("19").div(BigNumber.from("10")))); // Three epochs should raise - await expect( - token.mintableInTimeframe( - creationTime, - creationTime - .add(YEAR) - .mul(BigNumber.from("21").div(BigNumber.from("10"))) - ) - ).to.be.revertedWith("dev: too far in future"); + await expect(YMT.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("21").div(BigNumber.from("10"))))).to.be.revertedWith("dev: too far in future"); }); - it("test_available_supply", async function () { - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); - await ethers.provider.send("evm_increaseTime", [week]); + it("should calculate available supply correctly", async function () { + // 利用可能な供給量が正しく計算されるかを確認するテスト + const creationTime = await YMT.startEpochTime(); + const initialSupply = await YMT.totalSupply(); + const rate = await YMT.rate(); + await time.increase(week); - const latestBlock = await ethers.provider.getBlock("latest"); - const currentTime = BigNumber.from(latestBlock.timestamp); + const currentTime = BigNumber.from(await time.latest()); const timeElapsed = currentTime.sub(creationTime); const expected = initialSupply.add(timeElapsed.mul(rate)); - expect(await token.availableSupply()).to.equal(expected); + expect(await YMT.availableSupply()).to.equal(expected); }); }); }); diff --git a/test/v15/unitary/YMT/inflationDelay.test.ts b/test/v15/unitary/YMT/inflationDelay.test.ts index f7091f75..e1b84da2 100644 --- a/test/v15/unitary/YMT/inflationDelay.test.ts +++ b/test/v15/unitary/YMT/inflationDelay.test.ts @@ -1,21 +1,25 @@ import { ethers } from "hardhat"; import { expect } from "chai"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; +import { BigNumber } from "ethers"; +import { + YMT, + YMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const YEAR = Constants.YEAR; describe("YMT", function () { - let token: Contract; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { - const Token = await ethers.getContractFactory("YMT"); - token = await Token.deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); }); beforeEach(async () => { @@ -26,53 +30,53 @@ describe("YMT", function () { await snapshot.restore(); }); - describe("YMT InflationDelay", function () { - it("test_rate", async function () { - expect(await token.rate()).to.equal(0); + describe("YMT Inflation Delay Tests", function () { + it("should correctly calculate rate after delay", async function () { + // 遅延後のレートが正しく計算されるかを確認するテスト + expect(await YMT.rate()).to.equal(0); - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); + await time.increase(86401); - await token.updateMiningParameters(); + await YMT.updateMiningParameters(); - expect(await token.rate()).to.be.gt(0); + expect(await YMT.rate()).to.be.gt(0); }); - it("test_start_epoch_time", async function () { - const creationTime = await token.startEpochTime(); - // const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - // expect(creationTime).to.equal(now.add("86392").sub(YEAR)); + it("should correctly set start epoch time after delay", async function () { + // 遅延後に開始エポック時間が正しく設定されるかを確認するテスト + const creationTime = await YMT.startEpochTime(); + const now = BigNumber.from(await time.latest()); + expect(creationTime).to.equal(now.add("86400").sub(YEAR)); - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); + await time.increase(86401); - await token.updateMiningParameters(); + await YMT.updateMiningParameters(); - expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); + expect(await YMT.startEpochTime()).to.equal(creationTime.add(YEAR)); }); - it("test_mining_epoch", async function () { - expect(await token.miningEpoch()).to.equal(-1); + it("should correctly update mining epoch after delay", async function () { + // 遅延後にマイニングエポックが正しく更新されるかを確認するテスト + expect(await YMT.miningEpoch()).to.equal(-1); - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); + await time.increase(86401); - await token.updateMiningParameters(); + await YMT.updateMiningParameters(); - expect(await token.miningEpoch()).to.equal(0); + expect(await YMT.miningEpoch()).to.equal(0); }); - it("test_available_supply", async function () { - expect(await token.availableSupply()).to.equal( + it("should correctly update available supply after delay", async function () { + // 遅延後に利用可能な供給量が正しく更新されるかを確認するテスト + expect(await YMT.availableSupply()).to.equal( ethers.utils.parseEther("450000000") ); - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); + await time.increase(86401); - await token.updateMiningParameters(); + await YMT.updateMiningParameters(); - expect(await token.availableSupply()).to.be.gt( + expect(await YMT.availableSupply()).to.be.gt( ethers.utils.parseEther("450000000") ); }); diff --git a/test/v15/unitary/YMT/mint.test.ts b/test/v15/unitary/YMT/mint.test.ts index 317fb71d..ccd956aa 100644 --- a/test/v15/unitary/YMT/mint.test.ts +++ b/test/v15/unitary/YMT/mint.test.ts @@ -1,11 +1,16 @@ import { ethers } from "hardhat"; import { expect } from "chai"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; +import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -13,15 +18,14 @@ const ZERO_ADDRESS = Constants.ZERO_ADDRESS; describe("YMT", function () { let accounts: SignerWithAddress[]; - let token: Contract; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("YMT"); - token = await Token.deploy(); - await ethers.provider.send("evm_increaseTime", [86401]); - await token.updateMiningParameters(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + await time.increase(86401); + await YMT.updateMiningParameters(); }); beforeEach(async () => { @@ -32,72 +36,74 @@ describe("YMT", function () { await snapshot.restore(); }); - describe("YMT Mint", function () { - it("test_available_supply", async function () { - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); + describe("YMT Mint Tests", function () { + it("should match the available supply with expected supply", async function () { + // 現在の供給量と期待される供給量が一致するかテストする + const creationTime = await YMT.startEpochTime(); + const initialSupply = await YMT.totalSupply(); + const rate = await YMT.rate(); - await ethers.provider.send("evm_increaseTime", [week]); - await ethers.provider.send("evm_mine", []); + await time.increase(week); const currentBlock = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp + await time.latest() ); - // const currentBlock = await ethers.provider.getBlock('latest'); const expected = initialSupply.add( currentBlock.sub(creationTime).mul(rate) ); - expect(await token.availableSupply()).to.equal(expected); + expect(await YMT.availableSupply()).to.equal(expected); }); - it("test_mint", async function () { - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); + it("should mint the correct amount to a user", async function () { + // ユーザーに正しい量をミントするかテストする + await YMT.setMinter(accounts[0].address); + const creationTime = await YMT.startEpochTime(); + const initialSupply = await YMT.totalSupply(); + const rate = await YMT.rate(); - await ethers.provider.send("evm_increaseTime", [week]); + await time.increase(week); const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp + await time.latest() ); const amount = currentTime.sub(creationTime).mul(rate); - await token.mint(accounts[1].address, amount); + await YMT.mint(accounts[1].address, amount); - expect(await token.balanceOf(accounts[1].address)).to.equal(amount); - expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(amount); + expect(await YMT.totalSupply()).to.equal(initialSupply.add(amount)); }); - it("test_overmint", async function () { - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const rate = await token.rate(); + it("should revert minting when amount exceeds limit", async function () { + // 限度を超えたミントはリバートされるかテストする + await YMT.setMinter(accounts[0].address); + const creationTime = await YMT.startEpochTime(); + const rate = await YMT.rate(); - await ethers.provider.send("evm_increaseTime", [week]); - await ethers.provider.send("evm_mine", []); + await time.increase(week); const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp + await time.latest() ); const amount = currentTime.sub(creationTime).add(2).mul(rate); - await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( + await expect(YMT.mint(accounts[1].address, amount)).to.be.revertedWith( "dev: exceeds allowable mint amount" ); }); - it("test_ymtMinter_only", async function () { - await token.setMinter(accounts[0].address); + it("should only allow minting from ymtMinter", async function () { + // ymtMinterからのみミントが許可されるかテストする + await YMT.setMinter(accounts[0].address); await expect( - token.connect(accounts[1]).mint(accounts[1].address, 0) + YMT.connect(accounts[1]).mint(accounts[1].address, 0) ).to.be.revertedWith("dev: ymtMinter only"); }); - it("test_zero_address", async function () { - await token.setMinter(accounts[0].address); - await expect(token.mint(ZERO_ADDRESS, 0)).to.be.revertedWith( + it("should revert minting to a zero address", async function () { + // ゼロアドレスへのミントがリバートされるかテストする + await YMT.setMinter(accounts[0].address); + await expect(YMT.mint(ZERO_ADDRESS, 0)).to.be.revertedWith( "dev: zero address" - ); + ); }); }); }); diff --git a/test/v15/unitary/YMT/setters.test.ts b/test/v15/unitary/YMT/setters.test.ts index f461dd43..87bedb6d 100644 --- a/test/v15/unitary/YMT/setters.test.ts +++ b/test/v15/unitary/YMT/setters.test.ts @@ -4,46 +4,55 @@ import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, +} from "../../../../typechain"; describe("YMT", function () { let accounts: SignerWithAddress[]; - let token: Contract; + let YMT: YMT; let snapshot: SnapshotRestorer; - beforeEach(async function () { - snapshot = await takeSnapshot(); + before(async function () { accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("YMT"); - token = await Token.deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); }); afterEach(async () => { await snapshot.restore(); }); - describe("YMT Setters", function () { - it("should revert when non-admin tries to set ymtMinter", async function () { + describe("YMT Setters Tests", function () { + it("non-admin should not be able to set ymtMinter", async function () { + // 非管理者がymtMinterを設定できないことを確認するテスト await expect( - token.connect(accounts[1]).setMinter(accounts[2].address) + YMT.connect(accounts[1]).setMinter(accounts[2].address) ).to.be.revertedWith("dev: admin only"); }); - it("should revert when non-admin tries to set admin", async function () { + it("non-admin should not be able to set admin", async function () { + // 非管理者がadminを設定できないことを確認するテスト await expect( - token.connect(accounts[1]).setAdmin(accounts[2].address) + YMT.connect(accounts[1]).setAdmin(accounts[2].address) ).to.be.revertedWith("dev: admin only"); }); - it("should allow admin to set ymtMinter", async function () { - await token.connect(accounts[0]).setMinter(accounts[1].address); - expect(await token.ymtMinter()).to.equal(accounts[1].address); + it("admin should be able to set ymtMinter", async function () { + // 管理者がymtMinterを設定できることを確認するテスト + await YMT.setMinter(accounts[1].address); + expect(await YMT.ymtMinter()).to.equal(accounts[1].address); }); - it("should allow admin to set admin", async function () { - await token.connect(accounts[0]).setAdmin(accounts[1].address); - expect(await token.admin()).to.equal(accounts[1].address); + it("admin should be able to set admin", async function () { + // 管理者がadminを設定できることを確認するテスト + await YMT.setAdmin(accounts[1].address); + expect(await YMT.admin()).to.equal(accounts[1].address); }); }); }); From 432bf1f011b456cc83555c3da9e7c50c3d48238e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 14:17:58 +0900 Subject: [PATCH 107/412] Refactoring the YMT integration test code. --- .../integration/YMT/mintIntegration.test.ts | 101 ++++++++--------- .../YMT/mintableInTimeframe.test.ts | 107 +++++++++--------- 2 files changed, 103 insertions(+), 105 deletions(-) diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index 9d6beaf1..3ec738ae 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -1,34 +1,31 @@ import { ethers } from "hardhat"; import { expect } from "chai"; +import { BigNumber } from "ethers"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { BigNumber } from "ethers"; +import { + YMT, + YMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const year = Constants.year; -// Assuming you have a helper function to increase blockchain time -async function increaseTime(duration: number) { - await ethers.provider.send("evm_increaseTime", [duration]); - await ethers.provider.send("evm_mine", []); -} - describe("YMT", function () { let accounts: SignerWithAddress[]; - let token: Contract; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("YMT"); - token = await Token.deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - await increaseTime(86401); - await token.updateMiningParameters(); + await time.increase(86401); + await YMT.updateMiningParameters(); }); beforeEach(async () => { @@ -39,70 +36,66 @@ describe("YMT", function () { await snapshot.restore(); }); - describe("YMT MintIntegration", function () { - it("should mint the correct amount", async function () { - const duration = year; // Replace with dynamic value as needed - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); + describe("YMT Mint Integration Tests", function () { + it("should mint the correct amount of tokens", async function () { + // 指定された期間に応じて正しい量のトークンを発行する + const duration = year; + await YMT.setMinter(accounts[0].address); + const creationTime = await YMT.startEpochTime(); + const initialSupply = await YMT.totalSupply(); + const rate = await YMT.rate(); - await increaseTime(duration); + await time.increase(duration); - const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); + const currentTime = BigNumber.from(await time.latest()); const amount = currentTime.sub(creationTime).mul(rate); - await token.mint(accounts[1].address, amount); + await YMT.mint(accounts[1].address, amount); - expect(await token.balanceOf(accounts[1].address)).to.equal(amount); - expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(amount); + expect(await YMT.totalSupply()).to.equal(initialSupply.add(amount)); }); - it("should revert on overmint", async function () { - const duration = year; // Replace with dynamic value as needed - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const rate = await token.rate(); + it("should revert when trying to mint more than the available amount", async function () { + // 利用可能な量を超えるトークンを発行しようとした場合にリバートする + const duration = year; + await YMT.setMinter(accounts[0].address); + const creationTime = await YMT.startEpochTime(); + const rate = await YMT.rate(); - await increaseTime(duration); + await time.increase(duration); - const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); + const currentTime = BigNumber.from(await time.latest()); const amount = currentTime.sub(creationTime).add(2).mul(rate); - await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( + await expect(YMT.mint(accounts[1].address, amount)).to.be.revertedWith( "dev: exceeds allowable mint amount" ); }); - it("should mint multiple times correctly", async function () { - await token.setMinter(accounts[0].address); - let totalSupply = await token.totalSupply(); + it("should accurately mint tokens multiple times", async function () { + // 複数回にわたって正確にトークンを発行する + await YMT.setMinter(accounts[0].address); + let totalSupply = await YMT.totalSupply(); let balance = BigNumber.from(0); - let epochStart = await token.startEpochTime(); + let epochStart = Number(await YMT.startEpochTime()); - const durations = [year * 0.33, year * 0.5, year * 0.7]; // Replace with dynamic values as needed + const durations = [year * 0.33, year * 0.5, year * 0.7]; - for (const time of durations) { - await increaseTime(time); + for (const duration of durations) { + await time.increase(duration); - if ( - (await ethers.provider.getBlock("latest")).timestamp - epochStart > - year - ) { - await token.updateMiningParameters(); - epochStart = await token.startEpochTime(); + if (await time.latest() - epochStart > year) { + await YMT.updateMiningParameters(); + epochStart = Number(await YMT.startEpochTime()); } - const amount = (await token.availableSupply()).sub(totalSupply); - await token.mint(accounts[1].address, amount); + const amount = (await YMT.availableSupply()).sub(totalSupply); + await YMT.mint(accounts[1].address, amount); balance = balance.add(amount); totalSupply = totalSupply.add(amount); - expect(await token.balanceOf(accounts[1].address)).to.equal(balance); - expect(await token.totalSupply()).to.equal(totalSupply); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(balance); + expect(await YMT.totalSupply()).to.equal(totalSupply); } }); }); diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index 111f3e7c..6b89fb1f 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -1,40 +1,41 @@ import { ethers } from "hardhat"; import { expect } from "chai"; -import { BigNumber, Contract } from "ethers"; +import { BigNumber } from "ethers"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const year = Constants.year; const YEAR = Constants.YEAR; - -async function increaseTime(duration: BigNumber) { - await ethers.provider.send("evm_increaseTime", [duration.toNumber()]); - await ethers.provider.send("evm_mine", []); -} +const ten_to_the_18 = Constants.ten_to_the_18; +const ten_to_the_16 = Constants.ten_to_the_16; // Constants const INITIAL_RATE = BigNumber.from(55000000); -const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)) +const YEAR_1_SUPPLY = INITIAL_RATE.mul(ten_to_the_18) .div(YEAR) .mul(YEAR); const INITIAL_SUPPLY = BigNumber.from(450000000); -describe("YMT", function () { +describe.only("YMT", function () { let accounts: SignerWithAddress[]; - let token: Contract; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("YMT"); - token = await Token.deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - await increaseTime(BigNumber.from(86401)); - await token.updateMiningParameters(); + await time.increase(BigNumber.from(86401)); + await YMT.updateMiningParameters(); }); beforeEach(async () => { @@ -53,22 +54,22 @@ describe("YMT", function () { } // Adjust precision for BigNumber - const precisionAdjusted = BigNumber.from(10).pow(18).mul(precision); + const precisionAdjusted = ten_to_the_18.mul(precision); return a.sub(b).abs().lte(a.add(b).div(precisionAdjusted)); } // Helper function for theoretical supply calculation - async function theoreticalSupply(token: Contract): Promise { - const epoch = await token.miningEpoch(); - const q = BigNumber.from(10).pow(18).div(BigNumber.from(2).pow(2)); // Equivalent to 1/2**0.25 - let S = INITIAL_SUPPLY.mul(BigNumber.from(10).pow(18)); + async function theoreticalSupply(YMT: YMT): Promise { + const epoch = await YMT.miningEpoch(); + const q = ten_to_the_18.div(BigNumber.from(2).pow(2)); // Equivalent to 1/2**0.25 + let S = INITIAL_SUPPLY.mul(ten_to_the_18); if (epoch.gt(0)) { S = S.add( - YEAR_1_SUPPLY.mul(BigNumber.from(10).pow(18)) - .mul(BigNumber.from(10).pow(18).sub(q.pow(epoch))) - .div(BigNumber.from(10).pow(18).sub(q)) + YEAR_1_SUPPLY.mul(ten_to_the_18) + .mul(ten_to_the_18.sub(q.pow(epoch))) + .div(ten_to_the_18.sub(q)) ); } @@ -76,32 +77,33 @@ describe("YMT", function () { YEAR_1_SUPPLY.div(YEAR) .mul(q.pow(epoch)) .mul( - (await ethers.provider.getBlock("latest")).timestamp - - (await token.startEpochTime()) + await time.latest() - + Number(await YMT.startEpochTime()) ) ); return S; } - it("test_mintable_in_timeframe", async function () { - const t0 = Number(await token.startEpochTime()); + it("should calculate mintable amount correctly for a given timeframe", async function () { + // 与えられた時間枠で正しく発行可能な量を計算する + const t0 = Number(await YMT.startEpochTime()); // Ensure the exponentiation stays within safe integer limits const exponent = BigNumber.from(10).pow(1); // Adjust the exponent as necessary - await increaseTime(exponent); + await time.increase(exponent); - let t1 = (await ethers.provider.getBlock("latest")).timestamp; + let t1 = await time.latest(); if (t1 - t0 >= year) { - await token.updateMiningParameters(); + await YMT.updateMiningParameters(); } - t1 = (await ethers.provider.getBlock("latest")).timestamp; + t1 = await time.latest(); - const availableSupply = await token.availableSupply(); - const mintable = await token.mintableInTimeframe(t0, t1); + const availableSupply = await YMT.availableSupply(); + const mintable = await YMT.mintableInTimeframe(t0, t1); expect( availableSupply - .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) + .sub(INITIAL_SUPPLY.mul(ten_to_the_18)) .gte(mintable) ).to.equal(true); if (t1 == t0) { @@ -110,7 +112,7 @@ describe("YMT", function () { const tolerance = BigNumber.from("10000000"); // Adjust as needed for precision expect( availableSupply - .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) + .sub(INITIAL_SUPPLY.mul(ten_to_the_18)) .div(mintable) .sub(1) ).to.be.lt(tolerance); @@ -120,15 +122,16 @@ describe("YMT", function () { // const theoreticalSupply = BigNumber.from("EXPECTED_SUPPLY_CALCULATION"); expect( approx( - await theoreticalSupply(token), + await theoreticalSupply(YMT), availableSupply, - Constants.ten_to_the_16 + ten_to_the_16 ) ).to.equal(true); }); - it("test_random_range_year_one", async function () { - const creationTime = await token.startEpochTime(); + it("should calculate mintable amount for a random range within the first year", async function () { + // 最初の年内のランダムな範囲に対して発行可能な量を計算する + const creationTime = await YMT.startEpochTime(); const time1 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); const time2 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); const [start, end] = [creationTime.add(time1), creationTime.add(time2)]; @@ -136,12 +139,13 @@ describe("YMT", function () { const rate = YEAR_1_SUPPLY.div(YEAR); expect( - await token.mintableInTimeframe(sortedTimes[0], sortedTimes[1]) + await YMT.mintableInTimeframe(sortedTimes[0], sortedTimes[1]) ).to.equal(rate.mul(sortedTimes[1].sub(sortedTimes[0]))); }); - it("test_random_range_multiple_epochs", async function () { - const creationTime = await token.startEpochTime(); + it("should calculate mintable amount for a range spanning multiple epochs", async function () { + // 複数のエポックにまたがる範囲に対して発行可能な量を計算する + const creationTime = await YMT.startEpochTime(); const start = creationTime.add(YEAR.mul(2)); const duration = YEAR.mul(2); const end = start.add(duration); @@ -154,33 +158,34 @@ describe("YMT", function () { ); for (let i = startEpoch.toNumber(); i < endEpoch.toNumber(); i++) { - await increaseTime(YEAR); - await token.updateMiningParameters(); + await time.increase(YEAR); + await YMT.updateMiningParameters(); } - const mintable = await token.mintableInTimeframe(start, end); + const mintable = await YMT.mintableInTimeframe(start, end); if (startEpoch.eq(endEpoch)) { const expectedMintable = rate.mul(end.sub(start)); - expect(approx(mintable, expectedMintable, Constants.ten_to_the_16)).to + expect(approx(mintable, expectedMintable, ten_to_the_16)).to .be.true; } else { expect(mintable.lt(rate.mul(end))).to.be.true; } }); - it("test_available_supply", async function () { + it("should calculate available supply correctly", async function () { + // 利用可能な供給量を正しく計算する const duration = BigNumber.from(100000); - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); + const creationTime = await YMT.startEpochTime(); + const initialSupply = await YMT.totalSupply(); + const rate = await YMT.rate(); - await increaseTime(duration); + await time.increase(duration); const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp + await time.latest() ); const expected = initialSupply.add(now.sub(creationTime).mul(rate)); - expect(await token.availableSupply()).to.equal(expected); + expect(await YMT.availableSupply()).to.equal(expected); }); }); }); From 883f1608e1723f62766155e7afbbb0f57a0274ea Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 16:20:09 +0900 Subject: [PATCH 108/412] FeePool test names changed and comments added. --- .../integration/FeePool/checkpointTs.test.ts | 3 +- .../FeePool/distributefFeesStateful.test.ts | 3 +- test/v15/unitary/FeePool/checkpoints.test.ts | 16 ++++++---- test/v15/unitary/FeePool/claimMany.test.ts | 7 +++-- test/v15/unitary/FeePool/feePool.test.ts | 17 +++++++---- .../v15/unitary/FeePool/killFeeDistro.test.ts | 30 ++++++++++++------- 6 files changed, 49 insertions(+), 27 deletions(-) diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index af63026b..71448110 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -53,7 +53,8 @@ describe("FeePoolV2", function () { await snapshot.restore(); }); - it("test checkpoint total supply", async function () { + it("should accurately checkpoint the total supply", async function () { + // 総供給量のチェックポイントを正確に行うテスト const stAmount = generateUniqueRandomNumbers(MAX_EXAMPLES, 1e4, 100 * 1e4); const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); const stSleep = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 30); diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 07dc58fa..3161dccc 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -467,7 +467,8 @@ describe("FeePoolV2", function () { describe("test_deposit_withdraw_voting", function () { for (let i = 0; i < MAX_EXAMPLES; i++) { - it(`should distributes fee ${i}`, async () => { + it(`should distribute fee correctly in example ${i}`, async () => { + // 複数のアカウントに対して預金と引き出しのテストを実行し、FeeのETHが正しく分配されることを確認 // Corresponds initializer initialize_new_lock and initialize_transfer_fees // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html#initializers // initialize_new_lock: This is equivalent to `rule_new_lock` to make it more likely we have at least 2 accounts locked at the start of the test run. diff --git a/test/v15/unitary/FeePool/checkpoints.test.ts b/test/v15/unitary/FeePool/checkpoints.test.ts index cdb949a0..595fc5fc 100644 --- a/test/v15/unitary/FeePool/checkpoints.test.ts +++ b/test/v15/unitary/FeePool/checkpoints.test.ts @@ -23,7 +23,7 @@ const year = Constants.year; const MAX_UINT256 = Constants.MAX_UINT256; describe("FeePoolV2", () => { - let alice, bob, charlie: SignerWithAddress; + let alice: SignerWithAddress; let feePool: FeePoolV2; let veYMT: VeYMT; @@ -31,7 +31,7 @@ describe("FeePoolV2", () => { let snapshot: SnapshotRestorer; before(async function () { - [alice, bob, charlie] = await ethers.getSigners(); + [alice] = await ethers.getSigners(); YMT = await (await ethers.getContractFactory("YMT")).deploy(); @@ -66,7 +66,8 @@ describe("FeePoolV2", () => { (await time.latest()) + week * 52 ); }); - it("test_checkpoint_total_supply", async function () { + it("should correctly checkpoint total supply", async function () { + // 合計供給量のチェックポイントをテストする const startTime = await feePool.timeCursor(); const weekEpoch = Math.floor(((await time.latest()) + week) / week) * week; @@ -83,7 +84,8 @@ describe("FeePoolV2", () => { ); }); - it("test_advance_time_cursor", async function () { + it("should advance time cursor correctly", async function () { + // 時間カーソルの進行をテストする const startTime = (await feePool.timeCursor()).toNumber(); await time.increase(year); await feePool.checkpointTotalSupply(); @@ -100,7 +102,8 @@ describe("FeePoolV2", () => { expect(await feePool.veSupply(startTime + week * 40)).to.equal(0); }); - it("test_claim_checkpoints_total_supply", async function () { + it("should claim and checkpoint total supply correctly", async function () { + // 合計供給量のチェックポイントと請求をテストする const start_time = (await feePool.timeCursor()).toNumber(); await feePool.connect(alice)["claim()"](); @@ -110,7 +113,8 @@ describe("FeePoolV2", () => { ); }); - it("test_toggle_allow_checkpoint", async function () { + it("should toggle allow checkpoint correctly", async function () { + // チェックポイント許可の切り替えをテストする const lastTokenTime = (await feePool.lastTokenTime()).toNumber(); await time.increase(week); diff --git a/test/v15/unitary/FeePool/claimMany.test.ts b/test/v15/unitary/FeePool/claimMany.test.ts index bf056e51..a4f4dcc1 100644 --- a/test/v15/unitary/FeePool/claimMany.test.ts +++ b/test/v15/unitary/FeePool/claimMany.test.ts @@ -69,7 +69,8 @@ describe("FeePoolV2", () => { }); describe("test_claim_many", () => { - it("test_claim_many", async function () { + it("should correctly claim for multiple accounts", async function () { + // 複数アカウントからclaimを行うテスト await feePool.checkpointToken(); await time.increase(week); await feePool.checkpointToken(); @@ -102,7 +103,9 @@ describe("FeePoolV2", () => { expect((await ethers.provider.getBalance(charlie.address)).add(gas[2])).to.be.eq(balances[2]); }); - it("test_claim_many_same_account", async function () { + + it("should correctly claim multiple times for the same account", async function () { + // 同じアカウントから複数回のclaimを行うテスト await feePool.checkpointToken(); await time.increase(week); await feePool.checkpointToken(); diff --git a/test/v15/unitary/FeePool/feePool.test.ts b/test/v15/unitary/FeePool/feePool.test.ts index 605c62b2..73b8f86e 100644 --- a/test/v15/unitary/FeePool/feePool.test.ts +++ b/test/v15/unitary/FeePool/feePool.test.ts @@ -24,7 +24,7 @@ const week = Constants.week; const ten_to_the_18 = Constants.ten_to_the_18; const ten_to_the_19 = Constants.ten_to_the_19; -describe.only("FeePoolV2", () => { +describe("FeePoolV2", () => { let alice, bob, charlie: SignerWithAddress; let feePool: FeePoolV2; @@ -57,7 +57,8 @@ describe.only("FeePoolV2", () => { }); describe("test_fee_distribution", () => { - it("test_deposited_after", async function () { + it("should correctly claim after deposit", async function () { + // デポジット後のclaimテスト const amount = ethers.utils.parseEther("1000"); await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); @@ -87,7 +88,8 @@ describe.only("FeePoolV2", () => { expect(balanceAfter.sub(balanceBefore).add(await gasCostOf(tx))).to.be.eq(0); }); - it("test_deposited_during", async function () { + it("should correctly claim during deposit", async function () { + // デポジット中のclaimテスト const amount = ethers.utils.parseEther("1000"); await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); @@ -128,7 +130,8 @@ describe.only("FeePoolV2", () => { ); }); - it("test_deposited_before", async function () { + it("should correctly claim before deposit", async function () { + // デポジット前のclaimテスト const [alice, bob] = await ethers.getSigners(); const amount = ethers.utils.parseEther("1000"); @@ -163,7 +166,8 @@ describe.only("FeePoolV2", () => { ).to.be.gte(10 ** 18); }); - it("test_deposited_twice", async function () { + it("should correctly claim on double deposit", async function () { + // 二重デポジットのclaimテスト const amount = ethers.utils.parseEther("1000"); await YMT.approve(veYMT.address, amount.mul(10)); @@ -214,7 +218,8 @@ describe.only("FeePoolV2", () => { ).to.be.lt(10); }); - it("test_deposited_parallel", async function () { + it("should correctly claim on parallel deposit", async function () { + // 並行デポジットのclaimテスト const amount = ethers.utils.parseEther("1000"); await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); diff --git a/test/v15/unitary/FeePool/killFeeDistro.test.ts b/test/v15/unitary/FeePool/killFeeDistro.test.ts index 177955e4..277cebde 100644 --- a/test/v15/unitary/FeePool/killFeeDistro.test.ts +++ b/test/v15/unitary/FeePool/killFeeDistro.test.ts @@ -1,6 +1,5 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { Contract } from "ethers"; import { time, takeSnapshot, @@ -56,23 +55,27 @@ describe("FeePoolV2", () => { accounts = await ethers.getSigners(); }); - it("test_assumptions", async function () { + it("should verify initial assumptions", async function () { + // 基本的な初期値の確認テスト expect(await feePool.isKilled()).to.be.false; expect(await feePool.governance()).to.equal(alice.address); }); - it("test_kill", async function () { + it("should kill the fee pool", async function () { + // FeePoolの Killテスト await feePool.connect(alice).killMe(); expect(await feePool.isKilled()).to.be.true; }); - - it("test_multi_kill", async function () { + + it("should kill the fee pool multiple times", async function () { + // 複数回のFeePool Killテスト await feePool.connect(alice).killMe(); await feePool.connect(alice).killMe(); expect(await feePool.isKilled()).to.be.true; }); - it("test_killing_transfers_tokens", async function () { + it("should transfer tokens when killing the pool", async function () { + // FeePool kill時のトークン転送テスト await alice.sendTransaction({ to: feePool.address, value: 31337, @@ -86,7 +89,8 @@ describe("FeePoolV2", () => { expect(await ethers.provider.getBalance(alice.address)).to.equal(balance.add(31337)); }); - it("test_multi_kill_token_transfer", async function () { + it("should transfer tokens on multiple pool killings", async function () { + // 複数回のFeePool kill時のトークン転送テスト await bob.sendTransaction({ to: feePool.address, value: 10000, @@ -109,24 +113,28 @@ describe("FeePoolV2", () => { }); for (let idx = 1; idx <= 2; idx++) { - it(`test_only_admin_for_account_index_${idx}`, async function () { + // 一般ユーザーによるFeePool Killテスト + it(`should only allow admin to kill the pool for account index ${idx}`, async function () { await expect(feePool.connect(accounts[idx]).killMe()).to.be.reverted; }); - it(`test_cannot_claim_after_killed_for_account_index_${idx}`, async function () { + it(`should not allow claims after the pool is killed for account index ${idx}`, async function () { + // FeePool kill後のclaimテスト await feePool.connect(alice).killMe(); await expect(feePool.connect(accounts[idx])["claim()"]()).to.be .reverted; }); - it(`test_cannot_claim_for_after_killed_for_account_index_${idx}`, async function () { + it(`should not allow claims for specific user after the pool is killed for account index ${idx}`, async function () { + // FeePool kill後の特定ユーザーに対するclaimテスト await feePool.connect(alice).killMe(); await expect( feePool.connect(accounts[idx])["claim(address)"](alice.address) ).to.be.reverted; }); - it(`test_cannot_claim_many_after_killed_for_account_index_${idx}`, async function () { + it(`should not allow multiple claims after the pool is killed for account index ${idx}`, async function () { + // FeePool kill後の複数ユーザーに対するclaimテスト await feePool.connect(alice).killMe(); await expect( feePool From 486ebb00257767b9edd3de48a0f3a9282f47f25d Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 16:22:12 +0900 Subject: [PATCH 109/412] Refactoring the veYMT integration test code. --- .../YMT/mintableInTimeframe.test.ts | 2 +- .../veYMT/depositWithdrawVoting.test.ts | 55 +++++++++++-------- .../integration/veYMT/votingEscrow.test.ts | 35 ++++++------ .../veYMT/zeroBalanceAtUnlockTime.test.ts | 39 +++++++------ 4 files changed, 73 insertions(+), 58 deletions(-) diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index 6b89fb1f..20581cdf 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -25,7 +25,7 @@ const YEAR_1_SUPPLY = INITIAL_RATE.mul(ten_to_the_18) .mul(YEAR); const INITIAL_SUPPLY = BigNumber.from(450000000); -describe.only("YMT", function () { +describe("YMT", function () { let accounts: SignerWithAddress[]; let YMT: YMT; let snapshot: SnapshotRestorer; diff --git a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts index 27019149..1f61eb4d 100644 --- a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts +++ b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts @@ -1,12 +1,18 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; +import { BigNumber } from "ethers"; import { time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + MockToken, + MockToken__factory, + VeYMT, + VeYMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const ACCOUNT_NUM = 10; @@ -15,36 +21,33 @@ const STATEFUL_STEP_COUNT = 20; // ruleの実行回数 const YEAR = Constants.YEAR; const WEEK = Constants.WEEK; const NAX_UINT256 = Constants.MAX_UINT256; +const zero = Constants.zero; const ten_to_the_40 = BigNumber.from( "10000000000000000000000000000000000000000" ); describe("veYMT", function () { let accounts: SignerWithAddress[]; - let veYMT: Contract; - let token: Contract; + let veYMT: VeYMT; + let token: MockToken; let stAccountN: number; let stAccount: SignerWithAddress; - let stValue: BigNumber = BigNumber.from("0"); + let stValue: BigNumber = zero; let stLockDuration: BigNumber; let votingBalances: { [key: string]: BigNumber }[]; let unlockTime: BigNumber; let snapshot: SnapshotRestorer; - beforeEach(async () => { - snapshot = await takeSnapshot(); + before(async () => { accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); - const Token = await ethers.getContractFactory("MockToken"); - const VeYMT = await ethers.getContractFactory("veYMT"); - - token = await Token.deploy("Test Token", "TST", 18); - await token.deployed(); + token = await (await ethers.getContractFactory("MockToken")).deploy("Test Token", "TST", 18); - veYMT = await VeYMT.deploy(token.address); - await veYMT.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(token.address); //init for (let i = 0; i < ACCOUNT_NUM; i++) { @@ -55,14 +58,22 @@ describe("veYMT", function () { .connect(accounts[i]) .approve(veYMT.address, NAX_UINT256); } + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); //setup votingBalances = new Array(ACCOUNT_NUM).fill({ - value: BigNumber.from("0"), - unlockTime: BigNumber.from("0"), + value: zero, + unlockTime: zero, }); }); + afterEach(async () => { + await snapshot.restore(); + }); + //--------------------------------------------- functions -----------------------------------------------------------// function rdmValue(a): BigNumber { @@ -225,7 +236,7 @@ describe("veYMT", function () { } else { // console.log("--success, account:", stAccountN); await veYMT.connect(stAccount).withdraw(); - votingBalances[stAccountN]["value"] = BigNumber.from("0"); + votingBalances[stAccountN]["value"] = zero; } } @@ -265,7 +276,7 @@ describe("veYMT", function () { } // console.log("invariant_escrow_current_balances"); - let total_supply = BigNumber.from("0"); + let total_supply = zero; let timestamp = BigNumber.from(await time.latest()); for (let i = 0; i < ACCOUNT_NUM; i++) { @@ -283,7 +294,7 @@ describe("veYMT", function () { expect(await veYMT["totalSupply()"]()).to.equal(total_supply); // console.log("invariant_historic_balances"); - total_supply = BigNumber.from("0"); + total_supply = zero; let blocknumber = (await time.latestBlock()) - 4; // console.log(blocknumber); @@ -306,9 +317,9 @@ describe("veYMT", function () { ]; describe("test_deposit_withdraw_voting", function () { - //set arbitral number of repeats + // 指定された回数だけテストを繰り返す for (let x = 0; x < MAX_EXAMPLES; x++) { - it(`Try ${x}`, async () => { + it(`should perform test iteration ${x}`, async () => { let steps = Math.floor(Math.random() * (STATEFUL_STEP_COUNT - 1)) + 1; for (let i = 0; i < steps; i++) { let n = (await rdmValue(func.length)).toNumber(); @@ -318,8 +329,4 @@ describe("veYMT", function () { }); } }); - - afterEach(async () => { - await snapshot.restore(); - }); }); diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v15/integration/veYMT/votingEscrow.test.ts index edde5608..e80e3ac2 100644 --- a/test/v15/integration/veYMT/votingEscrow.test.ts +++ b/test/v15/integration/veYMT/votingEscrow.test.ts @@ -1,12 +1,18 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; +import { BigNumber } from "ethers"; import { time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, + VeYMT, + VeYMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; type Stage = { @@ -22,7 +28,7 @@ const MAXTIME = Constants.year * 4; // 126144000 const SCALE = 1e20; const TOL = (120 / week) * SCALE; -describe("Voting Powers Test", function () { +describe("veYMT", function () { // Test voting power in the following scenario. // Alice: // ~~~~~~~ @@ -49,24 +55,20 @@ describe("Voting Powers Test", function () { // After the test is done, check all over again with balanceOfAt / totalSupplyAt let alice, bob: SignerWithAddress; - let veYMT: Contract; - let token: Contract; + let veYMT: VeYMT; + let YMT: YMT; let t0: number; let wTotal, wAlice, wBob: BigNumber; let snapshot: SnapshotRestorer; before(async function () { - snapshot = await takeSnapshot(); - const YMT = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); - - token = await YMT.deploy(); - await token.deployed(); + [alice, bob] = await ethers.getSigners(); - veYMT = await VeYMT.deploy(token.address); - await veYMT.deployed(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - [alice, bob] = await ethers.getSigners(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); }); beforeEach(async () => { @@ -78,12 +80,13 @@ describe("Voting Powers Test", function () { }); it("test voting powers", async function () { + // AliceとBobの投票権の変化をテストし、期待通りの動作を確認する const amount: BigNumber = ethers.utils.parseEther("1000"); - await token.connect(alice).transfer(bob.address, amount); + await YMT.connect(alice).transfer(bob.address, amount); const stages: { [key: string]: Stage | Stage[] } = {}; - await token.connect(alice).approve(veYMT.address, amount.mul(10)); - await token.connect(bob).approve(veYMT.address, amount.mul(10)); + await YMT.connect(alice).approve(veYMT.address, amount.mul(10)); + await YMT.connect(bob).approve(veYMT.address, amount.mul(10)); expect(await veYMT["totalSupply()"]()).to.equal(0); expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); diff --git a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts index dc38a38b..373bac34 100644 --- a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts +++ b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts @@ -1,37 +1,40 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { Contract } from "ethers"; import { time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, + VeYMT, + VeYMT__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; +const ten_to_the_18 = Constants.ten_to_the_18; -describe("Voting Escrow tests", function () { +describe("veYMT", function () { let accounts: SignerWithAddress[]; - let veYMT: Contract; - let token: Contract; + let veYMT: VeYMT; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async () => { accounts = await ethers.getSigners(); - const YMT = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); - - token = await YMT.deploy(); - await token.deployed(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - veYMT = await VeYMT.deploy(token.address); - await veYMT.deployed(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); - await token + await YMT .connect(accounts[0]) - .approve(veYMT.address, ethers.utils.parseEther("1")); + .approve(veYMT.address, ten_to_the_18); }); beforeEach(async () => { @@ -42,6 +45,7 @@ describe("Voting Escrow tests", function () { await snapshot.restore(); }); + // 様々な期間でロックを作成し、バランスがゼロになることを確認するテスト for ( let st_initial = week * 2; st_initial <= week * 52; @@ -53,11 +57,11 @@ describe("Voting Escrow tests", function () { const expectedUnlock = (await time.latest()) + st_initial; await veYMT .connect(accounts[0]) - .createLock(ethers.utils.parseEther("1"), expectedUnlock); + .createLock(ten_to_the_18, expectedUnlock); const actualUnlock = (await veYMT.locked(accounts[0].address))[1]; - await time.increase(actualUnlock - (await time.latest()) - 5); + await time.increase(Number(actualUnlock) - (await time.latest()) - 5); expect( await veYMT["balanceOf(address)"](accounts[0].address) @@ -71,6 +75,7 @@ describe("Voting Escrow tests", function () { }); } + // 様々な期間でロックを延長し、バランスがゼロになることを確認するテスト for ( let st_initial = week * 2; st_initial <= week * 52; @@ -83,7 +88,7 @@ describe("Voting Escrow tests", function () { await veYMT .connect(accounts[0]) .createLock( - ethers.utils.parseEther("1"), + ten_to_the_18, (await time.latest()) + st_initial ); @@ -98,7 +103,7 @@ describe("Voting Escrow tests", function () { await veYMT.locked(accounts[0].address) )[1]; - await time.increase(extendedActualUnlock - (await time.latest()) - 5); + await time.increase(Number(extendedActualUnlock) - (await time.latest()) - 5); expect( await veYMT["balanceOf(address)"](accounts[0].address) From 217cfcf5f4b40684b627e0f95cd8c0654baf23c7 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 17:05:40 +0900 Subject: [PATCH 110/412] Refactoring the YmtMinter unitary test code. --- test/v15/unitary/YmtMinter/Minter.test.ts | 142 ++++++---------------- 1 file changed, 35 insertions(+), 107 deletions(-) diff --git a/test/v15/unitary/YmtMinter/Minter.test.ts b/test/v15/unitary/YmtMinter/Minter.test.ts index 174c059b..195d8d5f 100644 --- a/test/v15/unitary/YmtMinter/Minter.test.ts +++ b/test/v15/unitary/YmtMinter/Minter.test.ts @@ -82,12 +82,10 @@ describe("YmtMinter", function () { let priorityRegistry: PriorityRegistry; let PRICE: BigNumber; let accounts: SignerWithAddress[]; - let ownerAddress: string; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - ownerAddress = await accounts[0].getAddress(); mockFeePool = await getFakeProxy(contractVersion["FeePool"]); mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); @@ -205,15 +203,6 @@ describe("YmtMinter", function () { .deposit({ value: ethers.utils.parseEther("100") }); } await yamato.borrow(ethers.utils.parseEther("100000")); - - // for (const account of accounts) { - // mockLpToken.transfer(account.address, ten_to_the_18); - // } - // for (let i = 1; i <= 3; i++) { - // for (const gauge of threeScoreRegistry) { - // await mockLpToken.connect(accounts[i]).approve(gauge, ten_to_the_18); - // } - // } }); beforeEach(async () => { @@ -225,37 +214,34 @@ describe("YmtMinter", function () { }); describe("YmtMinter Behavior", function () { - // Test basic mint functionality - it("test_mint", async () => { + + // 借入を行い、時間を進めた後にmintを実行 + it("Basic mint functionality test", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_17); await time.increase(month); - await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); //gauge_address, msg.sender, mint + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); //scoreRegistry_address, msg.sender, mint let expected = await scoreRegistry.integrateFraction(accounts[1].address); - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(expected.gt(zero)).to.be.equal(true); expect(await YMT.balanceOf(accounts[1].address)).to.equal(expected); expect( await ymtMinter.minted(accounts[1].address, scoreRegistry.address) ).to.equal(expected); }); - // Test minting immediately after setup - it("test_mint_immediate", async () => { + // 借入を行い、時間を進めた後にmintを実行 + it("Mint immediately after setup", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_18); - let t0 = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); + let t0 = BigNumber.from(await time.latest()); let moment = t0.add(WEEK).div(WEEK).mul(WEEK).add("5"); - await ethers.provider.send("evm_setNextBlockTimestamp", [ - moment.toNumber(), - ]); + await time.setNextBlockTimestamp(moment); //mint expect( @@ -270,8 +256,8 @@ describe("YmtMinter", function () { ).to.equal(balance); }); - // Test multiple mint operations on the same gauge - it("test_mint_multiple_same_gauge", async () => { + // 同じscoreRegistryでの複数のmint操作をテスト + it("Multiple mint operations on the same scoreRegistry", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_18); @@ -290,26 +276,8 @@ describe("YmtMinter", function () { ).to.equal(expected); //tracks fine }); - // Test minting across multiple gauges - it("test_mint_multiple_gauges", async () => { - //setup - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_17); - - await time.increase(month); - - - await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); - - let minted = await ymtMinter.minted(accounts[1].address, scoreRegistry.address); - expect(minted).to.equal( - await scoreRegistry.integrateFraction(accounts[1].address) - ); - }); - - // Test minting after withdrawing - it("test_mint_after_withdraw", async () => { + // 引き出し後のmintをテスト + it("Test minting after withdrawing", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_18); @@ -321,12 +289,12 @@ describe("YmtMinter", function () { await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); expect( - (await YMT.balanceOf(accounts[1].address)).gt(BigNumber.from("0")) + (await YMT.balanceOf(accounts[1].address)).gt(zero) ).to.equal(true); }); - // Test multiple mints after withdrawing - it("test_mint_multiple_after_withdraw", async () => { + // 引き出し後の複数回のmintをテスト + it("Test multiple mints after withdrawing", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_18); @@ -345,8 +313,8 @@ describe("YmtMinter", function () { expect(await YMT.balanceOf(accounts[1].address)).to.equal(balance); }); - // Test mint without any deposit - it("test_no_deposit", async () => { + // 預金なしでのmintをテスト + it("Test mint without any deposit", async () => { await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); expect(await YMT.balanceOf(accounts[1].address)).to.equal(zero); expect( @@ -354,78 +322,38 @@ describe("YmtMinter", function () { ).to.equal(zero); }); - // Test minting with an invalid gauge address - it("test_mint_not_a_gauge", async () => { + // 無効なscoreRegistryアドレスでのmintをテスト + it("Test minting with an invalid scoreRegistry address", async () => { await expect(ymtMinter.mint(accounts[1].address)).to.revertedWith( "dev: score is not added" ); }); - // Test minting before inflation begins - it("test_mint_before_inflation_begins", async function () { + // インフレ開始前のmintをテスト + it("Test minting before inflation begins", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_18); const startEpochTime = await YMT.startEpochTime(); const currentTime = await time.latest(); - const timeToSleep = startEpochTime.sub(currentTime).sub(5); - await ethers.provider.send("evm_increaseTime", [timeToSleep.toNumber()]); + // 時間を戻す + await ethers.provider.send("evm_increaseTime", [timeToSleep.toNumber()]); + await ethers.provider.send("evm_mine", []); + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); expect(await YMT.balanceOf(accounts[1].address)).to.equal( - BigNumber.from(0) + zero ); expect( await ymtMinter.minted(accounts[1].address, scoreRegistry.address) - ).to.equal(BigNumber.from(0)); + ).to.equal(zero); }); - // Test mintMany function with multiple gauges - // it("test_mintMany_function_multiple_gauges", async () => { - // //setup - // await scoreRegistry - // .connect(accounts[1]) - // .deposit(ten_to_the_17, accounts[1].address, false); - // await gauges[1] - // .connect(accounts[1]) - // .deposit(ten_to_the_17, accounts[1].address, false); - // await gauges[2] - // .connect(accounts[1]) - // .deposit(ten_to_the_17, accounts[1].address, false); - - // await time.increase(month); - - // let addresses = [ - // scoreRegistry.address, - // threeScoreRegistry[1], - // threeScoreRegistry[2], - // ZERO_ADDRESS, - // ZERO_ADDRESS, - // ZERO_ADDRESS, - // ZERO_ADDRESS, - // ZERO_ADDRESS, - // ]; - // await ymtMinter.connect(accounts[1]).mintMany(addresses); - - // //check - // let total_minted = BigNumber.from("0"); - - // for (let i = 0; i < 3; i++) { - // let gauge = gauges[i]; - // let minted = await ymtMinter.minted(accounts[1].address, gauge.address); - // expect(minted).to.equal( - // await gauge.integrateFraction(accounts[1].address) - // ); - // total_minted = total_minted.add(minted); - // } - - // expect(await YMT.balanceOf(accounts[1].address)).to.equal(total_minted); - // }); - - // Test toggling of the mint approval function - it("test_toggleApproveMint_function", async () => { + // mint承認関数のトグルをテスト + it("Test toggling of the mint approval function", async () => { await ymtMinter .connect(accounts[1]) .toggleApproveMint(accounts[2].address); @@ -447,8 +375,8 @@ describe("YmtMinter", function () { ).to.equal(false); }); - // Test minting on behalf of another user - it("test_mintFor_function", async () => { + // 他のユーザーのためにmintを行う関数をテスト + it("Test minting on behalf of another user", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_17); @@ -470,15 +398,15 @@ describe("YmtMinter", function () { .mintFor(scoreRegistry.address, accounts[1].address); let expected = await scoreRegistry.integrateFraction(accounts[1].address); - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); + expect(expected.gt(zero)).to.be.equal(true); expect(await YMT.balanceOf(accounts[1].address)).to.equal(expected); expect( await ymtMinter.minted(accounts[1].address, scoreRegistry.address) ).to.equal(expected); }); - // Test mintFor function when not approved - it("test_mintForFail_function", async () => { + // 承認なしでのmintFor関数をテスト + it("Test mintFor function when not approved", async () => { await yamato .connect(accounts[1]) .borrow(ten_to_the_17); From b81ff44d4244f94c6078c0fb895630886f54a34f Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 17:07:05 +0900 Subject: [PATCH 111/412] Refactoring the veYMT test code. --- .../integration/veYMT/votingEscrow.test.ts | 6 +-- .../unitary/veYMT/votingescrowAdmin.test.ts | 46 ++++++++++--------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v15/integration/veYMT/votingEscrow.test.ts index e80e3ac2..051c344c 100644 --- a/test/v15/integration/veYMT/votingEscrow.test.ts +++ b/test/v15/integration/veYMT/votingEscrow.test.ts @@ -79,8 +79,8 @@ describe("veYMT", function () { await snapshot.restore(); }); + // AliceとBobの投票権の変化をテストし、期待通りの動作を確認する it("test voting powers", async function () { - // AliceとBobの投票権の変化をテストし、期待通りの動作を確認する const amount: BigNumber = ethers.utils.parseEther("1000"); await YMT.connect(alice).transfer(bob.address, amount); const stages: { [key: string]: Stage | Stage[] } = {}; @@ -99,7 +99,7 @@ describe("veYMT", function () { // Move to timing which is good for testing - beginning of a UTC week await time.increase(timeToNextWeek); - await ethers.provider.send("evm_increaseTime", [hour]); + await time.increase(hour); stages["before_deposits"] = { blockNumber: await time.latestBlock(), @@ -169,7 +169,7 @@ describe("veYMT", function () { }); } // console.log("alice_in_0", stages["alice_in_0"]); - await ethers.provider.send("evm_increaseTime", [hour]); + await time.increase(hour); expect(await veYMT["balanceOf(address)"](alice.address)).to.equal(0); await veYMT.connect(alice).withdraw(); diff --git a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts index 3186eaa2..d9e9faec 100644 --- a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts +++ b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts @@ -1,30 +1,31 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { Contract } from "ethers"; import { takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, + VeYMT, + VeYMT__factory, +} from "../../../../typechain"; describe("veYMT", () => { - let veYMT: Contract; - let token: Contract; let accounts: SignerWithAddress[]; + let veYMT: VeYMT; + let YMT: YMT; let snapshot: SnapshotRestorer; before(async function () { - snapshot = await takeSnapshot(); - const YMT = await ethers.getContractFactory("YMT"); - const VeYMT = await ethers.getContractFactory("veYMT"); - - token = await YMT.deploy(); - await token.deployed(); + accounts = await ethers.getSigners(); - veYMT = await VeYMT.deploy(token.address); - await veYMT.deployed(); + YMT = await (await ethers.getContractFactory("YMT")).deploy(); - accounts = await ethers.getSigners(); + veYMT = await (( + await ethers.getContractFactory("veYMT") + )).deploy(YMT.address); }); beforeEach(async () => { @@ -35,39 +36,42 @@ describe("veYMT", () => { await snapshot.restore(); }); - it("test_commit_admin_only", async function () { + // 管理者権限がない場合のapplyTransferOwnership関数テスト + it("Commit ownership transfer by non-admin", async function () { await expect( veYMT.connect(accounts[1]).commitTransferOwnership(accounts[1].address) ).to.be.revertedWith("admin only"); }); - it("test_apply_admin_only", async function () { + // 管理者権限がない場合のapplyTransferOwnership関数テスト + it("Apply ownership transfer by non-admin", async function () { await expect( veYMT.connect(accounts[1]).applyTransferOwnership() ).to.be.revertedWith("admin only"); }); - it("test_commit_transfer_ownership", async function () { + // commitTransferOwnershipテスト + it("Successful commitment of transfer ownership", async function () { await veYMT - .connect(accounts[0]) .commitTransferOwnership(accounts[1].address); expect(await veYMT.admin()).to.equal(accounts[0].address); expect(await veYMT.futureAdmin()).to.equal(accounts[1].address); }); - it("test_apply_transfer_ownership", async function () { + // commitTransferOwnershipテスト + it("Successful application of transfer ownership", async function () { await veYMT - .connect(accounts[0]) .commitTransferOwnership(accounts[1].address); - await veYMT.connect(accounts[0]).applyTransferOwnership(); + await veYMT.applyTransferOwnership(); expect(await veYMT.admin()).to.equal(accounts[1].address); }); - it("test_apply_without_commit", async function () { + // コミットなしでのapplyTransferOwnership関数テスト + it("Apply transfer ownership without commit", async function () { await expect( - veYMT.connect(accounts[0]).applyTransferOwnership() + veYMT.applyTransferOwnership() ).to.be.revertedWith("admin not set"); }); }); From 911aaf7e07b2a0dde3d1dfa7583c8e7d1dc9b37f Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 17:19:55 +0900 Subject: [PATCH 112/412] Unnecessary constants removed. --- test/v15/Constants.ts | 30 +++---------------- .../YMT/mintableInTimeframe.test.ts | 2 +- 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/test/v15/Constants.ts b/test/v15/Constants.ts index 961d8393..003c55f0 100644 --- a/test/v15/Constants.ts +++ b/test/v15/Constants.ts @@ -1,4 +1,5 @@ import { BigNumber } from "ethers"; +import { ethers } from "hardhat"; class Constants { static DAY = BigNumber.from(86400); @@ -12,21 +13,9 @@ class Constants { static month = 86400 * 30; static year = 86400 * 365; - static TYPE_WEIGHTS: BigNumber[] = [ - BigNumber.from(5).mul(BigNumber.from(10).pow(17)), - BigNumber.from(2).mul(BigNumber.from(10).pow(18)), - ]; - static GAUGE_WEIGHTS: BigNumber[] = [ - BigNumber.from(2).mul(BigNumber.from(10).pow(18)), - BigNumber.from(10).pow(18), - BigNumber.from(5).mul(BigNumber.from(10).pow(17)), - ]; + static INITIAL_SUPPLY = BigNumber.from(450000000); - static symbol = "Token"; - static decimal = 18; - static INITIAL_SUPPLY = BigNumber.from("1303030303000000000000000000"); - - static ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + static ZERO_ADDRESS = ethers.constants.AddressZero; static ten_to_the_24 = BigNumber.from("1000000000000000000000000"); static ten_to_the_21 = BigNumber.from("1000000000000000000000"); @@ -35,19 +24,8 @@ class Constants { static ten_to_the_18 = BigNumber.from("1000000000000000000"); static ten_to_the_17 = BigNumber.from("100000000000000000"); static ten_to_the_16 = BigNumber.from("10000000000000000"); - static ten_to_the_9 = BigNumber.from("1000000000"); - static a = BigNumber.from("2"); - static b = BigNumber.from("5"); static zero = BigNumber.from("0"); - static MAX_UINT256 = BigNumber.from( - "115792089237316195423570985008687907853269984665640564039457584007913129639935" - ); - - static GAUGE_TYPES = [ - BigNumber.from("1"), - BigNumber.from("1"), - BigNumber.from("2"), - ]; + static MAX_UINT256 = ethers.constants.MaxUint256; } export default Constants; diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index 20581cdf..198ed8d7 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -23,7 +23,7 @@ const INITIAL_RATE = BigNumber.from(55000000); const YEAR_1_SUPPLY = INITIAL_RATE.mul(ten_to_the_18) .div(YEAR) .mul(YEAR); -const INITIAL_SUPPLY = BigNumber.from(450000000); +const INITIAL_SUPPLY = Constants.INITIAL_SUPPLY; describe("YMT", function () { let accounts: SignerWithAddress[]; From 86779113149ce98d2e5d9aac2b1edf1ef43d9bba Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 17:20:28 +0900 Subject: [PATCH 113/412] Organise test code comments. --- .../integration/FeePool/checkpointTs.test.ts | 2 +- .../FeePool/distributefFeesStateful.test.ts | 2 +- .../ScoreRegistry/scoreRegistry.test.ts | 1 - .../integration/YMT/mintIntegration.test.ts | 7 ++--- .../YMT/mintableInTimeframe.test.ts | 8 +++--- .../integration/YmtMinter/components.test.ts | 1 - .../YmtMinter/minterIntegration.test.ts | 1 - .../unitary/ScoreRegistry/checkpoint.test.ts | 18 +++++++------ test/v15/unitary/ScoreRegistry/kick.test.ts | 26 +++++++++---------- test/v15/unitary/YMT/burn.test.ts | 9 ++++--- test/v15/unitary/YMT/epochTimeSupply.test.ts | 15 ++++++----- test/v15/unitary/YMT/inflationDelay.test.ts | 9 ++++--- test/v15/unitary/YMT/mint.test.ts | 11 ++++---- test/v15/unitary/YMT/setters.test.ts | 9 ++++--- test/v15/unitary/YmtMinter/Minter.test.ts | 1 - 15 files changed, 62 insertions(+), 58 deletions(-) diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index 71448110..a7e305da 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -53,8 +53,8 @@ describe("FeePoolV2", function () { await snapshot.restore(); }); + // 総供給量のチェックポイントを正確に行うテスト it("should accurately checkpoint the total supply", async function () { - // 総供給量のチェックポイントを正確に行うテスト const stAmount = generateUniqueRandomNumbers(MAX_EXAMPLES, 1e4, 100 * 1e4); const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); const stSleep = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 30); diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 3161dccc..9226e752 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -466,9 +466,9 @@ describe("FeePoolV2", function () { ]; describe("test_deposit_withdraw_voting", function () { + // 複数のアカウントに対して預金と引き出しのテストを実行し、FeeのETHが正しく分配されることを確認 for (let i = 0; i < MAX_EXAMPLES; i++) { it(`should distribute fee correctly in example ${i}`, async () => { - // 複数のアカウントに対して預金と引き出しのテストを実行し、FeeのETHが正しく分配されることを確認 // Corresponds initializer initialize_new_lock and initialize_transfer_fees // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html#initializers // initialize_new_lock: This is equivalent to `rule_new_lock` to make it more likely we have at least 2 accounts locked at the start of the test run. diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts index 53e2e3f9..12da2b2d 100644 --- a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts +++ b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts @@ -191,7 +191,6 @@ describe("ScoreRegistry", function () { PRICE = BigNumber.from(260000).mul(1e18 + ""); - // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); mockFeed.fetchPrice.returns(PRICE); mockFeed.getPrice.returns(PRICE); mockFeed.lastGoodPrice.returns(PRICE); diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index 3ec738ae..82513cb1 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -37,8 +37,9 @@ describe("YMT", function () { }); describe("YMT Mint Integration Tests", function () { + + // 指定された期間に応じて正しい量のトークンを発行する it("should mint the correct amount of tokens", async function () { - // 指定された期間に応じて正しい量のトークンを発行する const duration = year; await YMT.setMinter(accounts[0].address); const creationTime = await YMT.startEpochTime(); @@ -55,8 +56,8 @@ describe("YMT", function () { expect(await YMT.totalSupply()).to.equal(initialSupply.add(amount)); }); + // 利用可能な量を超えるトークンを発行しようとした場合にリバートする it("should revert when trying to mint more than the available amount", async function () { - // 利用可能な量を超えるトークンを発行しようとした場合にリバートする const duration = year; await YMT.setMinter(accounts[0].address); const creationTime = await YMT.startEpochTime(); @@ -71,8 +72,8 @@ describe("YMT", function () { ); }); + // 複数回にわたって正確にトークンを発行する it("should accurately mint tokens multiple times", async function () { - // 複数回にわたって正確にトークンを発行する await YMT.setMinter(accounts[0].address); let totalSupply = await YMT.totalSupply(); let balance = BigNumber.from(0); diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index 198ed8d7..62cabfe0 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -85,8 +85,8 @@ describe("YMT", function () { return S; } + // 与えられた時間枠で正しく発行可能な量を計算する it("should calculate mintable amount correctly for a given timeframe", async function () { - // 与えられた時間枠で正しく発行可能な量を計算する const t0 = Number(await YMT.startEpochTime()); // Ensure the exponentiation stays within safe integer limits @@ -129,8 +129,8 @@ describe("YMT", function () { ).to.equal(true); }); + // 最初の年内のランダムな範囲に対して発行可能な量を計算する it("should calculate mintable amount for a random range within the first year", async function () { - // 最初の年内のランダムな範囲に対して発行可能な量を計算する const creationTime = await YMT.startEpochTime(); const time1 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); const time2 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); @@ -143,8 +143,8 @@ describe("YMT", function () { ).to.equal(rate.mul(sortedTimes[1].sub(sortedTimes[0]))); }); + // 複数のエポックにまたがる範囲に対して発行可能な量を計算する it("should calculate mintable amount for a range spanning multiple epochs", async function () { - // 複数のエポックにまたがる範囲に対して発行可能な量を計算する const creationTime = await YMT.startEpochTime(); const start = creationTime.add(YEAR.mul(2)); const duration = YEAR.mul(2); @@ -172,8 +172,8 @@ describe("YMT", function () { } }); + // 利用可能な供給量を正しく計算する it("should calculate available supply correctly", async function () { - // 利用可能な供給量を正しく計算する const duration = BigNumber.from(100000); const creationTime = await YMT.startEpochTime(); const initialSupply = await YMT.totalSupply(); diff --git a/test/v15/integration/YmtMinter/components.test.ts b/test/v15/integration/YmtMinter/components.test.ts index 94923702..bd9e3708 100644 --- a/test/v15/integration/YmtMinter/components.test.ts +++ b/test/v15/integration/YmtMinter/components.test.ts @@ -189,7 +189,6 @@ describe("YmtMinter components", function () { PRICE = BigNumber.from(260000).mul(1e18 + ""); - // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); mockFeed.fetchPrice.returns(PRICE); mockFeed.getPrice.returns(PRICE); mockFeed.lastGoodPrice.returns(PRICE); diff --git a/test/v15/integration/YmtMinter/minterIntegration.test.ts b/test/v15/integration/YmtMinter/minterIntegration.test.ts index 67c4a4c0..183b6bb0 100644 --- a/test/v15/integration/YmtMinter/minterIntegration.test.ts +++ b/test/v15/integration/YmtMinter/minterIntegration.test.ts @@ -182,7 +182,6 @@ describe("YmtMinter integration", function () { PRICE = BigNumber.from(200000).mul(1e18 + ""); - // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); mockFeed.fetchPrice.returns(PRICE); mockFeed.getPrice.returns(PRICE); mockFeed.lastGoodPrice.returns(PRICE); diff --git a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts index 32de2c42..5e5c67c2 100644 --- a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts +++ b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts @@ -183,7 +183,6 @@ describe("ScoreRegistry checkpoint", function () { PRICE = BigNumber.from(260000).mul(1e18 + ""); - // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); mockFeed.fetchPrice.returns(PRICE); mockFeed.getPrice.returns(PRICE); mockFeed.lastGoodPrice.returns(PRICE); @@ -197,24 +196,27 @@ describe("ScoreRegistry checkpoint", function () { await snapshot.restore(); }); - it("test_user_checkpoint", async function () { - // Assuming `userCheckpoint` is a function on your contract + // ユーザーのチェックポイント機能をテスト + it("Test user checkpoint", async function () { + // userCheckpoint関数がスコアレジストリに存在すると仮定 await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); }); - it("test_user_checkpoint_new_period", async function () { + // 新しい期間でのユーザーチェックポイント機能をテスト + it("Test user checkpoint in new period", async function () { await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); - // Increase the time on the blockchain + // ブロックチェーン上で時間を進める await time.increase(year * 1.1); await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); }); - it("test_user_checkpoint_wrong_account", async function () { - // Expect the transaction to be reverted with the specified error message + // 誤ったアカウントでのユーザーチェックポイント機能をテスト + it("Test user checkpoint with wrong account", async function () { + // 指定されたエラーメッセージでトランザクションがリバートされることを期待 await expect( scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[2].address) ).to.be.revertedWith("dev: unauthorized"); }); -}); +}); \ No newline at end of file diff --git a/test/v15/unitary/ScoreRegistry/kick.test.ts b/test/v15/unitary/ScoreRegistry/kick.test.ts index 3d8407dc..998eb718 100644 --- a/test/v15/unitary/ScoreRegistry/kick.test.ts +++ b/test/v15/unitary/ScoreRegistry/kick.test.ts @@ -186,7 +186,6 @@ describe("ScoreRegistry kick", function () { PRICE = BigNumber.from(260000).mul(1e18 + ""); - // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); mockFeed.fetchPrice.returns(PRICE); mockFeed.getPrice.returns(PRICE); mockFeed.lastGoodPrice.returns(PRICE); @@ -213,50 +212,51 @@ describe("ScoreRegistry kick", function () { await snapshot.restore(); }); - it("test kick functionality", async function () { - // Forward time by 2 weeks + 5 seconds + // キック機能をテスト + it("Test kick functionality", async function () { + // 2週間と5秒進める await time.increase(2 * week + 5); - // Alice approves tokens to voting escrow and creates a lock + // アリスがトークンを承認し、ロックを作成 await YMT.connect(accounts[1]).approve(veYMT.address, MAX_UINT256); await veYMT .connect(accounts[1]) .createLock( LOCK_AMOUNT, - (await ethers.provider.getBlock("latest")).timestamp + 4 * week + await time.latest() + 4 * week ); await yamato .connect(accounts[1]) .borrow(DEPOSIT_AMOUNT); - // Check working balance of Alice in Gauge + // ゲージ内のアリスのワーキングバランスをチェック expect(await scoreRegistry.workingBalances(accounts[1].address)).to.equal( DEPOSIT_AMOUNT.mul("25").div("10") ); - // Forward time by 1 week + // 1週間進める await time.increase(week); - // Bob tries to kick Alice but should fail because it's not allowed yet + // ボブがアリスをキックしようとするが、まだ許可されていないため失敗する await expect( scoreRegistry.connect(accounts[1]).kick(accounts[1].address) ).to.be.revertedWith("Not allowed"); - // Forward time by 4 weeks + // 4週間進める await time.increase(4 * week); - // Now Bob kicks Alice + // ボブがアリスをキックする await scoreRegistry.connect(accounts[1]).kick(accounts[1].address); - // Check the working balance of Alice after kick + // キック後のアリスのワーキングバランスをチェック expect(await scoreRegistry.workingBalances(accounts[1].address)).to.equal( LOCK_AMOUNT.mul(4).mul("25").div("10") ); - // Trying to kick again should fail as it's not needed + // 再度キックしようとすると、必要がないため失敗する await expect( scoreRegistry.connect(accounts[1]).kick(accounts[1].address) ).to.be.revertedWith("Not needed"); }); -}); +}); \ No newline at end of file diff --git a/test/v15/unitary/YMT/burn.test.ts b/test/v15/unitary/YMT/burn.test.ts index 50c081e2..a0fba260 100644 --- a/test/v15/unitary/YMT/burn.test.ts +++ b/test/v15/unitary/YMT/burn.test.ts @@ -30,8 +30,9 @@ describe("YMT", function () { }); describe("YMT Burn Function Tests", function () { + + // 特定の量のトークンを正しくburnさせるテスト it("should correctly burn a specific amount of tokens", async function () { - // 特定の量のトークンを正しく燃焼させるテスト const balance: BigNumber = await YMT.balanceOf(accounts[0].address); const initialSupply: BigNumber = await YMT.totalSupply(); @@ -43,8 +44,8 @@ describe("YMT", function () { expect(await YMT.totalSupply()).to.equal(initialSupply.sub(31337)); }); + // 非管理者が自分のトークンをburnさせることを許可するテスト it("should allow non-admin to burn their tokens", async function () { - // 非管理者が自分のトークンを燃焼させることを許可するテスト const initialSupply: BigNumber = await YMT.totalSupply(); await YMT.transfer(accounts[1].address, 1000000); @@ -56,8 +57,8 @@ describe("YMT", function () { expect(await YMT.totalSupply()).to.equal(initialSupply.sub(31337)); }); + // トークン供給の全量を正しくburnさせるテスト it("should correctly burn the entire token supply", async function () { - // トークン供給の全量を正しく燃焼させるテスト const initialSupply: BigNumber = await YMT.totalSupply(); await YMT.burn(initialSupply); @@ -66,8 +67,8 @@ describe("YMT", function () { expect(await YMT.totalSupply()).to.equal(0); }); + // 総供給量を超える量をburnさせようとした場合にリバートするテスト it("should revert if burning more than the total supply", async function () { - // 総供給量を超える量を燃焼させようとした場合にリバートするテスト const initialSupply: BigNumber = await YMT.totalSupply(); await expect( diff --git a/test/v15/unitary/YMT/epochTimeSupply.test.ts b/test/v15/unitary/YMT/epochTimeSupply.test.ts index f17a708c..ef92f776 100644 --- a/test/v15/unitary/YMT/epochTimeSupply.test.ts +++ b/test/v15/unitary/YMT/epochTimeSupply.test.ts @@ -33,8 +33,9 @@ describe("YMT", function () { }); describe("YMT Epoch Time and Supply Tests", function () { + + // 開始エポック時間が正しく更新されるかを確認するテスト it("should update start epoch time correctly", async function () { - // 開始エポック時間が正しく更新されるかを確認するテスト const creationTime: BigNumber = await YMT.startEpochTime(); await time.increase(year); @@ -45,14 +46,14 @@ describe("YMT", function () { expect(await YMT.startEpochTime()).to.equal(creationTime.add(YEAR)); }); + // 同じエポック内で開始エポック時間が更新されないことを確認するテスト it("should not update epoch time in the same epoch", async function () { - // 同じエポック内で開始エポック時間が更新されないことを確認するテスト await YMT.startEpochTimeWrite(); await YMT.startEpochTimeWrite(); }); + // マイニングパラメータが正しく更新されるかを確認するテスト it("should update mining parameters correctly", async function () { - // マイニングパラメータが正しく更新されるかを確認するテスト const creationTime = await YMT.startEpochTime(); const now = BigNumber.from(await time.latest()); const newEpoch = creationTime.add(YEAR).sub(now); @@ -60,8 +61,8 @@ describe("YMT", function () { await YMT.updateMiningParameters(); }); + // マイニングパラメータが早すぎる場合に更新されないことを確認するテスト it("should not update mining parameters too soon", async function () { - // マイニングパラメータが早すぎる場合に更新されないことを確認するテスト const creationTime = await YMT.startEpochTime(); const now = BigNumber.from(await time.latest()); const newEpoch = creationTime.add(YEAR).sub(now); @@ -69,14 +70,14 @@ describe("YMT", function () { await expect(YMT.updateMiningParameters()).to.be.revertedWith("dev: too soon!"); }); + // 終了時間が開始時間より前の場合にmintable timeframeがリバートされることを確認するテスト it("should revert mintable timeframe if end is before start", async function () { - // 終了時間が開始時間より前の場合にmintable timeframeがリバートされることを確認するテスト const creationTime = await YMT.startEpochTime(); await expect(YMT.mintableInTimeframe(creationTime.add(1), creationTime)).to.be.revertedWith("dev: start > end"); }); + // 複数のエポックにわたるmintableな量を計算するテスト it("should calculate mintable amount over multiple epochs", async function () { - // 複数のエポックにわたるmintableな量を計算するテスト const creationTime = await YMT.startEpochTime(); // Two epochs should not raise @@ -86,8 +87,8 @@ describe("YMT", function () { await expect(YMT.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("21").div(BigNumber.from("10"))))).to.be.revertedWith("dev: too far in future"); }); + // 利用可能な供給量が正しく計算されるかを確認するテスト it("should calculate available supply correctly", async function () { - // 利用可能な供給量が正しく計算されるかを確認するテスト const creationTime = await YMT.startEpochTime(); const initialSupply = await YMT.totalSupply(); const rate = await YMT.rate(); diff --git a/test/v15/unitary/YMT/inflationDelay.test.ts b/test/v15/unitary/YMT/inflationDelay.test.ts index e1b84da2..f7303a45 100644 --- a/test/v15/unitary/YMT/inflationDelay.test.ts +++ b/test/v15/unitary/YMT/inflationDelay.test.ts @@ -31,8 +31,9 @@ describe("YMT", function () { }); describe("YMT Inflation Delay Tests", function () { + + // 遅延後のレートが正しく計算されるかを確認するテスト it("should correctly calculate rate after delay", async function () { - // 遅延後のレートが正しく計算されるかを確認するテスト expect(await YMT.rate()).to.equal(0); await time.increase(86401); @@ -42,8 +43,8 @@ describe("YMT", function () { expect(await YMT.rate()).to.be.gt(0); }); + // 遅延後に開始エポック時間が正しく設定されるかを確認するテスト it("should correctly set start epoch time after delay", async function () { - // 遅延後に開始エポック時間が正しく設定されるかを確認するテスト const creationTime = await YMT.startEpochTime(); const now = BigNumber.from(await time.latest()); expect(creationTime).to.equal(now.add("86400").sub(YEAR)); @@ -55,8 +56,8 @@ describe("YMT", function () { expect(await YMT.startEpochTime()).to.equal(creationTime.add(YEAR)); }); + // 遅延後にマイニングエポックが正しく更新されるかを確認するテスト it("should correctly update mining epoch after delay", async function () { - // 遅延後にマイニングエポックが正しく更新されるかを確認するテスト expect(await YMT.miningEpoch()).to.equal(-1); await time.increase(86401); @@ -66,8 +67,8 @@ describe("YMT", function () { expect(await YMT.miningEpoch()).to.equal(0); }); + // 遅延後に利用可能な供給量が正しく更新されるかを確認するテスト it("should correctly update available supply after delay", async function () { - // 遅延後に利用可能な供給量が正しく更新されるかを確認するテスト expect(await YMT.availableSupply()).to.equal( ethers.utils.parseEther("450000000") ); diff --git a/test/v15/unitary/YMT/mint.test.ts b/test/v15/unitary/YMT/mint.test.ts index ccd956aa..a967af38 100644 --- a/test/v15/unitary/YMT/mint.test.ts +++ b/test/v15/unitary/YMT/mint.test.ts @@ -37,8 +37,9 @@ describe("YMT", function () { }); describe("YMT Mint Tests", function () { + + // 現在の供給量と期待される供給量が一致するかテストする it("should match the available supply with expected supply", async function () { - // 現在の供給量と期待される供給量が一致するかテストする const creationTime = await YMT.startEpochTime(); const initialSupply = await YMT.totalSupply(); const rate = await YMT.rate(); @@ -54,8 +55,8 @@ describe("YMT", function () { expect(await YMT.availableSupply()).to.equal(expected); }); + // ユーザーに正しい量をミントするかテストする it("should mint the correct amount to a user", async function () { - // ユーザーに正しい量をミントするかテストする await YMT.setMinter(accounts[0].address); const creationTime = await YMT.startEpochTime(); const initialSupply = await YMT.totalSupply(); @@ -73,8 +74,8 @@ describe("YMT", function () { expect(await YMT.totalSupply()).to.equal(initialSupply.add(amount)); }); + // 限度を超えたミントはリバートされるかテストする it("should revert minting when amount exceeds limit", async function () { - // 限度を超えたミントはリバートされるかテストする await YMT.setMinter(accounts[0].address); const creationTime = await YMT.startEpochTime(); const rate = await YMT.rate(); @@ -90,16 +91,16 @@ describe("YMT", function () { ); }); + // ymtMinterからのみミントが許可されるかテストする it("should only allow minting from ymtMinter", async function () { - // ymtMinterからのみミントが許可されるかテストする await YMT.setMinter(accounts[0].address); await expect( YMT.connect(accounts[1]).mint(accounts[1].address, 0) ).to.be.revertedWith("dev: ymtMinter only"); }); + // ゼロアドレスへのミントがリバートされるかテストする it("should revert minting to a zero address", async function () { - // ゼロアドレスへのミントがリバートされるかテストする await YMT.setMinter(accounts[0].address); await expect(YMT.mint(ZERO_ADDRESS, 0)).to.be.revertedWith( "dev: zero address" diff --git a/test/v15/unitary/YMT/setters.test.ts b/test/v15/unitary/YMT/setters.test.ts index 87bedb6d..dbb1f2ca 100644 --- a/test/v15/unitary/YMT/setters.test.ts +++ b/test/v15/unitary/YMT/setters.test.ts @@ -29,28 +29,29 @@ describe("YMT", function () { }); describe("YMT Setters Tests", function () { + + // 非管理者がymtMinterを設定できないことを確認するテスト it("non-admin should not be able to set ymtMinter", async function () { - // 非管理者がymtMinterを設定できないことを確認するテスト await expect( YMT.connect(accounts[1]).setMinter(accounts[2].address) ).to.be.revertedWith("dev: admin only"); }); + // 非管理者がadminを設定できないことを確認するテスト it("non-admin should not be able to set admin", async function () { - // 非管理者がadminを設定できないことを確認するテスト await expect( YMT.connect(accounts[1]).setAdmin(accounts[2].address) ).to.be.revertedWith("dev: admin only"); }); + // 管理者がymtMinterを設定できることを確認するテスト it("admin should be able to set ymtMinter", async function () { - // 管理者がymtMinterを設定できることを確認するテスト await YMT.setMinter(accounts[1].address); expect(await YMT.ymtMinter()).to.equal(accounts[1].address); }); + // 管理者がadminを設定できることを確認するテスト it("admin should be able to set admin", async function () { - // 管理者がadminを設定できることを確認するテスト await YMT.setAdmin(accounts[1].address); expect(await YMT.admin()).to.equal(accounts[1].address); }); diff --git a/test/v15/unitary/YmtMinter/Minter.test.ts b/test/v15/unitary/YmtMinter/Minter.test.ts index 195d8d5f..2c06d61a 100644 --- a/test/v15/unitary/YmtMinter/Minter.test.ts +++ b/test/v15/unitary/YmtMinter/Minter.test.ts @@ -188,7 +188,6 @@ describe("YmtMinter", function () { PRICE = BigNumber.from(260000).mul(1e18 + ""); - // CJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); mockFeed.fetchPrice.returns(PRICE); mockFeed.getPrice.returns(PRICE); mockFeed.lastGoodPrice.returns(PRICE); From deb46fe0138dd8060d43a6738b1bdce82b48b295 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 17:22:38 +0900 Subject: [PATCH 114/412] code format --- contracts/CurrencyOSV3.sol | 8 +- .../Interfaces/IScoreWeightController.sol | 6 +- contracts/ScoreRegistry.sol | 25 ++-- contracts/ScoreWeightController.sol | 21 +--- contracts/YamatoDepositorV3.sol | 7 +- contracts/YamatoWithdrawerV3.sol | 7 +- contracts/YmtMinter.sol | 3 +- .../integration/FeePool/checkpointTs.test.ts | 11 +- .../FeePool/distributefFeesStateful.test.ts | 45 ++++--- .../ScoreRegistry/scoreRegistry.test.ts | 114 ++++++++++-------- .../integration/YMT/mintIntegration.test.ts | 8 +- .../YMT/mintableInTimeframe.test.ts | 31 ++--- .../integration/YmtMinter/components.test.ts | 8 +- .../YmtMinter/minterIntegration.test.ts | 55 +++++---- .../veYMT/depositWithdrawVoting.test.ts | 8 +- .../integration/veYMT/votingEscrow.test.ts | 11 +- .../veYMT/zeroBalanceAtUnlockTime.test.ts | 13 +- test/v15/testHelpers.ts | 12 +- test/v15/unitary/FeePool/claimMany.test.ts | 23 ++-- test/v15/unitary/FeePool/feePool.test.ts | 27 +++-- .../v15/unitary/FeePool/killFeeDistro.test.ts | 10 +- .../unitary/ScoreRegistry/checkpoint.test.ts | 14 ++- test/v15/unitary/ScoreRegistry/kick.test.ts | 11 +- test/v15/unitary/YMT/burn.test.ts | 12 +- test/v15/unitary/YMT/epochTimeSupply.test.ts | 30 +++-- test/v15/unitary/YMT/inflationDelay.test.ts | 6 +- test/v15/unitary/YMT/mint.test.ts | 20 +-- test/v15/unitary/YMT/setters.test.ts | 6 +- test/v15/unitary/YmtMinter/Minter.test.ts | 53 +++----- .../unitary/veYMT/votingescrowAdmin.test.ts | 12 +- 30 files changed, 311 insertions(+), 306 deletions(-) diff --git a/contracts/CurrencyOSV3.sol b/contracts/CurrencyOSV3.sol index 523d0f0f..4933d0da 100644 --- a/contracts/CurrencyOSV3.sol +++ b/contracts/CurrencyOSV3.sol @@ -83,7 +83,9 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { } function setYmtMinter(address ymtMinterAddr) external onlyGovernance { - bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); + bytes32 YMT_MINTER_KEY = bytes32( + keccak256(abi.encode(YMT_MINTER_SLOT_ID)) + ); assembly { sstore(YMT_MINTER_KEY, ymtMinterAddr) } @@ -177,7 +179,9 @@ contract CurrencyOSV3 is ICurrencyOSV3, UUPSBase { } function ymtMinter() public view override returns (address _ymtMinter) { - bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); + bytes32 YMT_MINTER_KEY = bytes32( + keccak256(abi.encode(YMT_MINTER_SLOT_ID)) + ); assembly { _ymtMinter := sload(YMT_MINTER_KEY) } diff --git a/contracts/Interfaces/IScoreWeightController.sol b/contracts/Interfaces/IScoreWeightController.sol index 5c655c7c..ee15bf59 100644 --- a/contracts/Interfaces/IScoreWeightController.sol +++ b/contracts/Interfaces/IScoreWeightController.sol @@ -9,17 +9,13 @@ pragma solidity 0.8.4; //solhint-disable no-inline-assembly interface IScoreWeightController { - function veYMT() external view returns (address); function checkpointScore(address addr) external; function checkpoint(address addr_) external; - function addCurrency( - address addr_, - uint256 weight_ - ) external; + function addCurrency(address addr_, uint256 weight_) external; function scoreRelativeWeight( address addr, diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index d4d8b1d1..2b4b3154 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -73,18 +73,25 @@ contract ScoreRegistry is YamatoAction { mapping(int128 => uint256) public periodTimestamp; mapping(int128 => uint256) public integrateInvSupply; - function initialize(address ymtMinterAddr, address yamatoAddr) public initializer { + function initialize( + address ymtMinterAddr, + address yamatoAddr + ) public initializer { __YamatoAction_init(yamatoAddr); bytes32 YMT_KEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); bytes32 VEYMT_KEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); - bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); + bytes32 YMT_MINTER_KEY = bytes32( + keccak256(abi.encode(YMT_MINTER_SLOT_ID)) + ); bytes32 WEIGHT_CONTROLLER_KEY = bytes32( keccak256(abi.encode(WEIGHT_CONTROLLER_SLOT_ID)) ); address ymtAddr = IYmtMinter(ymtMinterAddr).YMT(); - address scoreWeightControllerAddr = IYmtMinter(ymtMinterAddr).scoreWeightController(); - address veYmtAddr = IScoreWeightController(scoreWeightControllerAddr).veYMT(); + address scoreWeightControllerAddr = IYmtMinter(ymtMinterAddr) + .scoreWeightController(); + address veYmtAddr = IScoreWeightController(scoreWeightControllerAddr) + .veYMT(); assembly { sstore(YMT_KEY, ymtAddr) @@ -128,7 +135,9 @@ contract ScoreRegistry is YamatoAction { if (block.timestamp > _st.periodTime) { uint256 _workingSupply = workingSupply; - IScoreWeightController(scoreWeightController()).checkpointScore(address(this)); + IScoreWeightController(scoreWeightController()).checkpointScore( + address(this) + ); uint256 _prevWeekTime = _st.periodTime; uint256 _weekTime = min( ((_st.periodTime + WEEK) / WEEK) * WEEK, @@ -218,7 +227,7 @@ contract ScoreRegistry is YamatoAction { _limit = min(debt_, _limit); uint256 _oldBal = workingBalances[addr_]; - if(debt_ > 0){ + if (debt_ > 0) { // Apply the coefficient based on the collateral ratio provided uint256 coefficient = calculateCoefficient(collateralRatio_); // Adjust the limit based on the coefficient @@ -321,7 +330,9 @@ contract ScoreRegistry is YamatoAction { } function ymtMinter() public view returns (address _ymtMinter) { - bytes32 YMT_MINTER_KEY = bytes32(keccak256(abi.encode(YMT_MINTER_SLOT_ID))); + bytes32 YMT_MINTER_KEY = bytes32( + keccak256(abi.encode(YMT_MINTER_SLOT_ID)) + ); assembly { _ymtMinter := sload(YMT_MINTER_KEY) } diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index fb3ed899..566f22f9 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -32,20 +32,13 @@ contract ScoreWeightController is UUPSBase { *@param _token `Token` contract address *@param _votingEscrow `VotingEscrow` contract address */ - function initialize( - address ymtAddr, - address veYmtAddr - ) public initializer { + function initialize(address ymtAddr, address veYmtAddr) public initializer { require(ymtAddr != address(0)); require(veYmtAddr != address(0)); __UUPSBase_init(); - bytes32 YMT_LEY = bytes32( - keccak256(abi.encode(YMT_SLOT_ID)) - ); - bytes32 VEYMT_LEY = bytes32( - keccak256(abi.encode(VEYMT_SLOT_ID)) - ); + bytes32 YMT_LEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); + bytes32 VEYMT_LEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); assembly { sstore(YMT_LEY, ymtAddr) sstore(VEYMT_LEY, veYmtAddr) @@ -118,18 +111,14 @@ contract ScoreWeightController is UUPSBase { ===================== */ function YMT() public view returns (address ymtAddr) { - bytes32 YMT_LEY = bytes32( - keccak256(abi.encode(YMT_SLOT_ID)) - ); + bytes32 YMT_LEY = bytes32(keccak256(abi.encode(YMT_SLOT_ID))); assembly { ymtAddr := sload(YMT_LEY) } } function veYMT() public view returns (address ymtAddr) { - bytes32 VEYMT_LEY = bytes32( - keccak256(abi.encode(VEYMT_SLOT_ID)) - ); + bytes32 VEYMT_LEY = bytes32(keccak256(abi.encode(VEYMT_SLOT_ID))); assembly { ymtAddr := sload(VEYMT_LEY) } diff --git a/contracts/YamatoDepositorV3.sol b/contracts/YamatoDepositorV3.sol index 5fed1199..d3f55a7d 100644 --- a/contracts/YamatoDepositorV3.sol +++ b/contracts/YamatoDepositorV3.sol @@ -47,7 +47,8 @@ contract YamatoDepositorV3 is IYamatoDepositor, YamatoAction { ); IYamato.Pledge memory pledge = IYamato(yamato()).getPledge(_sender); - (uint256 totalColl, uint256 totalDebt, , , , ) = IYamato(yamato()).getStates(); + (uint256 totalColl, uint256 totalDebt, , , , ) = IYamato(yamato()) + .getStates(); pledge.coll += _ethAmount; @@ -68,7 +69,7 @@ contract YamatoDepositorV3 is IYamatoDepositor, YamatoAction { IScoreRegistry _scoreRegistry = IScoreRegistry( IYamatoV4(yamato()).scoreRegistry() ); - if(pledge.debt > 0){ + if (pledge.debt > 0) { _scoreRegistry.checkpoint(_sender); } @@ -97,7 +98,7 @@ contract YamatoDepositorV3 is IYamatoDepositor, YamatoAction { /* Update score */ - if(pledge.debt > 0){ + if (pledge.debt > 0) { _scoreRegistry.updateScoreLimit( _sender, pledge.debt, diff --git a/contracts/YamatoWithdrawerV3.sol b/contracts/YamatoWithdrawerV3.sol index 45e74169..9c1af9b0 100644 --- a/contracts/YamatoWithdrawerV3.sol +++ b/contracts/YamatoWithdrawerV3.sol @@ -42,7 +42,8 @@ contract YamatoWithdrawerV3 is IYamatoWithdrawer, YamatoAction { */ IPriceFeedV3(priceFeed()).fetchPrice(); IYamato.Pledge memory pledge = IYamato(yamato()).getPledge(_sender); - (uint256 totalColl, uint256 totalDebt, , , , ) = IYamato(yamato()).getStates(); + (uint256 totalColl, uint256 totalDebt, , , , ) = IYamato(yamato()) + .getStates(); /* scoreRegistry checkpoint @@ -50,7 +51,7 @@ contract YamatoWithdrawerV3 is IYamatoWithdrawer, YamatoAction { IScoreRegistry _scoreRegistry = IScoreRegistry( IYamatoV4(yamato()).scoreRegistry() ); - if(pledge.debt > 0){ + if (pledge.debt > 0) { _scoreRegistry.checkpoint(_sender); } @@ -119,7 +120,7 @@ contract YamatoWithdrawerV3 is IYamatoWithdrawer, YamatoAction { /* Update score */ - if(pledge.debt > 0){ + if (pledge.debt > 0) { _scoreRegistry.updateScoreLimit( _sender, pledge.debt, diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index 09c89e06..cd4d1cfa 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -54,7 +54,8 @@ contract YmtMinter is function _mintFor(address scoreAddr_, address for_) internal { require( - IScoreWeightController(scoreWeightController()).scores(scoreAddr_) > 0, + IScoreWeightController(scoreWeightController()).scores(scoreAddr_) > + 0, "dev: score is not added" ); diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index a7e305da..0c32d229 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -59,12 +59,11 @@ describe("FeePoolV2", function () { const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); const stSleep = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 30); for (let i = 0; i < MAX_EXAMPLES; i++) { - await YMT - .connect(accounts[i]) - .approve(veYMT.address, MAX_UINT256); - await YMT - .connect(accounts[0]) - .transfer(await accounts[i].address, ethers.utils.parseEther("1000")); + await YMT.connect(accounts[i]).approve(veYMT.address, MAX_UINT256); + await YMT.connect(accounts[0]).transfer( + await accounts[i].address, + ethers.utils.parseEther("1000") + ); } let finalLock = 0; diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 9226e752..e7544bce 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -16,7 +16,13 @@ import { import { getProxy } from "../../../../src/testUtil"; import { contractVersion } from "../../../param/version"; import Constants from "../../Constants"; -import { randomValue, getRandomAccountNum, getRandomWeeks, getRandomAmounts, getRandomsTime } from "../../testHelpers"; +import { + randomValue, + getRandomAccountNum, + getRandomWeeks, + getRandomAmounts, + getRandomsTime, +} from "../../testHelpers"; // 参考) brownie Stateful Tests // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html @@ -59,12 +65,11 @@ describe("FeePoolV2", function () { for (let i = 0; i < ACCOUNT_NUM; i++) { // ensure accounts[:5] all have YMTs that may be locked - await YMT - .connect(accounts[0]) - .transfer(accounts[i].address, ethers.utils.parseEther("10000000")); - await YMT - .connect(accounts[i]) - .approve(veYMT.address, MAX_UINT256); + await YMT.connect(accounts[0]).transfer( + accounts[i].address, + ethers.utils.parseEther("10000000") + ); + await YMT.connect(accounts[i]).approve(veYMT.address, MAX_UINT256); userClaims[accounts[i].address] = []; } @@ -211,7 +216,7 @@ describe("FeePoolV2", function () { stAmount?: BigNumber, stTime?: BigNumber ) { - console.log("ruleIncreaseLockAmount") + console.log("ruleIncreaseLockAmount"); /* Increase the amount of an existing user lock. @@ -244,7 +249,7 @@ describe("FeePoolV2", function () { } async function ruleClaimFees(stAcct?: SignerWithAddress, stTime?: BigNumber) { - console.log("ruleClaimFees") + console.log("ruleClaimFees"); /* Claim fees for a user. @@ -277,7 +282,9 @@ describe("FeePoolV2", function () { const claimed = await ethers.provider.getBalance(stAcct.address); const tx = await feePool.connect(stAcct)["claim()"](); - const newClaimed = (await ethers.provider.getBalance(stAcct.address)).sub(claimed); + const newClaimed = (await ethers.provider.getBalance(stAcct.address)).sub( + claimed + ); userClaims[stAcct.address][tx.blockNumber] = [ newClaimed, await feePool.timeCursorOf(stAcct.address), @@ -285,7 +292,7 @@ describe("FeePoolV2", function () { } async function ruleTransferFees(stAmount?: BigNumber, stTime?: BigNumber) { - console.log("ruleTransferFees") + console.log("ruleTransferFees"); /* Transfer fees into the feePool and make a checkpoint. @@ -327,7 +334,7 @@ describe("FeePoolV2", function () { stAmount?: BigNumber, stTime?: BigNumber ) { - console.log("ruleTransferFeesWithoutCheckpoint") + console.log("ruleTransferFeesWithoutCheckpoint"); /* Transfer fees into the feePool without checkpointing. @@ -389,10 +396,12 @@ describe("FeePoolV2", function () { // Point: ${up} // `); // <---- - balanceList[acct.address] = await ethers.provider.getBalance(acct.address); + balanceList[acct.address] = await ethers.provider.getBalance( + acct.address + ); const claimTx = await feePool.connect(acct)["claim()"](); - const receipt = await claimTx.wait() - const gasCost = receipt.gasUsed.mul(receipt.effectiveGasPrice) + const receipt = await claimTx.wait(); + const gasCost = receipt.gasUsed.mul(receipt.effectiveGasPrice); balanceList[acct.address] = balanceList[acct.address].sub(gasCost); } @@ -445,7 +454,11 @@ describe("FeePoolV2", function () { // ------------------------------------------- for (const acct of accounts) { - expect((await ethers.provider.getBalance(acct.address)).sub(BigNumber.from(balanceList[acct.address]))).to.equal( + expect( + (await ethers.provider.getBalance(acct.address)).sub( + BigNumber.from(balanceList[acct.address]) + ) + ).to.equal( tokensPerUserPerWeek[acct.address].reduce( (a: BigNumber, b: BigNumber) => a.add(b), BigNumber.from("0") diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts index 12da2b2d..39f176ed 100644 --- a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts +++ b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts @@ -210,23 +210,23 @@ describe("ScoreRegistry", function () { }); describe("Gauge Integral Calculations", function () { - /** - * テスト: Gauge Integral Calculations - * 1. AliceとBobの保有量と積分の初期化を行う。 - * 2. 現在のブロックのタイムスタンプと初期レートを取得する。 - * 3. scoreWeightControllerを使って、scoreRegistryにタイプを追加し、その重みを変更する。 - * 4. AliceとBobがそれぞれETHをデポジットし、流動性を提供する。 - * 5. 積分の更新処理を行うための補助関数「update_integral」を定義する。 - * 6. Bobが預金または引き出しを繰り返し行い、Aliceがそれをランダムに行う40回のループを開始する。 - * a. ランダムな時間を経過させるシミュレーションを行う。 - * b. Bobがランダムに預金または引き出しを行う。 - * c. 20%の確率でAliceも預金または引き出しを行う。 - * d. 同じ秒数でのユーザーチェックポイントの更新が影響しないことを確認する。 - * e. AliceとBobの保有量が正しいことを確認する。 - * f. もう一度、ランダムな時間を経過させるシミュレーションを行う。 - * g. Aliceのユーザーチェックポイントを更新し、積分を更新する。 - * 7. Aliceの報酬が正しいことを確認する。 - */ + /** + * テスト: Gauge Integral Calculations + * 1. AliceとBobの保有量と積分の初期化を行う。 + * 2. 現在のブロックのタイムスタンプと初期レートを取得する。 + * 3. scoreWeightControllerを使って、scoreRegistryにタイプを追加し、その重みを変更する。 + * 4. AliceとBobがそれぞれETHをデポジットし、流動性を提供する。 + * 5. 積分の更新処理を行うための補助関数「update_integral」を定義する。 + * 6. Bobが預金または引き出しを繰り返し行い、Aliceがそれをランダムに行う40回のループを開始する。 + * a. ランダムな時間を経過させるシミュレーションを行う。 + * b. Bobがランダムに預金または引き出しを行う。 + * c. 20%の確率でAliceも預金または引き出しを行う。 + * d. 同じ秒数でのユーザーチェックポイントの更新が影響しないことを確認する。 + * e. AliceとBobの保有量が正しいことを確認する。 + * f. もう一度、ランダムな時間を経過させるシミュレーションを行う。 + * g. Aliceのユーザーチェックポイントを更新し、積分を更新する。 + * 7. Aliceの報酬が正しいことを確認する。 + */ it("should correctly calculate user integrals over randomized actions", async () => { // AliceとBobの保有量と積分を初期化 let alice_staked = zero; @@ -290,24 +290,25 @@ describe("ScoreRegistry", function () { let is_alice = Math.random() < 0.2; // ランダムな時間経過をシミュレート - let dt = randomValue(1, Math.floor(year / 5)) + let dt = randomValue(1, Math.floor(year / 5)); await time.increase(dt); // Bobの処理 let is_withdraw = i > 0 && Math.random() < 0.5; - const bob_balance = Number(await CJPY.balanceOf(accounts[2].address)) + const bob_balance = Number(await CJPY.balanceOf(accounts[2].address)); if (is_withdraw) { // 引き出し処理 - let amount = randomBigValue(1, bob_balance + 1) + let amount = randomBigValue(1, bob_balance + 1); await yamato.connect(accounts[2]).repay(amount); await update_integral(); bob_staked = bob_staked.sub(amount); } else { // 預金処理 - let amount = randomBigValue(1 + MIN_AMOUNT, Math.floor(bob_balance / 10) + 1 + MIN_AMOUNT); - await yamato - .connect(accounts[2]) - .borrow(amount); + let amount = randomBigValue( + 1 + MIN_AMOUNT, + Math.floor(bob_balance / 10) + 1 + MIN_AMOUNT + ); + await yamato.connect(accounts[2]).borrow(amount); await update_integral(); bob_staked = bob_staked.add(fee(amount)); @@ -315,20 +316,26 @@ describe("ScoreRegistry", function () { // Aliceの処理 if (is_alice) { - const alice_balance = Number(await CJPY.balanceOf(accounts[1].address)) + const alice_balance = Number( + await CJPY.balanceOf(accounts[1].address) + ); let is_withdraw_alice = alice_balance > 0 && Math.random() > 0.5; if (is_withdraw_alice) { // 払い戻し処理 - let amount_alice = randomBigValue(1, Math.floor(alice_balance / 10) + 1) + let amount_alice = randomBigValue( + 1, + Math.floor(alice_balance / 10) + 1 + ); await yamato.connect(accounts[1]).repay(amount_alice); await update_integral(); alice_staked = alice_staked.sub(amount_alice); } else { // 借入処理 - let amount_alice = randomBigValue(1 + MIN_AMOUNT, alice_balance + 1 + MIN_AMOUNT) - await yamato - .connect(accounts[1]) - .borrow(amount_alice); + let amount_alice = randomBigValue( + 1 + MIN_AMOUNT, + alice_balance + 1 + MIN_AMOUNT + ); + await yamato.connect(accounts[1]).borrow(amount_alice); await update_integral(); alice_staked = alice_staked.add(fee(amount_alice)); @@ -351,19 +358,19 @@ describe("ScoreRegistry", function () { expect(await CJPY.balanceOf(accounts[1].address)).to.equal( alice_staked ); - expect(await CJPY.balanceOf(accounts[2].address)).to.equal( - bob_staked - ); + expect(await CJPY.balanceOf(accounts[2].address)).to.equal(bob_staked); // ランダムな時間経過をさらにシミュレート - dt = randomValue(1, Math.floor(year / 20)) + dt = randomValue(1, Math.floor(year / 20)); await time.increase(dt); await scoreRegistry .connect(accounts[1]) .userCheckpoint(accounts[1].address); await update_integral(); - const reward = await scoreRegistry.integrateFraction(accounts[1].address); + const reward = await scoreRegistry.integrateFraction( + accounts[1].address + ); expect(approx(reward, integral, Constants.ten_to_the_18)).to.be.true; } }); @@ -403,17 +410,13 @@ describe("ScoreRegistry", function () { await YMT.connect(accounts[2]).approve(veYMT.address, MAX_UINT256); // Aliceがescrowにデポジットする。AliceはBOOSTを持っていることになる - let t = await time.latest() + let t = await time.latest(); - await veYMT - .connect(accounts[1]) - .createLock(ten_to_the_20, t + (week * 2)); + await veYMT.connect(accounts[1]).createLock(ten_to_the_20, t + week * 2); // ETHをdeposit for (let i = 0; i < 3; i++) { - await yamato - .connect(accounts[i]) - .deposit({ value: ten_to_the_20 }); + await yamato.connect(accounts[i]).deposit({ value: ten_to_the_20 }); } // AliceとBobが一部の流動性をデポジットする await ethers.provider.send("evm_setAutomine", [false]); @@ -421,7 +424,7 @@ describe("ScoreRegistry", function () { await yamato.connect(accounts[2]).borrow(ten_to_the_21); await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_setAutomine", [true]); - let now = await time.latest() + let now = await time.latest(); // 現在、Aliceは投票ロックを持っているが、Bobは持っていないことを確認する expect( @@ -438,8 +441,12 @@ describe("ScoreRegistry", function () { // チェックポイント更新 await ethers.provider.send("evm_setAutomine", [false]); - await scoreRegistry.connect(accounts[2]).userCheckpoint(accounts[2].address); - await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + await scoreRegistry + .connect(accounts[2]) + .userCheckpoint(accounts[2].address); + await scoreRegistry + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_setAutomine", [true]); @@ -456,7 +463,9 @@ describe("ScoreRegistry", function () { let rewards_alice = await scoreRegistry.integrateFraction( accounts[1].address ); - let rewards_bob = await scoreRegistry.integrateFraction(accounts[2].address); + let rewards_bob = await scoreRegistry.integrateFraction( + accounts[2].address + ); expect( rewards_alice.mul(BigNumber.from("10000000000000000")).div(rewards_bob) @@ -468,14 +477,20 @@ describe("ScoreRegistry", function () { await time.setNextBlockTimestamp(now + week * 4); await ethers.provider.send("evm_setAutomine", [false]); - await scoreRegistry.connect(accounts[2]).userCheckpoint(accounts[2].address); - await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + await scoreRegistry + .connect(accounts[2]) + .userCheckpoint(accounts[2].address); + await scoreRegistry + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_setAutomine", [true]); let old_rewards_alice = rewards_alice; let old_rewards_bob = rewards_bob; // 今、AliceはBobと同じ量を獲得した - rewards_alice = await scoreRegistry.integrateFraction(accounts[1].address); + rewards_alice = await scoreRegistry.integrateFraction( + accounts[1].address + ); rewards_bob = await scoreRegistry.integrateFraction(accounts[2].address); let d_alice = rewards_alice.sub(old_rewards_alice); @@ -484,5 +499,4 @@ describe("ScoreRegistry", function () { expect(d_alice.sub(d_bob)).to.equal(zero); }); }); - }); diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index 82513cb1..313c9303 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -7,10 +7,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { - YMT, - YMT__factory, -} from "../../../../typechain"; +import { YMT, YMT__factory } from "../../../../typechain"; import Constants from "../../Constants"; const year = Constants.year; @@ -37,7 +34,6 @@ describe("YMT", function () { }); describe("YMT Mint Integration Tests", function () { - // 指定された期間に応じて正しい量のトークンを発行する it("should mint the correct amount of tokens", async function () { const duration = year; @@ -84,7 +80,7 @@ describe("YMT", function () { for (const duration of durations) { await time.increase(duration); - if (await time.latest() - epochStart > year) { + if ((await time.latest()) - epochStart > year) { await YMT.updateMiningParameters(); epochStart = Number(await YMT.startEpochTime()); } diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index 62cabfe0..7e14ed3e 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -7,10 +7,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { - YMT, - YMT__factory, -} from "../../../../typechain"; +import { YMT, YMT__factory } from "../../../../typechain"; import Constants from "../../Constants"; const year = Constants.year; @@ -20,9 +17,7 @@ const ten_to_the_16 = Constants.ten_to_the_16; // Constants const INITIAL_RATE = BigNumber.from(55000000); -const YEAR_1_SUPPLY = INITIAL_RATE.mul(ten_to_the_18) - .div(YEAR) - .mul(YEAR); +const YEAR_1_SUPPLY = INITIAL_RATE.mul(ten_to_the_18).div(YEAR).mul(YEAR); const INITIAL_SUPPLY = Constants.INITIAL_SUPPLY; describe("YMT", function () { @@ -76,10 +71,7 @@ describe("YMT", function () { S = S.add( YEAR_1_SUPPLY.div(YEAR) .mul(q.pow(epoch)) - .mul( - await time.latest() - - Number(await YMT.startEpochTime()) - ) + .mul((await time.latest()) - Number(await YMT.startEpochTime())) ); return S; @@ -102,9 +94,7 @@ describe("YMT", function () { const availableSupply = await YMT.availableSupply(); const mintable = await YMT.mintableInTimeframe(t0, t1); expect( - availableSupply - .sub(INITIAL_SUPPLY.mul(ten_to_the_18)) - .gte(mintable) + availableSupply.sub(INITIAL_SUPPLY.mul(ten_to_the_18)).gte(mintable) ).to.equal(true); if (t1 == t0) { expect(mintable).to.equal(BigNumber.from(0)); @@ -121,11 +111,7 @@ describe("YMT", function () { // Replace this with the actual theoretical supply calculation // const theoreticalSupply = BigNumber.from("EXPECTED_SUPPLY_CALCULATION"); expect( - approx( - await theoreticalSupply(YMT), - availableSupply, - ten_to_the_16 - ) + approx(await theoreticalSupply(YMT), availableSupply, ten_to_the_16) ).to.equal(true); }); @@ -165,8 +151,7 @@ describe("YMT", function () { const mintable = await YMT.mintableInTimeframe(start, end); if (startEpoch.eq(endEpoch)) { const expectedMintable = rate.mul(end.sub(start)); - expect(approx(mintable, expectedMintable, ten_to_the_16)).to - .be.true; + expect(approx(mintable, expectedMintable, ten_to_the_16)).to.be.true; } else { expect(mintable.lt(rate.mul(end))).to.be.true; } @@ -181,9 +166,7 @@ describe("YMT", function () { await time.increase(duration); - const now = BigNumber.from( - await time.latest() - ); + const now = BigNumber.from(await time.latest()); const expected = initialSupply.add(now.sub(creationTime).mul(rate)); expect(await YMT.availableSupply()).to.equal(expected); }); diff --git a/test/v15/integration/YmtMinter/components.test.ts b/test/v15/integration/YmtMinter/components.test.ts index bd9e3708..80994145 100644 --- a/test/v15/integration/YmtMinter/components.test.ts +++ b/test/v15/integration/YmtMinter/components.test.ts @@ -198,9 +198,7 @@ describe("YmtMinter components", function () { ten_to_the_18 ); for (let i = 0; i < 4; i++) { - await yamato - .connect(accounts[i]) - .deposit({ value: ten_to_the_20 }); + await yamato.connect(accounts[i]).deposit({ value: ten_to_the_20 }); } }); @@ -221,9 +219,7 @@ describe("YmtMinter components", function () { await time.increase(week); for (let i = 0; i < 3; i++) { - await yamato - .connect(accounts[i + 1]) - .borrow(ten_to_the_18); + await yamato.connect(accounts[i + 1]).borrow(ten_to_the_18); depositTime.push(await time.latest()); // await showGaugeInfo(); diff --git a/test/v15/integration/YmtMinter/minterIntegration.test.ts b/test/v15/integration/YmtMinter/minterIntegration.test.ts index 183b6bb0..500e66f4 100644 --- a/test/v15/integration/YmtMinter/minterIntegration.test.ts +++ b/test/v15/integration/YmtMinter/minterIntegration.test.ts @@ -188,14 +188,16 @@ describe("YmtMinter integration", function () { await yamato.deposit({ value: ethers.utils.parseEther("100") }); await yamato.borrow(ethers.utils.parseEther("100000")); - for(let i = 0; i < 5; i++){ - await CJPY.transfer(accounts[i + 1].address, ethers.utils.parseEther("10000")); - } - await scoreWeightController - .addCurrency( - scoreRegistry.address, - ten_to_the_18 + for (let i = 0; i < 5; i++) { + await CJPY.transfer( + accounts[i + 1].address, + ethers.utils.parseEther("10000") ); + } + await scoreWeightController.addCurrency( + scoreRegistry.address, + ten_to_the_18 + ); }); beforeEach(async () => { @@ -206,10 +208,10 @@ describe("YmtMinter integration", function () { await snapshot.restore(); }); - function getICR(pledge, price){ - const {coll, debt} = pledge; - const collInCurrency = (coll.mul(price)).div(ten_to_the_18); - return (BigNumber.from("10000").mul(collInCurrency)).div(debt); + function getICR(pledge, price) { + const { coll, debt } = pledge; + const collInCurrency = coll.mul(price).div(ten_to_the_18); + return BigNumber.from("10000").mul(collInCurrency).div(debt); } function calculateCoefficient(collateralRatio) { @@ -218,7 +220,7 @@ describe("YmtMinter integration", function () { if (collateralRatio >= 15000) return BigNumber.from("15"); if (collateralRatio >= 13000) return BigNumber.from("10"); return BigNumber.from("0"); -} + } function approxEqual(actual, expected, tolerance) { // 差の絶対値を計算 @@ -233,7 +235,7 @@ describe("YmtMinter integration", function () { const amount = "100000"; await YMT.setMinter(ymtMinter.address); const ethAmounts = ["1.5", "1.25", "1", "0.75", "0.65"]; - for(let i = 0; i < 5; i++){ + for (let i = 0; i < 5; i++) { await yamato .connect(accounts[i + 1]) .deposit({ value: ethers.utils.parseEther(ethAmounts[i]) }); @@ -243,41 +245,48 @@ describe("YmtMinter integration", function () { await time.increase(week); // Bob and Charlie deposit to gauges with different weights - for(let i = 0; i < 5; i++){ + for (let i = 0; i < 5; i++) { await yamato .connect(accounts[i + 1]) .borrow(ethers.utils.parseEther(amount)); } // mockFeed.fetchPrice.returns(PRICE = BigNumber.from(180000).mul(1e18 + "")); await time.increase(month); - + const ICRs = []; - for(let i = 0; i < 5; i++){ + for (let i = 0; i < 5; i++) { const pledge = await yamato.getPledge(accounts[i + 1].address); ICRs.push(Number(getICR(pledge, PRICE))); - await yamato.connect(accounts[i + 1]).repay(ethers.utils.parseEther(amount)); + await yamato + .connect(accounts[i + 1]) + .repay(ethers.utils.parseEther(amount)); } // Claim for Bob now await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); const bobTokens = await YMT.balanceOf(accounts[1].address); await time.increase(month); - + // This won't give anything await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); expect(await YMT.balanceOf(accounts[1].address)).to.equal(bobTokens); let tokens = []; - for(let i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { await ymtMinter.connect(accounts[i + 1]).mint(scoreRegistry.address); const token = await YMT.balanceOf(accounts[i + 1].address); console.log("tokenBalance:", Number(token)); tokens.push(token); } - for(let i = 0; i < 5; i++) { - const ratio = calculateCoefficient(ICRs[i]) - expect(approxEqual(tokens[i], (tokens[4].mul(ratio)).div(BigNumber.from("10")), BigNumber.from(10).pow(19))).to.be.true; + for (let i = 0; i < 5; i++) { + const ratio = calculateCoefficient(ICRs[i]); + expect( + approxEqual( + tokens[i], + tokens[4].mul(ratio).div(BigNumber.from("10")), + BigNumber.from(10).pow(19) + ) + ).to.be.true; } }); }); - diff --git a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts index 1f61eb4d..de7d90af 100644 --- a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts +++ b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts @@ -43,7 +43,9 @@ describe("veYMT", function () { before(async () => { accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); - token = await (await ethers.getContractFactory("MockToken")).deploy("Test Token", "TST", 18); + token = await (( + await ethers.getContractFactory("MockToken") + )).deploy("Test Token", "TST", 18); veYMT = await (( await ethers.getContractFactory("veYMT") @@ -54,9 +56,7 @@ describe("veYMT", function () { await token .connect(accounts[i]) ._mintForTesting(accounts[i].address, ten_to_the_40); - await token - .connect(accounts[i]) - .approve(veYMT.address, NAX_UINT256); + await token.connect(accounts[i]).approve(veYMT.address, NAX_UINT256); } }); diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v15/integration/veYMT/votingEscrow.test.ts index 051c344c..914950fb 100644 --- a/test/v15/integration/veYMT/votingEscrow.test.ts +++ b/test/v15/integration/veYMT/votingEscrow.test.ts @@ -120,10 +120,10 @@ describe("veYMT", function () { const totalSupply = await veYMT["totalSupply()"](); const aliceBalance = await veYMT["balanceOf(address)"](alice.address); - expect(approx(totalSupply, amount.div(MAXTIME).mul(week - 2 * hour), TOL)).to - .be.true; - expect(approx(aliceBalance, amount.div(MAXTIME).mul(week - 2 * hour), TOL)).to - .be.true; + expect(approx(totalSupply, amount.div(MAXTIME).mul(week - 2 * hour), TOL)) + .to.be.true; + expect(approx(aliceBalance, amount.div(MAXTIME).mul(week - 2 * hour), TOL)) + .to.be.true; expect(await veYMT["balanceOf(address)"](bob.address)).to.equal(0); t0 = await time.latest(); @@ -362,7 +362,8 @@ describe("veYMT", function () { stages["alice_deposit"].blockNumber ); - expect(approx(wAlice, amount.div(MAXTIME).mul(week - hour), TOL)).to.be.true; + expect(approx(wAlice, amount.div(MAXTIME).mul(week - hour), TOL)).to.be + .true; expect( await veYMT.balanceOfAt(bob.address, stages["alice_deposit"].blockNumber) ).to.equal(0); diff --git a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts index 373bac34..e8230c0a 100644 --- a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts +++ b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts @@ -32,9 +32,7 @@ describe("veYMT", function () { await ethers.getContractFactory("veYMT") )).deploy(YMT.address); - await YMT - .connect(accounts[0]) - .approve(veYMT.address, ten_to_the_18); + await YMT.connect(accounts[0]).approve(veYMT.address, ten_to_the_18); }); beforeEach(async () => { @@ -87,10 +85,7 @@ describe("veYMT", function () { )} Week, extend: ${Math.floor(st_extend / week)} Week`, async () => { await veYMT .connect(accounts[0]) - .createLock( - ten_to_the_18, - (await time.latest()) + st_initial - ); + .createLock(ten_to_the_18, (await time.latest()) + st_initial); const initialUnlock = ( await veYMT.locked(accounts[0].address) @@ -103,7 +98,9 @@ describe("veYMT", function () { await veYMT.locked(accounts[0].address) )[1]; - await time.increase(Number(extendedActualUnlock) - (await time.latest()) - 5); + await time.increase( + Number(extendedActualUnlock) - (await time.latest()) - 5 + ); expect( await veYMT["balanceOf(address)"](accounts[0].address) diff --git a/test/v15/testHelpers.ts b/test/v15/testHelpers.ts index 5d12f357..ea5770e5 100644 --- a/test/v15/testHelpers.ts +++ b/test/v15/testHelpers.ts @@ -36,7 +36,11 @@ export function fee(amount: BigNumber): BigNumber { return amount.sub(amount.div(1000)); } -export function approx(value: BigNumber, target: BigNumber, tol: BigNumber): boolean { +export function approx( + value: BigNumber, + target: BigNumber, + tol: BigNumber +): boolean { if (value.isZero() && target.isZero()) { return true; } @@ -62,6 +66,6 @@ export function generateUniqueRandomNumbers( } export async function gasCostOf(tx) { - const receipt = await tx.wait() - return receipt.gasUsed.mul(receipt.effectiveGasPrice) -} \ No newline at end of file + const receipt = await tx.wait(); + return receipt.gasUsed.mul(receipt.effectiveGasPrice); +} diff --git a/test/v15/unitary/FeePool/claimMany.test.ts b/test/v15/unitary/FeePool/claimMany.test.ts index a4f4dcc1..f4587ed1 100644 --- a/test/v15/unitary/FeePool/claimMany.test.ts +++ b/test/v15/unitary/FeePool/claimMany.test.ts @@ -85,7 +85,9 @@ describe("FeePoolV2", () => { ) ); let balances = [ - (await ethers.provider.getBalance(alice.address)).add(await gasCostOf(tx1)), + (await ethers.provider.getBalance(alice.address)).add( + await gasCostOf(tx1) + ), await ethers.provider.getBalance(bob.address), await ethers.provider.getBalance(charlie.address), ]; @@ -95,13 +97,18 @@ describe("FeePoolV2", () => { const gas = []; for (let acct of [alice, bob, charlie]) { const tx2 = await feePool.connect(acct)["claim()"](); - gas.push(await gasCostOf(tx2)) + gas.push(await gasCostOf(tx2)); } - expect((await ethers.provider.getBalance(alice.address)).add(gas[0])).to.be.eq(balances[0]); - expect((await ethers.provider.getBalance(bob.address)).add(gas[1])).to.be.eq(balances[1]); - expect((await ethers.provider.getBalance(charlie.address)).add(gas[2])).to.be.eq(balances[2]); - + expect( + (await ethers.provider.getBalance(alice.address)).add(gas[0]) + ).to.be.eq(balances[0]); + expect( + (await ethers.provider.getBalance(bob.address)).add(gas[1]) + ).to.be.eq(balances[1]); + expect( + (await ethers.provider.getBalance(charlie.address)).add(gas[2]) + ).to.be.eq(balances[2]); }); it("should correctly claim multiple times for the same account", async function () { @@ -115,7 +122,9 @@ describe("FeePoolV2", () => { expect(expected).to.above(0); const balanceBefore = await ethers.provider.getBalance(alice.address); - const tx3 = await feePool.connect(alice).claimMany(Array(20).fill(alice.address)); + const tx3 = await feePool + .connect(alice) + .claimMany(Array(20).fill(alice.address)); const balanceAfter = await ethers.provider.getBalance(alice.address); const gas = await gasCostOf(tx3); diff --git a/test/v15/unitary/FeePool/feePool.test.ts b/test/v15/unitary/FeePool/feePool.test.ts index 73b8f86e..f7a8c5c2 100644 --- a/test/v15/unitary/FeePool/feePool.test.ts +++ b/test/v15/unitary/FeePool/feePool.test.ts @@ -85,7 +85,9 @@ describe("FeePoolV2", () => { const balanceBefore = await ethers.provider.getBalance(alice.address); const tx = await feePool.connect(alice)["claim()"](); const balanceAfter = await ethers.provider.getBalance(alice.address); - expect(balanceAfter.sub(balanceBefore).add(await gasCostOf(tx))).to.be.eq(0); + expect(balanceAfter.sub(balanceBefore).add(await gasCostOf(tx))).to.be.eq( + 0 + ); }); it("should correctly claim during deposit", async function () { @@ -121,7 +123,7 @@ describe("FeePoolV2", () => { await feePool.checkpointToken(); const balanceAlice = await ethers.provider.getBalance(alice.address); const tx = await feePool.connect(alice)["claim()"](); - const gasCost = await gasCostOf(tx) + const gasCost = await gasCostOf(tx); const afterBalanceAlice = await ethers.provider.getBalance(alice.address); expect(afterBalanceAlice.sub(balanceAlice).add(gasCost)).to.be.closeTo( @@ -160,10 +162,10 @@ describe("FeePoolV2", () => { await feePool.checkpointToken(); let balanceAlice = await ethers.provider.getBalance(alice.address); const tx = await feePool.connect(alice)["claim()"](); - balanceAlice = (await ethers.provider.getBalance(alice.address)).sub(balanceAlice).add(await gasCostOf(tx)); - expect( - Number(balanceAlice) - 10 ** 18 - ).to.be.gte(10 ** 18); + balanceAlice = (await ethers.provider.getBalance(alice.address)) + .sub(balanceAlice) + .add(await gasCostOf(tx)); + expect(Number(balanceAlice) - 10 ** 18).to.be.gte(10 ** 18); }); it("should correctly claim on double deposit", async function () { @@ -261,14 +263,15 @@ describe("FeePoolV2", () => { const txAlice = await feePool.connect(alice)["claim()"](); const txBob = await feePool.connect(bob)["claim()"](); - balanceAlice = (await ethers.provider.getBalance(alice.address)).sub(balanceAlice).add(await gasCostOf(txAlice)); - balanceBob = (await ethers.provider.getBalance(bob.address)).sub(balanceBob).add(await gasCostOf(txBob)); + balanceAlice = (await ethers.provider.getBalance(alice.address)) + .sub(balanceAlice) + .add(await gasCostOf(txAlice)); + balanceBob = (await ethers.provider.getBalance(bob.address)) + .sub(balanceBob) + .add(await gasCostOf(txBob)); expect(balanceAlice).to.equal(balanceBob); - expect(balanceAlice.add(balanceBob)).to.be.closeTo( - ten_to_the_19, - 20 - ); + expect(balanceAlice.add(balanceBob)).to.be.closeTo(ten_to_the_19, 20); }); }); }); diff --git a/test/v15/unitary/FeePool/killFeeDistro.test.ts b/test/v15/unitary/FeePool/killFeeDistro.test.ts index 277cebde..bc2291a0 100644 --- a/test/v15/unitary/FeePool/killFeeDistro.test.ts +++ b/test/v15/unitary/FeePool/killFeeDistro.test.ts @@ -66,7 +66,7 @@ describe("FeePoolV2", () => { await feePool.connect(alice).killMe(); expect(await feePool.isKilled()).to.be.true; }); - + it("should kill the fee pool multiple times", async function () { // 複数回のFeePool Killテスト await feePool.connect(alice).killMe(); @@ -86,7 +86,9 @@ describe("FeePoolV2", () => { balance = balance.sub(await gasCostOf(tx)); expect(await feePool.governance()).to.equal(alice.address); - expect(await ethers.provider.getBalance(alice.address)).to.equal(balance.add(31337)); + expect(await ethers.provider.getBalance(alice.address)).to.equal( + balance.add(31337) + ); }); it("should transfer tokens on multiple pool killings", async function () { @@ -109,7 +111,9 @@ describe("FeePoolV2", () => { balance = balance.sub(await gasCostOf(tx2)); expect(await feePool.governance()).to.equal(alice.address); - expect(await ethers.provider.getBalance(alice.address)).to.equal(balance.add(40000)); + expect(await ethers.provider.getBalance(alice.address)).to.equal( + balance.add(40000) + ); }); for (let idx = 1; idx <= 2; idx++) { diff --git a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts index 5e5c67c2..e269822d 100644 --- a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts +++ b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts @@ -199,17 +199,23 @@ describe("ScoreRegistry checkpoint", function () { // ユーザーのチェックポイント機能をテスト it("Test user checkpoint", async function () { // userCheckpoint関数がスコアレジストリに存在すると仮定 - await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + await scoreRegistry + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); }); // 新しい期間でのユーザーチェックポイント機能をテスト it("Test user checkpoint in new period", async function () { - await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + await scoreRegistry + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); // ブロックチェーン上で時間を進める await time.increase(year * 1.1); - await scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[1].address); + await scoreRegistry + .connect(accounts[1]) + .userCheckpoint(accounts[1].address); }); // 誤ったアカウントでのユーザーチェックポイント機能をテスト @@ -219,4 +225,4 @@ describe("ScoreRegistry checkpoint", function () { scoreRegistry.connect(accounts[1]).userCheckpoint(accounts[2].address) ).to.be.revertedWith("dev: unauthorized"); }); -}); \ No newline at end of file +}); diff --git a/test/v15/unitary/ScoreRegistry/kick.test.ts b/test/v15/unitary/ScoreRegistry/kick.test.ts index 998eb718..a690332e 100644 --- a/test/v15/unitary/ScoreRegistry/kick.test.ts +++ b/test/v15/unitary/ScoreRegistry/kick.test.ts @@ -221,14 +221,9 @@ describe("ScoreRegistry kick", function () { await YMT.connect(accounts[1]).approve(veYMT.address, MAX_UINT256); await veYMT .connect(accounts[1]) - .createLock( - LOCK_AMOUNT, - await time.latest() + 4 * week - ); + .createLock(LOCK_AMOUNT, (await time.latest()) + 4 * week); - await yamato - .connect(accounts[1]) - .borrow(DEPOSIT_AMOUNT); + await yamato.connect(accounts[1]).borrow(DEPOSIT_AMOUNT); // ゲージ内のアリスのワーキングバランスをチェック expect(await scoreRegistry.workingBalances(accounts[1].address)).to.equal( @@ -259,4 +254,4 @@ describe("ScoreRegistry kick", function () { scoreRegistry.connect(accounts[1]).kick(accounts[1].address) ).to.be.revertedWith("Not needed"); }); -}); \ No newline at end of file +}); diff --git a/test/v15/unitary/YMT/burn.test.ts b/test/v15/unitary/YMT/burn.test.ts index a0fba260..de55ff06 100644 --- a/test/v15/unitary/YMT/burn.test.ts +++ b/test/v15/unitary/YMT/burn.test.ts @@ -6,10 +6,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { - YMT, - YMT__factory, -} from "../../../../typechain"; +import { YMT, YMT__factory } from "../../../../typechain"; describe("YMT", function () { let accounts: SignerWithAddress[]; @@ -30,7 +27,6 @@ describe("YMT", function () { }); describe("YMT Burn Function Tests", function () { - // 特定の量のトークンを正しくburnさせるテスト it("should correctly burn a specific amount of tokens", async function () { const balance: BigNumber = await YMT.balanceOf(accounts[0].address); @@ -71,9 +67,9 @@ describe("YMT", function () { it("should revert if burning more than the total supply", async function () { const initialSupply: BigNumber = await YMT.totalSupply(); - await expect( - YMT.burn(initialSupply.add(1)) - ).to.be.revertedWith("ERC20: burn amount exceeds balance"); + await expect(YMT.burn(initialSupply.add(1))).to.be.revertedWith( + "ERC20: burn amount exceeds balance" + ); }); }); }); diff --git a/test/v15/unitary/YMT/epochTimeSupply.test.ts b/test/v15/unitary/YMT/epochTimeSupply.test.ts index ef92f776..bfb09c9a 100644 --- a/test/v15/unitary/YMT/epochTimeSupply.test.ts +++ b/test/v15/unitary/YMT/epochTimeSupply.test.ts @@ -6,10 +6,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; -import { - YMT, - YMT__factory, -} from "../../../../typechain"; +import { YMT, YMT__factory } from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -33,7 +30,6 @@ describe("YMT", function () { }); describe("YMT Epoch Time and Supply Tests", function () { - // 開始エポック時間が正しく更新されるかを確認するテスト it("should update start epoch time correctly", async function () { const creationTime: BigNumber = await YMT.startEpochTime(); @@ -67,13 +63,17 @@ describe("YMT", function () { const now = BigNumber.from(await time.latest()); const newEpoch = creationTime.add(YEAR).sub(now); await time.increase(newEpoch.sub(BigNumber.from("3"))); - await expect(YMT.updateMiningParameters()).to.be.revertedWith("dev: too soon!"); + await expect(YMT.updateMiningParameters()).to.be.revertedWith( + "dev: too soon!" + ); }); // 終了時間が開始時間より前の場合にmintable timeframeがリバートされることを確認するテスト it("should revert mintable timeframe if end is before start", async function () { const creationTime = await YMT.startEpochTime(); - await expect(YMT.mintableInTimeframe(creationTime.add(1), creationTime)).to.be.revertedWith("dev: start > end"); + await expect( + YMT.mintableInTimeframe(creationTime.add(1), creationTime) + ).to.be.revertedWith("dev: start > end"); }); // 複数のエポックにわたるmintableな量を計算するテスト @@ -81,10 +81,22 @@ describe("YMT", function () { const creationTime = await YMT.startEpochTime(); // Two epochs should not raise - await YMT.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("19").div(BigNumber.from("10")))); + await YMT.mintableInTimeframe( + creationTime, + creationTime + .add(YEAR) + .mul(BigNumber.from("19").div(BigNumber.from("10"))) + ); // Three epochs should raise - await expect(YMT.mintableInTimeframe(creationTime, creationTime.add(YEAR).mul(BigNumber.from("21").div(BigNumber.from("10"))))).to.be.revertedWith("dev: too far in future"); + await expect( + YMT.mintableInTimeframe( + creationTime, + creationTime + .add(YEAR) + .mul(BigNumber.from("21").div(BigNumber.from("10"))) + ) + ).to.be.revertedWith("dev: too far in future"); }); // 利用可能な供給量が正しく計算されるかを確認するテスト diff --git a/test/v15/unitary/YMT/inflationDelay.test.ts b/test/v15/unitary/YMT/inflationDelay.test.ts index f7303a45..339e07e8 100644 --- a/test/v15/unitary/YMT/inflationDelay.test.ts +++ b/test/v15/unitary/YMT/inflationDelay.test.ts @@ -6,10 +6,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; -import { - YMT, - YMT__factory, -} from "../../../../typechain"; +import { YMT, YMT__factory } from "../../../../typechain"; import Constants from "../../Constants"; const YEAR = Constants.YEAR; @@ -31,7 +28,6 @@ describe("YMT", function () { }); describe("YMT Inflation Delay Tests", function () { - // 遅延後のレートが正しく計算されるかを確認するテスト it("should correctly calculate rate after delay", async function () { expect(await YMT.rate()).to.equal(0); diff --git a/test/v15/unitary/YMT/mint.test.ts b/test/v15/unitary/YMT/mint.test.ts index a967af38..e1630b08 100644 --- a/test/v15/unitary/YMT/mint.test.ts +++ b/test/v15/unitary/YMT/mint.test.ts @@ -7,10 +7,7 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { - YMT, - YMT__factory, -} from "../../../../typechain"; +import { YMT, YMT__factory } from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -37,7 +34,6 @@ describe("YMT", function () { }); describe("YMT Mint Tests", function () { - // 現在の供給量と期待される供給量が一致するかテストする it("should match the available supply with expected supply", async function () { const creationTime = await YMT.startEpochTime(); @@ -46,9 +42,7 @@ describe("YMT", function () { await time.increase(week); - const currentBlock = BigNumber.from( - await time.latest() - ); + const currentBlock = BigNumber.from(await time.latest()); const expected = initialSupply.add( currentBlock.sub(creationTime).mul(rate) ); @@ -64,9 +58,7 @@ describe("YMT", function () { await time.increase(week); - const currentTime = BigNumber.from( - await time.latest() - ); + const currentTime = BigNumber.from(await time.latest()); const amount = currentTime.sub(creationTime).mul(rate); await YMT.mint(accounts[1].address, amount); @@ -82,9 +74,7 @@ describe("YMT", function () { await time.increase(week); - const currentTime = BigNumber.from( - await time.latest() - ); + const currentTime = BigNumber.from(await time.latest()); const amount = currentTime.sub(creationTime).add(2).mul(rate); await expect(YMT.mint(accounts[1].address, amount)).to.be.revertedWith( "dev: exceeds allowable mint amount" @@ -104,7 +94,7 @@ describe("YMT", function () { await YMT.setMinter(accounts[0].address); await expect(YMT.mint(ZERO_ADDRESS, 0)).to.be.revertedWith( "dev: zero address" - ); + ); }); }); }); diff --git a/test/v15/unitary/YMT/setters.test.ts b/test/v15/unitary/YMT/setters.test.ts index dbb1f2ca..8e7d1333 100644 --- a/test/v15/unitary/YMT/setters.test.ts +++ b/test/v15/unitary/YMT/setters.test.ts @@ -5,10 +5,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { - YMT, - YMT__factory, -} from "../../../../typechain"; +import { YMT, YMT__factory } from "../../../../typechain"; describe("YMT", function () { let accounts: SignerWithAddress[]; @@ -29,7 +26,6 @@ describe("YMT", function () { }); describe("YMT Setters Tests", function () { - // 非管理者がymtMinterを設定できないことを確認するテスト it("non-admin should not be able to set ymtMinter", async function () { await expect( diff --git a/test/v15/unitary/YmtMinter/Minter.test.ts b/test/v15/unitary/YmtMinter/Minter.test.ts index 2c06d61a..c11ef2f8 100644 --- a/test/v15/unitary/YmtMinter/Minter.test.ts +++ b/test/v15/unitary/YmtMinter/Minter.test.ts @@ -213,12 +213,9 @@ describe("YmtMinter", function () { }); describe("YmtMinter Behavior", function () { - // 借入を行い、時間を進めた後にmintを実行 it("Basic mint functionality test", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_17); + await yamato.connect(accounts[1]).borrow(ten_to_the_17); await time.increase(month); @@ -234,9 +231,7 @@ describe("YmtMinter", function () { // 借入を行い、時間を進めた後にmintを実行 it("Mint immediately after setup", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_18); + await yamato.connect(accounts[1]).borrow(ten_to_the_18); let t0 = BigNumber.from(await time.latest()); let moment = t0.add(WEEK).div(WEEK).mul(WEEK).add("5"); @@ -257,9 +252,7 @@ describe("YmtMinter", function () { // 同じscoreRegistryでの複数のmint操作をテスト it("Multiple mint operations on the same scoreRegistry", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_18); + await yamato.connect(accounts[1]).borrow(ten_to_the_18); await time.increase(month); await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); let balance = await YMT.balanceOf(accounts[1].address); @@ -277,29 +270,29 @@ describe("YmtMinter", function () { // 引き出し後のmintをテスト it("Test minting after withdrawing", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_18); + await yamato.connect(accounts[1]).borrow(ten_to_the_18); await time.increase(week * 2); - const balance = await CJPY.connect(accounts[1]).balanceOf(accounts[1].address); + const balance = await CJPY.connect(accounts[1]).balanceOf( + accounts[1].address + ); await CJPY.transfer(accounts[1].address, ten_to_the_18.sub(balance)); await yamato.connect(accounts[1]).repay(ten_to_the_18); await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); - expect( - (await YMT.balanceOf(accounts[1].address)).gt(zero) - ).to.equal(true); + expect((await YMT.balanceOf(accounts[1].address)).gt(zero)).to.equal( + true + ); }); // 引き出し後の複数回のmintをテスト it("Test multiple mints after withdrawing", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_18); + await yamato.connect(accounts[1]).borrow(ten_to_the_18); await time.increase(10); - const CJPY_balance = await CJPY.connect(accounts[1]).balanceOf(accounts[1].address); + const CJPY_balance = await CJPY.connect(accounts[1]).balanceOf( + accounts[1].address + ); await CJPY.transfer(accounts[1].address, ten_to_the_18.sub(CJPY_balance)); await yamato.connect(accounts[1]).repay(ten_to_the_18); await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); @@ -330,9 +323,7 @@ describe("YmtMinter", function () { // インフレ開始前のmintをテスト it("Test minting before inflation begins", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_18); + await yamato.connect(accounts[1]).borrow(ten_to_the_18); const startEpochTime = await YMT.startEpochTime(); const currentTime = await time.latest(); const timeToSleep = startEpochTime.sub(currentTime).sub(5); @@ -340,12 +331,10 @@ describe("YmtMinter", function () { // 時間を戻す await ethers.provider.send("evm_increaseTime", [timeToSleep.toNumber()]); await ethers.provider.send("evm_mine", []); - + await ymtMinter.connect(accounts[1]).mint(scoreRegistry.address); - expect(await YMT.balanceOf(accounts[1].address)).to.equal( - zero - ); + expect(await YMT.balanceOf(accounts[1].address)).to.equal(zero); expect( await ymtMinter.minted(accounts[1].address, scoreRegistry.address) ).to.equal(zero); @@ -376,9 +365,7 @@ describe("YmtMinter", function () { // 他のユーザーのためにmintを行う関数をテスト it("Test minting on behalf of another user", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_17); + await yamato.connect(accounts[1]).borrow(ten_to_the_17); await time.increase(month); @@ -406,9 +393,7 @@ describe("YmtMinter", function () { // 承認なしでのmintFor関数をテスト it("Test mintFor function when not approved", async () => { - await yamato - .connect(accounts[1]) - .borrow(ten_to_the_17); + await yamato.connect(accounts[1]).borrow(ten_to_the_17); await time.increase(month); diff --git a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts index d9e9faec..8f3d3e52 100644 --- a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts +++ b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts @@ -52,8 +52,7 @@ describe("veYMT", () => { // commitTransferOwnershipテスト it("Successful commitment of transfer ownership", async function () { - await veYMT - .commitTransferOwnership(accounts[1].address); + await veYMT.commitTransferOwnership(accounts[1].address); expect(await veYMT.admin()).to.equal(accounts[0].address); expect(await veYMT.futureAdmin()).to.equal(accounts[1].address); @@ -61,8 +60,7 @@ describe("veYMT", () => { // commitTransferOwnershipテスト it("Successful application of transfer ownership", async function () { - await veYMT - .commitTransferOwnership(accounts[1].address); + await veYMT.commitTransferOwnership(accounts[1].address); await veYMT.applyTransferOwnership(); expect(await veYMT.admin()).to.equal(accounts[1].address); @@ -70,8 +68,8 @@ describe("veYMT", () => { // コミットなしでのapplyTransferOwnership関数テスト it("Apply transfer ownership without commit", async function () { - await expect( - veYMT.applyTransferOwnership() - ).to.be.revertedWith("admin not set"); + await expect(veYMT.applyTransferOwnership()).to.be.revertedWith( + "admin not set" + ); }); }); From 401090a0b610bcd88e5f97b302680cb2583d0384 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 17:25:39 +0900 Subject: [PATCH 115/412] Change hardhat testing to V1.5 --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 854917be..d4bbfbc3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -153,7 +153,7 @@ module.exports = { }, paths: { // tests: "./test/unit", - tests: "./test/fork", + tests: "./test/v15", }, defender: { apiKey: process.env.DEFENDER_TEAM_API_KEY, From 5bfc611416d7fb7fa65a67afd158a81893d14b7b Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 7 Dec 2023 17:30:07 +0900 Subject: [PATCH 116/412] Delete curve fork directory --- contracts/curveFork/CRV.sol | 241 ----- contracts/curveFork/FeeDistributor.sol | 625 ------------- contracts/curveFork/GaugeController.sol | 690 -------------- contracts/curveFork/LiquidityGaugeV6.sol | 756 --------------- contracts/curveFork/Minter.sol | 94 -- contracts/curveFork/VotingEscrow.sol | 863 ------------------ contracts/curveFork/interfaces/ICRV.sol | 13 - .../curveFork/interfaces/IGaugeController.sol | 24 - .../curveFork/interfaces/ILiquidityGauge.sol | 13 - contracts/curveFork/interfaces/IMinter.sol | 14 - .../interfaces/ISmartWalletChecker.sol | 10 - .../curveFork/interfaces/IVotingEscrow.sol | 25 - test/fork/Constants.ts | 52 -- test/fork/helper.ts | 67 -- .../ERC20CRV/mintIntegration.test.ts | 105 --- .../ERC20CRV/mintableInTimeframe.test.ts | 182 ---- .../FeeDistributor/checkpointTs.test.ts | 123 --- .../distributefFeesStateful.test.ts | 521 ----------- .../GaugeController/typesAndWeights.test.ts | 272 ------ .../GaugeController/voteWeight.test.ts | 340 ------- .../LiquidityGauge/liquidityGauge.test.ts | 397 -------- .../integration/Minter/components.test.ts | 351 ------- .../Minter/minterIntegration.test.ts | 195 ---- .../depositWithdrawVoting.test.ts | 339 ------- .../VotingEscrow/votingEscrow.test.ts | 568 ------------ .../zeroBalanceAtUnlockTime.test.ts | 116 --- test/fork/unitary/ERC20CRV/burn.test.ts | 68 -- .../unitary/ERC20CRV/epochTimeSupply.test.ts | 118 --- .../unitary/ERC20CRV/inflationDelay.test.ts | 80 -- test/fork/unitary/ERC20CRV/mint.test.ts | 99 -- test/fork/unitary/ERC20CRV/setters.test.ts | 49 - .../FeeDistributor/checkpoints.test.ts | 139 --- .../unitary/FeeDistributor/claimMany.test.ts | 157 ---- .../FeeDistributor/feeDistribution.test.ts | 285 ------ .../FeeDistributor/killFeeDistro.test.ts | 127 --- .../gaugecontrollerAdmin.test.ts | 67 -- .../GaugeController/gaugesWeights.test.ts | 138 --- .../GaugeController/timestamps.test.ts | 54 -- .../GaugeController/totalWeight.test.ts | 76 -- .../fork/unitary/GaugeController/vote.test.ts | 151 --- .../GaugeController/voteWeightUnitary.test.ts | 110 --- .../unitary/LiquidityGauge/checkpoint.test.ts | 50 - .../LiquidityGauge/depositWithdraw.test.ts | 114 --- test/fork/unitary/LiquidityGauge/kick.test.ts | 91 -- test/fork/unitary/Minter/Minter.test.ts | 372 -------- .../VotingEscrow/votingescrowAdmin.test.ts | 76 -- 46 files changed, 9417 deletions(-) delete mode 100644 contracts/curveFork/CRV.sol delete mode 100644 contracts/curveFork/FeeDistributor.sol delete mode 100644 contracts/curveFork/GaugeController.sol delete mode 100644 contracts/curveFork/LiquidityGaugeV6.sol delete mode 100644 contracts/curveFork/Minter.sol delete mode 100644 contracts/curveFork/VotingEscrow.sol delete mode 100644 contracts/curveFork/interfaces/ICRV.sol delete mode 100644 contracts/curveFork/interfaces/IGaugeController.sol delete mode 100644 contracts/curveFork/interfaces/ILiquidityGauge.sol delete mode 100644 contracts/curveFork/interfaces/IMinter.sol delete mode 100644 contracts/curveFork/interfaces/ISmartWalletChecker.sol delete mode 100644 contracts/curveFork/interfaces/IVotingEscrow.sol delete mode 100644 test/fork/Constants.ts delete mode 100644 test/fork/helper.ts delete mode 100644 test/fork/integration/ERC20CRV/mintIntegration.test.ts delete mode 100644 test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts delete mode 100644 test/fork/integration/FeeDistributor/checkpointTs.test.ts delete mode 100644 test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts delete mode 100644 test/fork/integration/GaugeController/typesAndWeights.test.ts delete mode 100644 test/fork/integration/GaugeController/voteWeight.test.ts delete mode 100644 test/fork/integration/LiquidityGauge/liquidityGauge.test.ts delete mode 100644 test/fork/integration/Minter/components.test.ts delete mode 100644 test/fork/integration/Minter/minterIntegration.test.ts delete mode 100644 test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts delete mode 100644 test/fork/integration/VotingEscrow/votingEscrow.test.ts delete mode 100644 test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts delete mode 100644 test/fork/unitary/ERC20CRV/burn.test.ts delete mode 100644 test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts delete mode 100644 test/fork/unitary/ERC20CRV/inflationDelay.test.ts delete mode 100644 test/fork/unitary/ERC20CRV/mint.test.ts delete mode 100644 test/fork/unitary/ERC20CRV/setters.test.ts delete mode 100644 test/fork/unitary/FeeDistributor/checkpoints.test.ts delete mode 100644 test/fork/unitary/FeeDistributor/claimMany.test.ts delete mode 100644 test/fork/unitary/FeeDistributor/feeDistribution.test.ts delete mode 100644 test/fork/unitary/FeeDistributor/killFeeDistro.test.ts delete mode 100644 test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts delete mode 100644 test/fork/unitary/GaugeController/gaugesWeights.test.ts delete mode 100644 test/fork/unitary/GaugeController/timestamps.test.ts delete mode 100644 test/fork/unitary/GaugeController/totalWeight.test.ts delete mode 100644 test/fork/unitary/GaugeController/vote.test.ts delete mode 100644 test/fork/unitary/GaugeController/voteWeightUnitary.test.ts delete mode 100644 test/fork/unitary/LiquidityGauge/checkpoint.test.ts delete mode 100644 test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts delete mode 100644 test/fork/unitary/LiquidityGauge/kick.test.ts delete mode 100644 test/fork/unitary/Minter/Minter.test.ts delete mode 100644 test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts diff --git a/contracts/curveFork/CRV.sol b/contracts/curveFork/CRV.sol deleted file mode 100644 index c2484650..00000000 --- a/contracts/curveFork/CRV.sol +++ /dev/null @@ -1,241 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.18; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -/// @title Curve DAO Token -/// @notice ERC20 with piecewise-linear mining supply. -contract CRV is ERC20 { - event UpdateMiningParameters(uint256 time, uint256 rate, uint256 supply); - event SetMinter(address minter); - event SetAdmin(address admin); - - address public minter; - address public admin; - - // General constants - uint256 constant YEAR = 365 days; - - // Allocation: - // ========= - // * shareholders - 30% - // * emplyees - 3% - // * DAO-controlled reserve - 5% - // * Early users - 5% - // == 43% == - // left for inflation: 57% - - // Supply parameters - uint256 constant INITIAL_SUPPLY = 1_303_030_303; - uint256 constant INITIAL_RATE = (274_815_283 * 10 ** 18) / YEAR; // leading to 43% premine - uint256 constant RATE_REDUCTION_TIME = YEAR; - uint256 constant RATE_REDUCTION_COEFFICIENT = 1189207115002721024; // 2 ** (1/4) * 1e18 - uint256 constant RATE_DENOMINATOR = 10 ** 18; - uint256 constant INFLATION_DELAY = 1 days; - - // Supply variables - int128 public miningEpoch; - uint256 public startEpochTime; - uint256 public rate; - - uint256 startEpochSupply; - - constructor() ERC20("Curve", "CRV") { - uint256 initSupply = INITIAL_SUPPLY * 10 ** decimals(); - _mint(msg.sender, initSupply); - - admin = msg.sender; - - startEpochTime = - block.timestamp + - INFLATION_DELAY - - RATE_REDUCTION_TIME; - miningEpoch = -1; - rate = 0; - startEpochSupply = initSupply; - } - - // @dev Update mining rate and supply at the start of the epoch - // Any modifying mining call must also call this - function _updateMiningParameters() internal { - uint256 _rate = rate; - uint256 _startEpochSupply = startEpochSupply; - - startEpochTime += RATE_REDUCTION_TIME; - miningEpoch += 1; - - if (_rate == 0) { - _rate = INITIAL_RATE; - } else { - _startEpochSupply += _rate * RATE_REDUCTION_TIME; - startEpochSupply = _startEpochSupply; - _rate = (_rate * RATE_DENOMINATOR) / RATE_REDUCTION_COEFFICIENT; - } - - rate = _rate; - - emit UpdateMiningParameters(block.timestamp, _rate, _startEpochSupply); - } - - // @notice Update mining rate and supply at the start of the epoch - // @dev Callable by any address, but only once per epoch - // Total supply becomes slightly larger if(this function is called late - function updateMiningParameters() external { - require( - block.timestamp >= startEpochTime + RATE_REDUCTION_TIME, - "dev: too soon!" - ); // dev: too soon! - _updateMiningParameters(); - } - - // @notice Get timestamp of the current mining epoch start - // while simultaneously updating mining parameters - // @return Timestamp of the epoch - function startEpochTimeWrite() external returns (uint256) { - uint256 _startEpochTime = startEpochTime; - if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { - _updateMiningParameters(); - return startEpochTime; - } else { - return _startEpochTime; - } - } - - // @notice Get timestamp of the next mining epoch start - // while simultaneously updating mining parameters - // @return Timestamp of the next epoch - function futureEpochTimeWrite() external returns (uint256) { - uint256 _startEpochTime = startEpochTime; - if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { - _updateMiningParameters(); - return startEpochTime + RATE_REDUCTION_TIME; - } else { - return _startEpochTime + RATE_REDUCTION_TIME; - } - } - - function _availableSupply() internal view returns (uint256) { - return startEpochSupply + (block.timestamp - startEpochTime) * rate; - } - - // @notice Current number of tokens in existence (claimed or unclaimed) - function availableSupply() external view returns (uint256) { - return _availableSupply(); - } - - // @notice How much supply is mintable from start timestamp till end timestamp - // @param start Start of the time interval (timestamp) - // @param end End of the time interval (timestamp) - // @return Tokens mintable from `start` till `end` - function mintableInTimeframe( - uint256 start, - uint256 end - ) external view returns (uint256) { - require(start <= end, "dev: start > end"); // dev: start > end - uint256 to_mint = 0; - uint256 currentEpochTime = startEpochTime; - uint256 currentRate = rate; - - // Special case if(end is in future (not yet minted) epoch - if (end > currentEpochTime + RATE_REDUCTION_TIME) { - currentEpochTime += RATE_REDUCTION_TIME; - currentRate = - (currentRate * RATE_DENOMINATOR) / - RATE_REDUCTION_COEFFICIENT; - } - - require( - end <= currentEpochTime + RATE_REDUCTION_TIME, - "dev: too far in future" - ); // dev: too far in future - - // Curve will not work in 1000 years. Darn! - for (uint i; i < 999; ) { - if (end >= currentEpochTime) { - uint256 currentEnd = end; - if (currentEnd > currentEpochTime + RATE_REDUCTION_TIME) { - currentEnd = currentEpochTime + RATE_REDUCTION_TIME; - } - - uint256 currentStart = start; - if (currentStart >= currentEpochTime + RATE_REDUCTION_TIME) { - break; // We should never get here but what if... - } else if (currentStart < currentEpochTime) { - currentStart = currentEpochTime; - } - to_mint += currentRate * (currentEnd - currentStart); - if (start >= currentEpochTime) { - break; - } - } - - currentEpochTime -= RATE_REDUCTION_TIME; - currentRate = - (currentRate * RATE_REDUCTION_COEFFICIENT) / - RATE_DENOMINATOR; // double-division with rounding made rate a bit less => good - require(currentRate <= INITIAL_RATE, "This should never happen"); // This should never happen - - unchecked { - i++; - } - } - - return to_mint; - } - - // @notice Set the minter address - // @dev Only callable once, when minter has not yet been set - // @param _minter Address of the minter - function setMinter(address _minter) external onlyAdmin { - require( - _minter != address(0), - "dev: can set the minter only once, at creation" - ); // dev: can set the minter only once, at creation - minter = _minter; - emit SetMinter(_minter); - } - - // @notice Set the new admin. - // @dev After all is set up, admin only can change the token name - // @param _admin New admin address - function setAdmin(address _admin) external onlyAdmin { - admin = _admin; - emit SetAdmin(_admin); - } - - // @notice Mint `_value` tokens and assign them to `_to` - // @dev Emits a Transfer event originating from 0x00 - // @param _to The account that will receive the created tokens - // @param _value The amount that will be created - // @return bool success - function mint(address _to, uint256 _value) external returns (bool) { - require(msg.sender == minter, "dev: minter only"); // dev: minter only - require(_to != address(0), "dev: zero address"); // dev: zero address - - if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { - _updateMiningParameters(); - } - require( - totalSupply() + _value <= _availableSupply(), - "dev: exceeds allowable mint amount" - ); - - _mint(_to, _value); - - return true; - } - - // @notice Burn `_value` tokens belonging to `msg.sender` - // @dev Emits a Transfer event with a destination of 0x00 - // @param _value The amount that will be burned - // @return bool success - function burn(uint256 _value) external returns (bool) { - _burn(msg.sender, _value); - return true; - } - - modifier onlyAdmin() { - require(admin == msg.sender, "dev: admin only"); - _; - } -} diff --git a/contracts/curveFork/FeeDistributor.sol b/contracts/curveFork/FeeDistributor.sol deleted file mode 100644 index 391f9b21..00000000 --- a/contracts/curveFork/FeeDistributor.sol +++ /dev/null @@ -1,625 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/utils/math/Math.sol"; - -struct Point { - int128 bias; - int128 slope; - uint256 ts; - uint256 blk; -} - -interface IVotingEscrow { - function userPointEpoch(address addr) external view returns (uint256); - - function epoch() external view returns (uint256); - - function userPointHistory( - address addr, - uint256 loc - ) external view returns (Point memory); - - function pointHistory(uint256 loc) external view returns (Point memory); - - function checkpoint() external; -} - -contract FeeDistributor is ReentrancyGuard { - uint256 public constant WEEK = 7 * 86400; - uint256 public constant TOKEN_CHECKPOINT_DEADLINE = 86400; - - uint256 public startTime; - uint256 public timeCursor; - mapping(address => uint256) public timeCursorOf; - mapping(address => uint256) public userEpochOf; - - uint256 public lastTokenTime; - mapping(uint256 => uint256) public tokensPerWeek; - - address public votingEscrow; - address public token; - uint256 public totalReceived; - uint256 public tokenLastBalance; - - mapping(uint256 => uint256) public veSupply; // VE total supply at week bounds - - address public admin; - address public futureAdmin; - bool public canCheckpointToken; - address public emergencyReturn; - bool public isKilled; - - event CommitAdmin(address indexed admin); - event ApplyAdmin(address indexed admin); - event ToggleAllowCheckpointToken(bool toggleFlag); - event CheckpointToken(uint256 time, uint256 tokens); - event Claimed( - address indexed recipient, - uint256 amount, - uint256 claimEpoch, - uint256 maxEpoch - ); - - /*** - * @notice Contract constructor - * @param votingEscrow_ VotingEscrow contract address - * @param startTime_ Epoch time for fee distribution to start - * @param token_ Fee token address (3CRV) - * @param admin_ Admin address - * @param emergencyReturn_ Address to transfer `_token` balance to if this contract is killed - */ - constructor( - address votingEscrow_, - uint256 startTime_, - address token_, - address admin_, - address emergencyReturn_ - ) { - uint256 t = (startTime_ / WEEK) * WEEK; - startTime = t; - lastTokenTime = t; - timeCursor = t; - token = token_; - votingEscrow = votingEscrow_; - admin = admin_; - emergencyReturn = emergencyReturn_; - } - - function _checkpointToken() internal { - uint256 _tokenBalance = IERC20(token).balanceOf(address(this)); - uint256 _toDistribute = _tokenBalance - tokenLastBalance; - tokenLastBalance = _tokenBalance; - - uint256 _t = lastTokenTime; - uint256 _sinceLast = block.timestamp - _t; - lastTokenTime = block.timestamp; - uint256 _thisWeek = (_t / WEEK) * WEEK; - uint256 _nextWeek = 0; - - for (uint256 i; i < 20; ) { - _nextWeek = _thisWeek + WEEK; - if (block.timestamp < _nextWeek) { - if (_sinceLast == 0 && block.timestamp == _t) { - tokensPerWeek[_thisWeek] += _toDistribute; - } else { - tokensPerWeek[_thisWeek] += - (_toDistribute * (block.timestamp - _t)) / - _sinceLast; - } - break; - } else { - if (_sinceLast == 0 && _nextWeek == _t) { - tokensPerWeek[_thisWeek] += _toDistribute; - } else { - tokensPerWeek[_thisWeek] += - (_toDistribute * (_nextWeek - _t)) / - _sinceLast; - } - } - _t = _nextWeek; - _thisWeek = _nextWeek; - unchecked { - ++i; - } - } - - emit CheckpointToken(block.timestamp, _toDistribute); - } - - /*** - * @notice Update the token checkpoint - * @dev Calculates the total number of tokens to be distributed in a given week. - During setup for the initial distribution this function is only callable - by the contract owner. Beyond initial distro, it can be enabled for anyone - to call. - */ - function checkpointToken() external { - require( - msg.sender == admin || - (canCheckpointToken && - block.timestamp > lastTokenTime + 1 hours), - "Unauthorized" - ); - _checkpointToken(); - } - - function _findTimestampEpoch( - address ve_, - uint256 timestamp_ - ) internal view returns (uint256) { - uint256 _min = 0; - uint256 _max = IVotingEscrow(ve_).epoch(); - - unchecked { - for (uint256 i; i < 128; i++) { - if (_min >= _max) { - break; - } - uint256 _mid = (_min + _max + 2) / 2; - Point memory _pt = IVotingEscrow(ve_).pointHistory(_mid); - if (_pt.ts <= timestamp_) { - _min = _mid; - } else { - _max = _mid - 1; - } - } - } - return _min; - } - - function _findTimestampUserEpoch( - address ve_, - address user_, - uint256 timestamp_, - uint256 maxUserEpoch_ - ) internal view returns (uint256) { - uint256 _min = 0; - uint256 _max = maxUserEpoch_; - - unchecked { - for (uint256 i; i < 128; i++) { - if (_min >= _max) { - break; - } - uint256 _mid = (_min + _max + 2) / 2; - Point memory _pt = IVotingEscrow(ve_).userPointHistory( - user_, - _mid - ); - if (_pt.ts <= timestamp_) { - _min = _mid; - } else { - _max = _mid - 1; - } - } - } - return _min; - } - - /*** - * @notice Get the veYNWK balance for `user_` at `timestamp_` - * @param user_ Address to query balance for - * @param timestamp_ Epoch time - * @return uint256 veYNWK balance - */ - function veForAt( - address user_, - uint256 timestamp_ - ) external view returns (uint256) { - address _ve = votingEscrow; - uint256 _maxUserEpoch = IVotingEscrow(_ve).userPointEpoch(user_); - uint256 _epoch = _findTimestampUserEpoch( - _ve, - user_, - timestamp_, - _maxUserEpoch - ); - Point memory _pt = IVotingEscrow(_ve).userPointHistory(user_, _epoch); - int128 _balance = _pt.bias - - _pt.slope * - int128(int256(timestamp_ - _pt.ts)); - if (_balance < 0) { - return 0; - } else { - return uint256(uint128(_balance)); - } - } - - function _checkpointTotalSupply() internal { - address _ve = votingEscrow; - uint256 _t = timeCursor; - uint256 _roundedTimestamp = (block.timestamp / WEEK) * WEEK; - IVotingEscrow(_ve).checkpoint(); - - for (uint256 i; i < 20; ) { - if (_t > _roundedTimestamp) { - break; - } else { - uint256 _epoch = _findTimestampEpoch(_ve, _t); - Point memory _pt = IVotingEscrow(_ve).pointHistory(_epoch); - int128 _dt = 0; - if (_t > _pt.ts) { - _dt = int128(int256(_t) - int256(_pt.ts)); - } - veSupply[_t] = uint256(int256(_pt.bias - _pt.slope * _dt)); - _t += WEEK; - } - unchecked { - ++i; - } - } - - timeCursor = _t; - } - - /*** - * @notice Update the veCRV total supply checkpoint - * @dev The checkpoint is also updated by the first claimant each new epoch week. This function may be called independently of a claim, to reduce claiming gas costs. - */ - function checkpointTotalSupply() external { - address _ve = votingEscrow; - uint256 _t = timeCursor; - uint256 _roundedTimestamp = (block.timestamp / WEEK) * WEEK; - IVotingEscrow(_ve).checkpoint(); - - for (uint256 i; i < 20; ) { - if (_t > _roundedTimestamp) { - break; - } else { - uint256 _epoch = _findTimestampEpoch(_ve, _t); - Point memory _pt = IVotingEscrow(_ve).pointHistory(_epoch); - uint256 _dt = 0; - if (_t > _pt.ts) { - _dt = uint256(int256(_t) - int256(_pt.ts)); - } - - int128 _balance = _pt.bias - _pt.slope * int128(int256(_dt)); - if (_balance < 0) { - veSupply[_t] = 0; - } else { - veSupply[_t] = uint256(uint128(_balance)); - } - } - _t += WEEK; - unchecked { - ++i; - } - } - - timeCursor = _t; - } - - function _claim( - address addr_, - address ve_, - uint256 lastTokenTime_ - ) internal returns (uint256) { - // Minimal user_epoch is 0 (if user had no point) - uint256 _userEpoch = 0; - uint256 _toDistribute = 0; - - uint256 _maxUserEpoch = IVotingEscrow(ve_).userPointEpoch(addr_); - uint256 _startTime = startTime; - - if (_maxUserEpoch == 0) { - // No lock = no fees - return 0; - } - - uint256 _weekCursor = timeCursorOf[addr_]; - if (_weekCursor == 0) { - // Need to do the initial binary search - _userEpoch = _findTimestampUserEpoch( - ve_, - addr_, - _startTime, - _maxUserEpoch - ); - } else { - _userEpoch = userEpochOf[addr_]; - } - - if (_userEpoch == 0) { - _userEpoch = 1; - } - - Point memory _userPoint = IVotingEscrow(ve_).userPointHistory( - addr_, - _userEpoch - ); - - if (_weekCursor == 0) { - _weekCursor = ((_userPoint.ts + WEEK - 1) / WEEK) * WEEK; - } - - if (_weekCursor >= lastTokenTime_) { - return 0; - } - - if (_weekCursor < _startTime) { - _weekCursor = _startTime; - } - - Point memory _oldUserPoint = Point({bias: 0, slope: 0, ts: 0, blk: 0}); - - // Iterate over weeks - for (uint256 i; i < 50; ) { - if (_weekCursor >= lastTokenTime_) { - break; - } else if ( - _weekCursor >= _userPoint.ts && _userEpoch <= _maxUserEpoch - ) { - _userEpoch += 1; - _oldUserPoint = Point({ - bias: _userPoint.bias, - slope: _userPoint.slope, - ts: _userPoint.ts, - blk: _userPoint.blk - }); - if (_userEpoch > _maxUserEpoch) { - _userPoint = Point({bias: 0, slope: 0, ts: 0, blk: 0}); - } else { - _userPoint = IVotingEscrow(ve_).userPointHistory( - addr_, - _userEpoch - ); - } - } else { - int256 _dt = int256(_weekCursor) - int256(_oldUserPoint.ts); - int256 _balanceOf = int256(_oldUserPoint.bias) - - _dt * - int256(_oldUserPoint.slope); - if ( - int256(_oldUserPoint.bias) - - _dt * - int256(_oldUserPoint.slope) < - 0 - ) { - _balanceOf = 0; - } - - if (_balanceOf == 0 && _userEpoch > _maxUserEpoch) { - break; - } - if (_balanceOf > 0) { - _toDistribute += - (uint256(_balanceOf) * tokensPerWeek[_weekCursor]) / - veSupply[_weekCursor]; - } - _weekCursor += WEEK; - } - unchecked { - ++i; - } - } - - _userEpoch = Math.min(_maxUserEpoch, _userEpoch - 1); - userEpochOf[addr_] = _userEpoch; - timeCursorOf[addr_] = _weekCursor; - - emit Claimed(addr_, _toDistribute, _userEpoch, _maxUserEpoch); - - return _toDistribute; - } - - /*** - * @notice Claim fees for `msg.sender` - * @dev Each call to claim look at a maximum of 50 user veCRV points. - For accounts with many veCRV related actions, this function - may need to be called more than once to claim all available - fees. In the `Claimed` event that fires, if `claim_epoch` is - less than `max_epoch`, the account may claim again. - * @return uint256 Amount of fees claimed in the call - */ - function claim() external nonReentrant returns (uint256) { - require(!isKilled, "Contract is killed"); - address _addr = msg.sender; - if (block.timestamp >= timeCursor) { - _checkpointTotalSupply(); - } - - uint256 _lastTokenTime = lastTokenTime; - - if ( - canCheckpointToken && (block.timestamp > _lastTokenTime + 1 hours) - ) { - _checkpointToken(); - _lastTokenTime = block.timestamp; - } - - unchecked { - _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; - } - - uint256 _amount = _claim(_addr, votingEscrow, _lastTokenTime); - if (_amount != 0) { - require(IERC20(token).transfer(_addr, _amount), "Transfer failed"); - tokenLastBalance -= _amount; - } - - return _amount; - } - - /*** - * @notice Claim fees for `addr_` - * @dev Each call to claim look at a maximum of 50 user veCRV points. - For accounts with many veCRV related actions, this function - may need to be called more than once to claim all available - fees. In the `Claimed` event that fires, if `claim_epoch` is - less than `max_epoch`, the account may claim again. - * @param addr_ Address to claim fees for - * @return uint256 Amount of fees claimed in the call - */ - function claim(address addr_) external nonReentrant returns (uint256) { - require(!isKilled, "Contract is killed"); - - if (block.timestamp >= timeCursor) { - _checkpointTotalSupply(); - } - - uint256 _lastTokenTime = lastTokenTime; - - if ( - canCheckpointToken && (block.timestamp > _lastTokenTime + 1 hours) - ) { - _checkpointToken(); - _lastTokenTime = block.timestamp; - } - - unchecked { - _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; - } - - uint256 amount = _claim(addr_, votingEscrow, _lastTokenTime); - if (amount != 0) { - require(IERC20(token).transfer(addr_, amount), "Transfer failed"); - tokenLastBalance -= amount; - } - - return amount; - } - - /*** - * @notice Make multiple fee claims in a single call - * @dev Used to claim for many accounts at once, or to make - multiple claims for the same address when that address - has significant veCRV history - * @param receivers_ List of addresses to claim for. Claiming - terminates at the first `ZERO_ADDRESS`. - * @return bool success - */ - function claimMany( - address[] memory receivers_ - ) external nonReentrant returns (bool) { - require(!isKilled, "Contract is killed"); - - if (block.timestamp >= timeCursor) { - _checkpointTotalSupply(); - } - - uint256 _lastTokenTime = lastTokenTime; - - if ( - canCheckpointToken && (block.timestamp > _lastTokenTime + 1 hours) - ) { - _checkpointToken(); - _lastTokenTime = block.timestamp; - } - - _lastTokenTime = (_lastTokenTime / WEEK) * WEEK; - uint256 _total = 0; - uint256 _l = receivers_.length; - for (uint256 i; i < _l; ) { - address _addr = receivers_[i]; - if (_addr == address(0)) { - break; - } - - uint256 _amount = _claim(_addr, votingEscrow, _lastTokenTime); - if (_amount != 0) { - require( - IERC20(token).transfer(_addr, _amount), - "Transfer failed" - ); - _total += _amount; - } - unchecked { - ++i; - } - } - - if (_total != 0) { - tokenLastBalance -= _total; - } - - return true; - } - - /*** - * @notice Receive 3CRV into the contract and trigger a token checkpoint - * @param coin_ Address of the coin being received (must be 3CRV) - * @return bool success - */ - function burn(address coin_) external returns (bool) { - require(coin_ == address(token), "Invalid token"); - require(!isKilled, "Contract is killed"); - - uint256 _amount = IERC20(token).balanceOf(msg.sender); - if (_amount > 0) { - IERC20(token).transferFrom(msg.sender, address(this), _amount); - if ( - canCheckpointToken && block.timestamp > lastTokenTime + 1 hours - ) { - _checkpointToken(); - } - } - return true; - } - - /*** - * @notice Commit transfer of ownership - * @param addr_ New admin address - */ - function commitAdmin(address addr_) external onlyAdmin { - futureAdmin = addr_; - emit CommitAdmin(addr_); - } - - /*** - * @notice Apply transfer of ownership - */ - function applyAdmin() external onlyAdmin { - require(futureAdmin != address(0), "No admin set"); - admin = futureAdmin; - emit ApplyAdmin(futureAdmin); - } - - /*** - * @notice Toggle permission for checkpointing by any account - */ - function toggleAllowCheckpointToken() external onlyAdmin { - canCheckpointToken = !canCheckpointToken; - emit ToggleAllowCheckpointToken(canCheckpointToken); - } - - /*** - * @notice Kill the contract - * @dev Killing transfers the entire 3CRV balance to the emergency return address - and blocks the ability to claim or burn. The contract cannot be unkilled. - */ - function killMe() external onlyAdmin { - isKilled = true; - require( - IERC20(token).transfer( - emergencyReturn, - IERC20(token).balanceOf(address(this)) - ), - "Transfer failed" - ); - } - - /*** - * @notice Recover ERC20 tokens from this contract - * @dev Tokens are sent to the emergency return address. - * @param coin_ Token address - * @return bool success - */ - function recoverBalance(address coin_) external onlyAdmin returns (bool) { - require(coin_ != address(token), "Cannot recover this token"); - - uint256 _amount = IERC20(coin_).balanceOf(address(this)); - require( - IERC20(coin_).transfer(emergencyReturn, _amount), - "Transfer failed" - ); - return true; - } - - modifier onlyAdmin() { - require(admin == msg.sender, "Access denied"); - _; - } -} diff --git a/contracts/curveFork/GaugeController.sol b/contracts/curveFork/GaugeController.sol deleted file mode 100644 index 29eb50fa..00000000 --- a/contracts/curveFork/GaugeController.sol +++ /dev/null @@ -1,690 +0,0 @@ -pragma solidity 0.8.18; - -/*** - *@title Gauge Controller - * SPDX-License-Identifier: MIT - *@notice Controls liquidity gauges and the issuance of token through the gauges - */ - -//dao-contracts -import "./interfaces/ICRV.sol"; -import "./interfaces/IVotingEscrow.sol"; - -contract GaugeController { - // 7 * 86400 seconds - all future times are rounded by week - // uint256 constant WEEK = 604800; - uint256 constant WEEK = 7 days; - - // Cannot change weight votes more often than once in 10 days. - uint256 constant WEIGHT_VOTE_DELAY = 10 days; - - struct Point { - uint256 bias; - uint256 slope; - } - - struct VotedSlope { - uint256 slope; - uint256 power; - uint256 end; - } - - event CommitOwnership(address admin); - event ApplyOwnership(address admin); - event AddType(string name, int128 typeId); - event NewTypeWeight( - int128 typeId, - uint256 time, - uint256 weight, - uint256 totalWeight - ); - - event NewGaugeWeight( - address gaugeAddress, - uint256 time, - uint256 weight, - uint256 totalWeight - ); - event VoteForGauge( - uint256 time, - address user, - address gaugeAddr, - uint256 weight - ); - event NewGauge(address addr, int128 gaugeType, uint256 weight); - - uint256 constant MULTIPLIER = 10 ** 18; - - // Can and will be a smart contract - address public admin; - // Can and will be a smart contract - address public futureAdmin; - // CRV token - address public token; - // Voting escrow - address public votingEscrow; - - int128 public nGaugeTypes; - int128 public nGauges; //number of gauges - mapping(int128 => string) public gaugeTypeNames; - - // Needed for enumeration - address[1000000000] public gauges; - - // we increment values by 1 prior to storing them here so we can rely on a value - // of zero as meaning the gauge has not been set mapping(address => int128) gaugeTypes; - mapping(address => int128) public gaugeTypes_; - - mapping(address => mapping(address => VotedSlope)) public voteUserSlopes; // user -> gauge_addr -> VotedSlope - - mapping(address => uint256) public voteUserPower; // Total vote power used by user - mapping(address => mapping(address => uint256)) public lastUserVote; // Last user vote's timestamp for each gauge address - - // Past and scheduled points for gauge weight, sum of weights per type, total weight - // Point is for bias+slope - // changes_* are for changes in slope - // time_* are for the last change timestamp - // timestamps are rounded to whole weeks - - mapping(address => mapping(uint256 => Point)) public pointsWeight; // gauge_addr -> time -> Point - mapping(address => mapping(uint256 => uint256)) public changesWeight; // gauge_addr -> time -> slope - mapping(address => uint256) public timeWeight; // gauge_addr -> last scheduled time (next week) - - mapping(int128 => mapping(uint256 => Point)) public pointsSum; // type_id -> time -> Point - mapping(int128 => mapping(uint256 => uint256)) public changesSum; // type_id -> time -> slope - uint256[1000000000] public timeSum; // type_id -> last scheduled time (next week) - - mapping(uint256 => uint256) public pointsTotal; // time -> total weight - uint256 public timeTotal; // last scheduled time - - mapping(int128 => mapping(uint256 => uint256)) public pointsTypeWeight; // type_id -> time -> type weight - uint256[1000000000] public timeTypeWeight; // type_id -> last scheduled time (next week) - - /*** - *@notice Contract constructor - *@param _token `Token` contract address - *@param _votingEscrow `VotingEscrow` contract address - */ - constructor(address token_, address votingEscrow_) { - require(token_ != address(0)); - require(votingEscrow_ != address(0)); - - admin = msg.sender; - token = token_; - votingEscrow = votingEscrow_; - timeTotal = (block.timestamp / WEEK) * WEEK; - } - - /*** - * @notice Transfer ownership of GaugeController to `addr` - * @param addr_ Address to have ownership transferred to - */ - function commitTransferOwnership(address addr_) external onlyAdmin { - futureAdmin = addr_; - emit CommitOwnership(addr_); - } - - /*** - * @notice Apply pending ownership transfer - */ - function applyTransferOwnership() external onlyAdmin { - address _admin = futureAdmin; - require(_admin != address(0), "admin not set"); - admin = _admin; - emit ApplyOwnership(_admin); - } - - /*** - *@notice Get gauge type for address - *@param addr_ Gauge address - *@return Gauge type id - */ - function gaugeTypes(address addr_) external view returns (int128) { - int128 _gaugeType = gaugeTypes_[addr_]; - require(_gaugeType != 0, "dev: gauge is not added"); - - return _gaugeType - 1; - } - - /*** - *@notice Fill historic type weights week-over-week for missed checkins - * and return the type weight for the future week - *@param _gauge_type Gauge type id - *@return Type weight - */ - function _getTypeWeight(int128 gaugeType_) internal returns (uint256) { - uint256 _gaugeType = uint256(uint128(gaugeType_)); - uint256 _t = timeTypeWeight[_gaugeType]; - if (_t > 0) { - uint256 _w = pointsTypeWeight[gaugeType_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - pointsTypeWeight[gaugeType_][_t] = _w; - if (_t > block.timestamp) { - timeTypeWeight[_gaugeType] = _t; - } - unchecked { - ++i; - } - } - return _w; - } else { - return 0; - } - } - - /*** - *@notice Fill sum of gauge weights for the same type week-over-week for - * missed checkins and return the sum for the future week - *@param gaugeType_ Gauge type id - *@return Sum of weights - */ - function _getSum(int128 gaugeType_) internal returns (uint256) { - uint256 _gaugeType = uint256(uint128(gaugeType_)); - uint256 _t = timeSum[_gaugeType]; - if (_t > 0) { - Point memory _pt = pointsSum[gaugeType_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - uint256 _dBias = _pt.slope * WEEK; - if (_pt.bias > _dBias) { - _pt.bias -= _dBias; - uint256 _dSlope = changesSum[gaugeType_][_t]; - _pt.slope -= _dSlope; - } else { - _pt.bias = 0; - _pt.slope = 0; - } - pointsSum[gaugeType_][_t] = _pt; - if (_t > block.timestamp) { - timeSum[_gaugeType] = _t; - } - unchecked { - ++i; - } - } - return _pt.bias; - } else { - return 0; - } - } - - /*** - *@notice Fill historic total weights week-over-week for missed checkins - * and return the total for the future week - *@return Total weight - */ - function _getTotal() internal returns (uint256) { - uint256 _t = timeTotal; - int128 _nGaugeTypes = nGaugeTypes; - if (_t > block.timestamp) { - // If we have already checkpointed - still need to change the value - _t -= WEEK; - } - uint256 _pt = pointsTotal[_t]; - - for (int128 _gaugeType; _gaugeType < 100; ) { - if (_gaugeType == _nGaugeTypes) { - break; - } - _getSum(_gaugeType); - _getTypeWeight(_gaugeType); - unchecked { - ++_gaugeType; - } - } - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - _pt = 0; - // Scales as n_types * n_unchecked_weeks (hopefully 1 at most) - for (int128 _gaugeType; _gaugeType < 100; ) { - if (_gaugeType == _nGaugeTypes) { - break; - } - uint256 _typeSum = pointsSum[_gaugeType][_t].bias; - uint256 _typeWeight = pointsTypeWeight[_gaugeType][_t]; - _pt += _typeSum * _typeWeight; - unchecked { - ++_gaugeType; - } - } - pointsTotal[_t] = _pt; - - if (_t > block.timestamp) { - timeTotal = _t; - } - unchecked { - ++i; - } - } - return _pt; - } - - /*** - *@notice Fill historic gauge weights week-over-week for missed checkins - * and return the total for the future week - *@param gaugeAddr_ Address of the gauge - *@return Gauge weight - */ - function _getWeight(address gaugeAddr_) internal returns (uint256) { - uint256 _t = timeWeight[gaugeAddr_]; - if (_t > 0) { - Point memory _pt = pointsWeight[gaugeAddr_][_t]; - for (uint256 i; i < 500; ) { - if (_t > block.timestamp) { - break; - } - _t += WEEK; - uint256 _dBias = _pt.slope * WEEK; - if (_pt.bias > _dBias) { - _pt.bias -= _dBias; - uint256 _dSlope = changesWeight[gaugeAddr_][_t]; - _pt.slope -= _dSlope; - } else { - _pt.bias = 0; - _pt.slope = 0; - } - pointsWeight[gaugeAddr_][_t] = _pt; - if (_t > block.timestamp) { - timeWeight[gaugeAddr_] = _t; - } - unchecked { - ++i; - } - } - return _pt.bias; - } else { - return 0; - } - } - - /*** - *@notice Add gauge `addr` of type `gauge_type` with weight `weight` - *@param addr_ Gauge address - *@param gaugeType_ Gauge type - */ - function addGauge( - address addr_, - int128 gaugeType_, - uint256 weight_ - ) external onlyAdmin { - require( - (gaugeType_ >= 0) && (gaugeType_ < nGaugeTypes), - "gauge_type 0 means unset" - ); //gauge_type 0 means unset - require(gaugeTypes_[addr_] == 0, "cannot add the same gauge twice"); - int128 _n = nGauges; - unchecked { - nGauges = _n + 1; - } - gauges[uint256(uint128(_n))] = addr_; - uint256 _gaugeType = uint256(uint128(gaugeType_)); - gaugeTypes_[addr_] = gaugeType_ + 1; - uint256 _nextTime; - unchecked { - _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - - if (weight_ > 0) { - uint256 _typeWeight = _getTypeWeight(gaugeType_); - uint256 _oldSum = _getSum(gaugeType_); - uint256 _oldTotal = _getTotal(); - - pointsSum[gaugeType_][_nextTime].bias = weight_ + _oldSum; - timeSum[_gaugeType] = _nextTime; - pointsTotal[_nextTime] = _oldTotal + (_typeWeight * weight_); - timeTotal = _nextTime; - - pointsWeight[addr_][_nextTime].bias = weight_; - } - if (timeSum[_gaugeType] == 0) { - timeSum[_gaugeType] = _nextTime; - } - timeWeight[addr_] = _nextTime; - - emit NewGauge(addr_, gaugeType_, weight_); - } - - /*** - * @notice Checkpoint to fill data common for all gauges - */ - function checkpoint() external { - _getTotal(); - } - - /*** - *@notice Checkpoint to fill data for both a specific gauge and common for all gauges - *@param addr_ Gauge address - */ - function checkpointGauge(address addr_) external { - _getWeight(addr_); - _getTotal(); - } - - /*** - *@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18 - * (e.g. 1.0 == 1e18). Inflation which will be received by it is - * inflation_rate * relative_weight / 1e18 - *@param addr_ Gauge address - *@param time_ Relative weight at the specified timestamp in the past or present - *@return Value of relative weight normalized to 1e18 - */ - function _gaugeRelativeWeight( - address addr_, - uint256 time_ - ) internal view returns (uint256) { - uint256 _t = (time_ / WEEK) * WEEK; - uint256 _totalWeight = pointsTotal[_t]; - - if (_totalWeight > 0) { - int128 _gaugeType = gaugeTypes_[addr_] - 1; - uint256 _typeWeight = pointsTypeWeight[_gaugeType][_t]; - uint256 _gaugeWeight = pointsWeight[addr_][_t].bias; - - return (MULTIPLIER * _typeWeight * _gaugeWeight) / _totalWeight; - } else { - return 0; - } - } - - /*** - *@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18 - * (e.g. 1.0 == 1e18). Inflation which will be received by it is - * inflation_rate * relative_weight / 1e18 - *@param addr_ Gauge address - *@param time_ Relative weight at the specified timestamp in the past or present - *@return Value of relative weight normalized to 1e18 - */ - function gaugeRelativeWeight( - address addr_, - uint256 time_ - ) external view returns (uint256) { - //default value - if (time_ == 0) { - time_ = block.timestamp; - } - - return _gaugeRelativeWeight(addr_, time_); - } - - function gaugeRelativeWeightWrite( - address addr_, - uint256 time_ - ) external returns (uint256) { - //default value - if (time_ == 0) { - time_ = block.timestamp; - } - - _getWeight(addr_); - _getTotal(); // Also calculates get_sum - return _gaugeRelativeWeight(addr_, time_); - } - - /*** - *@notice Change type weight - *@param typeId_ Type id - *@param weight_ New type weight - */ - function _changeTypeWeight(int128 typeId_, uint256 weight_) internal { - uint256 _oldWeight = _getTypeWeight(typeId_); - uint256 _oldSum = _getSum(typeId_); - uint256 _totalWeight = _getTotal(); - uint256 _nextTime; - unchecked { - _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - uint256 _typeId = uint256(uint128(typeId_)); - - _totalWeight = - _totalWeight + - (_oldSum * weight_) - - (_oldSum * _oldWeight); - pointsTotal[_nextTime] = _totalWeight; - pointsTypeWeight[typeId_][_nextTime] = weight_; - timeTotal = _nextTime; - timeTypeWeight[_typeId] = _nextTime; - - emit NewTypeWeight(typeId_, _nextTime, weight_, _totalWeight); - } - - /*** - *@notice Add gauge type with name `name_` and weight `weight` //ex. type=1, Liquidity, 1*1e18 - *@param name_ Name of gauge type - *@param weight_ Weight of gauge type - */ - function addType(string memory name_, uint256 weight_) external onlyAdmin { - int128 _typeId = nGaugeTypes; - gaugeTypeNames[_typeId] = name_; - unchecked { - nGaugeTypes = _typeId + 1; - } - if (weight_ != 0) { - _changeTypeWeight(_typeId, weight_); - emit AddType(name_, _typeId); - } - } - - /*** - *@notice Change gauge type `type_id` weight to `weight` - *@param typeId_ Gauge type id - *@param weight_ New Gauge weight - */ - function changeTypeWeight( - int128 typeId_, - uint256 weight_ - ) external onlyAdmin { - _changeTypeWeight(typeId_, weight_); - } - - function _changeGaugeWeight(address addr_, uint256 weight_) internal { - // Change gauge weight - // Only needed when testing in reality - int128 _gaugeType = gaugeTypes_[addr_] - 1; - uint256 _oldGaugeWeight = _getWeight(addr_); - uint256 _typeWeight = _getTypeWeight(_gaugeType); - uint256 _oldSum = _getSum(_gaugeType); - uint256 _totalWeight = _getTotal(); - uint256 _nextTime; - unchecked { - _nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - - pointsWeight[addr_][_nextTime].bias = weight_; - timeWeight[addr_] = _nextTime; - - uint256 newSum = _oldSum + weight_ - _oldGaugeWeight; - pointsSum[_gaugeType][_nextTime].bias = newSum; - timeSum[uint256(uint128(_gaugeType))] = _nextTime; - - _totalWeight = - _totalWeight + - (newSum * _typeWeight) - - (_oldSum * _typeWeight); - pointsTotal[_nextTime] = _totalWeight; - timeTotal = _nextTime; - - emit NewGaugeWeight(addr_, block.timestamp, weight_, _totalWeight); - } - - /*** - *@notice Change weight of gauge `addr` to `weight` - *@param addr_ `GaugeController` contract address - *@param weight_ New Gauge weight - */ - function changeGaugeWeight( - address addr_, - uint256 weight_ - ) external onlyAdmin { - _changeGaugeWeight(addr_, weight_); - } - - struct VotingParameter { - //to avoid "Stack too deep" issue - uint256 slope; - uint256 lockEnd; - uint256 _nGauges; - uint256 nextTime; - int128 gaugeType; - uint256 oldDt; - uint256 oldBias; - } - - /**** - *@notice Allocate voting power for changing pool weights - *@param gaugeAddr_ Gauge which `msg.sender` votes for - *@param userWeight_ Weight for a gauge in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0. bps = basis points - */ - function voteForGaugeWeights( - address gaugeAddr_, - uint256 userWeight_ - ) external { - VotingParameter memory _vp; - _vp.slope = uint256( - uint128(IVotingEscrow(votingEscrow).getLastUserSlope(msg.sender)) - ); - _vp.lockEnd = IVotingEscrow(votingEscrow).lockedEnd(msg.sender); - _vp._nGauges = uint256(uint128(nGauges)); - unchecked { - _vp.nextTime = ((block.timestamp + WEEK) / WEEK) * WEEK; - } - require(_vp.lockEnd > _vp.nextTime, "Your token lock expires too soon"); - require( - (userWeight_ >= 0) && (userWeight_ <= 10000), - "You used all your voting power" - ); - unchecked { - require( - block.timestamp >= - lastUserVote[msg.sender][gaugeAddr_] + WEIGHT_VOTE_DELAY, - "Cannot vote so often" - ); - } - - _vp.gaugeType = gaugeTypes_[gaugeAddr_] - 1; - require(_vp.gaugeType >= 0, "Gauge not added"); - // Prepare slopes and biases in memory - VotedSlope memory _oldSlope = voteUserSlopes[msg.sender][gaugeAddr_]; - _vp.oldDt = 0; - if (_oldSlope.end > _vp.nextTime) { - _vp.oldDt = _oldSlope.end - _vp.nextTime; - } - _vp.oldBias = _oldSlope.slope * _vp.oldDt; - VotedSlope memory _newSlope = VotedSlope({ - slope: (_vp.slope * userWeight_) / 10000, - end: _vp.lockEnd, - power: userWeight_ - }); - uint256 _newDt = _vp.lockEnd - _vp.nextTime; // dev: raises when expired - uint256 _newBias = _newSlope.slope * _newDt; - - // Check and update powers (weights) used - uint256 _powerUsed = voteUserPower[msg.sender]; - _powerUsed = _powerUsed + _newSlope.power - _oldSlope.power; - voteUserPower[msg.sender] = _powerUsed; - require( - (_powerUsed >= 0) && (_powerUsed <= 10000), - "Used too much power" - ); - - //// Remove old and schedule new slope changes - // Remove slope changes for old slopes - // Schedule recording of initial slope for nextTime - uint256 _oldWeightBias = _getWeight(gaugeAddr_); - uint256 _oldWeightSlope = pointsWeight[gaugeAddr_][_vp.nextTime].slope; - uint256 _oldSumBias = _getSum(_vp.gaugeType); - uint256 _oldSumSlope = pointsSum[_vp.gaugeType][_vp.nextTime].slope; - - pointsWeight[gaugeAddr_][_vp.nextTime].bias = - max(_oldWeightBias + _newBias, _vp.oldBias) - - _vp.oldBias; - pointsSum[_vp.gaugeType][_vp.nextTime].bias = - max(_oldSumBias + _newBias, _vp.oldBias) - - _vp.oldBias; - if (_oldSlope.end > _vp.nextTime) { - pointsWeight[gaugeAddr_][_vp.nextTime].slope = - max(_oldWeightSlope + _newSlope.slope, _oldSlope.slope) - - _oldSlope.slope; - pointsSum[_vp.gaugeType][_vp.nextTime].slope = - max(_oldSumSlope + _newSlope.slope, _oldSlope.slope) - - _oldSlope.slope; - } else { - pointsWeight[gaugeAddr_][_vp.nextTime].slope += _newSlope.slope; - pointsSum[_vp.gaugeType][_vp.nextTime].slope += _newSlope.slope; - } - if (_oldSlope.end > block.timestamp) { - // Cancel old slope changes if they still didn't happen - changesWeight[gaugeAddr_][_oldSlope.end] -= _oldSlope.slope; - changesSum[_vp.gaugeType][_oldSlope.end] -= _oldSlope.slope; - } - // Add slope changes for new slopes - changesWeight[gaugeAddr_][_newSlope.end] += _newSlope.slope; - changesSum[_vp.gaugeType][_newSlope.end] += _newSlope.slope; - - _getTotal(); - - voteUserSlopes[msg.sender][gaugeAddr_] = _newSlope; - - // Record last action time - lastUserVote[msg.sender][gaugeAddr_] = block.timestamp; - - emit VoteForGauge(block.timestamp, msg.sender, gaugeAddr_, userWeight_); - } - - /*** - *@notice Get current gauge weight - *@param addr_ Gauge address - *@return Gauge weight - */ - function getGaugeWeight(address addr_) external view returns (uint256) { - return pointsWeight[addr_][timeWeight[addr_]].bias; - } - - /*** - *@notice Get current type weight - *@param typeId_ Type id - *@return Type weight - */ - function getTypeWeight(int128 typeId_) external view returns (uint256) { - uint256 _typeId = uint256(uint128(typeId_)); - return pointsTypeWeight[typeId_][timeTypeWeight[_typeId]]; - } - - /*** - *@notice Get current total (type-weighted) weight - *@return Total weight - */ - function getTotalWeight() external view returns (uint256) { - return pointsTotal[timeTotal]; - } - - /*** - *@notice Get sum of gauge weights per type - *@param typeId_ Type id - *@return Sum of gauge weights - */ - function getWeightsSumPerType( - int128 typeId_ - ) external view returns (uint256) { - uint256 _typeId = uint256(uint128(typeId_)); - return pointsSum[typeId_][timeSum[_typeId]].bias; - } - - function max(uint256 _a, uint256 _b) internal pure returns (uint256) { - return _a >= _b ? _a : _b; - } - - modifier onlyAdmin() { - require(admin == msg.sender, "admin only"); - _; - } -} diff --git a/contracts/curveFork/LiquidityGaugeV6.sol b/contracts/curveFork/LiquidityGaugeV6.sol deleted file mode 100644 index 7a7b93af..00000000 --- a/contracts/curveFork/LiquidityGaugeV6.sol +++ /dev/null @@ -1,756 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; - -import "./interfaces/IGaugeController.sol"; -import "./interfaces/ICRV.sol"; -import "./interfaces/IMinter.sol"; -import "./interfaces/IVotingEscrow.sol"; - -interface IERC20Extended { - function symbol() external view returns (string memory); -} - -contract LiquidityGaugeV6 is ReentrancyGuard { - event Deposit(address indexed provider, uint256 value); - event Withdraw(address indexed provider, uint256 value); - event UpdateLiquidityLimit( - address user, - uint256 originalBalance, - uint256 originalSupply, - uint256 workingBalance, - uint256 workingSupply - ); - event CommitOwnership(address indexed admin); - event ApplyOwnership(address indexed admin); - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval( - address indexed owner, - address indexed spender, - uint256 value - ); - - struct Reward { - address token; - address distributor; - uint256 periodFinish; - uint256 rate; - uint256 lastUpdate; - uint256 integral; - } - - // to avoid "stack too deep" - struct CheckPointParameters { - int128 period; - uint256 periodTime; - uint256 integrateInvSupply; - uint256 inflationParams; - uint256 rate; - uint256 newRate; - uint256 prevFutureEpoch; - uint256 workingBalance; - uint256 workingSupply; - } - - // to avoid "stack too deep" - struct RewardParameters { - uint256 userBalance; - address receiver; - uint256 rewardCount; - address token; - uint256 integral; - uint256 lastUpdate; - uint256 duration; - uint256 integralFor; - uint256 newClaimable; - uint256 claimData; - uint256 totalClaimable; - uint256 totalClaimed; - } - - // Constants - uint256 public constant MAX_REWARDS = 8; - uint256 public constant TOKENLESS_PRODUCTION = 40; - uint256 public constant WEEK = 604800; - string public constant VERSION = "v6.0.0"; // <- updated from v5.0.0 (adds `create_from_blueprint` pattern) - - bytes32 public constant EIP712_TYPEHASH = - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - bytes32 public constant EIP2612_TYPEHASH = - keccak256( - "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" - ); - bytes32 public constant VERSION_HASH = keccak256(abi.encodePacked(VERSION)); - - // Immutable Variables - bytes32 public immutable NAME_HASH; - uint256 public immutable CACHED_CHAIN_ID; - bytes32 public immutable salt; - bytes32 public immutable CACHED_DOMAIN_SEPARATOR; - - // ERC20 - mapping(address => uint256) public balanceOf; - uint256 public totalSupply; - mapping(address => mapping(address => uint256)) public allowance; - - string public name; - string public symbol; - - // ERC2612 - mapping(address => uint256) public nonces; - - // Gauge - address public admin; - address public lpToken; - - address public token; - address public votingEscrow; - address public minter; - address public gaugeController; - - bool public isKilled; - - // [future_epoch_time uint40][inflation_rate uint216] - uint256 public futureEpochTime; - uint256 public inflationRate; - - // For tracking external rewards - uint256 public rewardCount; - mapping(address => Reward) public rewardData; // Assuming you've already defined a 'Reward' struct - - // claimant -> default reward receiver - mapping(address => address) public rewardsReceiver; - - // reward token -> claiming address -> integral - mapping(address => mapping(address => uint256)) public rewardIntegralFor; - - // user -> [uint128 claimable amount][uint128 claimed amount] - mapping(address => mapping(address => uint256)) public claimData; - - mapping(address => uint256) public workingBalances; - uint256 public workingSupply; - - // 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint - mapping(address => uint256) public integrateInvSupplyOf; - mapping(address => uint256) public integrateCheckpointOf; - - // ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint - mapping(address => uint256) public integrateFraction; - - // The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint - int128 public period; - - // array of reward tokens - address[MAX_REWARDS] public rewardTokens; // Assuming MAX_REWARDS is a constant already defined - - // Using dynamic array instead of fixed 100000000000000000000000000000 array to avoid warning about collisions - uint256[100000000000000000000000000000] public periodTimestamp; - uint256[100000000000000000000000000000] public integrateInvSupply; - - constructor(address lpToken_, address minter_) { - require(lpToken == address(0)); - - lpToken = lpToken_; - minter = minter_; - token = IMinter(minter).token(); - gaugeController = IMinter(minter).controller(); - votingEscrow = IGaugeController(gaugeController).votingEscrow(); - - string memory _symbol = IERC20Extended(lpToken_).symbol(); - name = string(abi.encodePacked("Curve.fi ", _symbol, " Gauge Deposit")); - symbol = string(abi.encodePacked(_symbol, "-gauge")); - - periodTimestamp[0] = block.timestamp; - admin = msg.sender; - - // Assuming you have the CRV20 interface defined somewhere for the following line - inflationRate = ICRV(token).rate(); - futureEpochTime = ICRV(token).futureEpochTimeWrite(); - - NAME_HASH = keccak256(abi.encodePacked(name)); - salt = blockhash(block.number - 1); - CACHED_CHAIN_ID = block.chainid; - CACHED_DOMAIN_SEPARATOR = keccak256( - abi.encodePacked( - EIP712_TYPEHASH, - NAME_HASH, - VERSION_HASH, - CACHED_CHAIN_ID, - address(this), - salt - ) - ); - } - - function _domainSeparator() internal view returns (bytes32) { - if (block.chainid != CACHED_CHAIN_ID) { - return - keccak256( - abi.encode( - EIP712_TYPEHASH, - NAME_HASH, - VERSION_HASH, - block.chainid, - address(this), - salt - ) - ); - } - return CACHED_DOMAIN_SEPARATOR; - } - - function _checkpoint(address addr) internal { - CheckPointParameters memory _st; - - _st.period = period; - _st.periodTime = periodTimestamp[uint256(uint128(_st.period))]; - _st.integrateInvSupply = integrateInvSupply[ - uint256(uint128(_st.period)) - ]; - - _st.rate = inflationRate; - _st.prevFutureEpoch = futureEpochTime; - _st.newRate = _st.rate; - - if (_st.prevFutureEpoch >= _st.periodTime) { - futureEpochTime = ICRV(token).futureEpochTimeWrite(); - _st.newRate = ICRV(token).rate(); - inflationRate = _st.newRate; - } - - if (isKilled) { - _st.rate = 0; - _st.newRate = 0; - } - - if (block.timestamp > _st.periodTime) { - uint256 _workingSupply = workingSupply; - IGaugeController(gaugeController).checkpointGauge(address(this)); - uint256 _prevWeekTime = _st.periodTime; - uint256 _weekTime = min( - ((_st.periodTime + WEEK) / WEEK) * WEEK, - block.timestamp - ); - - for (uint256 i = 0; i < 500; ) { - uint256 dt = _weekTime - _prevWeekTime; - uint256 w = IGaugeController(gaugeController) - .gaugeRelativeWeight( - address(this), - (_prevWeekTime / WEEK) * WEEK - ); - - if (_workingSupply > 0) { - if ( - _st.prevFutureEpoch >= _prevWeekTime && - _st.prevFutureEpoch < _weekTime - ) { - _st.integrateInvSupply += - (_st.rate * - w * - (_st.prevFutureEpoch - _prevWeekTime)) / - _workingSupply; - _st.rate = _st.newRate; - _st.integrateInvSupply += - (_st.rate * w * (_weekTime - _st.prevFutureEpoch)) / - _workingSupply; - } else { - _st.integrateInvSupply += - (_st.rate * w * dt) / - _workingSupply; - } - } - - if (_weekTime == block.timestamp) { - break; - } - _prevWeekTime = _weekTime; - _weekTime = min(_weekTime + WEEK, block.timestamp); - unchecked { - ++i; - } - } - } - - _st.period += 1; - period = _st.period; - periodTimestamp[uint256(uint128(_st.period))] = block.timestamp; - integrateInvSupply[uint256(uint128(_st.period))] = _st - .integrateInvSupply; - - uint256 _workingBalance = workingBalances[addr]; - integrateFraction[addr] += - (_workingBalance * - (_st.integrateInvSupply - integrateInvSupplyOf[addr])) / - 10 ** 18; - integrateInvSupplyOf[addr] = _st.integrateInvSupply; - integrateCheckpointOf[addr] = block.timestamp; - } - - function _checkpointRewards( - address user_, - uint256 totalSupply_, - bool claim_, - address receiver_ - ) internal { - RewardParameters memory _rp; - _rp.userBalance = 0; - _rp.receiver = receiver_; - if (user_ != address(0)) { - _rp.userBalance = balanceOf[user_]; - if (claim_ && receiver_ == address(0)) { - _rp.receiver = rewardsReceiver[user_]; - if (_rp.receiver == address(0)) { - _rp.receiver = user_; - } - } - } - - _rp.rewardCount = rewardCount; - for (uint256 i = 0; i < MAX_REWARDS; i++) { - if (i == _rp.rewardCount) { - break; - } - _rp.token = rewardTokens[i]; - - _rp.integral = rewardData[_rp.token].integral; - _rp.lastUpdate = min( - block.timestamp, - rewardData[_rp.token].periodFinish - ); - _rp.duration = _rp.lastUpdate - rewardData[_rp.token].lastUpdate; - if (_rp.duration != 0) { - rewardData[_rp.token].lastUpdate = _rp.lastUpdate; - if (totalSupply_ != 0) { - _rp.integral += - (_rp.duration * rewardData[_rp.token].rate * 10 ** 18) / - totalSupply_; - rewardData[_rp.token].integral = _rp.integral; - } - } - - if (user_ != address(0)) { - _rp.integralFor = rewardIntegralFor[_rp.token][user_]; - _rp.newClaimable = 0; - - if (_rp.integralFor < _rp.integral) { - rewardIntegralFor[_rp.token][user_] = _rp.integral; - _rp.newClaimable = - (_rp.userBalance * (_rp.integral - _rp.integralFor)) / - 10 ** 18; - } - - _rp.claimData = claimData[user_][_rp.token]; - _rp.totalClaimable = (_rp.claimData >> 128) + _rp.newClaimable; - if (_rp.totalClaimable > 0) { - _rp.totalClaimed = _rp.claimData & ((1 << 128) - 1); - if (claim_) { - bytes memory data = abi.encodeWithSignature( - "transfer(address,uint256)", - _rp.receiver, - _rp.totalClaimable - ); - (bool success, bytes memory response) = _rp.token.call( - data - ); - require( - success && - (response.length == 0 || - abi.decode(response, (bool))), - "Transfer failed" - ); - claimData[user_][_rp.token] = - _rp.totalClaimed + - _rp.totalClaimable; - } else if (_rp.newClaimable > 0) { - claimData[user_][_rp.token] = - _rp.totalClaimed + - (_rp.totalClaimable << 128); - } - } - } - } - } - - function _updateLiquidityLimit( - address addr_, - uint256 l_, - uint256 L_ - ) internal { - uint256 _votingBalance = IVotingEscrow(votingEscrow).balanceOf(addr_); - uint256 _votingTotal = IERC20(votingEscrow).totalSupply(); - - uint256 _lim = (l_ * TOKENLESS_PRODUCTION) / 100; - if (_votingTotal > 0) { - _lim += - (((L_ * _votingBalance) / _votingTotal) * - (100 - TOKENLESS_PRODUCTION)) / - 100; - } - - _lim = min(l_, _lim); - uint256 _oldBal = workingBalances[addr_]; - workingBalances[addr_] = _lim; - uint256 _workingSupply = workingSupply + _lim - _oldBal; - workingSupply = _workingSupply; - - emit UpdateLiquidityLimit(addr_, l_, L_, _lim, _workingSupply); - } - - function _transfer(address from_, address to_, uint256 value_) internal { - _checkpoint(from_); - _checkpoint(to_); - - if (value_ != 0) { - uint256 _totalSupply = totalSupply; - bool _isRewards = rewardCount != 0; - if (_isRewards) { - _checkpointRewards(from_, _totalSupply, false, address(0)); - } - uint256 _newBalance = balanceOf[from_] - value_; - balanceOf[from_] = _newBalance; - _updateLiquidityLimit(from_, _newBalance, _totalSupply); - - if (_isRewards) { - _checkpointRewards(to_, _totalSupply, false, address(0)); - } - _newBalance = balanceOf[to_] + value_; - balanceOf[to_] = _newBalance; - _updateLiquidityLimit(to_, _newBalance, _totalSupply); - } - - emit Transfer(from_, to_, value_); - } - - // TODO initial value - // addr_ = msg.sender - // claimRewards_ = false - function deposit( - uint256 value_, - address addr_, - bool claimRewards_ - ) external nonReentrant { - _checkpoint(addr_); - - if (value_ != 0) { - bool _isRewards = rewardCount != 0; - uint256 _totalSupply = totalSupply; - if (_isRewards) { - _checkpointRewards( - addr_, - _totalSupply, - claimRewards_, - address(0) - ); - } - - _totalSupply += value_; - uint256 _newBalance = balanceOf[addr_] + value_; - balanceOf[addr_] = _newBalance; - totalSupply = _totalSupply; - - _updateLiquidityLimit(addr_, _newBalance, _totalSupply); - - IERC20(lpToken).transferFrom(msg.sender, address(this), value_); - } - - emit Deposit(addr_, value_); - emit Transfer(address(0), addr_, value_); - } - - // TODO initial value - // claimRewards_ = false - function withdraw( - uint256 value_, - bool claimRewards_ - ) external nonReentrant { - _checkpoint(msg.sender); - - if (value_ != 0) { - bool _isRewards = rewardCount != 0; - uint256 _totalSupply = totalSupply; - if (_isRewards) { - _checkpointRewards( - msg.sender, - _totalSupply, - claimRewards_, - address(0) - ); - } - - _totalSupply -= value_; - uint256 _newBalance = balanceOf[msg.sender] - value_; - balanceOf[msg.sender] = _newBalance; - totalSupply = _totalSupply; - - _updateLiquidityLimit(msg.sender, _newBalance, _totalSupply); - - IERC20(lpToken).transfer(msg.sender, value_); - } - - emit Withdraw(msg.sender, value_); - emit Transfer(msg.sender, address(0), value_); - } - - // TODO - // addr_ = msg.sender, address receiver_ = address(0) - function claimRewards( - address addr_, - address receiver_ - ) external nonReentrant { - if (receiver_ != address(0)) { - require(addr_ == msg.sender); - } - _checkpointRewards(addr_, totalSupply, true, receiver_); - } - - function transferFrom( - address from_, - address to_, - uint256 value_ - ) external nonReentrant returns (bool) { - uint256 _allowance = allowance[from_][msg.sender]; - if (_allowance != type(uint256).max) { - allowance[from_][msg.sender] = _allowance - value_; - } - _transfer(from_, to_, value_); - return true; - } - - function transfer( - address to_, - uint256 value_ - ) external nonReentrant returns (bool) { - _transfer(msg.sender, to_, value_); - return true; - } - - function approve(address spender_, uint256 value_) external returns (bool) { - allowance[msg.sender][spender_] = value_; - emit Approval(msg.sender, spender_, value_); - return true; - } - - function permit( - address owner_, - address spender_, - uint256 value_, - uint256 deadline_, - uint8 v_, - bytes32 r_, - bytes32 s_ - ) external returns (bool) { - require(owner_ != address(0)); - require(block.timestamp <= deadline_, "Expired"); - - uint256 _nonce = nonces[owner_]; - bytes32 _digest = keccak256( - abi.encodePacked( - "\x19\x01", - _domainSeparator(), - keccak256( - abi.encode( - EIP2612_TYPEHASH, - owner_, - spender_, - value_, - _nonce, - deadline_ - ) - ) - ) - ); - require(ecrecover(_digest, v_, r_, s_) == owner_, "Invalid sig"); - - allowance[owner_][spender_] = value_; - nonces[owner_] = _nonce + 1; - - emit Approval(owner_, spender_, value_); - return true; - } - - function increaseAllowance( - address spender_, - uint256 addedValue_ - ) external returns (bool) { - uint256 _currentAllowance = allowance[msg.sender][spender_]; - uint256 _newAllowance = _currentAllowance + addedValue_; - allowance[msg.sender][spender_] = _newAllowance; - emit Approval(msg.sender, spender_, _newAllowance); - return true; - } - - function decreaseAllowance( - address spender_, - uint256 subtractedValue_ - ) external returns (bool) { - uint256 _currentAllowance = allowance[msg.sender][spender_]; - uint256 _newAllowance = _currentAllowance - subtractedValue_; - allowance[msg.sender][spender_] = _newAllowance; - emit Approval(msg.sender, spender_, _newAllowance); - return true; - } - - function userCheckpoint(address addr_) external returns (bool) { - require( - msg.sender == addr_ || msg.sender == minter, - "dev: unauthorized" - ); - _checkpoint(addr_); - _updateLiquidityLimit(addr_, balanceOf[addr_], totalSupply); - return true; - } - - function setRewardsReceiver(address receiver_) external { - rewardsReceiver[msg.sender] = receiver_; - } - - function kick(address addr_) external { - uint256 _tLast = integrateCheckpointOf[addr_]; - uint256 _tVe = IVotingEscrow(votingEscrow).userPointHistoryTs( - addr_, - IVotingEscrow(votingEscrow).userPointEpoch(addr_) - ); - uint256 _balance = balanceOf[addr_]; - - require( - ERC20(votingEscrow).balanceOf(addr_) == 0 || _tVe > _tLast, - "Not allowed" - ); - require( - workingBalances[addr_] > (_balance * TOKENLESS_PRODUCTION) / 100, - "Not needed" - ); - - _checkpoint(addr_); - _updateLiquidityLimit(addr_, balanceOf[addr_], totalSupply); - } - - function depositRewardToken( - address rewardToken_, - uint256 amount_ - ) external nonReentrant { - require( - msg.sender == rewardData[rewardToken_].distributor, - "Invalid dist" - ); - - _checkpointRewards(address(0), totalSupply, false, address(0)); - - bytes memory _data = abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - msg.sender, - address(this), - amount_ - ); - (bool success, bytes memory response) = rewardToken_.call(_data); - require(success); - require(response.length == 0 || abi.decode(response, (bool))); - - uint256 _periodFinish = rewardData[rewardToken_].periodFinish; - if (block.timestamp >= _periodFinish) { - rewardData[rewardToken_].rate = amount_ / WEEK; - } else { - uint256 remaining = _periodFinish - block.timestamp; - uint256 leftover = remaining * rewardData[rewardToken_].rate; - rewardData[rewardToken_].rate = (amount_ + leftover) / WEEK; - } - - rewardData[rewardToken_].lastUpdate = block.timestamp; - rewardData[rewardToken_].periodFinish = block.timestamp + WEEK; - } - - function addReward( - address rewardToken_, - address distributor_ - ) external onlyAdmin { - require(rewardCount < MAX_REWARDS, "Max"); - require(rewardData[rewardToken_].distributor == address(0), "Has dist"); - - rewardData[rewardToken_].distributor = distributor_; - rewardTokens[rewardCount] = rewardToken_; - ++rewardCount; - } - - function setRewardDistributor( - address rewardToken_, - address distributor_ - ) external { - address currentDistributor = rewardData[rewardToken_].distributor; - require(msg.sender == currentDistributor || msg.sender == admin); - require(currentDistributor != address(0)); - require(distributor_ != address(0)); - - rewardData[rewardToken_].distributor = distributor_; - } - - function setKilled(bool isKilled_) external onlyAdmin { - isKilled = isKilled_; - } - - function claimedReward( - address addr_, - address token_ - ) external view returns (uint256) { - return claimData[addr_][token_] % 2 ** 128; - } - - function claimableReward( - address user_, - address rewardToken_ - ) external view returns (uint256) { - uint256 integral = rewardData[rewardToken_].integral; - uint256 totalSupply_ = totalSupply; - if (totalSupply_ != 0) { - uint256 lastUpdate = block.timestamp < - rewardData[rewardToken_].periodFinish - ? block.timestamp - : rewardData[rewardToken_].periodFinish; - uint256 duration = lastUpdate - rewardData[rewardToken_].lastUpdate; - integral += - (duration * rewardData[rewardToken_].rate * 10 ** 18) / - totalSupply_; - } - uint256 integralFor = rewardIntegralFor[rewardToken_][user_]; - uint256 newClaimable = (balanceOf[user_] * (integral - integralFor)) / - 10 ** 18; - - return (claimData[user_][rewardToken_] >> 128) + newClaimable; - } - - function claimableTokens(address addr_) external returns (uint256) { - _checkpoint(addr_); - return - integrateFraction[addr_] - - IMinter(minter).minted(addr_, address(this)); - } - - function integrateCheckpoint() external view returns (uint256) { - return periodTimestamp[uint256(uint128(period))]; - } - - function decimals() external pure returns (uint256) { - return 18; - } - - function version() external pure returns (string memory) { - return VERSION; - } - - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparator(); - } - - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } - - modifier onlyAdmin() { - require(msg.sender == admin); - _; - } -} diff --git a/contracts/curveFork/Minter.sol b/contracts/curveFork/Minter.sol deleted file mode 100644 index 9c7cc88b..00000000 --- a/contracts/curveFork/Minter.sol +++ /dev/null @@ -1,94 +0,0 @@ -pragma solidity 0.8.18; - -/*** - *@title Token Minter - * SPDX-License-Identifier: MIT - */ - -import "./interfaces/ICRV.sol"; -import "./interfaces/ILiquidityGauge.sol"; -import "./interfaces/IGaugeController.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; - -contract Minter is ReentrancyGuard { - event Minted(address indexed recipient, address gauge, uint256 minted); - - address public token; - address public controller; - - // user -> gauge -> value - mapping(address => mapping(address => uint256)) public minted; // minted amount of user from specific gauge. - - // minter -> user -> can mint? - mapping(address => mapping(address => bool)) public allowedToMintFor; // A can mint for B if [A => B => true]. - - constructor(address _token, address _controller) { - token = _token; - controller = _controller; - } - - function _mintFor(address gaugeAddr_, address for_) internal { - require( - IGaugeController(controller).gaugeTypes(gaugeAddr_) >= 0, - "dev: gauge is not added" - ); - - ILiquidityGauge(gaugeAddr_).userCheckpoint(for_); - uint256 totalMint = ILiquidityGauge(gaugeAddr_).integrateFraction(for_); - uint256 _toMint = totalMint - minted[for_][gaugeAddr_]; - - if (_toMint != 0) { - ICRV(token).mint(for_, _toMint); - minted[for_][gaugeAddr_] = totalMint; - - emit Minted(for_, gaugeAddr_, totalMint); - } - } - - /*** - *@notice Mint everything which belongs to `msg.sender` and send to them - *@param gaugeAddr_ `LiquidityGauge` address to get mintable amount from - */ - function mint(address gaugeAddr_) external nonReentrant { - _mintFor(gaugeAddr_, msg.sender); - } - - /*** - *@notice Mint everything which belongs to `msg.sender` across multiple gauges - *@param gaugeAddrs_ List of `LiquidityGauge` addresses - *@dev address[8]: 8 has randomly decided and has no meaning. - */ - function mintMany(address[8] memory gaugeAddrs_) external nonReentrant { - for (uint256 i; i < 8; ) { - if (gaugeAddrs_[i] == address(0)) { - break; - } - _mintFor(gaugeAddrs_[i], msg.sender); - unchecked { - ++i; - } - } - } - - /*** - *@notice Mint tokens for `for_` - *@dev Only possible when `msg.sender` has been approved via `toggle_approve_mint` - *@param gaugeAddr_ `LiquidityGauge` address to get mintable amount from - *@param for_ Address to mint to - */ - function mintFor(address gaugeAddr_, address for_) external nonReentrant { - if (allowedToMintFor[msg.sender][for_]) { - _mintFor(gaugeAddr_, for_); - } - } - - /*** - *@notice allow `mintingUser` to mint for `msg.sender` - *@param mintingUser_ Address to toggle permission for - */ - function toggleApproveMint(address mintingUser_) external { - allowedToMintFor[mintingUser_][msg.sender] = !allowedToMintFor[ - mintingUser_ - ][msg.sender]; - } -} diff --git a/contracts/curveFork/VotingEscrow.sol b/contracts/curveFork/VotingEscrow.sol deleted file mode 100644 index f20b6c8b..00000000 --- a/contracts/curveFork/VotingEscrow.sol +++ /dev/null @@ -1,863 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -// Voting escrow to have time-weighted votes -// Votes have a weight depending on time, so that users are committed -// to the future of (whatever they are voting for). -// The weight in this implementation is linear, and lock cannot be more than maxtime: -// w ^ -// 1 + / -// | / -// | / -// | / -// |/ -// 0 +--------+------> time -// maxtime (4 years?) - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; - -// Interface for checking whether address belongs to a whitelisted -// type of a smart wallet. -// When new types are added - the whole contract is changed -// The check() method is modifying to be able to use caching -// for individual wallet addresses -import "./interfaces/ISmartWalletChecker.sol"; - -contract VotingEscrow is ReentrancyGuard { - struct Point { - int128 bias; - int128 slope; // - dweight / dt - uint256 ts; //timestamp - uint256 blk; // block - } - - // We cannot really do block numbers per se b/c slope is per time, not per block - // and per block could be fairly bad b/c Ethereum changes blocktimes. - // What we can do is to extrapolate ***At functions - struct LockedBalance { - int128 amount; - uint256 end; - } - - uint128 private constant DEPOSIT_FOR_TYPE = 0; - uint128 private constant CREATE_LOCK_TYPE = 1; - uint128 private constant INCREASE_LOCK_AMOUNT = 2; - uint128 private constant INCREASE_UNLOCK_TIME = 3; - - event CommitOwnership(address admin); - event ApplyOwnership(address admin); - event Deposit( - address indexed provider, - uint256 value, - uint256 indexed locktime, - uint128 _type, - uint256 ts - ); - event Withdraw(address indexed provider, uint256 value, uint256 ts); - event Supply(uint256 prevSupply, uint256 supply); - - uint256 public constant WEEK = 7 * 86400; // all future times are rounded by week - uint256 public constant MAXTIME = 4 * 365 * 86400; // 4 years - uint256 public constant MULTIPLIER = 1e18; - - address public token; - uint256 public supply; - - mapping(address => LockedBalance) public locked; - - //everytime user deposit/withdraw/change_locktime, these values will be updated; - uint256 public epoch; - - mapping(uint256 => Point) public pointHistory; // epoch -> unsigned point. - mapping(address => mapping(uint256 => Point)) public userPointHistory; // user -> Point[user_epoch] - mapping(address => uint256) public userPointEpoch; - mapping(uint256 => int128) public slopeChanges; // time -> signed slope change - - // Aragon's view methods for compatibility - address public controller; - bool public transfersEnabled; - - string public name; - string public symbol; - string public version; - uint256 public decimals; - - // Checker for whitelisted (smart contract) wallets which are allowed to deposit - // The goal is to prevent tokenizing the escrow - address public futureSmartWalletChecker; - address public smartWalletChecker; - - address public admin; - address public futureAdmin; - - /*** - * @notice Contract constructor - * @param tokenAddr_ `YMWK` token address - * @param name_ Token name - * @param symbol_ Token symbol - * @param version_ Contract version - required for Aragon compatibility - */ - constructor( - address tokenAddr_, - string memory name_, - string memory symbol_, - string memory version_ - ) { - admin = msg.sender; - token = tokenAddr_; - pointHistory[0].blk = block.number; - pointHistory[0].ts = block.timestamp; - controller = msg.sender; - transfersEnabled = true; - - uint256 decimalsFromToken = ERC20(tokenAddr_).decimals(); - require( - decimalsFromToken <= 255, - "Decimals should be less than or equal to 255" - ); - decimals = decimalsFromToken; - - name = name_; - symbol = symbol_; - version = version_; - } - - /*** - * @notice Transfer ownership of VotingEscrow contract to `addr_` - * @param addr Address to have ownership transferred to - */ - function commitTransferOwnership(address addr_) external onlyAdmin { - futureAdmin = addr_; - emit CommitOwnership(addr_); - } - - /*** - * @notice Apply ownership transfer - */ - function applyTransferOwnership() external onlyAdmin { - address _admin = futureAdmin; - require(_admin != address(0), "admin not set"); - admin = _admin; - emit ApplyOwnership(_admin); - } - - /*** - * @notice Set an external contract to check for approved smart contract wallets - * @param addr_ Address of Smart contract checker - */ - function commitSmartWalletChecker(address addr_) external onlyAdmin { - futureSmartWalletChecker = addr_; - } - - /*** - * @notice Apply setting external contract to check approved smart contract wallets - */ - function applySmartWalletChecker() external onlyAdmin { - smartWalletChecker = futureSmartWalletChecker; - } - - /*** - *@notice Check if the call is from a whitelisted smart contract, revert if not - *@param addr_ Address to be checked - */ - function assertNotContract(address addr_) internal { - if (addr_ != tx.origin) { - address _checker = smartWalletChecker; - if ( - _checker != address(0) && - ISmartWalletChecker(_checker).check(addr_) - ) { - return; - } - revert("Smart contract depositors not allowed"); - } - } - - /*** - * @notice Get the most recently recorded rate of voting power decrease for `addr` - * @param addr_ Address of the user wallet - * @return Value of the slope - */ - function getLastUserSlope(address addr_) external view returns (int128) { - uint256 _uEpoch = userPointEpoch[addr_]; - return userPointHistory[addr_][_uEpoch].slope; - } - - /*** - * @notice Get the timestamp for checkpoint `idx_` for `addr_` - * @param addr_ User wallet address - * @param idx_ User epoch number - * @return Epoch time of the checkpoint - */ - function userPointHistoryTs( - address addr_, - uint256 idx_ - ) external view returns (uint256) { - return userPointHistory[addr_][idx_].ts; - } - - /*** - * @notice Get timestamp when `addr_`'s lock finishes - * @param addr_ User wallet - * @return Epoch time of the lock end - */ - function lockedEnd(address addr_) external view returns (uint256) { - return locked[addr_].end; - } - - /*** - * @notice Record global and per-user data to checkpoint - * @param addr_ User's wallet address. No user checkpoint if 0x0 - * @param oldLocked_ Pevious locked amount / end lock time for the user - * @param newLocked_ New locked amount / end lock time for the user - */ - function _checkpoint( - address addr_, - LockedBalance memory oldLocked_, - LockedBalance memory newLocked_ - ) internal { - Point memory _uOld; - Point memory _uNew; - int128 _oldDSlope = 0; - int128 _newDSlope = 0; - uint256 _epoch = epoch; - - if (addr_ != address(0)) { - // Calculate slopes and biases - // Kept at zero when they have to - if (oldLocked_.end > block.timestamp && oldLocked_.amount > 0) { - unchecked { - _uOld.slope = int128(oldLocked_.amount / int256(MAXTIME)); - } - _uOld.bias = - _uOld.slope * - int128(uint128(oldLocked_.end - block.timestamp)); - } - - if (newLocked_.end > block.timestamp && newLocked_.amount > 0) { - unchecked { - _uNew.slope = int128( - uint128(newLocked_.amount) / uint128(MAXTIME) - ); - } - _uNew.bias = - _uNew.slope * - int128(uint128(newLocked_.end - block.timestamp)); - } - - // Read values of scheduled changes in the slope - // old_locked.end can be in the past and in the future - // new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros - _oldDSlope = slopeChanges[oldLocked_.end]; - if (newLocked_.end != 0) { - if (newLocked_.end == oldLocked_.end) { - _newDSlope = _oldDSlope; - } else { - _newDSlope = slopeChanges[newLocked_.end]; - } - } - } - - Point memory _lastPoint = Point({ - bias: 0, - slope: 0, - ts: block.timestamp, - blk: block.number - }); - if (_epoch > 0) { - _lastPoint = Point({ - bias: pointHistory[_epoch].bias, - slope: pointHistory[_epoch].slope, - ts: pointHistory[_epoch].ts, - blk: pointHistory[_epoch].blk - }); - } - uint256 _lastCheckpoint = _lastPoint.ts; - - // initial_last_point is used for extrapolation to calculate block number - // (approximately, for *At methods) and save them - // as we cannot figure that out exactly from inside the contract - Point memory _initialLastPoint = Point({ - bias: _lastPoint.bias, - slope: _lastPoint.slope, - ts: _lastPoint.ts, - blk: _lastPoint.blk - }); - uint256 _blockSlope = 0; - if (block.timestamp > _lastPoint.ts) { - _blockSlope = - (MULTIPLIER * (block.number - _lastPoint.blk)) / - (block.timestamp - _lastPoint.ts); - } - // If last point is already recorded in this block, slope=0 - // But that's ok b/c we know the block in such case - - // Go over weeks to fill history and calculate what the current point is - uint256 _ti; - unchecked { - _ti = (_lastCheckpoint / WEEK) * WEEK; - } - for (uint256 i; i < 255; ) { - // Hopefully it won't happen that this won't get used in 5 years! - // If it does, users will be able to withdraw but vote weight will be broken - _ti += WEEK; - int128 _dSlope = 0; - if (_ti > block.timestamp) { - _ti = block.timestamp; - } else { - _dSlope = slopeChanges[_ti]; - } - - _lastPoint.bias -= - _lastPoint.slope * - int128(uint128(_ti) - uint128(_lastCheckpoint)); - _lastPoint.slope += _dSlope; - - if (_lastPoint.bias < 0) { - // This can happen - _lastPoint.bias = 0; - } - if (_lastPoint.slope < 0) { - // This cannot happen - just in case - _lastPoint.slope = 0; - } - - _lastCheckpoint = _ti; - _lastPoint.ts = _ti; - _lastPoint.blk = - _initialLastPoint.blk + - (_blockSlope * (_ti - _initialLastPoint.ts)) / - MULTIPLIER; - - _epoch += 1; - - if (_ti == block.timestamp) { - _lastPoint.blk = block.number; - break; - } else { - pointHistory[_epoch] = Point({ - bias: _lastPoint.bias, - slope: _lastPoint.slope, - ts: _lastPoint.ts, - blk: _lastPoint.blk - }); - } - unchecked { - ++i; - } - } - - epoch = _epoch; - // Now point_history is filled until t=now - - if (addr_ != address(0)) { - // If last point was in this block, the slope change has been applied already - // But in such case we have 0 slope(s) - _lastPoint.slope += (_uNew.slope - _uOld.slope); - _lastPoint.bias += (_uNew.bias - _uOld.bias); - if (_lastPoint.slope < 0) { - _lastPoint.slope = 0; - } - if (_lastPoint.bias < 0) { - _lastPoint.bias = 0; - } - } - - // Record the changed point into history - pointHistory[_epoch] = _lastPoint; - - address _addr = addr_; // To avoid being "Stack Too Deep" - - if (_addr != address(0)) { - // Schedule the slope changes (slope is going down) - // We subtract new_user_slope from [new_locked.end] - // and add old_user_slope to [old_locked.end] - if (oldLocked_.end > block.timestamp) { - // old_dslope was - u_old.slope, so we cancel that - _oldDSlope += _uOld.slope; - if (newLocked_.end == oldLocked_.end) { - _oldDSlope -= _uNew.slope; // It was a new deposit, not extension - } - slopeChanges[oldLocked_.end] = _oldDSlope; - } - - if (newLocked_.end > block.timestamp) { - if (newLocked_.end > oldLocked_.end) { - _newDSlope -= _uNew.slope; // old slope disappeared at this point - slopeChanges[newLocked_.end] = _newDSlope; - } - // else: we recorded it already in old_dslope - } - - // Now handle user history - uint256 _userEpoch; - unchecked { - _userEpoch = userPointEpoch[_addr] + 1; - } - userPointEpoch[_addr] = _userEpoch; - - _uNew.ts = block.timestamp; - _uNew.blk = block.number; - userPointHistory[_addr][_userEpoch] = _uNew; - } - } - - /*** - * @notice Deposit and lock tokens for a user - * @param addr_ User's wallet address - * @param value_ Amount to deposit - * @param unlockTime_ New time when to unlock the tokens, or 0 if unchanged - * @param lockedBalance_ Previous locked amount / timestamp - */ - function _depositFor( - address addr_, - uint256 value_, - uint256 unlockTime_, - LockedBalance memory lockedBalance_, - uint128 type_ - ) internal { - LockedBalance memory _locked = LockedBalance( - lockedBalance_.amount, - lockedBalance_.end - ); - LockedBalance memory _oldLocked = LockedBalance( - lockedBalance_.amount, - lockedBalance_.end - ); - - uint256 _supplyBefore = supply; - supply = _supplyBefore + value_; - - // Adding to existing lock, or if a lock is expired - creating a new one - _locked.amount += int128(uint128(value_)); - if (unlockTime_ != 0) { - _locked.end = unlockTime_; - } - locked[addr_] = _locked; - - // Possibilities: - // Both old_locked.end could be current or expired (>/< block.timestamp) - // value == 0 (extend lock) or value > 0 (add to lock or extend lock) - // _locked.end > block.timestamp (always) - _checkpoint(addr_, _oldLocked, _locked); - - if (value_ != 0) { - require( - ERC20(token).transferFrom(addr_, address(this), value_), - "Transfer failed" - ); - } - - emit Deposit(addr_, value_, _locked.end, type_, block.timestamp); - emit Supply(_supplyBefore, _supplyBefore + value_); - } - - /*** - * @notice Record global data to checkpoint - */ - function checkpoint() external { - LockedBalance memory _old; - LockedBalance memory _new; - _checkpoint(address(0), _old, _new); - } - - /*** - * @notice Deposit `_value` tokens for `_addr` and add to the lock - * @dev Anyone (even a smart contract) can deposit for someone else, but - cannot extend their locktime and deposit for a brand new user - * @param addr_ User's wallet address - * @param value_ Amount to add to user's lock - */ - function depositFor(address addr_, uint256 value_) external nonReentrant { - LockedBalance memory _locked = locked[addr_]; - - require(value_ > 0, "Need non-zero value"); - require(_locked.amount > 0, "No existing lock found"); - require( - _locked.end > block.timestamp, - "Cannot add to expired lock. Withdraw" - ); - - _depositFor(addr_, value_, 0, locked[addr_], DEPOSIT_FOR_TYPE); - } - - /*** - * @notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time` - * @param value_ Amount to deposit - * @param unlockTime_ Epoch time when tokens unlock, rounded down to whole weeks - */ - function createLock( - uint256 value_, - uint256 unlockTime_ - ) external nonReentrant { - assertNotContract(msg.sender); - - uint256 _unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; - LockedBalance memory _locked = locked[msg.sender]; - - require(value_ > 0, "Need non-zero value"); - require(_locked.amount == 0, "Withdraw old tokens first"); - require( - _unlockTimeRounded > block.timestamp, - "Can only lock until time in the future" - ); - require( - _unlockTimeRounded <= block.timestamp + MAXTIME, - "Voting lock can be 4 years max" - ); - - _depositFor( - msg.sender, - value_, - _unlockTimeRounded, - _locked, - CREATE_LOCK_TYPE - ); - } - - /*** - * @notice Deposit `_value` additional tokens for `msg.sender` - without modifying the unlock time - * @param value_ Amount of tokens to deposit and add to the lock - */ - function increaseAmount(uint256 value_) external nonReentrant { - assertNotContract(msg.sender); - - LockedBalance memory _locked = locked[msg.sender]; - - require(value_ > 0, "Need non-zero value"); - require(_locked.amount > 0, "No existing lock found"); - require( - _locked.end > block.timestamp, - "Cannot add to expired lock. Withdraw" - ); - - _depositFor(msg.sender, value_, 0, _locked, INCREASE_LOCK_AMOUNT); - } - - /*** - * @notice Extend the unlock time for `msg.sender` to `_unlock_time` - * @param unlockTime_ New epoch time for unlocking - */ - function increaseUnlockTime(uint256 unlockTime_) external nonReentrant { - assertNotContract(msg.sender); - - LockedBalance memory _locked = locked[msg.sender]; - uint256 _unlockTimeRounded; - unchecked { - _unlockTimeRounded = (unlockTime_ / WEEK) * WEEK; - } - - require(_locked.end > block.timestamp, "Lock expired"); - require(_locked.amount > 0, "Nothing is locked"); - require( - _unlockTimeRounded > _locked.end, - "Can only increase lock duration" - ); - require( - _unlockTimeRounded <= block.timestamp + MAXTIME, - "Voting lock can be 4 years max" - ); - - _depositFor( - msg.sender, - 0, - _unlockTimeRounded, - _locked, - INCREASE_UNLOCK_TIME - ); - } - - /*** - * @notice Withdraw all tokens for `msg.sender` - * @dev Only possible if the lock has expired - */ - function withdraw() external nonReentrant { - LockedBalance memory _locked = LockedBalance( - locked[msg.sender].amount, - locked[msg.sender].end - ); - require(block.timestamp >= _locked.end, "The lock didn't expire"); - uint256 _value = uint256(int256(_locked.amount)); - - LockedBalance memory _oldLocked = LockedBalance( - locked[msg.sender].amount, - locked[msg.sender].end - ); - - _locked.end = 0; - _locked.amount = 0; - locked[msg.sender] = LockedBalance(_locked.amount, _locked.end); - uint256 _supplyBefore = supply; - supply = _supplyBefore - _value; - - // old_locked can have either expired <= timestamp or zero end - // _locked has only 0 end - // Both can have >= 0 amount - _checkpoint(msg.sender, _oldLocked, _locked); - - require(ERC20(token).transfer(msg.sender, _value), "Transfer failed"); - - emit Withdraw(msg.sender, _value, block.timestamp); - emit Supply(_supplyBefore, _supplyBefore - _value); - } - - // The following ERC20/minime-compatible methods are not real balanceOf and supply! - // They measure the weights for the purpose of voting, so they don't represent - // real coins. - - /*** - * @notice Binary search to estimate epoch for block number - * @param block_ Block to find - * @param maxEpoch_ Don't go beyond this epoch - * @return Approximate epoch for block - */ - function findBlockEpoch( - uint256 block_, - uint256 maxEpoch_ - ) internal view returns (uint256) { - // Binary search - uint256 _min = 0; - uint256 _max = maxEpoch_; - unchecked { - for (uint256 i; i < 128; i++) { - // Will be always enough for 128-bit numbers - if (_min >= _max) { - break; - } - uint256 _mid = (_min + _max + 1) / 2; - if (pointHistory[_mid].blk <= block_) { - _min = _mid; - } else { - _max = _mid - 1; - } - } - } - return _min; - } - - /*** - * @notice Get the current voting power for `msg.sender` - * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility - * @param addr_ User wallet address - * @param t_ Epoch time to return voting power at - * @return User voting power - */ - function balanceOf( - address addr_, - uint256 t_ - ) external view returns (uint256) { - uint256 epoch_ = userPointEpoch[addr_]; - if (epoch_ == 0) { - return 0; - } else { - Point memory lastPoint = userPointHistory[addr_][epoch_]; - lastPoint.bias -= - lastPoint.slope * - int128(int256(t_) - int256(lastPoint.ts)); - if (lastPoint.bias < 0) { - lastPoint.bias = 0; - } - return uint256(int256(lastPoint.bias)); - } - } - - /*** - * @notice Get the current voting power for `msg.sender` - * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility - * @param addr_ User wallet address - * @return User voting power - */ - function balanceOf(address addr_) external view returns (uint256) { - uint256 epoch_ = userPointEpoch[addr_]; - if (epoch_ == 0) { - return 0; - } else { - Point memory lastPoint = userPointHistory[addr_][epoch_]; - lastPoint.bias -= - lastPoint.slope * - int128(int256(block.timestamp) - int256(lastPoint.ts)); - if (lastPoint.bias < 0) { - lastPoint.bias = 0; - } - return uint256(int256(lastPoint.bias)); - } - } - - /*** - * @notice Measure voting power of `addr` at block height `_block` - * @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime - * @param addr_ User's wallet address - * @param block_ Block to calculate the voting power at - * @return Voting power - */ - function balanceOfAt( - address addr_, - uint256 block_ - ) external view returns (uint256) { - // Copying and pasting totalSupply code because Vyper cannot pass by - // reference yet - require(block_ <= block.number, "Cannot look up future block"); - - // Binary search - uint256 min_ = 0; - uint256 max_ = userPointEpoch[addr_]; - - unchecked { - for (uint i = 0; i < 128; i++) { - // Will be always enough for 128-bit numbers - if (min_ >= max_) { - break; - } - uint256 mid_ = (min_ + max_ + 1) / 2; - if (userPointHistory[addr_][mid_].blk <= block_) { - min_ = mid_; - } else { - max_ = mid_ - 1; - } - } - } - - Point memory _upoint = userPointHistory[addr_][min_]; - uint256 _maxEpoch = epoch; - uint256 _epoch = findBlockEpoch(block_, _maxEpoch); - Point memory _point0 = pointHistory[_epoch]; - uint256 _dBlock = 0; - uint256 _dt = 0; - - if (_epoch < _maxEpoch) { - Point memory _point1 = pointHistory[_epoch + 1]; - _dBlock = _point1.blk - _point0.blk; - _dt = _point1.ts - _point0.ts; - } else { - _dBlock = block.number - _point0.blk; - _dt = block.timestamp - _point0.ts; - } - - uint256 _blockTime = _point0.ts; - if (_dBlock != 0) { - _blockTime += (_dt * (block_ - _point0.blk)) / _dBlock; - } - - _upoint.bias -= - _upoint.slope * - int128(int256(_blockTime) - int256(_upoint.ts)); - if (_upoint.bias >= 0) { - return uint256(int256(_upoint.bias)); - } else { - return 0; - } - } - - /*** - * @notice Calculate total voting power at some point in the past - * @param point_ The point (bias/slope) to start search from - * @param t_ Time to calculate the total voting power at - * @return Total voting power at that time - */ - function supplyAt( - Point memory point_, - uint256 t_ - ) internal view returns (uint256) { - Point memory _lastPoint = point_; - uint256 _ti; - unchecked { - _ti = (_lastPoint.ts / WEEK) * WEEK; - } - for (uint256 i; i < 255; ) { - _ti += WEEK; - int128 _dSlope = 0; - if (_ti > t_) { - _ti = t_; - } else { - _dSlope = slopeChanges[_ti]; - } - _lastPoint.bias -= - _lastPoint.slope * - int128(int256(_ti) - int256(_lastPoint.ts)); - if (_ti == t_) { - break; - } - _lastPoint.slope += _dSlope; - _lastPoint.ts = _ti; - unchecked { - ++i; - } - } - - if (_lastPoint.bias < 0) { - _lastPoint.bias = 0; - } - return uint256(int256(_lastPoint.bias)); - } - - /*** - * @notice Calculate total voting power - * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility - * @param t_ Time to calculate the total voting power at - * @return Total voting power - */ - function totalSupply(uint256 t_) external view returns (uint256) { - uint256 _epoch = epoch; - Point memory _lastPoint = pointHistory[_epoch]; - return supplyAt(_lastPoint, t_); - } - - /*** - * @notice Calculate total voting power - * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility - * @return Total voting power - */ - function totalSupply() external view returns (uint256) { - uint256 _epoch = epoch; - Point memory _lastPoint = pointHistory[_epoch]; - return supplyAt(_lastPoint, block.timestamp); - } - - /*** - * @notice Calculate total voting power at some point in the past - * @param block_ Block to calculate the total voting power at - * @return Total voting power at `_block` - */ - function totalSupplyAt(uint256 block_) external view returns (uint256) { - require(block_ <= block.number, "Invalid block number"); - uint256 _epoch = epoch; - uint256 _targetEpoch = findBlockEpoch(block_, _epoch); - - Point memory _point = pointHistory[_targetEpoch]; - uint256 _dt = 0; - if (_targetEpoch < _epoch) { - Point memory _pointNext = pointHistory[_targetEpoch + 1]; - if (_point.blk != _pointNext.blk) { - _dt = - ((block_ - _point.blk) * (_pointNext.ts - _point.ts)) / - (_pointNext.blk - _point.blk); - } - } else { - if (_point.blk != block.number) { - _dt = - ((block_ - _point.blk) * (block.timestamp - _point.ts)) / - (block.number - _point.blk); - } - } - // Now _dt contains info on how far are we beyond point - return supplyAt(_point, _point.ts + _dt); - } - - /*** - * @dev Dummy method required for Aragon compatibility - */ - function changeController(address newController) external { - require( - msg.sender == controller, - "Only the controller can call this function" - ); - controller = newController; - } - - modifier onlyAdmin() { - require(admin == msg.sender, "admin only"); - _; - } -} diff --git a/contracts/curveFork/interfaces/ICRV.sol b/contracts/curveFork/interfaces/ICRV.sol deleted file mode 100644 index 6395011e..00000000 --- a/contracts/curveFork/interfaces/ICRV.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -interface ICRV { - function mint(address to_, uint256 value_) external returns (bool); - - function approve(address spender_, uint256 value_) external; - - function rate() external view returns (uint256); - - function futureEpochTimeWrite() external returns (uint256); -} diff --git a/contracts/curveFork/interfaces/IGaugeController.sol b/contracts/curveFork/interfaces/IGaugeController.sol deleted file mode 100644 index 5c0e6f07..00000000 --- a/contracts/curveFork/interfaces/IGaugeController.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -interface IGaugeController { - function gaugeTypes(address addr_) external view returns (uint256); - - function votingEscrow() external view returns (address); - - function checkpointGauge(address addr) external; - - function addType(string memory name_, uint256 weight_) external; - - function addGauge( - address addr_, - int128 gaugeType_, - uint256 weight_ - ) external; - - function gaugeRelativeWeight( - address addr, - uint256 time - ) external view returns (uint256); -} diff --git a/contracts/curveFork/interfaces/ILiquidityGauge.sol b/contracts/curveFork/interfaces/ILiquidityGauge.sol deleted file mode 100644 index 97c98530..00000000 --- a/contracts/curveFork/interfaces/ILiquidityGauge.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -interface ILiquidityGauge { - function userCheckpoint(address addr_) external returns (bool); - - function integrateFraction(address addr_) external view returns (uint256); - - function deposit(uint256 _value, address _addr) external; - - function withdraw(uint256 _value) external; -} diff --git a/contracts/curveFork/interfaces/IMinter.sol b/contracts/curveFork/interfaces/IMinter.sol deleted file mode 100644 index 48d4be26..00000000 --- a/contracts/curveFork/interfaces/IMinter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -interface IMinter { - function token() external view returns (address); - - function controller() external view returns (address); - - function minted( - address user_, - address gauge_ - ) external view returns (uint256); -} diff --git a/contracts/curveFork/interfaces/ISmartWalletChecker.sol b/contracts/curveFork/interfaces/ISmartWalletChecker.sol deleted file mode 100644 index a281ba89..00000000 --- a/contracts/curveFork/interfaces/ISmartWalletChecker.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -// Interface for checking whether address belongs to a whitelisted type of a smart wallet. -// When new types are added - the whole contract is changed -// The check() method is modifying to be able to use caching -// for individual wallet addresses -interface ISmartWalletChecker { - function check(address addr_) external returns (bool); -} diff --git a/contracts/curveFork/interfaces/IVotingEscrow.sol b/contracts/curveFork/interfaces/IVotingEscrow.sol deleted file mode 100644 index 6d4414e9..00000000 --- a/contracts/curveFork/interfaces/IVotingEscrow.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -interface IVotingEscrow { - function getLastUserSlope(address addr_) external view returns (int128); - - function lockedEnd(address addr_) external view returns (uint256); - - function balanceOf( - address addr_, - uint256 t_ - ) external view returns (uint256); - - function balanceOf(address addr) external view returns (uint256); - - function totalSupply(uint256 t_) external view returns (uint256); - - function userPointEpoch(address _user) external view returns (uint256); - - function userPointHistoryTs( - address addr, - uint256 epoch - ) external view returns (uint256); -} diff --git a/test/fork/Constants.ts b/test/fork/Constants.ts deleted file mode 100644 index f00a9c15..00000000 --- a/test/fork/Constants.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { BigNumber } from "ethers"; - -class Constants { - static DAY = BigNumber.from(86400); - static WEEK = BigNumber.from(86400 * 7); - static MONTH = BigNumber.from(86400 * 30); - static YEAR = BigNumber.from(86400 * 365); - - static day = 86400; - static week = 86400 * 7; - static month = 86400 * 30; - static year = 86400 * 365; - - static TYPE_WEIGHTS: BigNumber[] = [ - BigNumber.from(5).mul(BigNumber.from(10).pow(17)), - BigNumber.from(2).mul(BigNumber.from(10).pow(18)), - ]; - static GAUGE_WEIGHTS: BigNumber[] = [ - BigNumber.from(2).mul(BigNumber.from(10).pow(18)), - BigNumber.from(10).pow(18), - BigNumber.from(5).mul(BigNumber.from(10).pow(17)), - ]; - - static symbol = "Token"; - static decimal = 18; - static INITIAL_SUPPLY = BigNumber.from("1303030303000000000000000000"); - - static ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; - - static ten_to_the_24 = BigNumber.from("1000000000000000000000000"); - static ten_to_the_21 = BigNumber.from("1000000000000000000000"); - static ten_to_the_20 = BigNumber.from("100000000000000000000"); - static ten_to_the_19 = BigNumber.from("10000000000000000000"); - static ten_to_the_18 = BigNumber.from("1000000000000000000"); - static ten_to_the_17 = BigNumber.from("100000000000000000"); - static ten_to_the_16 = BigNumber.from("10000000000000000"); - static ten_to_the_9 = BigNumber.from("1000000000"); - static a = BigNumber.from("2"); - static b = BigNumber.from("5"); - static zero = BigNumber.from("0"); - static MAX_UINT256 = BigNumber.from( - "115792089237316195423570985008687907853269984665640564039457584007913129639935" - ); - - static GAUGE_TYPES = [ - BigNumber.from("1"), - BigNumber.from("1"), - BigNumber.from("2"), - ]; -} - -export default Constants; diff --git a/test/fork/helper.ts b/test/fork/helper.ts deleted file mode 100644 index 67021869..00000000 --- a/test/fork/helper.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ethers } from "hardhat"; - -async function deployContracts() { - const [creator, alice, bob, charly] = await ethers.getSigners(); - - // Constants - const TYPE_WEIGHTS = [ - ethers.utils.parseUnits("1", 17), - ethers.utils.parseUnits("1", 18), - ]; - const GAUGE_WEIGHTS = [ - ethers.utils.parseUnits("1", 18), - ethers.utils.parseUnits("1", 18), - ethers.utils.parseUnits("1", 17), - ]; - const TEN_TO_THE_21 = ethers.utils.parseUnits("1", 21); - - // Contract factories - const Token = await ethers.getContractFactory("CRV"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - const GaugeController = await ethers.getContractFactory("GaugeController"); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); - const TestLP = await ethers.getContractFactory("TestLP"); - const Minter = await ethers.getContractFactory("Minter"); - - // Contract deployments - const token = await Token.deploy(); - const votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - const gaugeController = await GaugeController.deploy( - token.address, - votingEscrow.address - ); - const mockLpToken = await TestLP.deploy( - "tokenDAO LP token", - "iToken", - 18, - TEN_TO_THE_21.mul(2) - ); - const minter = await Minter.deploy(token.address, gaugeController.address); - - const lg1 = await LiquidityGauge.deploy(mockLpToken.address, minter.address); - const lg2 = await LiquidityGauge.deploy(mockLpToken.address, minter.address); - const lg3 = await LiquidityGauge.deploy(mockLpToken.address, minter.address); - - await token.setMinter(minter.address); - - return { - creator, - alice, - bob, - charly, - token, - votingEscrow, - gaugeController, - mockLpToken, - minter, - threeGauges: [lg1.address, lg2.address, lg3.address], - gauges: [lg1, lg2, lg3], - }; -} - -export { deployContracts }; diff --git a/test/fork/integration/ERC20CRV/mintIntegration.test.ts b/test/fork/integration/ERC20CRV/mintIntegration.test.ts deleted file mode 100644 index 8fd54e97..00000000 --- a/test/fork/integration/ERC20CRV/mintIntegration.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { BigNumber } from "ethers"; - -// Assuming you have a helper function to increase blockchain time -async function increaseTime(duration: number) { - await ethers.provider.send("evm_increaseTime", [duration]); - await ethers.provider.send("evm_mine", []); -} - -const YEAR = 365 * 24 * 60 * 60; // seconds in a year - -describe("ERC20CRV", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("CRV"); - token = await Token.deploy(); - - await increaseTime(86401); - await token.updateMiningParameters(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("ERC20CRV MintIntegration", function () { - it("should mint the correct amount", async function () { - const duration = YEAR; // Replace with dynamic value as needed - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); - - await increaseTime(duration); - - const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const amount = currentTime.sub(creationTime).mul(rate); - await token.mint(accounts[1].address, amount); - - expect(await token.balanceOf(accounts[1].address)).to.equal(amount); - expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); - }); - - it("should revert on overmint", async function () { - const duration = YEAR; // Replace with dynamic value as needed - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const rate = await token.rate(); - - await increaseTime(duration); - - const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const amount = currentTime.sub(creationTime).add(2).mul(rate); - await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( - "dev: exceeds allowable mint amount" - ); - }); - - it("should mint multiple times correctly", async function () { - await token.setMinter(accounts[0].address); - let totalSupply = await token.totalSupply(); - let balance = BigNumber.from(0); - let epochStart = await token.startEpochTime(); - - const durations = [YEAR * 0.33, YEAR * 0.5, YEAR * 0.7]; // Replace with dynamic values as needed - - for (const time of durations) { - await increaseTime(time); - - if ( - (await ethers.provider.getBlock("latest")).timestamp - epochStart > - YEAR - ) { - await token.updateMiningParameters(); - epochStart = await token.startEpochTime(); - } - - const amount = (await token.availableSupply()).sub(totalSupply); - await token.mint(accounts[1].address, amount); - - balance = balance.add(amount); - totalSupply = totalSupply.add(amount); - - expect(await token.balanceOf(accounts[1].address)).to.equal(balance); - expect(await token.totalSupply()).to.equal(totalSupply); - } - }); - }); -}); diff --git a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts b/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts deleted file mode 100644 index f392031d..00000000 --- a/test/fork/integration/ERC20CRV/mintableInTimeframe.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { BigNumber, Contract } from "ethers"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import Constants from "../../Constants"; - -async function increaseTime(duration: BigNumber) { - await ethers.provider.send("evm_increaseTime", [duration.toNumber()]); - await ethers.provider.send("evm_mine", []); -} - -// Constants -const YEAR = Constants.YEAR; -const INITIAL_RATE = BigNumber.from(274815283); -const YEAR_1_SUPPLY = INITIAL_RATE.mul(BigNumber.from(10).pow(18)) - .div(YEAR) - .mul(YEAR); -const INITIAL_SUPPLY = BigNumber.from(1303030303); - -describe("ERC20CRV", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let snapshot: SnapshotRestorer; - const year = Constants.year; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("CRV"); - token = await Token.deploy(); - - await increaseTime(BigNumber.from(86401)); - await token.updateMiningParameters(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("ERC20CRV MintableInTimeframe", function () { - // Helper function for approximate equality - function approx(a: BigNumber, b: BigNumber, precision): boolean { - if (a.isZero() && b.isZero()) { - return true; - } - - // Adjust precision for BigNumber - const precisionAdjusted = BigNumber.from(10).pow(18).mul(precision); - - return a.sub(b).abs().lte(a.add(b).div(precisionAdjusted)); - } - - // Helper function for theoretical supply calculation - async function theoreticalSupply(token: Contract): Promise { - const epoch = await token.miningEpoch(); - const q = BigNumber.from(10).pow(18).div(BigNumber.from(2).pow(2)); // Equivalent to 1/2**0.25 - let S = INITIAL_SUPPLY.mul(BigNumber.from(10).pow(18)); - - if (epoch.gt(0)) { - S = S.add( - YEAR_1_SUPPLY.mul(BigNumber.from(10).pow(18)) - .mul(BigNumber.from(10).pow(18).sub(q.pow(epoch))) - .div(BigNumber.from(10).pow(18).sub(q)) - ); - } - - S = S.add( - YEAR_1_SUPPLY.div(YEAR) - .mul(q.pow(epoch)) - .mul( - (await ethers.provider.getBlock("latest")).timestamp - - (await token.startEpochTime()) - ) - ); - - return S; - } - - it("test_mintable_in_timeframe", async function () { - const t0 = Number(await token.startEpochTime()); - - // Ensure the exponentiation stays within safe integer limits - const exponent = BigNumber.from(10).pow(1); // Adjust the exponent as necessary - await increaseTime(exponent); - - let t1 = (await ethers.provider.getBlock("latest")).timestamp; - if (t1 - t0 >= year) { - await token.updateMiningParameters(); - } - t1 = (await ethers.provider.getBlock("latest")).timestamp; - - const availableSupply = await token.availableSupply(); - const mintable = await token.mintableInTimeframe(t0, t1); - expect( - availableSupply - .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) - .gte(mintable) - ).to.equal(true); - if (t1 == t0) { - expect(mintable).to.equal(BigNumber.from(0)); - } else { - const tolerance = BigNumber.from("10000000"); // Adjust as needed for precision - expect( - availableSupply - .sub(INITIAL_SUPPLY.mul(Constants.ten_to_the_18)) - .div(mintable) - .sub(1) - ).to.be.lt(tolerance); - } - - // Replace this with the actual theoretical supply calculation - // const theoreticalSupply = BigNumber.from("EXPECTED_SUPPLY_CALCULATION"); - expect( - approx( - await theoreticalSupply(token), - availableSupply, - Constants.ten_to_the_16 - ) - ).to.equal(true); - }); - - it("test_random_range_year_one", async function () { - const creationTime = await token.startEpochTime(); - const time1 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); - const time2 = BigNumber.from(Math.floor(Math.random() * YEAR.toNumber())); - const [start, end] = [creationTime.add(time1), creationTime.add(time2)]; - const sortedTimes = start.lt(end) ? [start, end] : [end, start]; - const rate = YEAR_1_SUPPLY.div(YEAR); - - expect( - await token.mintableInTimeframe(sortedTimes[0], sortedTimes[1]) - ).to.equal(rate.mul(sortedTimes[1].sub(sortedTimes[0]))); - }); - - it("test_random_range_multiple_epochs", async function () { - const creationTime = await token.startEpochTime(); - const start = creationTime.add(YEAR.mul(2)); - const duration = YEAR.mul(2); - const end = start.add(duration); - - const startEpoch = start.sub(creationTime).div(YEAR); - const endEpoch = end.sub(creationTime).div(YEAR); - const exponent = startEpoch.mul(25); - const rate = YEAR_1_SUPPLY.div(YEAR).div( - BigNumber.from(2).pow(exponent.div(100)) - ); - - for (let i = startEpoch.toNumber(); i < endEpoch.toNumber(); i++) { - await increaseTime(YEAR); - await token.updateMiningParameters(); - } - - const mintable = await token.mintableInTimeframe(start, end); - if (startEpoch.eq(endEpoch)) { - const expectedMintable = rate.mul(end.sub(start)); - expect(approx(mintable, expectedMintable, Constants.ten_to_the_16)).to - .be.true; - } else { - expect(mintable.lt(rate.mul(end))).to.be.true; - } - }); - - it("test_available_supply", async function () { - const duration = BigNumber.from(100000); - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); - - await increaseTime(duration); - - const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const expected = initialSupply.add(now.sub(creationTime).mul(rate)); - expect(await token.availableSupply()).to.equal(expected); - }); - }); -}); diff --git a/test/fork/integration/FeeDistributor/checkpointTs.test.ts b/test/fork/integration/FeeDistributor/checkpointTs.test.ts deleted file mode 100644 index 0507b80d..00000000 --- a/test/fork/integration/FeeDistributor/checkpointTs.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const DAY = 86400; -const WEEK = DAY * 7; -const MAX_EXAMPLES = 10; - -describe("FeeDistributor", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let coinA: Contract; - let votingEscrow: Contract; - let feeDistributor: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - - const FeeDistributor = await ethers.getContractFactory("FeeDistributor"); - const CRV = await ethers.getContractFactory("CRV"); - const Token = await ethers.getContractFactory("MockToken"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - coinA = await Token.deploy("Coin A", "USDA", 18); - await coinA.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - feeDistributor = await FeeDistributor.deploy( - votingEscrow.address, - await time.latest(), - coinA.address, - accounts[0].address, - accounts[0].address - ); - await feeDistributor.deployed(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - function generateUniqueRandomNumbers( - count: number, - min: number, - max: number - ): number[] { - const set = new Set(); - while (set.size < count) { - const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; - set.add(randomValue); - } - return Array.from(set); - } - - it("test checkpoint total supply", async function () { - const stAmount = generateUniqueRandomNumbers(MAX_EXAMPLES, 1e4, 100 * 1e4); - const stLocktime = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 52); - const stSleep = generateUniqueRandomNumbers(MAX_EXAMPLES, 1, 30); - for (let i = 0; i < MAX_EXAMPLES; i++) { - await token - .connect(accounts[i]) - .approve(votingEscrow.address, ethers.constants.MaxUint256); - await token - .connect(accounts[0]) - .transfer(await accounts[i].address, ethers.utils.parseEther("1000")); - } - - let finalLock = 0; - for (let i = 0; i < MAX_EXAMPLES; i++) { - const sleepTime = Math.floor(stSleep[i] * 86400); - await ethers.provider.send("evm_increaseTime", [sleepTime]); - const lockTime = (await time.latest()) + sleepTime + WEEK * stLocktime[i]; - finalLock = Math.max(finalLock, lockTime); - - await votingEscrow - .connect(accounts[i]) - .createLock( - BigNumber.from(stAmount[i].toFixed(0)).mul((1e14).toString()), - Math.floor(lockTime) - ); - } - - while ((await time.latest()) < finalLock) { - const weekEpoch = - Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; // WEEK * WEEK; - - await time.increaseTo(weekEpoch); - - const weekBlock = await time.latestBlock(); - - await ethers.provider.send("evm_increaseTime", [1]); - - // Max: 42 weeks - // doing checkpoint 3 times is enough - for (let i = 0; i < 3; i++) { - await feeDistributor.connect(accounts[0]).checkpointTotalSupply(); - } - - const expected = await votingEscrow.totalSupplyAt(weekBlock); - const actual = await feeDistributor.veSupply(weekEpoch); - console.log(`expected: ${expected} actual: ${actual}`); - expect(actual).to.equal(expected); - } - }); -}); diff --git a/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts b/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts deleted file mode 100644 index fe4a66b3..00000000 --- a/test/fork/integration/FeeDistributor/distributefFeesStateful.test.ts +++ /dev/null @@ -1,521 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -// 参考) brownie Stateful Tests -// https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html - -const ACCOUNT_NUM = 5; -const MAX_EXAMPLES = 50; -const STATEFUL_STEP_COUNT = 30; -const WEEK = 86400 * 7; -const YEAR = 86400 * 365; -const two_to_the_256_minus_1 = BigNumber.from("2") - .pow(BigNumber.from("256")) - .sub(BigNumber.from("1")); -const MOUNT_DECIMALS = 3; - -// Helper functions to generate random variables -----> -function randomBigValue(min: number, max: number): BigNumber { - return BigNumber.from( - Math.floor(Math.random() * (max - min) + min).toString() - ); -} -function randomValue(min: number, max: number): number { - return Math.floor(Math.random() * (max - min) + min); -} -function getRandomAccountNum(): number { - // Corresponds strategy("address", length=5) - let rdm = Math.floor(Math.random() * ACCOUNT_NUM); //0~9 integer - return rdm; -} -function getRandomWeeks(): BigNumber { - // Corresponds strategy("uint256", min_value=1, max_value=12) - return randomBigValue(1, 12); -} -function getRandomAmounts(): BigNumber { - // Corresponds strategy("decimal", min_value=1, max_value=100, places=3) - return randomBigValue( - 1 * 10 ** MOUNT_DECIMALS, - 100 * 10 ** MOUNT_DECIMALS - ).mul(BigNumber.from(10).pow(18 - MOUNT_DECIMALS)); -} -function getRandomsTime(): BigNumber { - // Corresponds strategy("uint256", min_value=0, max_value=86400 * 3) - return randomBigValue(0, 86400 * 3); -} -// ------------------------------------------------ - -describe("FeeDistributor", function () { - let accounts: SignerWithAddress[]; - let votingEscrow: Contract; - let distributor: Contract; - let feeCoin: Contract; - let token: Contract; - - let lockedUntil: { [key: string]: number } = {}; - let fees: { [key: number]: BigNumber } = {}; // timestamp -> amount - let userClaims: { [key: string]: { [key: number]: BigNumber[] } } = {}; // address -> timestamp -> [claimed, timeCursor] - let totalFees: BigNumber = ethers.utils.parseEther("1"); - - let snapshot: SnapshotRestorer; - - beforeEach(async () => { - snapshot = await takeSnapshot(); - accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); - - lockedUntil = {}; - fees = {}; - userClaims = {}; - totalFees = ethers.utils.parseEther("1"); - - const CRV = await ethers.getContractFactory("CRV"); - const Token = await ethers.getContractFactory("MockToken"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - const FeeDistributor = await ethers.getContractFactory("FeeDistributor"); - - token = await CRV.deploy(); - await token.deployed(); - - feeCoin = await Token.deploy("Test Token", "TST", 18); - await feeCoin.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - for (let i = 0; i < ACCOUNT_NUM; i++) { - // ensure accounts[:5] all have tokens that may be locked - await token - .connect(accounts[0]) - .transfer(accounts[i].address, ethers.utils.parseEther("10000000")); - await token - .connect(accounts[i]) - .approve(votingEscrow.address, two_to_the_256_minus_1); - - userClaims[accounts[i].address] = []; - } - - // accounts[0] locks 10,000,000 tokens for 2 years - longer than the maximum duration of the test - await votingEscrow - .connect(accounts[0]) - .createLock( - ethers.utils.parseEther("10000000"), - (await time.latest()) + YEAR * 2 - ); - - lockedUntil = { - [accounts[0].address]: ( - await votingEscrow.lockedEnd(accounts[0].address) - ).toNumber(), - }; - - // a week later we deploy the fee distributor - // await ethers.provider.send("evm_increaseTime", [WEEK]); - await time.increase(WEEK); - - distributor = await FeeDistributor.deploy( - votingEscrow.address, - await time.latest(), - feeCoin.address, - accounts[0].address, - accounts[0].address - ); - await distributor.deployed(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - //--------------------------------------------- functions -----------------------------------------------------------// - async function _checkActiveLock(stAcct: SignerWithAddress) { - // check if `st_acct` has an active lock - if (!lockedUntil[stAcct.address]) { - return false; - } - - const currentTime = await time.latest(); - - if (lockedUntil[stAcct.address] < currentTime) { - await votingEscrow.connect(stAcct).withdraw(); - delete lockedUntil[stAcct.address]; - return false; - } - - return true; - } - //--------------------------------------------- randomly excuted functions -----------------------------------------------------------// - async function ruleNewLock( - stAcct?: SignerWithAddress, - stAmount?: BigNumber, - stWeeks?: BigNumber, - stTime?: BigNumber - ) { - /* - Add a new user lock. - - Arguments - --------- - stAcct : SignerWithAddress - Account to lock tokens for. If this account already has an active - lock, the rule is skipped. - stAmount: BigNumber - Amount of tokens to lock. - stWeeks: BigNumber - Duration of lock, given in weeks. - stTime: BigNumber - Duration to sleep before action, in seconds. - */ - stAcct = stAcct || accounts[getRandomAccountNum()]; - stAmount = stAmount || getRandomAmounts(); - stWeeks = stWeeks || getRandomWeeks(); - stTime = stTime || getRandomsTime(); - - console.log(` - ruleNewLock --- - stAcct: ${ - stAcct.address - }, stAmount: ${stAmount.toString()}, stWeeks: ${stWeeks.toString()}, stTime: ${stTime.toString()} - `); - - stTime.gt(0) && (await time.increase(stTime)); - - if (!(await _checkActiveLock(stAcct))) { - const until = - (Math.floor((await time.latest()) / WEEK) + stWeeks.toNumber()) * WEEK; - await votingEscrow.connect(stAcct).createLock(stAmount, until); - lockedUntil[stAcct.address] = until; - } - } - - async function ruleExtendLock( - stAcct?: SignerWithAddress, - stWeeks?: BigNumber, - stTime?: BigNumber - ) { - /* - Extend an existing user lock. - - Arguments - --------- - stAcct: SignerWithAddress - Account to extend lock for. If this account does not have an active - lock, the rule is skipped. - stWeeks: BigNumber - Duration to extend the lock, given in weeks. - stTime: BigNumber - Duration to sleep before action, in seconds. - */ - stAcct = stAcct || accounts[getRandomAccountNum()]; - stWeeks = stWeeks || getRandomWeeks(); - stTime = stTime || getRandomsTime(); - - console.log(` - ruleExtendLock --- stAmount ${ - stAcct.address - }, stAmount: ${stWeeks.toString()}, stTime: ${stTime.toString()} - `); - - stTime.gt(0) && (await time.increase(stTime)); - - if (await _checkActiveLock(stAcct)) { - const until = - (Math.floor( - (await votingEscrow.lockedEnd(stAcct.address)).toNumber() / WEEK - ) + - stWeeks.toNumber()) * - WEEK; - const newUntil = Math.min( - until, - Math.floor(((await time.latest()) + YEAR * 4) / WEEK) * WEEK - ); - await votingEscrow.connect(stAcct).increaseUnlockTime(newUntil); - lockedUntil[stAcct.address] = newUntil; - } - } - - async function ruleIncreaseLockAmount( - stAcct?: SignerWithAddress, - stAmount?: BigNumber, - stTime?: BigNumber - ) { - /* - Increase the amount of an existing user lock. - - Arguments - --------- - stAcct : SignerWithAddress - Account to increase lock amount for. If this account does not have an - active lock, the rule is skipped. - stAmount : BigNumber - Amount of tokens to add to lock. - stTime : number - Duration to sleep before action, in seconds. - */ - stAcct = accounts[getRandomAccountNum()]; - stAmount = getRandomAmounts(); - stTime = getRandomsTime(); - - console.log(` - ruleIncreaseLockAmount --- stAmount ${ - stAcct.address - }, stAmount: ${stAmount.toString()}, stTime: ${stTime.toString()} - `); - - stTime.gt(0) && (await time.increase(stTime)); - - if (await _checkActiveLock(stAcct)) { - await votingEscrow.connect(stAcct).increaseAmount(stAmount); - } - } - - async function ruleClaimFees(stAcct?: SignerWithAddress, stTime?: BigNumber) { - /* - Claim fees for a user. - - Arguments - --------- - stAcct : SignerWithAddress - Account to claim fees for. - stTime : number - Duration to sleep before action, in seconds. - */ - stAcct = accounts[getRandomAccountNum()]; - stTime = getRandomsTime(); - - console.log(` - ruleClaimFees --- stAmount ${stAcct.address}, stTime: ${stTime.toString()} - `); - - stTime.gt(0) && (await time.increase(stTime)); - - // For debug --- - // const t0: number = (await distributor.startTime()).toNumber(); - // const ue = await distributor.userEpochOf(stAcct.address); - // const up = await votingEscrow.userPointHistory(stAcct.address, ue); - // console.log(`Week: - // ${Math.floor(((await time.latest()) - t0) / WEEK)} - - // Point: ${up} - // `); - // --- - - const claimed = await feeCoin.balanceOf(stAcct.address); - const tx = await distributor.connect(stAcct)["claim()"](); - const newClaimed = (await feeCoin.balanceOf(stAcct.address)).sub(claimed); - userClaims[stAcct.address][tx.blockNumber] = [ - newClaimed, - await distributor.timeCursorOf(stAcct.address), - ]; - } - - async function ruleTransferFees(stAmount?: BigNumber, stTime?: BigNumber) { - /* - Transfer fees into the distributor and make a checkpoint. - - If this is the first checkpoint, `can_checkpoint_token` is also - enabled. - - Arguments - --------- - stAmount : BigNumber - Amount of fee tokens to add to the distributor. - stTime : number - Duration to sleep before action, in seconds. - */ - stAmount = stAmount || getRandomAmounts(); - stTime = stTime || getRandomsTime(); - - console.log(` - ruleTransferFees --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} - `); - - stTime.gt(0) && (await time.increase(stTime)); - - const tx = await feeCoin._mintForTesting(distributor.address, stAmount); - - if (!(await distributor.canCheckpointToken())) { - await distributor.toggleAllowCheckpointToken(); - await distributor.checkpointToken(); - } - - fees[tx.blockNumber] = stAmount; - totalFees = totalFees.add(stAmount); - } - - async function ruleTransferFeesWithoutCheckpoint( - stAmount?: BigNumber, - stTime?: BigNumber - ) { - /* - Transfer fees into the distributor without checkpointing. - - Arguments - --------- - stAmount : BigNumber - Amount of fee tokens to add to the distributor. - stTime : number - Duration to sleep before action, in seconds. - */ - stAmount = stAmount || getRandomAmounts(); - stTime = stTime || getRandomsTime(); - - console.log(` - ruleTransferFeesWithoutCheckpoint --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} - `); - - stTime.gt(0) && (await time.increase(stTime)); - - const tx = await feeCoin._mintForTesting(distributor.address, stAmount); - - fees[tx.blockNumber] = stAmount; - totalFees = totalFees.add(stAmount); - } - - async function teardown() { - /* - Claim fees for all accounts and verify that only dust remains. - */ - console.log("teardown----"); - if (!(await distributor.canCheckpointToken())) { - //if no token checkpoint occured, add 100,000 tokens prior to teardown - await ruleTransferFees( - ethers.utils.parseEther("100000"), - BigNumber.from("0") - ); - } - - // Need two checkpoints to get tokens fully distributed - // Because tokens for current week are obtained in the next week - // And that is by design - await distributor.checkpointToken(); - await ethers.provider.send("evm_increaseTime", [WEEK * 2]); - await distributor.checkpointToken(); - - for (const acct of accounts) { - // For debug ---> - // const t0: number = (await distributor.startTime()).toNumber(); - // const ue = await distributor.userEpochOf(acct.address); - // const up = await votingEscrow.userPointHistory(acct.address, ue); - // console.log(`Week: - // ${Math.floor(((await time.latest()) - t0) / WEEK)} - - // Point: ${up} - // `); - // <---- - - await distributor.connect(acct)["claim()"](); - } - - const t0: number = (await distributor.startTime()).toNumber(); - const t1: number = Math.floor((await time.latest()) / WEEK) * WEEK; - - const tokensPerUserPerWeek = []; - for (const acct of accounts) { - tokensPerUserPerWeek[acct.address] = []; - for (let w = t0; w < t1 + WEEK; w += WEEK) { - const tokens = (await distributor.tokensPerWeek(w)) - .mul(await distributor.veForAt(acct.address, w)) - .div(await distributor.veSupply(w)); - tokensPerUserPerWeek[acct.address].push(tokens); - } - } - - // Display results-------------------------------------------------- - // console.log(`Results: ${tokensPerUserPerWeek}`); - // console.log(`TokensPerUserPerWeek------------`); - // Object.entries(tokensPerUserPerWeek).forEach(([key, val]) => { - // console.log(`${key}: ${val}`); - // }); - // console.log(``); - // console.log(`Fees-----------`); - // Object.entries(fees).forEach(([key, val]) => { - // console.log(`${key}: ${val}`); - // }); - // console.log(``); - // console.log(`Total Fee-----------`); - // console.log(totalFees.toString()); - // console.log(``); - // console.log(`User claims---------`); - // Object.entries(userClaims).forEach(([key, val]) => { - // console.log(`${key}:`); - // Object.entries(val).forEach(([k, v]) => { - // console.log(`${k}: ${v}`); - // }); - // }); - // console.log(``); - // console.log(`User balances---------`); - // for (const acct of accounts) { - // console.log( - // acct.address, - // (await feeCoin.balanceOf(acct.address)).toString() - // ); - // } - // console.log(`feeCoin balance of Distributor--------`); - // console.log((await feeCoin.balanceOf(distributor.address)).toString()); - // ------------------------------------------- - - for (const acct of accounts) { - expect(await feeCoin.balanceOf(acct.address)).to.equal( - tokensPerUserPerWeek[acct.address].reduce( - (a: BigNumber, b: BigNumber) => a.add(b), - BigNumber.from("0") - ) - ); - } - - // Check if all fees are distributed - expect(await feeCoin.balanceOf(distributor.address)).to.be.lt(100); - } - - let func = [ - "ruleNewLock", - "ruleExtendLock", - "ruleIncreaseLockAmount", - "ruleClaimFees", - "ruleTransferFees", - "ruleTransferFeesWithoutCheckpoint", - ]; - - describe("test_deposit_withdraw_voting", function () { - for (let i = 0; i < MAX_EXAMPLES; i++) { - it(`should distributes fee ${i}`, async () => { - // Corresponds initializer initialize_new_lock and initialize_transfer_fees - // https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html#initializers - // initialize_new_lock: This is equivalent to `rule_new_lock` to make it more likely we have at least 2 accounts locked at the start of the test run. - // initialize_transfer_fees: This is equivalent to `rule_transfer_fees` to make it more likely that claimable fees are available from the start of the test. - const initializerSeed = Math.random(); - if (initializerSeed < 0.2) { - await ruleNewLock(); - await ruleTransferFees(); - } else if (initializerSeed < 0.4) { - await ruleTransferFees(); - await ruleNewLock(); - } else if (initializerSeed < 0.6) { - await ruleNewLock(); - } else if (initializerSeed < 0.8) { - await ruleTransferFees(); - } - - const steps = randomValue(1, STATEFUL_STEP_COUNT); - for (let x = 0; x < steps; x++) { - let n = randomValue(0, func.length); - await eval(func[n])(); - } - - await teardown(); - }); - } - }); -}); diff --git a/test/fork/integration/GaugeController/typesAndWeights.test.ts b/test/fork/integration/GaugeController/typesAndWeights.test.ts deleted file mode 100644 index 63719c6b..00000000 --- a/test/fork/integration/GaugeController/typesAndWeights.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -// 参考) brownie Stateful Tests -// https://eth-brownie.readthedocs.io/en/stable/tests-hypothesis-stateful.html - -type GaugeInfo = { - contract: Contract; - type: number; - weight: BigNumber; -}; -const ACCOUNT_NUM = 5; -const MAX_EXAMPLES = 50; -const STATEFUL_STEP_COUNT = 10; -const WEEK = 86400 * 7; - -// Validate gauge weights and gauge weight sum. - -// Strategies -// ---------- -// st_type : Decimal -// Gauge type, multiplied by `len(self.gauges)` to choose a value -// st_gauge_weight : int -// Gauge weight -// st_type_wiehgt : int -// Type weight - -// Helper functions to generate random variables -----> -function randomBigValue(min: number, max: number): BigNumber { - return BigNumber.from( - Math.floor(Math.random() * (max - min) + min).toString() - ); -} -function randomValue(min: number, max: number): number { - return Math.floor(Math.random() * (max - min) + min); -} -function getRandomType(): number { - // Corresponds strategy("decimal", min_value=0, max_value="0.99999999") - return randomValue(0, 99999999) / 100000000; -} -function getRandomWeight(): BigNumber { - // Corresponds strategy("uint", min_value=10 ** 17, max_value=10 ** 19) - return randomBigValue(10 ** 17, 10 ** 19); -} -// ------------------------------------------------ - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let votingEscrow: Contract; - let gaugeController: Contract; - let mockLpToken: Contract; - let token: Contract; - let minter: Contract; - - let typeWeights: BigNumber[] = []; - let gauges: GaugeInfo[] = []; - - let snapshot: SnapshotRestorer; - - beforeEach(async () => { - snapshot = await takeSnapshot(); - accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); - - const MockLpToken = await ethers.getContractFactory("TestLP"); - const Token = await ethers.getContractFactory("CRV"); - const Minter = await ethers.getContractFactory("Minter"); - const GaugeController = await ethers.getContractFactory("GaugeController"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await Token.deploy(); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - gaugeController = await GaugeController.deploy( - token.address, - votingEscrow.address - ); - await gaugeController.deployed(); - - minter = await Minter.deploy(token.address, gaugeController.address); - await minter.deployed(); - - mockLpToken = await MockLpToken.deploy( - "Curve LP token", - "usdCrv", - 18, - ethers.utils.parseEther("10") - ); - await mockLpToken.deployed(); - - typeWeights = []; - gauges = []; - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - //--------------------------------------------- helper functions -----------------------------------------------------------// - - function _gaugeWeight(idx: number): BigNumber { - return gauges.reduce((sum, gauge) => { - return gauge.type === idx ? sum.add(gauge.weight) : sum; - }, BigNumber.from("0")); - } - - //--------------------------------------------- Initializer functions -----------------------------------------------------------// - async function initializeAddType() { - await ruleAddType(); - - // Check - await checkInvariants(); - } - //--------------------------------------------- randomly exceuted functions -----------------------------------------------------------// - async function ruleAddType(stTypeWeight?: BigNumber) { - /* - Add a new gauge type. - */ - stTypeWeight = stTypeWeight || getRandomWeight(); - await gaugeController.connect(accounts[0]).addType("Type!", stTypeWeight); - typeWeights.push(stTypeWeight); - - console.log(`ruleAddType --- stTypeWeight: ${stTypeWeight.toString()}`); - - // Check - await checkInvariants(); - } - - async function ruleAddGauge(gaugeType?: number, stGaugeWeight?: BigNumber) { - /* - Add a new gauge. - - If no types have been added, this rule has not effect. - */ - const stType = getRandomType(); - stGaugeWeight = stGaugeWeight || getRandomWeight(); - - if (typeWeights.length === 0) return; - - gaugeType = gaugeType || Math.floor(stType * typeWeights.length); - console.log( - `ruleAddGauge --- gaugeType: ${gaugeType}, stGaugeWeight: ${stGaugeWeight.toString()}` - ); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); - const gauge = await LiquidityGauge.deploy( - mockLpToken.address, - minter.address - ); - await gauge.deployed(); - - await gaugeController - .connect(accounts[0]) - .addGauge(gauge.address, gaugeType, stGaugeWeight); - - gauges.push({ contract: gauge, type: gaugeType, weight: stGaugeWeight }); - - // Check - await checkInvariants(); - } - - async function checkInvariants() { - await invariantGaugeWeightSums(); - await invariantTotalTypeWeight(); - await invariantRelativeGaugeWeight(); - } - - async function invariantGaugeWeightSums() { - // Validate the gauge weight sums per type. - for (let i = 0; i < typeWeights.length; i++) { - const gaugeWeightSum = _gaugeWeight(i); - expect(await gaugeController.getWeightsSumPerType(i)).to.be.eq( - gaugeWeightSum - ); - } - } - - async function invariantTotalTypeWeight() { - // Validate the total weight. - const totalWeight = typeWeights.reduce((total, weight, idx) => { - return total.add(_gaugeWeight(idx).mul(weight)); - }, BigNumber.from("0")); - - expect(await gaugeController.getTotalWeight()).to.be.eq(totalWeight); - } - - async function invariantRelativeGaugeWeight() { - // Validate the relative gauge weights. - await ethers.provider.send("evm_increaseTime", [WEEK]); - - const totalWeight = typeWeights.reduce((total, weight, idx) => { - return total.add(_gaugeWeight(idx).mul(weight)); - }, BigNumber.from("0")); - - for (let i = 0; i < gauges.length; i++) { - await gaugeController - .connect(accounts[0]) - .checkpointGauge(gauges[i].contract.address); - const expected = BigNumber.from("10") - .pow(18) - .mul(typeWeights[gauges[i].type]) - .mul(gauges[i].weight) - .div(totalWeight); - expect( - await gaugeController.gaugeRelativeWeight( - gauges[i].contract.address, - await time.latest() - ) - ).to.be.eq(expected); - } - } - - async function showStates() { - console.log("States ------------------------"); - console.log("typeWeights.length: ", typeWeights.length); - console.log("gauges.length: ", gauges.length); - for (let i = 0; i < typeWeights.length; i++) { - console.log( - `getWeightsSumPerType(${i}): ${( - await gaugeController.getWeightsSumPerType(i) - ).toString()}` - ); - } - const totalWeight = typeWeights.reduce((total, weight, idx) => { - return total.add(_gaugeWeight(idx).mul(weight)); - }, BigNumber.from("0")); - console.log(`totalWeight: ${totalWeight.toString()}`); - - for (let i = 0; i < gauges.length; i++) { - console.log( - `gaugeRelativeWeight(${gauges[i].contract.address}): ${( - await gaugeController.gaugeRelativeWeight( - gauges[i].contract.address, - await time.latest() - ) - ).toString()}` - ); - } - console.log("------------------------"); - } - - let func = ["ruleAddType", "ruleAddGauge"]; - - describe("gauge weights and gauge weight sum", function () { - for (let i = 0; i < MAX_EXAMPLES; i++) { - it(`tests gauge weights and gauge weight sum ${i}`, async () => { - await initializeAddType(); - - const steps = randomValue(1, STATEFUL_STEP_COUNT); - for (let x = 0; x < steps; x++) { - let n = randomValue(0, func.length); - await eval(func[n])(); - } - - // await showStates(); - }); - } - }); -}); diff --git a/test/fork/integration/GaugeController/voteWeight.test.ts b/test/fork/integration/GaugeController/voteWeight.test.ts deleted file mode 100644 index ff527572..00000000 --- a/test/fork/integration/GaugeController/voteWeight.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const WEEK = 86400 * 7; -const MAX_EXAMPLES = 10; -const SCALE = 1e20; - -type SlopeData = { - initialBias: BigNumber; - duration: number; -}; - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let otherAccount: SignerWithAddress; - let gaugeController: Contract; - let threeGauges: Contract[3] = []; - let mockLpToken: Contract; - let minter: Contract; - let token: Contract; - let votingEscrow: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = (await ethers.getSigners()).slice(0, 3); - otherAccount = (await ethers.getSigners())[4]; - - const MockLpToken = await ethers.getContractFactory("TestLP"); - const Token = await ethers.getContractFactory("CRV"); - const Minter = await ethers.getContractFactory("Minter"); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); - const GaugeController = await ethers.getContractFactory("GaugeController"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await Token.deploy(); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - gaugeController = await GaugeController.deploy( - token.address, - votingEscrow.address - ); - await gaugeController.deployed(); - - minter = await Minter.deploy(token.address, gaugeController.address); - await minter.deployed(); - - mockLpToken = await MockLpToken.deploy( - "Curve LP token", - "usdCrv", - 18, - ethers.utils.parseEther("10") - ); - await mockLpToken.deployed(); - - for (let i = 0; i < 3; i++) { - threeGauges.push( - await LiquidityGauge.deploy(mockLpToken.address, minter.address) - ); - } - - await gaugeController.addType("Liquidity", ethers.utils.parseEther("1")); - for (let i = 0; i < 3; i++) { - await gaugeController.addGauge(threeGauges[i].address, 0, 0); - } - - for (let i = 0; i < 3; i++) { - await token - .connect(accounts[0]) - .transfer(accounts[i].address, ethers.utils.parseEther("100000")); - await token - .connect(accounts[i]) - .approve(votingEscrow.address, ethers.utils.parseEther("100000")); - } - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - // Helper functions to generate random variables -----> - function randomBigValue(min: BigNumber, max: BigNumber): BigNumber { - return BigNumber.from(ethers.utils.randomBytes(32)) - .mod(max.sub(min)) - .add(min); - } - function randomValue(min: number, max: number): number { - return Math.floor(Math.random() * (max - min) + min); - } - function getRandomDeposits(): BigNumber[] { - // Corresponds strategy("uint256[3]", min_value=10 ** 21, max_value=10 ** 23) - return [ - randomBigValue( - ethers.utils.parseEther("1000"), - ethers.utils.parseEther("100000") - ), - randomBigValue( - ethers.utils.parseEther("1000"), - ethers.utils.parseEther("100000") - ), - randomBigValue( - ethers.utils.parseEther("1000"), - ethers.utils.parseEther("100000") - ), - ]; - } - function getRandomLength(): number[] { - // Corresponds strategy("uint256[3]", min_value=52, max_value=100) - return [randomValue(52, 100), randomValue(52, 100), randomValue(52, 100)]; - } - function getRandomVotes(): number[][] { - // Corresponds strategy("uint[2][3]", min_value=0, max_value=5) - return [ - [randomValue(0, 5), randomValue(0, 5)], - [randomValue(0, 5), randomValue(0, 5)], - [randomValue(0, 5), randomValue(0, 5)], - ]; - } - // ------------------------------------------------ - - function generateUniqueRandomNumbers( - count: number, - min: number, - max: number - ): number[] { - const set = new Set(); - while (set.size < count) { - const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; - set.add(randomValue); - } - return Array.from(set); - } - - for (let iteration = 0; iteration < MAX_EXAMPLES; iteration++) { - it(`tests vote weight ${iteration}`, async function () { - // Test that gauge weights correctly adjust over time. - // Strategies - // --------- - // stDeposits : [int, int, int] - // Number of coins to be deposited per account - // stLength : [int, int, int] - // Policy duration in weeks - // stVotes : [(int, int), (int, int), (int, int)] - // (vote for gauge 0, vote for gauge 1) for each account, in units of 10% - - const stDeposits = getRandomDeposits(); - const stLength = getRandomLength(); - const stVotes = getRandomVotes(); - - // Dammy fixed data for debug - // const stDeposits = [ - // BigNumber.from("41798790756112223739711"), - // BigNumber.from("6086480295267465049249"), - // BigNumber.from("18446478515299272788115"), - // ]; - // const stLength = [67, 75, 55]; - // const stVotes = [ - // [3, 2], - // [0, 2], - // [3, 1], - // ]; - // Init 10 s before the week change - let t0 = await time.latest(); - let t1 = Math.floor((t0 + 2 * WEEK) / WEEK) * WEEK - 10; - // console.log(`t0: ${t0}, t1: ${t1}`); - await ethers.provider.send("evm_increaseTime", [t1 - t0]); - // Deposit for voting - let timestamp = t1; - for (let i = 0; i < accounts.length; i++) { - await votingEscrow - .connect(accounts[i]) - .createLock(stDeposits[i], timestamp + stLength[i] * WEEK); - } - - let tx; - let txReceipt; - // Place votes - let votes: number[][] = []; - for (let i = 0; i < accounts.length; i++) { - votes.push(stVotes[i].map((x) => x * 1000)); - votes[i].push(10000 - votes[i].reduce((a, b) => a + b, 0)); - for (let x = 0; x < accounts.length; x++) { - tx = await gaugeController - .connect(accounts[i]) - .voteForGaugeWeights(threeGauges[x].address, votes[i][x]); - } - txReceipt = await tx.wait(); - } - // Vote power assertions - everyone used all voting power - for (let i = 0; i < accounts.length; i++) { - expect( - await gaugeController.voteUserPower(accounts[i].address) - ).to.equal(10000); - } - // Calculate slope data, build model functions - let slopeData: SlopeData[] = []; - for (let i = 0; i < accounts.length; i++) { - const initialBias: BigNumber = ( - await votingEscrow.getLastUserSlope(accounts[i].address) - ).mul( - (await votingEscrow.locked(accounts[i].address)).end.sub(timestamp) - ); - const duration = - Math.floor((timestamp + stLength[i] * WEEK) / WEEK) * WEEK - - timestamp; - slopeData.push({ initialBias, duration }); - } - const maxDuration = Math.max( - ...slopeData.map(({ duration }) => duration) - ); - const models = (idx: number, relativeTime: number) => { - const { initialBias, duration } = slopeData[idx]; - // console.log( - // "model data: ", - // initialBias.toString(), - // duration, - // relativeTime - // ); - const coef = 1 - (relativeTime * maxDuration) / duration; - const relBias = initialBias - .mul(BigNumber.from(Math.floor(coef * SCALE).toString())) - .div(SCALE.toString()); - return relBias.lt(0) ? BigNumber.from("0") : relBias; - }; - await time.increase(WEEK * 4); - let txTimestamp = (await ethers.provider.getBlock(txReceipt.blockNumber)) - .timestamp; - while (txTimestamp < timestamp + 1.5 * maxDuration) { - // console.log( - // "blockNumber txTimestamp: ", - // txReceipt.blockNumber, - // txTimestamp - // ); - for (let i = 0; i < 3; i++) { - tx = await gaugeController - .connect(otherAccount) - .checkpointGauge(threeGauges[i].address); - } - txReceipt = await tx.wait(); - txTimestamp = (await ethers.provider.getBlock(txReceipt.blockNumber)) - .timestamp; - const relativeTime = - Math.floor((txTimestamp / WEEK) * WEEK - timestamp) / maxDuration; - // console.log( - // `Calculating relative time... txTimestamp: ${txTimestamp}, timestamp: ${timestamp}, maxDuration: ${maxDuration}` - // ); - const weights: BigNumber[] = await Promise.all( - threeGauges.map(async (gauge, i) => { - return await gaugeController.gaugeRelativeWeight( - gauge.address, - await time.latest() - ); - }) - ); - let theoreticalWeights: BigNumber[] = []; - if (relativeTime < 1) { - // console.log( - // `vote: [${votes[0]}], [${votes[1]}], [${votes[2]}], relativeTime: ${relativeTime}` - // ); - // console.log(`models: ${models(0, relativeTime).toString()}`); - theoreticalWeights = [ - votes.reduce( - (sum, vote, i) => - sum.add( - BigNumber.from(vote[0].toString()).mul( - models(i, relativeTime) - ) - ), - BigNumber.from(0) - ), - votes.reduce( - (sum, vote, i) => - sum.add( - BigNumber.from(vote[1].toString()).mul( - models(i, relativeTime) - ) - ), - BigNumber.from(0) - ), - votes.reduce( - (sum, vote, i) => - sum.add( - BigNumber.from(vote[2].toString()).mul( - models(i, relativeTime) - ) - ), - BigNumber.from(0) - ), - ]; - const totalTheoreticalWeight = theoreticalWeights.reduce( - (sum, weight) => sum.add(weight), - BigNumber.from(0) - ); - theoreticalWeights = theoreticalWeights.map((w) => - w.isZero() - ? w - : w.mul(BigNumber.from(10).pow(18)).div(totalTheoreticalWeight) - ); - } else { - theoreticalWeights = [ - BigNumber.from(0), - BigNumber.from(0), - BigNumber.from(0), - ]; - } - if (relativeTime !== 1) { - for (let i = 0; i < 3; i++) { - const weightDiff = weights[i].sub(theoreticalWeights[i]).abs(); - const maxAllowedDiff = BigNumber.from(txTimestamp - timestamp) - .div(WEEK) - .add(1) - .mul(BigNumber.from(10).pow(18)); // Adjust scale - // console.log( - // "weightDiff & maxAllowedDiff: ", - // weightDiff.toString(), - // maxAllowedDiff.toString() - // ); - expect(weightDiff.lte(maxAllowedDiff)).to.be.true; - } - } - await time.increase(WEEK * 4); - } - }); - } -}); diff --git a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts b/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts deleted file mode 100644 index fafae31b..00000000 --- a/test/fork/integration/LiquidityGauge/liquidityGauge.test.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; - -describe("LiquidityGauge", function () { - let accounts: any; - let gaugeController: Contract; - let token: Contract; - let votingEscrow: Contract; - let mockLpToken: Contract; - let threeGauges: String[]; - let gauges: Contract[]; - - let snapshot: SnapshotRestorer; - const ten_to_the_18 = Constants.ten_to_the_18; - const ten_to_the_20 = Constants.ten_to_the_20; - const ten_to_the_21 = Constants.ten_to_the_21; - const MAX_UINT256 = Constants.MAX_UINT256; - const zero = Constants.zero; - const WEEK = Constants.WEEK; - - beforeEach(async () => { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ - gaugeController, - threeGauges, - gauges, - votingEscrow, - token, - mockLpToken, - } = await deployContracts()); - await gaugeController.addType("none", BigNumber.from("0")); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("Gauge Integral Calculations", function () { - /** - * テスト: test_gauge_integral - * 1. AliceとBobの保有量と積分の初期化を行う。 - * 2. 現在のブロックのタイムスタンプと初期レートを取得する。 - * 3. タイプを追加し、その重みを変更する。 - * 4. LPトークンをAliceとBobに均等に送信する。 - * 5. 積分の更新処理を行うための補助関数「update_integral」を定義する。 - * 6. Bobが預金または引き出しを繰り返し行い、Aliceがそれをランダムに行う10回のループを開始する。 - * a. ランダムな時間を経過させるシミュレーションを行う。 - * b. Bobがランダムに預金または引き出しを行う。 - * c. 20%の確率でAliceも預金または引き出しを行う。 - * d. 同じ秒数でのユーザーチェックポイントの更新が影響しないことを確認する。 - * e. AliceとBobの保有量が正しいことを確認する。 - * f. もう一度、ランダムな時間を経過させるシミュレーションを行う。 - * g. Aliceのユーザーチェックポイントを更新し、積分を更新する。 - * 7. テストが終了する。 - */ - it("should correctly calculate user integrals over randomized actions", async () => { - // AliceとBobの保有量と積分を初期化 - let alice_staked = BigNumber.from("0"); - let bob_staked = BigNumber.from("0"); - let integral = BigNumber.from("0"); - - // 最新のブロックのタイムスタンプを取得 - let checkpoint = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - // 初期レートの取得 - let checkpoint_rate = await token.rate(); - let checkpoint_supply = BigNumber.from("0"); - let checkpoint_balance = BigNumber.from("0"); - - // タイプの追加とその重みの変更 - await gaugeController.addType("Liquidity", BigNumber.from("0")); - await gaugeController.changeTypeWeight(1, ten_to_the_18); - await gaugeController.addGauge(threeGauges[0], 1, ten_to_the_18); - - // LPトークンを送信 - const creatorBalance = await mockLpToken.balanceOf(accounts[0].address); - await mockLpToken.transfer( - accounts[1].address, - creatorBalance.div(BigNumber.from("2")) - ); - await mockLpToken.transfer( - accounts[2].address, - creatorBalance.div(BigNumber.from("2")) - ); - - // 積分を更新する関数 - async function update_integral() { - let t1 = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - let rate1 = await token.rate(); - let t_epoch = await token.startEpochTime(); - let rate_x_time = BigNumber.from("0"); - - // checkpoint >= t_epoch - if (checkpoint.gte(t_epoch)) { - // t1 - checkpoint * rate1 - rate_x_time = t1.sub(checkpoint).mul(rate1); - } else { - // t_epoch - checkpoint * checkpoint_rate ( t1 - t_epoch * rate1) - rate_x_time = t_epoch - .sub(checkpoint) - .mul(checkpoint_rate) - .add(t1.sub(t_epoch).mul(rate1)); - } - - // checkpoint_supply > 0 - if (checkpoint_supply.gt(BigNumber.from("0"))) { - // integral + rate_x_time * checkpoint_balance / checkpoint_supply - integral = integral.add( - rate_x_time.mul(checkpoint_balance).div(checkpoint_supply) - ); - } - - checkpoint_rate = rate1; - checkpoint = t1; - checkpoint_supply = await gauges[0].totalSupply(); - checkpoint_balance = await gauges[0].balanceOf(accounts[1].address); - } - - // Bobは常に預金または引き出しを行い、Aliceはそれをあまり行わない - for (let i = 0; i < 10; i++) { - let is_alice = Math.random() < 0.2; - - // ランダムな時間経過をシミュレート - let dt = BigNumber.from( - Math.floor(Math.random() * 86400 * 73).toString() - ).add(BigNumber.from("1")); - await ethers.provider.send("evm_increaseTime", [dt.toNumber()]); - - // Bobの処理 - let is_withdraw = i > 0 && Math.random() < 0.5; - - if (is_withdraw) { - // 引き出し処理 - let amount = BigNumber.from( - Math.floor(Math.random() * 10000).toString() - ) - .mul(await gauges[0].balanceOf(accounts[2].address)) - .div(BigNumber.from("10000")); - await gauges[0].connect(accounts[2]).withdraw(amount, false); - await update_integral(); - bob_staked = bob_staked.sub(amount); - } else { - // 預金処理 - let amount = BigNumber.from( - Math.floor(Math.random() * 10000).toString() - ) - .mul(await mockLpToken.balanceOf(accounts[2].address)) - .div(BigNumber.from("10")) - .div(BigNumber.from("10000")); - await mockLpToken - .connect(accounts[2]) - .approve(threeGauges[0], amount); - await gauges[0] - .connect(accounts[2]) - .deposit(amount, accounts[2].address, false); - await update_integral(); - bob_staked = bob_staked.add(amount); - } - - // Aliceの処理 - if (is_alice) { - let is_withdraw_alice = - (await gauges[0].balanceOf(accounts[1].address)) > 0 && - Math.random() > 0.5; - if (is_withdraw_alice) { - // 引き出し処理 - let amount_alice = BigNumber.from( - Math.floor(Math.random() * 10000).toString() - ) - .mul(await gauges[0].balanceOf(accounts[1].address)) - .div(BigNumber.from("10")) - .div(BigNumber.from("10000")); - await gauges[0].connect(accounts[1]).withdraw(amount_alice, false); - await update_integral(); - alice_staked = alice_staked.sub(amount_alice); - } else { - // 預金処理 - let amount_alice = BigNumber.from( - Math.floor(Math.random() * 10000).toString() - ) - .mul(await mockLpToken.balanceOf(accounts[1].address)) - .div(BigNumber.from("10000")); - await mockLpToken - .connect(accounts[1]) - .approve(threeGauges[0], amount_alice); - await gauges[0] - .connect(accounts[1]) - .deposit(amount_alice, accounts[1].address, false); - await update_integral(); - alice_staked = alice_staked.add(amount_alice); - } - } - - // 同じ秒数でのチェックポイントの更新は影響しないことの確認 - if (Math.random() < 0.5) { - await gauges[0] - .connect(accounts[1]) - .userCheckpoint(accounts[1].address); - } - if (Math.random() < 0.5) { - await gauges[0] - .connect(accounts[2]) - .userCheckpoint(accounts[2].address); - } - - // 保有量の確認 - expect(await gauges[0].balanceOf(accounts[1].address)).to.equal( - alice_staked - ); - expect(await gauges[0].balanceOf(accounts[2].address)).to.equal( - bob_staked - ); - expect(await gauges[0].totalSupply()).to.equal( - alice_staked.add(bob_staked) - ); - - // ランダムな時間経過をさらにシミュレート - dt = BigNumber.from( - Math.floor(Math.random() * 86400 * 19).toString() - ).add(BigNumber.from("1")); - await ethers.provider.send("evm_increaseTime", [dt.toNumber()]); - - await gauges[0] - .connect(accounts[1]) - .userCheckpoint(accounts[1].address); - await update_integral(); - } - }); - }); - - /** - * test_mining_with_votelock の全体的な流れ: - * - * 1. 2週間と5秒の時間を進める。 - * 2. ゲージとコントローラーをセットアップし、適切なレートを設定する。 - * 3. AliceとBobにトークンを転送し、それぞれのアドレスに関連する承認を設定する。 - * 4. Aliceは投票のエスクローにトークンをロックすることで、BOOSTを取得する。 - * 5. AliceとBobはそれぞれ流動性をデポジットする。 - * 6. Aliceの投票ロックの存在とBobの投票ロックの不在を確認する。 - * 7. 時間をさらに進め、両方のユーザーのチェックポイントを更新する。 - * 8. 4週間後、AliceとBobの投票エスクローのバランスが0であることを確認する。 - * 9. Aliceが投票ロックでTokenを獲得したため、彼女はBobの2.5倍のリワードを獲得することを確認する。 - * 10. さらに時間を進め、両方のユーザーのチェックポイントを更新する。 - * 11. 最終的に、AliceとBobが同じ量のリワードを獲得していることを確認する。 - */ - describe("Mining with Vote Locking", function () { - it("should distribute rewards according to vote lock status", async () => { - // 2週間と5秒時間を進める - await ethers.provider.send("evm_increaseTime", [ - WEEK.mul(BigNumber.from("2")).add(BigNumber.from("5")).toNumber(), - ]); - - // ゲージをコントローラーに接続して適切なレートなどを設定する - await gaugeController.addType("Liquidity", 0); - await gaugeController.changeTypeWeight(1, ten_to_the_18); - await gaugeController.addGauge(threeGauges[0], 1, ten_to_the_18); - - // トークンの準備 - await token.transfer(accounts[1].address, ten_to_the_20); - await token.transfer(accounts[2].address, ten_to_the_20); - - await token - .connect(accounts[1]) - .approve(votingEscrow.address, MAX_UINT256); - await token - .connect(accounts[2]) - .approve(votingEscrow.address, MAX_UINT256); - const creatorBalance = await mockLpToken.balanceOf(accounts[0].address); - await mockLpToken.transfer( - accounts[1].address, - creatorBalance.div(BigNumber.from("2")) - ); - await mockLpToken.transfer( - accounts[2].address, - creatorBalance.div(BigNumber.from("2")) - ); - - await mockLpToken - .connect(accounts[1]) - .approve(threeGauges[0], MAX_UINT256); - await mockLpToken - .connect(accounts[2]) - .approve(threeGauges[0], MAX_UINT256); - - // Aliceがescrowにデポジットする。AliceはBOOSTを持っていることになる - let t = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - - await votingEscrow - .connect(accounts[1]) - .createLock(ten_to_the_20, t.add(WEEK.mul(BigNumber.from("2")))); - - // AliceとBobが一部の流動性をデポジットする - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_21, accounts[1].address, false); - await gauges[0] - .connect(accounts[2]) - .deposit(ten_to_the_21, accounts[2].address, false); - let now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - - // 現在、Aliceは投票ロックを持っているが、Bobは持っていないことを確認する - expect( - await votingEscrow["balanceOf(address,uint256)"]( - accounts[1].address, - now - ) - ).to.not.equal(zero); - expect( - await votingEscrow["balanceOf(address,uint256)"]( - accounts[2].address, - now - ) - ).to.equal(zero); - - // 時間を進めてチェックポイントを更新する - now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - await ethers.provider.send("evm_setNextBlockTimestamp", [ - now.add(WEEK.mul(BigNumber.from("4"))).toNumber(), - ]); - - // チェックポイント更新 - await ethers.provider.send("evm_setAutomine", [false]); - await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); - await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); - await ethers.provider.send("evm_mine", []); - await ethers.provider.send("evm_setAutomine", [true]); - - // 4週間後、balanceOfは0であるべき - now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - expect( - await votingEscrow["balanceOf(address,uint256)"]( - accounts[1].address, - now - ) - ).to.equal(zero); - expect( - await votingEscrow["balanceOf(address,uint256)"]( - accounts[2].address, - now - ) - ).to.equal(zero); - - // AliceはTokenを投票ロックしたので、2.5倍のTokenを獲得 - let rewards_alice = await gauges[0].integrateFraction( - accounts[1].address - ); - let rewards_bob = await gauges[0].integrateFraction(accounts[2].address); - expect( - rewards_alice.mul(BigNumber.from("10000000000000000")).div(rewards_bob) - ).to.equal(BigNumber.from("25000000000000000")); - - // 時間を進めてチェックポイントを更新: 今は誰もがTokenを投票ロックしていない - now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - await ethers.provider.send("evm_setNextBlockTimestamp", [ - now.add(WEEK.mul(BigNumber.from("4"))).toNumber(), - ]); - - await ethers.provider.send("evm_setAutomine", [false]); - await gauges[0].connect(accounts[2]).userCheckpoint(accounts[2].address); - await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); - await ethers.provider.send("evm_mine", []); - await ethers.provider.send("evm_setAutomine", [true]); - let old_rewards_alice = rewards_alice; - let old_rewards_bob = rewards_bob; - // 今、AliceはBobと同じ量を獲得した - rewards_alice = await gauges[0].integrateFraction(accounts[1].address); - rewards_bob = await gauges[0].integrateFraction(accounts[2].address); - console.log(rewards_alice); - console.log(rewards_bob); - let d_alice = rewards_alice.sub(old_rewards_alice); - let d_bob = rewards_bob.sub(old_rewards_bob); - - expect(d_alice.sub(d_bob)).to.equal(zero); - }); - }); -}); diff --git a/test/fork/integration/Minter/components.test.ts b/test/fork/integration/Minter/components.test.ts deleted file mode 100644 index 079ae64b..00000000 --- a/test/fork/integration/Minter/components.test.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const ACCOUNT_NUM = 4; -const NUMBER_OF_ATTEMPTS = 30; -const SCALE = BigNumber.from((1e20).toString()); -const WEEK = 86400 * 7; -const MONTH = 86400 * 30; - -describe("Minter components", function () { - let accounts: SignerWithAddress[]; - let gaugeController: Contract; - let liquidityGauge: Contract; - let mockLpToken: Contract; - let minter: Contract; - let token: Contract; - let votingEscrow: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - const MockLpToken = await ethers.getContractFactory("TestLP"); - const Token = await ethers.getContractFactory("CRV"); - const Minter = await ethers.getContractFactory("Minter"); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); - const GaugeController = await ethers.getContractFactory("GaugeController"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await Token.deploy(); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - gaugeController = await GaugeController.deploy( - token.address, - votingEscrow.address - ); - await gaugeController.deployed(); - - minter = await Minter.deploy(token.address, gaugeController.address); - await minter.deployed(); - - mockLpToken = await MockLpToken.deploy( - "Curve LP token", - "usdCrv", - 18, - ethers.utils.parseEther("10") - ); - await mockLpToken.deployed(); - - liquidityGauge = await LiquidityGauge.deploy( - mockLpToken.address, - minter.address - ); - await liquidityGauge.deployed(); - - await token.setMinter(minter.address); - await gaugeController.addType("Liquidity", ethers.utils.parseEther("1")); - await gaugeController.addGauge( - liquidityGauge.address, - 0, - ethers.utils.parseEther("10") - ); - - for (let i = 0; i < ACCOUNT_NUM; i++) { - await mockLpToken - .connect(accounts[0]) - .transfer(accounts[i].address, ethers.utils.parseEther("1")); - await mockLpToken - .connect(accounts[i]) - .approve(liquidityGauge.address, ethers.utils.parseEther("1")); - } - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - function generateUniqueRandomNumbers( - count: number, - min: number, - max: number - ): number[] { - const set = new Set(); - while (set.size < count) { - const randomValue = Math.floor(Math.random() * (max - min + 1)) + min; - set.add(randomValue); - } - return Array.from(set); - } - - function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { - if (value.isZero() && target.isZero()) { - return true; - } - - const diff = value.sub(target).abs(); - const sum = value.add(target); - const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); - - // console.log( - // `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` - // ); - // console.log( - // `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` - // ); - - return ratio.lte(tol); - } - - async function showGaugeInfo() { - console.log("Gauge info----"); - console.log( - "GaugeWeight: ", - (await gaugeController.getGaugeWeight(liquidityGauge.address)).toString() - ); - console.log( - "TypeWeight: ", - (await gaugeController.getTypeWeight(0)).toString() - ); - console.log( - "TotalWeight: ", - (await gaugeController.getTotalWeight()).toString() - ); - console.log( - "TypeWeightSum: ", - (await gaugeController.getWeightsSumPerType(0)).toString() - ); - console.log("TimeTotal: ", (await gaugeController.timeTotal()).toString()); - console.log( - "pointsTotal: ", - ( - await gaugeController.pointsTotal( - (await gaugeController.timeTotal()).toString() - ) - ).toString() - ); - console.log( - "gaugeRelativeWeight: ", - ( - await gaugeController.gaugeRelativeWeight( - liquidityGauge.address, - await time.latest() - ) - ).toString() - ); - console.log( - "TotalSupply: ", - (await liquidityGauge.totalSupply()).toString() - ); - console.log("----"); - } - - for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { - it(`tests duration ${i}`, async function () { - const stDuration = generateUniqueRandomNumbers(3, WEEK, MONTH); - const depositTime: number[] = []; - - await time.increase(WEEK); - - for (let i = 0; i < 3; i++) { - await liquidityGauge - .connect(accounts[i + 1]) - .deposit( - ethers.utils.parseEther("1"), - accounts[i + 1].address, - false - ); - depositTime.push(await time.latest()); - - // await showGaugeInfo(); - } - - const durations: number[] = []; - const balances: BigNumber[] = []; - for (let i = 0; i < 3; i++) { - await time.increase(stDuration[i]); - await liquidityGauge - .connect(accounts[i + 1]) - .withdraw(ethers.utils.parseEther("1"), false); - - const duration = (await time.latest()) - depositTime[i]; - durations.push(duration); - await minter.connect(accounts[i + 1]).mint(liquidityGauge.address); - const balance = await token.balanceOf(accounts[i + 1].address); - balances.push(balance); - - // await showGaugeInfo(); - } - - const totalMinted: BigNumber = balances.reduce( - (a: BigNumber, b: BigNumber) => a.add(b), - ethers.BigNumber.from(0) - ); - const weight1 = Math.floor(durations[0]); - const weight2 = Math.floor(weight1 + (durations[1] - durations[0]) * 1.5); - const weight3 = Math.floor(weight2 + (durations[2] - durations[1]) * 3); - const totalWeight = weight1 + weight2 + weight3; - - console.log( - `Total minted: ${totalMinted.toString()}, Total Weight: ${totalWeight.toString()}` - ); - console.log( - `Balance 1: ${balances[0]} (${balances[0] - .mul(SCALE) - .div(totalMinted)}) Weight 1: ${weight1.toString()} (${ - (100 * weight1) / totalWeight - }%)` - ); - console.log( - `Balance 2: ${balances[1]} (${balances[1] - .mul(SCALE) - .div(totalMinted)}) Weight 2: ${weight2.toString()} (${ - (100 * weight2) / totalWeight - }%)` - ); - console.log( - `Balance 3: ${balances[2]} (${balances[2] - .mul(SCALE) - .div(totalMinted)}) Weight 3: ${weight3.toString()} (${ - (100 * weight3) / totalWeight - }%)` - ); - - expect( - approx( - balances[0].mul(SCALE).div(totalMinted), - BigNumber.from(weight1).mul(SCALE).div(totalWeight), - BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE - ) - ).to.be.true; - expect( - approx( - balances[1].mul(SCALE).div(totalMinted), - BigNumber.from(weight2).mul(SCALE).div(totalWeight), - BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE - ) - ).to.be.true; - expect( - approx( - balances[2].mul(SCALE).div(totalMinted), - BigNumber.from(weight3).mul(SCALE).div(totalWeight), - BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE - ) - ).to.be.true; - }); - } - - for (let i = 0; i < NUMBER_OF_ATTEMPTS; i++) { - it(`tests amounts ${i}`, async function () { - const stAmounts = generateUniqueRandomNumbers(3, 1e17, 1e18); - const depositTime: number[] = []; - - for (let i = 0; i < 3; i++) { - await liquidityGauge - .connect(accounts[i + 1]) - .deposit(stAmounts[i].toString(), accounts[i + 1].address, false); - depositTime.push(await time.latest()); - } - - await time.increase(MONTH); - - const balances: BigNumber[] = []; - for (let i = 0; i < 3; i++) { - liquidityGauge - .connect(accounts[i + 1]) - .withdraw(stAmounts[i].toString(), false); - } - - for (let i = 0; i < 3; i++) { - await minter.connect(accounts[i + 1]).mint(liquidityGauge.address); - balances.push(await token.balanceOf(accounts[i + 1].address)); - } - const totalDeposited: number = stAmounts.reduce( - (a: number, b: number) => a + b, - 0 - ); - const totalMinted: BigNumber = balances.reduce( - (a: BigNumber, b: BigNumber) => a.add(b), - ethers.BigNumber.from(0) - ); - - console.log( - `Total deposited: ${totalDeposited.toString()}, Total minted: ${totalMinted.toString()}` - ); - console.log( - `Balance 1: ${balances[0]} (${balances[0] - .mul(SCALE) - .div(totalMinted)}) Deposited 1: ${stAmounts[0].toString()} (${ - (100 * stAmounts[0]) / totalDeposited - }%)` - ); - console.log( - `Balance 2: ${balances[1]} (${balances[1] - .mul(SCALE) - .div(totalMinted)}) Deposited 2: ${stAmounts[1].toString()} (${ - (100 * stAmounts[1]) / totalDeposited - }%)` - ); - console.log( - `Balance 3: ${balances[2]} (${balances[2] - .mul(SCALE) - .div(totalMinted)}) Deposited 3: ${stAmounts[2].toString()} (${ - (100 * stAmounts[2]) / totalDeposited - }%)` - ); - - expect( - approx( - balances[0].mul(SCALE).div(totalMinted), - BigNumber.from(stAmounts[0].toString()) - .mul(SCALE) - .div(totalDeposited.toString()), - BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE - ) - ).to.be.true; - expect( - approx( - balances[1].mul(SCALE).div(totalMinted), - BigNumber.from(stAmounts[1].toString()) - .mul(SCALE) - .div(totalDeposited.toString()), - BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE - ) - ).to.be.true; - expect( - approx( - balances[2].mul(SCALE).div(totalMinted), - BigNumber.from(stAmounts[2].toString()) - .mul(SCALE) - .div(totalDeposited.toString()), - BigNumber.from(10).pow(16) // = (10 ** -4) * SCALE - ) - ).to.be.true; - }); - } -}); diff --git a/test/fork/integration/Minter/minterIntegration.test.ts b/test/fork/integration/Minter/minterIntegration.test.ts deleted file mode 100644 index 6b234f3a..00000000 --- a/test/fork/integration/Minter/minterIntegration.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const SCALE = BigNumber.from(10).pow(20); -const WEEK = 86400 * 7; -const MONTH = 86400 * 30; -const W = BigNumber.from(10).pow(18); - -describe("Minter integration", function () { - let admin, bob, charlie, dan: SignerWithAddress; - let gaugeController: Contract; - let threeGauges: Contract[3] = []; - let mockLpToken: Contract; - let minter: Contract; - let token: Contract; - let votingEscrow: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - [admin, bob, charlie, dan] = await ethers.getSigners(); - const MockLpToken = await ethers.getContractFactory("TestLP"); - const Token = await ethers.getContractFactory("CRV"); - const Minter = await ethers.getContractFactory("Minter"); - const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV6"); - const GaugeController = await ethers.getContractFactory("GaugeController"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await Token.deploy(); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - gaugeController = await GaugeController.deploy( - token.address, - votingEscrow.address - ); - await gaugeController.deployed(); - - minter = await Minter.deploy(token.address, gaugeController.address); - await minter.deployed(); - - mockLpToken = await MockLpToken.deploy( - "Curve LP token", - "usdCrv", - 18, - ethers.utils.parseEther("10") - ); - await mockLpToken.deployed(); - - for (let i = 0; i < 3; i++) { - threeGauges.push( - await LiquidityGauge.deploy(mockLpToken.address, minter.address) - ); - } - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - function approx(value: BigNumber, target: BigNumber, tol: BigNumber) { - if (value.isZero() && target.isZero()) { - return true; - } - - const diff = value.sub(target).abs(); - const sum = value.add(target); - const ratio = diff.mul(2).mul(BigNumber.from(SCALE)).div(sum); - - console.log( - `Value: ${value.toString()}, Target: ${target.toString()}, Tol: ${tol.toString()}` - ); - console.log( - `Diff: ${diff.toString()}, Sum: ${sum.toString()}, Ratio: ${ratio.toString()}` - ); - - return ratio.lte(tol); - } - - it(`tests mint`, async function () { - const amount = ethers.utils.parseEther("1"); - await token.connect(admin).setMinter(minter.address); - - const typeWeights = [W.div(2), W.mul(2)]; - const gaugeWeights = [W.mul(2), W.mul(1), W.div(2)]; - const gaugeTypes = [0, 0, 1]; - - // Set up types - for (let i = 0; i < typeWeights.length; i++) { - await gaugeController.connect(admin).addType("Liquidity", 0); - await gaugeController.connect(admin).changeTypeWeight(i, typeWeights[i]); - } - - // Set up gauges - for (let i = 0; i < threeGauges.length; i++) { - await gaugeController - .connect(admin) - .addGauge(threeGauges[i].address, gaugeTypes[i], gaugeWeights[i]); - } - - // Transfer tokens to Bob, Charlie and Dan - await mockLpToken.transfer(bob.address, amount); - await mockLpToken.transfer(charlie.address, amount); - await mockLpToken.transfer(dan.address, amount); - - // For weights to activate - await time.increase(WEEK); - - // Bob and Charlie deposit to gauges with different weights - await mockLpToken.connect(bob).approve(threeGauges[1].address, amount); - await threeGauges[1].connect(bob).deposit(amount, bob.address, false); - await mockLpToken.connect(charlie).approve(threeGauges[2].address, amount); - await threeGauges[2] - .connect(charlie) - .deposit(amount, charlie.address, false); - - await time.increase(MONTH); - - await mockLpToken.connect(dan).approve(threeGauges[1].address, amount); - await threeGauges[1].connect(dan).deposit(amount, dan.address, false); - - await time.increase(MONTH); - - // cannot withdraw too much - await expect(threeGauges[1].connect(bob).withdraw(amount.add(1), false)).to - .be.reverted; - - // Withdraw - await threeGauges[1].connect(bob).withdraw(amount, false); - await threeGauges[2].connect(charlie).withdraw(amount, false); - await threeGauges[1].connect(dan).withdraw(amount, false); - - // Balances after withdrawal - expect(await mockLpToken.balanceOf(bob.address)).to.equal(amount); - expect(await mockLpToken.balanceOf(charlie.address)).to.equal(amount); - expect(await mockLpToken.balanceOf(dan.address)).to.equal(amount); - - // Claim for Bob now - await minter.connect(bob).mint(threeGauges[1].address); - const bobTokens = await token.balanceOf(bob.address); - - await time.increase(MONTH); - - // This won't give anything - await minter.connect(bob).mint(threeGauges[1].address); - expect(await token.balanceOf(bob.address)).to.equal(bobTokens); - - await minter.connect(charlie).mint(threeGauges[2].address); - const charlieTokens = await token.balanceOf(charlie.address); - await minter.connect(dan).mint(threeGauges[1].address); - const danTokens = await token.balanceOf(dan.address); - - const S = bobTokens.add(charlieTokens).add(danTokens); - const ww = gaugeWeights.map((w, i) => w.mul(typeWeights[gaugeTypes[i]])); - const Sw = ww[1].add(ww[2]); // Gauge 0 not used - - // Bob and Charlie were there for full time, gauges 1 and 2 - // Dan was in gauge 1 for half the time - expect( - approx( - bobTokens.mul(SCALE).div(S), - ww[1].mul(SCALE).mul(3).div(4).div(Sw), // 0.75 * ww[1] / Sw - BigNumber.from(10).pow(14).mul(2) // = 2e-6 * SCALE - ) - ).to.be.true; - expect( - approx( - charlieTokens.mul(SCALE).div(S), - ww[2].mul(SCALE).div(Sw), // ww[2] / Sw - BigNumber.from(10).pow(14).mul(2) // = 2e-6 * SCALE - ) - ).to.be.true; - expect( - approx( - danTokens.mul(SCALE).div(S), - ww[1].mul(SCALE).div(4).div(Sw), // 0.25 * ww[1] / Sw - BigNumber.from(10).pow(14).mul(2) // = 2e-6 * SCALE - ) - ).to.be.true; - }); -}); diff --git a/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts b/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts deleted file mode 100644 index 4cd75157..00000000 --- a/test/fork/integration/VotingEscrow/depositWithdrawVoting.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const ACCOUNT_NUM = 10; -const MAX_EXAMPLES = 30; // テストの試行回数 -const STATEFUL_STEP_COUNT = 20; // ruleの実行回数 -const YEAR = BigNumber.from(86400 * 365); -const WEEK = BigNumber.from(86400 * 7); -const two_to_the_256_minus_1 = BigNumber.from("2") - .pow(BigNumber.from("256")) - .sub(BigNumber.from("1")); -const ten_to_the_40 = BigNumber.from( - "10000000000000000000000000000000000000000" -); - -describe("VotingEscrow", function () { - let accounts: SignerWithAddress[]; - let votingEscrow: Contract; - let token: Contract; - - let stAccountN: number; - let stAccount: SignerWithAddress; - let stValue: BigNumber = BigNumber.from("0"); - let stLockDuration: BigNumber; - let votingBalances: { [key: string]: BigNumber }[]; - let unlockTime: BigNumber; - - let snapshot: SnapshotRestorer; - - beforeEach(async () => { - snapshot = await takeSnapshot(); - accounts = (await ethers.getSigners()).slice(0, ACCOUNT_NUM); - - const Token = await ethers.getContractFactory("MockToken"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await Token.deploy("Test Token", "TST", 18); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - //init - for (let i = 0; i < ACCOUNT_NUM; i++) { - await token - .connect(accounts[i]) - ._mintForTesting(accounts[i].address, ten_to_the_40); - await token - .connect(accounts[i]) - .approve(votingEscrow.address, two_to_the_256_minus_1); - } - - //setup - votingBalances = new Array(ACCOUNT_NUM).fill({ - value: BigNumber.from("0"), - unlockTime: BigNumber.from("0"), - }); - }); - - //--------------------------------------------- functions -----------------------------------------------------------// - - function rdmValue(a): BigNumber { - let rdm = BigNumber.from(Math.floor(Math.random() * a).toString()); - return rdm; - } - - //--------------------------------------------- randomly excuted functions -----------------------------------------------------------// - async function ruleCreateLock() { - console.log("ruleCreateLock"); - - //stAccount - let rdm = Math.floor(Math.random() * 10); //0~9 integer - stAccountN = rdm; - stAccount = accounts[stAccountN]; - - //stValue - stValue = rdmValue(9007199254740991); - - //number of weeks to lock a deposit - stLockDuration = rdmValue(255); //uint8.max - - let timestamp = BigNumber.from(await time.latest()); - unlockTime = timestamp.add(WEEK.mul(stLockDuration)).div(WEEK).mul(WEEK); - - if (stValue.eq(0)) { - // console.log("--revert: 1"); - await expect( - votingEscrow.connect(stAccount).createLock(stValue, unlockTime) - ).to.revertedWith("need non-zero value"); - } else if (votingBalances[stAccountN]["value"].gt("0")) { - // console.log("--revert: 2"); - await expect( - votingEscrow.connect(stAccount).createLock(stValue, unlockTime) - ).to.revertedWith("Withdraw old tokens first"); - } else if (unlockTime.lte(timestamp)) { - // console.log("--revert: 3"); - await expect( - votingEscrow.connect(stAccount).createLock(stValue, unlockTime) - ).to.revertedWith("Can only lock until time in the future"); - } else if (unlockTime.gte(timestamp.add(YEAR.mul("4")))) { - // console.log("--revert: 4"); - await expect( - votingEscrow.connect(stAccount).createLock(stValue, unlockTime) - ).to.revertedWith("Voting lock can be 4 years max"); - } else { - // console.log("--success, account:", stAccountN); - const tx = await votingEscrow - .connect(stAccount) - .createLock(stValue, unlockTime); - const receipt = await tx.wait(); - votingBalances[stAccountN] = { - value: stValue, - unlockTime: receipt.events[1]["args"]["locktime"], - }; - } - } - - async function ruleIncreaseAmount() { - console.log("ruleIncreaseAmount"); - - //stAccount - let rdm = Math.floor(Math.random() * 10); //0~9 integer - stAccountN = rdm; - stAccount = accounts[stAccountN]; - - //stValue - stValue = rdmValue(9007199254740991); - - let timestamp = BigNumber.from(await time.latest()); - - if (stValue.eq(0)) { - // console.log("--revert: 1"); - await expect( - votingEscrow.connect(stAccount).increaseAmount(stValue) - ).to.revertedWith("dev: need non-zero value"); - } else if (votingBalances[stAccountN]["value"].eq("0")) { - // console.log("--revert: 2"); - await expect( - votingEscrow.connect(stAccount).increaseAmount(stValue) - ).to.revertedWith("No existing lock found"); - } else if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { - // console.log("--revert: 3"); - await expect( - votingEscrow.connect(stAccount).increaseAmount(stValue) - ).to.revertedWith("Cannot add to expired lock. Withdraw"); - } else { - await votingEscrow.connect(stAccount).increaseAmount(stValue); - votingBalances[stAccountN]["value"] = - votingBalances[stAccountN]["value"].add(stValue); - - // console.log( - // "--success, account:", - // stAccountN, - // "new balance:", - // votingBalances[stAccountN]["value"].toString() - // ); - } - } - - async function ruleIncreaseUnlockTime() { - console.log("ruleIncreaseUnlockTime"); - - //stAccount - let rdm = Math.floor(Math.random() * 10); //0~9 integer - stAccountN = rdm; - stAccount = accounts[stAccountN]; - - //unlockTime - let timestamp = BigNumber.from(await time.latest()); - stLockDuration = rdmValue(255); //number of weeks - let unlockTime = timestamp - .add(stLockDuration.mul(WEEK)) - .div(WEEK) - .mul(WEEK); - - if (votingBalances[stAccountN]["unlockTime"].lte(timestamp)) { - // console.log("--revert: 1"); - await expect( - votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) - ).to.revertedWith("Lock expired"); - } else if (votingBalances[stAccountN]["value"].eq("0")) { - // console.log("--revert: 2"); - await expect( - votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) - ).to.revertedWith("Nothing is locked"); - } else if (votingBalances[stAccountN]["unlockTime"].gte(unlockTime)) { - // console.log("--revert: 3"); - await expect( - votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) - ).to.revertedWith("Can only increase lock duration"); - } else if (unlockTime.gt(timestamp.add(YEAR.mul("4")))) { - // console.log("--revert: 4"); - await expect( - votingEscrow.connect(stAccount).increaseUnlockTime(unlockTime) - ).to.revertedWith("Voting lock can be 4 years max"); - } else { - // console.log("--success, account:", stAccountN); - const tx = await votingEscrow - .connect(stAccount) - .increaseUnlockTime(unlockTime); - const receipt = await tx.wait(); - votingBalances[stAccountN]["unlockTime"] = - receipt.events[0]["args"]["locktime"]; - } - } - - async function ruleWithdraw() { - console.log("ruleWithdraw"); - // Withdraw tokens from the voting escrow. - - //stAccount - let rdm = Math.floor(Math.random() * 10); //0~9 integer - stAccountN = rdm; - stAccount = accounts[stAccountN]; - - let timestamp = BigNumber.from(await time.latest()); - - if (votingBalances[stAccountN]["unlockTime"].gt(timestamp)) { - // console.log("--reverted"); - await expect(votingEscrow.connect(stAccount).withdraw()).to.revertedWith( - "The lock didn't expire" - ); - } else { - // console.log("--success, account:", stAccountN); - await votingEscrow.connect(stAccount).withdraw(); - votingBalances[stAccountN]["value"] = BigNumber.from("0"); - } - } - - async function ruleCheckpoint() { - console.log("ruleCheckpoint"); - - //stAccount - let rdm = Math.floor(Math.random() * 10); //0~9 integer - stAccountN = rdm; - stAccount = accounts[stAccountN]; - - await votingEscrow.connect(stAccount).checkpoint(); - } - - async function ruleAdvanceTime() { - console.log("ruleAdvanceTime"); - - let stSleepDuration = Math.floor(Math.random() * 3) + 1; //1~4 - - await time.increase(WEEK.mul(stSleepDuration).toNumber()); - - if (stSleepDuration == 1) { - console.log("Time advanced"); - } else { - console.log("Time advanced"); - } - } - - async function checkInvariants() { - // console.log("=====Invariant checks====="); - - // console.log("invariant_token_balances"); - for (let i = 0; i < ACCOUNT_NUM; i++) { - expect(await token.balanceOf(accounts[i].address)).to.equal( - ten_to_the_40.sub(votingBalances[i]["value"]) - ); - } - - // console.log("invariant_escrow_current_balances"); - let total_supply = BigNumber.from("0"); - let timestamp = BigNumber.from(await time.latest()); - - for (let i = 0; i < ACCOUNT_NUM; i++) { - let data = votingBalances[i]; - - let balance = await votingEscrow["balanceOf(address)"]( - accounts[i].address - ); - total_supply = total_supply.add(balance); - - if (data["unlockTime"].gt(timestamp) && data["value"].div(YEAR).gt("0")) { - expect(balance.isZero()).to.equal(false); - } else if (data["value"].isZero() || data["unlockTime"].lte(timestamp)) { - expect(balance.isZero()).to.equal(true); - } - } - expect(await votingEscrow["totalSupply()"]()).to.equal(total_supply); - - // console.log("invariant_historic_balances"); - total_supply = BigNumber.from("0"); - let blocknumber = (await time.latestBlock()) - 4; - // console.log(blocknumber); - - for (let i = 0; i < ACCOUNT_NUM; i++) { - total_supply = total_supply.add( - await votingEscrow.balanceOfAt(accounts[i].address, blocknumber) - ); - } - - expect(await votingEscrow.totalSupplyAt(blocknumber)).to.equal( - total_supply - ); - } - - let func = [ - "ruleCreateLock", - "ruleIncreaseAmount", - "ruleIncreaseUnlockTime", - "ruleWithdraw", - "ruleCheckpoint", - "ruleAdvanceTime", - ]; - - describe("test_deposit_withdraw_voting", function () { - //set arbitral number of repeats - for (let x = 0; x < MAX_EXAMPLES; x++) { - it(`Try ${x}`, async () => { - let steps = Math.floor(Math.random() * (STATEFUL_STEP_COUNT - 1)) + 1; - for (let i = 0; i < steps; i++) { - let n = (await rdmValue(func.length)).toNumber(); - await eval(func[n])(); - await checkInvariants(); - } - }); - } - }); - - afterEach(async () => { - await snapshot.restore(); - }); -}); diff --git a/test/fork/integration/VotingEscrow/votingEscrow.test.ts b/test/fork/integration/VotingEscrow/votingEscrow.test.ts deleted file mode 100644 index e47f0740..00000000 --- a/test/fork/integration/VotingEscrow/votingEscrow.test.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -type Stage = { - blockNumber: number; - timestamp: number; - bias?: string; -}; - -const H = 3600; -const DAY = 86400; -const WEEK = 7 * DAY; -const MAXTIME = DAY * 365 * 4; // 126144000 -const SCALE = 1e20; -const TOL = (120 / WEEK) * SCALE; - -describe("Voting Powers Test", function () { - // Test voting power in the following scenario. - // Alice: - // ~~~~~~~ - // ^ - // | * * - // | | \ | \ - // | | \ | \ - // +-+---+---+------+---> t - - // Bob: - // ~~~~~~~ - // ^ - // | * - // | | \ - // | | \ - // +-+---+---+---+--+---> t - - // Alice has 100% of voting power in the first period. - // She has 2/3 power at the start of 2nd period, with Bob having 1/2 power - // (due to smaller locktime). - // Alice's power grows to 100% by Bob's unlock. - - // Checking that totalSupply is appropriate. - - // After the test is done, check all over again with balanceOfAt / totalSupplyAt - let alice, bob: SignerWithAddress; - let votingEscrow: Contract; - let token: Contract; - let t0: number; - let wTotal, wAlice, wBob: BigNumber; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - const CRV = await ethers.getContractFactory("CRV"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - [alice, bob] = await ethers.getSigners(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - it("test voting powers", async function () { - const amount: BigNumber = ethers.utils.parseEther("1000"); - await token.connect(alice).transfer(bob.address, amount); - const stages: { [key: string]: Stage | Stage[] } = {}; - - await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); - await token.connect(bob).approve(votingEscrow.address, amount.mul(10)); - - expect(await votingEscrow["totalSupply()"]()).to.equal(0); - expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - - const timeToNextWeek = - (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - - (await time.latest()); - - // Move to timing which is good for testing - beginning of a UTC week - await time.increase(timeToNextWeek); - - await ethers.provider.send("evm_increaseTime", [H]); - - stages["before_deposits"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - - await votingEscrow - .connect(alice) - .createLock(amount, (await time.latest()) + WEEK); - - stages["alice_deposit"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - - // console.log("alice_deposit", stages["alice_deposit"]); - - await time.increase(H); - - const totalSupply = await votingEscrow["totalSupply()"](); - const aliceBalance = await votingEscrow["balanceOf(address)"]( - alice.address - ); - - expect(approx(totalSupply, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to - .be.true; - expect(approx(aliceBalance, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to - .be.true; - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - - t0 = await time.latest(); - - stages["alice_in_0"] = []; - stages["alice_in_0"].push({ - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }); - - // console.log("alice_in_0", stages["alice_in_0"]); - - // Simulating the passage of time with 7 days and 24 hours per day - for (let i = 0; i < 7; i++) { - for (let _ = 0; _ < 24; _++) { - await time.increase(H); - } - - const dt = (await time.latest()) - t0; - const totalSupply = await votingEscrow["totalSupply()"](); - const aliceBalance = await votingEscrow["balanceOf(address)"]( - alice.address - ); - - expect( - approx( - totalSupply, - amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), - TOL - ) - ).to.be.true; - expect( - approx( - aliceBalance, - amount.div(MAXTIME).mul(Math.max(WEEK - 2 * H - dt, 0)), - TOL - ) - ).to.be.true; - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - - stages["alice_in_0"].push({ - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - bias: aliceBalance.toString(), - }); - } - // console.log("alice_in_0", stages["alice_in_0"]); - await ethers.provider.send("evm_increaseTime", [H]); - - expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); - await votingEscrow.connect(alice).withdraw(); - stages["alice_withdraw"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - - // console.log("alice_withdraw", stages["alice_withdraw"]); - - expect(await votingEscrow["totalSupply()"]()).to.equal(0); - expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - - await time.increase(H); - - // Calculate the next week for round counting - const nextWeek = - (Math.floor((await time.latest()) / WEEK) + 1) * WEEK - - (await time.latest()); - - await time.increase(nextWeek); - - await votingEscrow - .connect(alice) - .createLock(amount, (await time.latest()) + 2 * WEEK); - - stages["alice_deposit_2"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - - // console.log("alice_deposit_2", stages["alice_deposit_2"]); - - expect( - approx( - await votingEscrow["totalSupply()"](), - amount.div(MAXTIME).mul(2 * WEEK), - TOL - ) - ).to.be.true; - expect( - approx( - await votingEscrow["balanceOf(address)"](alice.address), - amount.div(MAXTIME).mul(2 * WEEK), - TOL - ) - ).to.be.true; - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - - await votingEscrow - .connect(bob) - .createLock(amount, (await time.latest()) + WEEK); - - stages["bob_deposit_2"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - - // console.log("bob_deposit_2", stages["bob_deposit_2"]); - - expect( - approx( - await votingEscrow["totalSupply()"](), - amount.div(MAXTIME).mul(3 * WEEK), - TOL - ) - ).to.be.true; - expect( - approx( - await votingEscrow["balanceOf(address)"](alice.address), - amount.div(MAXTIME).mul(2 * WEEK), - TOL - ) - ).to.be.true; - expect( - approx( - await votingEscrow["balanceOf(address)"](bob.address), - amount.div(MAXTIME).mul(WEEK), - TOL - ) - ).to.be.true; - - t0 = await time.latest(); - await time.increase(H); - - stages["alice_bob_in_2"] = []; - // Beginning of week: weight 3 - // End of week: weight 1 - for (let i = 0; i < 7; i++) { - for (let _ = 0; _ < 24; _++) { - await time.increase(H); - } - const dt = (await time.latest()) - t0; - wTotal = await votingEscrow["totalSupply()"](); - wAlice = await votingEscrow["balanceOf(address)"](alice.address); - wBob = await votingEscrow["balanceOf(address)"](bob.address); - expect(wTotal).to.equal(wAlice.add(wBob)); - expect( - approx(wAlice, amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), TOL) - ).to.be.true; - expect(approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), TOL)) - .to.be.true; - - stages["alice_bob_in_2"].push({ - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - bias: `${wAlice.toString()}, ${wBob.toString()}`, - }); - } - // console.log("alice_bob_in_2", stages["alice_bob_in_2"]); - - await time.increase(H); - - await votingEscrow.connect(bob).withdraw(); - t0 = await time.latest(); - stages["bob_withdraw_1"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - const wTotal1 = await votingEscrow["totalSupply()"](); - const wAlice1 = await votingEscrow["balanceOf(address)"](alice.address); - expect(wAlice1).to.equal(wTotal1); - expect(approx(wTotal1, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be - .true; - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - - await time.increase(H); - - stages["alice_in_2"] = []; - for (let i = 0; i < 7; i++) { - for (let _ = 0; _ < 24; _++) { - await time.increase(H); - } - const dt = (await time.latest()) - t0; - wTotal = await votingEscrow["totalSupply()"](); - wAlice = await votingEscrow["balanceOf(address)"](alice.address); - expect(wTotal).to.equal(wAlice); - expect( - approx( - wTotal, - amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), - TOL - ) - ).to.be.true; - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - stages["alice_in_2"].push({ - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }); - } - // console.log("alice_in_2", stages["alice_in_2"]); - - await votingEscrow.connect(alice).withdraw(); - stages["alice_withdraw_2"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - - await time.increase(H); - - await votingEscrow.connect(bob).withdraw(); - stages["bob_withdraw_2"] = { - blockNumber: await time.latestBlock(), - timestamp: await time.latest(), - }; - // console.log("bob_withdraw_2", stages["bob_withdraw_2"]); - - expect(await votingEscrow["totalSupply()"]()).to.equal(0); - expect(await votingEscrow["balanceOf(address)"](alice.address)).to.equal(0); - expect(await votingEscrow["balanceOf(address)"](bob.address)).to.equal(0); - - // Now test historical balanceOfAt and others - expect( - await votingEscrow.balanceOfAt( - alice.address, - stages["before_deposits"].blockNumber - ) - ).to.equal(0); - expect( - await votingEscrow.balanceOfAt( - bob.address, - stages["before_deposits"].blockNumber - ) - ).to.equal(0); - expect( - await votingEscrow.totalSupplyAt(stages["before_deposits"].blockNumber) - ).to.equal(0); - - wAlice = await votingEscrow.balanceOfAt( - alice.address, - stages["alice_deposit"].blockNumber - ); - - expect(approx(wAlice, amount.div(MAXTIME).mul(WEEK - H), TOL)).to.be.true; - expect( - await votingEscrow.balanceOfAt( - bob.address, - stages["alice_deposit"].blockNumber - ) - ).to.equal(0); - wTotal = await votingEscrow.totalSupplyAt( - stages["alice_deposit"].blockNumber - ); - expect(wAlice).to.equal(wTotal); - - for (let i = 0; i < stages["alice_in_0"].length; i++) { - const block = stages["alice_in_0"][i].blockNumber; - wAlice = await votingEscrow.balanceOfAt(alice.address, block); - wBob = await votingEscrow.balanceOfAt(bob.address, block); - wTotal = await votingEscrow.totalSupplyAt(block); - expect(wBob).to.equal(0); - expect(wAlice).to.equal(wTotal); - const timeLeft = Math.floor((WEEK * (7 - i)) / 7) - 2 * H; - const error1h = (H / timeLeft) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks - expect(approx(wAlice, amount.div(MAXTIME).mul(timeLeft), error1h)).to.be - .true; - } - - wTotal = await votingEscrow.totalSupplyAt( - stages["alice_withdraw"].blockNumber - ); - wAlice = await votingEscrow.balanceOfAt( - alice.address, - stages["alice_withdraw"].blockNumber - ); - wBob = await votingEscrow.balanceOfAt( - bob.address, - stages["alice_withdraw"].blockNumber - ); - expect(wAlice).to.equal(wBob); - expect(wAlice).to.equal(wTotal); - expect(wTotal).to.equal(0); - - wTotal = await votingEscrow.totalSupplyAt( - stages["alice_deposit_2"].blockNumber - ); - wAlice = await votingEscrow.balanceOfAt( - alice.address, - stages["alice_deposit_2"].blockNumber - ); - expect(approx(wTotal, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; - expect(wTotal).to.equal(wAlice); - expect( - await votingEscrow.balanceOfAt( - bob.address, - stages["alice_deposit_2"].blockNumber - ) - ).to.equal(0); - - wTotal = await votingEscrow.totalSupplyAt( - stages["bob_deposit_2"].blockNumber - ); - wAlice = await votingEscrow.balanceOfAt( - alice.address, - stages["bob_deposit_2"].blockNumber - ); - wBob = await votingEscrow.balanceOfAt( - bob.address, - stages["bob_deposit_2"].blockNumber - ); - expect(wTotal).to.equal(wAlice.add(wBob)); - expect(approx(wTotal, amount.div(MAXTIME).mul(3 * WEEK), TOL)).to.be.true; - expect(approx(wAlice, amount.div(MAXTIME).mul(2 * WEEK), TOL)).to.be.true; - - t0 = stages["bob_deposit_2"].timestamp; - for (let i = 0; i < stages["alice_bob_in_2"].length; i++) { - const block = stages["alice_bob_in_2"][i].blockNumber; - wAlice = await votingEscrow.balanceOfAt(alice.address, block); - wBob = await votingEscrow.balanceOfAt(bob.address, block); - wTotal = await votingEscrow.totalSupplyAt(block); - expect(wTotal).to.equal(wAlice.add(wBob)); - const dt = stages["alice_bob_in_2"][i].timestamp - t0; - const error1h = (H / (2 * WEEK - i * DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks - expect( - approx( - wAlice, - amount.div(MAXTIME).mul(Math.max(2 * WEEK - dt, 0)), - error1h - ) - ).to.be.true; - expect( - approx(wBob, amount.div(MAXTIME).mul(Math.max(WEEK - dt, 0)), error1h) - ).to.be.true; - } - - wTotal = await votingEscrow.totalSupplyAt( - stages["bob_withdraw_1"].blockNumber - ); - wAlice = await votingEscrow.balanceOfAt( - alice.address, - stages["bob_withdraw_1"].blockNumber - ); - wBob = await votingEscrow.balanceOfAt( - bob.address, - stages["bob_withdraw_1"].blockNumber - ); - expect(wTotal).to.equal(wAlice); - expect(approx(wTotal, amount.div(MAXTIME).mul(WEEK - 2 * H), TOL)).to.be - .true; - expect(wBob).to.equal(0); - - t0 = stages["bob_withdraw_1"].timestamp; - for (let i = 0; i < stages["alice_in_2"].length; i++) { - const block = stages["alice_in_2"][i].blockNumber; - wAlice = await votingEscrow.balanceOfAt(alice.address, block); - const wBob = await votingEscrow.balanceOfAt(bob.address, block); - wTotal = await votingEscrow.totalSupplyAt(block); - expect(wTotal).to.equal(wAlice); - expect(wBob).to.equal(0); - const dt = stages["alice_in_2"][i].timestamp - t0; - const error1h = (H / (WEEK - i * DAY + DAY)) * SCALE; // Rounding error of 1 block is possible, and we have 1h blocks - - expect( - approx( - wTotal, - amount.div(MAXTIME).mul(Math.max(WEEK - dt - 2 * H, 0)), - error1h - ) - ).to.be.true; - } - - wTotal = await votingEscrow.totalSupplyAt( - stages["bob_withdraw_2"].blockNumber - ); - wAlice = await votingEscrow.balanceOfAt( - alice.address, - stages["bob_withdraw_2"].blockNumber - ); - const wBob4 = await votingEscrow.balanceOfAt( - bob.address, - stages["bob_withdraw_2"].blockNumber - ); - expect(wTotal).to.equal(0); - expect(wAlice).to.equal(0); - expect(wBob4).to.equal(0); - - await showStats(); - }); - - function approx(value: BigNumber, target: BigNumber, tol: number) { - if (value.isZero() && target.isZero()) { - return true; - } - - const diff = value.sub(target).abs(); - const sum = value.add(target); - const ratio = diff.mul(2).mul(BigNumber.from(SCALE.toString())).div(sum); - - return ratio.lte(BigNumber.from(tol.toString())); - } - - async function showStats() { - const initialBlock = (await votingEscrow.pointHistory(0)).blk.toNumber(); - const latestBlock = await time.latestBlock(); - for (let i = initialBlock; i < latestBlock; i++) { - let a = await votingEscrow.balanceOfAt(alice.address, `${i}`); - let b = await votingEscrow.balanceOfAt(bob.address, `${i}`); - console.log(`Block ${i}: a: ${a.toString()} b: ${b.toString()}`); - } - const epoch = (await votingEscrow.epoch()).toNumber(); - const aliceEpoch = ( - await votingEscrow.userPointEpoch(alice.address) - ).toNumber(); - const bobEpoch = ( - await votingEscrow.userPointEpoch(bob.address) - ).toNumber(); - for (let i = 0; i <= epoch; i++) { - let p = await votingEscrow.pointHistory(i); - console.log( - `epoch: ${i}: bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` - ); - } - for (let i = 0; i <= aliceEpoch; i++) { - let p = await votingEscrow.userPointHistory(alice.address, i); - let balanceAt = p.blk.isZero() - ? 0 - : await votingEscrow.balanceOfAt(alice.address, p.blk.toNumber()); - console.log( - `alice epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` - ); - } - for (let i = 0; i < bobEpoch; i++) { - let p = await votingEscrow.userPointHistory(bob.address, i); - let balanceAt = p.blk.isZero() - ? 0 - : await votingEscrow.balanceOfAt(bob.address, p.blk.toNumber()); - console.log( - `bob epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` - ); - } - } -}); diff --git a/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts b/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts deleted file mode 100644 index 80938dab..00000000 --- a/test/fork/integration/VotingEscrow/zeroBalanceAtUnlockTime.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const WEEK = 86400 * 7; - -describe("Voting Escrow tests", function () { - let accounts: SignerWithAddress[]; - let votingEscrow: Contract; - let token: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async () => { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - - const CRV = await ethers.getContractFactory("CRV"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - await token - .connect(accounts[0]) - .approve(votingEscrow.address, ethers.utils.parseEther("1")); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - for ( - let st_initial = WEEK * 2; - st_initial <= WEEK * 52; - st_initial += WEEK * 2 - ) { - it(`create lock with zero balance: Lock ${Math.floor( - st_initial / WEEK - )} Week`, async () => { - const expectedUnlock = (await time.latest()) + st_initial; - await votingEscrow - .connect(accounts[0]) - .createLock(ethers.utils.parseEther("1"), expectedUnlock); - - const actualUnlock = (await votingEscrow.locked(accounts[0].address))[1]; - - await time.increase(actualUnlock - (await time.latest()) - 5); - - expect( - await votingEscrow["balanceOf(address)"](accounts[0].address) - ).to.not.equal(0); - - await time.increase(10); - - expect( - await votingEscrow["balanceOf(address)"](accounts[0].address) - ).to.equal(0); - }); - } - - for ( - let st_initial = WEEK * 2; - st_initial <= WEEK * 52; - st_initial += WEEK * 2 - ) { - for (let st_extend = WEEK; st_extend <= WEEK * 2; st_extend += WEEK) { - it(`increase unlock with zero balance: Lock ${Math.floor( - st_initial / WEEK - )} Week, extend: ${Math.floor(st_extend / WEEK)} Week`, async () => { - await votingEscrow - .connect(accounts[0]) - .createLock( - ethers.utils.parseEther("1"), - (await time.latest()) + st_initial - ); - - const initialUnlock = ( - await votingEscrow.locked(accounts[0].address) - )[1].toNumber(); - const extendedExpectedUnlock = initialUnlock + st_extend; - - await votingEscrow.increaseUnlockTime(extendedExpectedUnlock); - - const extendedActualUnlock = ( - await votingEscrow.locked(accounts[0].address) - )[1]; - - await time.increase(extendedActualUnlock - (await time.latest()) - 5); - - expect( - await votingEscrow["balanceOf(address)"](accounts[0].address) - ).to.not.equal(0); - - await time.increase(10); - - expect( - await votingEscrow["balanceOf(address)"](accounts[0].address) - ).to.equal(0); - }); - } - } -}); diff --git a/test/fork/unitary/ERC20CRV/burn.test.ts b/test/fork/unitary/ERC20CRV/burn.test.ts deleted file mode 100644 index 333e1fef..00000000 --- a/test/fork/unitary/ERC20CRV/burn.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../Helper"; - -describe("ERC20CRV", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ token } = await deployContracts()); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("ERC20CRV EpochTimeSupply", function () { - it("test_burn", async function () { - const balance: BigNumber = await token.balanceOf(accounts[0].address); - const initialSupply: BigNumber = await token.totalSupply(); - - await token.connect(accounts[0]).burn(31337); - - expect(await token.balanceOf(accounts[0].address)).to.equal( - balance.sub(31337) - ); - expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); - }); - - it("test_burn_not_admin", async function () { - const initialSupply: BigNumber = await token.totalSupply(); - - await token.transfer(accounts[1].address, 1000000); - await token.connect(accounts[1]).burn(31337); - - expect(await token.balanceOf(accounts[1].address)).to.equal( - 1000000 - 31337 - ); - expect(await token.totalSupply()).to.equal(initialSupply.sub(31337)); - }); - - it("test_burn_all", async function () { - const initialSupply: BigNumber = await token.totalSupply(); - - await token.connect(accounts[0]).burn(initialSupply); - - expect(await token.balanceOf(accounts[0].address)).to.equal(0); - expect(await token.totalSupply()).to.equal(0); - }); - - it("test_overburn", async function () { - const initialSupply: BigNumber = await token.totalSupply(); - - await expect( - token.connect(accounts[0]).burn(initialSupply.add(1)) - ).to.be.revertedWith("ERC20: burn amount exceeds balance"); - }); - }); -}); diff --git a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts b/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts deleted file mode 100644 index 925fef6a..00000000 --- a/test/fork/unitary/ERC20CRV/epochTimeSupply.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../Helper"; -import Constants from "../../Constants"; - -describe("ERC20CRV", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let snapshot: SnapshotRestorer; - - const week = Constants.week; - const YEAR = Constants.YEAR; - const year = Constants.year; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ token } = await deployContracts()); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("ERC20CRV EpochTimeSupply", function () { - it("test_start_epoch_time_write", async function () { - const creationTime: BigNumber = await token.startEpochTime(); - await ethers.provider.send("evm_increaseTime", [year]); - await ethers.provider.send("evm_mine", []); - - expect(await token.startEpochTime()).to.equal(creationTime); - - await token.startEpochTimeWrite(); - - expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); - }); - - it("test_start_epoch_time_write_same_epoch", async function () { - await token.startEpochTimeWrite(); - await token.startEpochTimeWrite(); - }); - - it("test_update_mining_parameters", async function () { - const creationTime = await token.startEpochTime(); - const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const newEpoch = creationTime.add(YEAR).sub(now); - await ethers.provider.send("evm_increaseTime", [newEpoch.toNumber()]); - await token.updateMiningParameters(); - }); - - it("test_update_mining_parameters_same_epoch", async function () { - const creationTime = await token.startEpochTime(); - const now = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const newEpoch = creationTime.add(YEAR).sub(now); - await ethers.provider.send("evm_increaseTime", [ - newEpoch.sub(BigNumber.from("3")).toNumber(), - ]); - await expect(token.updateMiningParameters()).to.be.revertedWith( - "dev: too soon!" - ); - }); - - it("test_mintable_in_timeframe_end_before_start", async function () { - const creationTime = await token.startEpochTime(); - await expect( - token.mintableInTimeframe(creationTime.add(1), creationTime) - ).to.be.revertedWith("dev: start > end"); - }); - - it("test_mintable_in_timeframe_multiple_epochs", async function () { - const creationTime = await token.startEpochTime(); - - // Two epochs should not raise - const mintable = BigNumber.from("19").div(BigNumber.from("10")); - await token.mintableInTimeframe( - creationTime, - creationTime - .add(YEAR) - .mul(BigNumber.from("19").div(BigNumber.from("10"))) - ); - - // Three epochs should raise - await expect( - token.mintableInTimeframe( - creationTime, - creationTime - .add(YEAR) - .mul(BigNumber.from("21").div(BigNumber.from("10"))) - ) - ).to.be.revertedWith("dev: too far in future"); - }); - - it("test_available_supply", async function () { - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); - await ethers.provider.send("evm_increaseTime", [week]); - - const latestBlock = await ethers.provider.getBlock("latest"); - const currentTime = BigNumber.from(latestBlock.timestamp); - - const timeElapsed = currentTime.sub(creationTime); - const expected = initialSupply.add(timeElapsed.mul(rate)); - - expect(await token.availableSupply()).to.equal(expected); - }); - }); -}); diff --git a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts b/test/fork/unitary/ERC20CRV/inflationDelay.test.ts deleted file mode 100644 index f78ad71b..00000000 --- a/test/fork/unitary/ERC20CRV/inflationDelay.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../Helper"; -import Constants from "../../Constants"; - -describe("ERC20CRV", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let snapshot: SnapshotRestorer; - - const YEAR = Constants.YEAR; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ token } = await deployContracts()); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("ERC20CRV InflationDelay", function () { - it("test_rate", async function () { - expect(await token.rate()).to.equal(0); - - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); - - await token.updateMiningParameters(); - - expect(await token.rate()).to.be.gt(0); - }); - - it("test_start_epoch_time", async function () { - const creationTime = await token.startEpochTime(); - // const now = BigNumber.from((await ethers.provider.getBlock("latest")).timestamp); - // expect(creationTime).to.equal(now.add("86392").sub(YEAR)); - - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); - - await token.updateMiningParameters(); - - expect(await token.startEpochTime()).to.equal(creationTime.add(YEAR)); - }); - - it("test_mining_epoch", async function () { - expect(await token.miningEpoch()).to.equal(-1); - - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); - - await token.updateMiningParameters(); - - expect(await token.miningEpoch()).to.equal(0); - }); - - it("test_available_supply", async function () { - expect(await token.availableSupply()).to.equal( - ethers.utils.parseEther("1303030303") - ); - - await ethers.provider.send("evm_increaseTime", [86401]); - await ethers.provider.send("evm_mine", []); - - await token.updateMiningParameters(); - - expect(await token.availableSupply()).to.be.gt( - ethers.utils.parseEther("1303030303") - ); - }); - }); -}); diff --git a/test/fork/unitary/ERC20CRV/mint.test.ts b/test/fork/unitary/ERC20CRV/mint.test.ts deleted file mode 100644 index e9027d20..00000000 --- a/test/fork/unitary/ERC20CRV/mint.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import Constants from "../../Constants"; - -describe("ERC20CRV", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let snapshot: SnapshotRestorer; - - const week = Constants.week; - const ZERO_ADDRESS = Constants.ZERO_ADDRESS; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("CRV"); - token = await Token.deploy(); - await ethers.provider.send("evm_increaseTime", [86401]); - await token.updateMiningParameters(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - describe("ERC20CRV Mint", function () { - it("test_available_supply", async function () { - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); - - await ethers.provider.send("evm_increaseTime", [week]); - await ethers.provider.send("evm_mine", []); - - const currentBlock = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - // const currentBlock = await ethers.provider.getBlock('latest'); - const expected = initialSupply.add( - currentBlock.sub(creationTime).mul(rate) - ); - expect(await token.availableSupply()).to.equal(expected); - }); - - it("test_mint", async function () { - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const initialSupply = await token.totalSupply(); - const rate = await token.rate(); - - await ethers.provider.send("evm_increaseTime", [week]); - - const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const amount = currentTime.sub(creationTime).mul(rate); - await token.mint(accounts[1].address, amount); - - expect(await token.balanceOf(accounts[1].address)).to.equal(amount); - expect(await token.totalSupply()).to.equal(initialSupply.add(amount)); - }); - - it("test_overmint", async function () { - await token.setMinter(accounts[0].address); - const creationTime = await token.startEpochTime(); - const rate = await token.rate(); - - await ethers.provider.send("evm_increaseTime", [week]); - await ethers.provider.send("evm_mine", []); - - const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const amount = currentTime.sub(creationTime).add(2).mul(rate); - await expect(token.mint(accounts[1].address, amount)).to.be.revertedWith( - "dev: exceeds allowable mint amount" - ); - }); - - it("test_minter_only", async function () { - await token.setMinter(accounts[0].address); - await expect( - token.connect(accounts[1]).mint(accounts[1].address, 0) - ).to.be.revertedWith("dev: minter only"); - }); - - it("test_zero_address", async function () { - await token.setMinter(accounts[0].address); - await expect(token.mint(ZERO_ADDRESS, 0)).to.be.revertedWith( - "dev: zero address" - ); - }); - }); -}); diff --git a/test/fork/unitary/ERC20CRV/setters.test.ts b/test/fork/unitary/ERC20CRV/setters.test.ts deleted file mode 100644 index fbe98e49..00000000 --- a/test/fork/unitary/ERC20CRV/setters.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -describe("ERC20CRV", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - const Token = await ethers.getContractFactory("CRV"); - token = await Token.deploy(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("ERC20CRV Setters", function () { - it("should revert when non-admin tries to set minter", async function () { - await expect( - token.connect(accounts[1]).setMinter(accounts[2].address) - ).to.be.revertedWith("dev: admin only"); - }); - - it("should revert when non-admin tries to set admin", async function () { - await expect( - token.connect(accounts[1]).setAdmin(accounts[2].address) - ).to.be.revertedWith("dev: admin only"); - }); - - it("should allow admin to set minter", async function () { - await token.connect(accounts[0]).setMinter(accounts[1].address); - expect(await token.minter()).to.equal(accounts[1].address); - }); - - it("should allow admin to set admin", async function () { - await token.connect(accounts[0]).setAdmin(accounts[1].address); - expect(await token.admin()).to.equal(accounts[1].address); - }); - }); -}); diff --git a/test/fork/unitary/FeeDistributor/checkpoints.test.ts b/test/fork/unitary/FeeDistributor/checkpoints.test.ts deleted file mode 100644 index d499b0f7..00000000 --- a/test/fork/unitary/FeeDistributor/checkpoints.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const DAY = 86400; -const WEEK = DAY * 7; -const YEAR = DAY * 365; - -describe("FeeDistributor", () => { - let alice: SignerWithAddress, - bob: SignerWithAddress, - charlie: SignerWithAddress; - - let distributor: Contract; - let votingEscrow: Contract; - let token: Contract; - let coinA: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - [alice, bob, charlie] = await ethers.getSigners(); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - const CRV = await ethers.getContractFactory("CRV"); - const Token = await ethers.getContractFactory("MockToken"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - coinA = await Token.deploy("Coin A", "USDA", 18); - await coinA.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - distributor = await Distributor.deploy( - votingEscrow.address, - await time.latest(), - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - // TODO - // 下記の観点のテストを追加 - // https://discord.com/channels/729808684359876718/729812922649542758/1162241240360816730 - - describe("test_checkpoints", () => { - beforeEach(async function () { - await token.approve(votingEscrow.address, ethers.constants.MaxUint256); - await votingEscrow.createLock( - ethers.utils.parseEther("1000"), - (await time.latest()) + WEEK * 52 - ); - }); - it("test_checkpoint_total_supply", async function () { - const startTime = await distributor.timeCursor(); - const weekEpoch = - Math.floor(((await time.latest()) + WEEK) / WEEK) * WEEK; - - await time.increaseTo(weekEpoch); - - const weekBlock = await time.latestBlock(); - - await distributor.checkpointTotalSupply(); - - expect(await distributor.veSupply(startTime)).to.equal(0); - expect(await distributor.veSupply(weekEpoch)).to.equal( - await votingEscrow.totalSupplyAt(weekBlock) - ); - }); - - it("test_advance_time_cursor", async function () { - const startTime = (await distributor.timeCursor()).toNumber(); - await time.increase(YEAR); - await distributor.checkpointTotalSupply(); - const newTimeCursor = (await distributor.timeCursor()).toNumber(); - expect(newTimeCursor).to.equal(startTime + WEEK * 20); - expect(await distributor.veSupply(startTime + WEEK * 19)).to.be.above(0); - expect(await distributor.veSupply(startTime + WEEK * 20)).to.equal(0); - - await distributor.checkpointTotalSupply(); - - expect(await distributor.timeCursor()).to.equal(startTime + WEEK * 40); - expect(await distributor.veSupply(startTime + WEEK * 20)).to.be.above(0); - expect(await distributor.veSupply(startTime + WEEK * 39)).to.be.above(0); - expect(await distributor.veSupply(startTime + WEEK * 40)).to.equal(0); - }); - - it("test_claim_checkpoints_total_supply", async function () { - const start_time = (await distributor.timeCursor()).toNumber(); - - await distributor.connect(alice)["claim()"](); - - expect((await distributor.timeCursor()).toNumber()).to.equal( - start_time + WEEK - ); - }); - - it("test_toggle_allow_checkpoint", async function () { - const lastTokenTime = (await distributor.lastTokenTime()).toNumber(); - - await time.increase(WEEK); - - await distributor.connect(alice)["claim()"](); - expect((await distributor.lastTokenTime()).toNumber()).to.equal( - lastTokenTime - ); - - await distributor.toggleAllowCheckpointToken(); - const tx = await distributor.connect(alice)["claim()"](); - const receipt = await tx.wait(); - const block = await ethers.provider.getBlock(receipt.blockNumber); - - expect((await distributor.lastTokenTime()).toNumber()).to.equal( - block.timestamp - ); - }); - }); -}); diff --git a/test/fork/unitary/FeeDistributor/claimMany.test.ts b/test/fork/unitary/FeeDistributor/claimMany.test.ts deleted file mode 100644 index 0f8616ca..00000000 --- a/test/fork/unitary/FeeDistributor/claimMany.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const DAY = 86400; -const WEEK = DAY * 7; - -describe("FeeDistributor", () => { - let snapshot: SnapshotRestorer; - let alice: SignerWithAddress, - bob: SignerWithAddress, - charlie: SignerWithAddress; - - let distributor: Contract; - let votingEscrow: Contract; - let token: Contract; - let coinA: Contract; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - [alice, bob, charlie] = await ethers.getSigners(); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - const CRV = await ethers.getContractFactory("CRV"); - const Token = await ethers.getContractFactory("MockToken"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - coinA = await Token.deploy("Coin A", "USDA", 18); - await coinA.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - distributor = await Distributor.deploy( - votingEscrow.address, - await time.latest(), - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("test_claim_many", () => { - const amount = ethers.utils.parseEther("1000"); - it("test_claim_many", async function () { - for (let acct of [alice, bob, charlie]) { - await token.connect(acct).approve(votingEscrow.address, amount.mul(10)); - await token.connect(alice).transfer(acct.address, amount); - await votingEscrow - .connect(acct) - .createLock(amount, (await time.latest()) + 8 * WEEK); - } - await time.increase(WEEK); - let startTime = await time.latest(); - await time.increase(WEEK * 5); - const Distributor = await ethers.getContractFactory("FeeDistributor"); - distributor = await Distributor.deploy( - votingEscrow.address, - startTime, - coinA.address, - alice.address, - alice.address - ); - await coinA._mintForTesting( - distributor.address, - ethers.utils.parseEther("10") - ); - await distributor.checkpointToken(); - await time.increase(WEEK); - await distributor.checkpointToken(); - - const snapshot = await takeSnapshot(); - - await distributor - .connect(alice) - .claimMany( - [alice.address, bob.address, charlie.address].concat( - Array(17).fill(ethers.constants.AddressZero) - ) - ); - let balances = [ - await coinA.balanceOf(alice.address), - await coinA.balanceOf(bob.address), - await coinA.balanceOf(charlie.address), - ]; - - await snapshot.restore(); - - await distributor.connect(alice)["claim()"](); - await distributor.connect(bob)["claim()"](); - await distributor.connect(charlie)["claim()"](); - expect(balances).to.deep.equal([ - await coinA.balanceOf(alice.address), - await coinA.balanceOf(bob.address), - await coinA.balanceOf(charlie.address), - ]); - }); - it("test_claim_many_same_account", async function () { - for (let acct of [alice, bob, charlie]) { - await token.connect(acct).approve(votingEscrow.address, amount.mul(10)); - await token.connect(alice).transfer(acct.address, amount); - await votingEscrow - .connect(acct) - .createLock(amount, (await time.latest()) + 8 * WEEK); - } - await time.increase(WEEK); - let startTime = await time.latest(); - await time.increase(WEEK * 5); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - distributor = await Distributor.deploy( - votingEscrow.address, - startTime, - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - - await coinA._mintForTesting( - distributor.address, - ethers.utils.parseEther("10") - ); - await distributor.checkpointToken(); - await time.increase(WEEK); - await distributor.checkpointToken(); - - const expected = await distributor.connect(alice).callStatic["claim()"](); - - expect(expected).to.above(0); - - const balanceBefore = await coinA.balanceOf(alice.address); - await distributor.connect(alice).claimMany(Array(20).fill(alice.address)); - const balanceAfter = await coinA.balanceOf(alice.address); - expect(balanceAfter.sub(balanceBefore)).to.be.eq(expected); - }); - }); -}); diff --git a/test/fork/unitary/FeeDistributor/feeDistribution.test.ts b/test/fork/unitary/FeeDistributor/feeDistribution.test.ts deleted file mode 100644 index 5499e9fc..00000000 --- a/test/fork/unitary/FeeDistributor/feeDistribution.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -const DAY = 86400; -const WEEK = DAY * 7; - -describe("FeeDistributor", () => { - let alice: SignerWithAddress, - bob: SignerWithAddress, - charlie: SignerWithAddress; - let distributor: Contract; - let votingEscrow: Contract; - let token: Contract; - let coinA: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - [alice, bob, charlie] = await ethers.getSigners(); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - const CRV = await ethers.getContractFactory("CRV"); - const Token = await ethers.getContractFactory("MockToken"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - coinA = await Token.deploy("Coin A", "USDA", 18); - await coinA.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - distributor = await Distributor.deploy( - votingEscrow.address, - await time.latest(), - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - }); - afterEach(async () => { - await snapshot.restore(); - }); - - describe("test_fee_distribution", () => { - it("test_deposited_after", async function () { - const amount = ethers.utils.parseEther("1000"); - await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); - await coinA._mintForTesting(bob.address, ethers.utils.parseEther("100")); - - for (let i = 0; i < 5; i++) { - for (let j = 0; j < 7; j++) { - await coinA - .connect(bob) - .transfer(distributor.address, ethers.utils.parseEther("1")); - await distributor.checkpointToken(); - await distributor.checkpointTotalSupply(); - await time.increase(DAY); - } - } - - await time.increase(WEEK); - await votingEscrow - .connect(alice) - .createLock(amount, (await time.latest()) + 3 * WEEK); - await time.increase(2 * WEEK); - - await distributor.connect(alice)["claim()"](); - - const balanceBefore = await coinA.balanceOf(alice.address); - await distributor.connect(alice)["claim()"](); - const balanceAfter = await coinA.balanceOf(alice.address); - expect(balanceAfter.sub(balanceBefore)).to.be.eq(0); - }); - - it("test_deposited_during", async function () { - const amount = ethers.utils.parseEther("1000"); - await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); - await coinA._mintForTesting(bob.address, ethers.utils.parseEther("100")); - - await time.increase(WEEK); - await votingEscrow - .connect(alice) - .createLock(amount, (await time.latest()) + 8 * WEEK); - await time.increase(WEEK); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - distributor = await Distributor.deploy( - votingEscrow.address, - await time.latest(), - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - - for (let i = 0; i < 3; i++) { - for (let j = 0; j < 7; j++) { - await coinA - .connect(bob) - .transfer(distributor.address, ethers.utils.parseEther("1")); - await distributor.checkpointToken(); - await distributor.checkpointTotalSupply(); - await time.increase(DAY); - } - } - - await time.increase(WEEK); - await distributor.checkpointToken(); - await distributor.connect(alice)["claim()"](); - - const balanceAlice = await coinA.balanceOf(alice.address); - const diff = Math.abs( - balanceAlice.sub(ethers.utils.parseEther("21")).toNumber() - ); - expect(diff).to.be.lessThan(10); - }); - - it("test_deposited_before", async function () { - const [alice, bob] = await ethers.getSigners(); - const amount = ethers.utils.parseEther("1000"); - - await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); - await coinA - .connect(bob) - ._mintForTesting(bob.address, ethers.utils.parseEther("100")); - - await votingEscrow - .connect(alice) - .createLock(amount, (await time.latest()) + 8 * WEEK); - await time.increase(WEEK); - const startTime = await time.latest(); - await time.increase(WEEK * 5); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - distributor = await Distributor.deploy( - votingEscrow.address, - startTime, - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - - await coinA - .connect(bob) - .transfer(distributor.address, ethers.utils.parseEther("10")); - await distributor.checkpointToken(); - await time.increase(WEEK); - await distributor.checkpointToken(); - await distributor.connect(alice)["claim()"](); - - const balanceAlice = await coinA.balanceOf(alice.address); - expect( - Math.abs(balanceAlice.sub(ethers.utils.parseEther("10")).toNumber()) - ).to.be.lessThan(10); - }); - - it("test_deposited_twice", async function () { - const amount = ethers.utils.parseEther("1000"); - - await token.approve(votingEscrow.address, amount.mul(10)); - await coinA._mintForTesting(bob.address, ethers.utils.parseEther("100")); - - const currentTimestamp = await time.latest(); - await votingEscrow.createLock(amount, currentTimestamp + 4 * WEEK); - - await time.increase(WEEK); - - const startTime = await time.latest(); - - await time.increase(3 * WEEK); - - await votingEscrow.connect(alice).withdraw(); - const excludeTime = Math.floor((await time.latest()) / WEEK) * WEEK; - await votingEscrow - .connect(alice) - .createLock(amount, (await time.latest()) + 4 * WEEK); - - await time.increase(2 * WEEK); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - distributor = await Distributor.deploy( - votingEscrow.address, - startTime, - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - - await coinA - .connect(bob) - .transfer(distributor.address, ethers.utils.parseEther("10")); - await distributor.checkpointToken(); - - await time.increase(WEEK); - - await distributor.checkpointToken(); - - await distributor.connect(alice)["claim()"](); - - const tokensToExclude = await distributor.tokensPerWeek(excludeTime); - - expect( - ethers.utils - .parseEther("10") - .sub(await coinA.balanceOf(alice.address)) - .sub(tokensToExclude) - ).to.be.lt(10); - }); - - it("test_deposited_parallel", async function () { - const amount = ethers.utils.parseEther("1000"); - - await token.connect(alice).approve(votingEscrow.address, amount.mul(10)); - await token.connect(bob).approve(votingEscrow.address, amount.mul(10)); - await token.connect(alice).transfer(bob.address, amount); - await coinA._mintForTesting( - charlie.address, - ethers.utils.parseEther("100") - ); - - const currentTimestamp = await time.latest(); - await votingEscrow - .connect(alice) - .createLock(amount, currentTimestamp + 8 * WEEK); - await votingEscrow - .connect(bob) - .createLock(amount, currentTimestamp + 8 * WEEK); - - await time.increase(WEEK); - - const startTime = await time.latest(); - - await time.increase(5 * WEEK); - - const Distributor = await ethers.getContractFactory("FeeDistributor"); - distributor = await Distributor.deploy( - votingEscrow.address, - startTime, - coinA.address, - alice.address, - alice.address - ); - await distributor.deployed(); - - await coinA - .connect(charlie) - .transfer(distributor.address, ethers.utils.parseEther("10")); - await distributor.checkpointToken(); - - await time.increase(WEEK); - - await distributor.checkpointToken(); - - await distributor.connect(alice)["claim()"](); - await distributor.connect(bob)["claim()"](); - - const balanceAlice = await coinA.balanceOf(alice.address); - const balanceBob = await coinA.balanceOf(bob.address); - - expect(balanceAlice).to.equal(balanceBob); - expect(balanceAlice.add(balanceBob)).to.be.closeTo( - ethers.utils.parseEther("10"), - 20 - ); - }); - }); -}); diff --git a/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts b/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts deleted file mode 100644 index e5ce78c2..00000000 --- a/test/fork/unitary/FeeDistributor/killFeeDistro.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { Contract } from "ethers"; -import { - time, - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -describe("FeeDistributor", () => { - let alice: SignerWithAddress, - bob: SignerWithAddress, - charlie: SignerWithAddress; - - let distributor: Contract; - let votingEscrow: Contract; - let token: Contract; - let coinA: Contract; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - [alice, bob, charlie] = await ethers.getSigners(); - - const CRV = await ethers.getContractFactory("CRV"); - const Token = await ethers.getContractFactory("MockToken"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - coinA = await Token.deploy("Coin A", "USDA", 18); - await coinA.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - }); - afterEach(async () => { - await snapshot.restore(); - }); - - describe("test_kill_fee_distro", () => { - let accounts: SignerWithAddress[]; - beforeEach(async () => { - const Distributor = await ethers.getContractFactory("FeeDistributor"); - distributor = await Distributor.deploy( - votingEscrow.address, - await time.latest(), - coinA.address, - alice.address, - bob.address - ); - await distributor.deployed(); - accounts = await ethers.getSigners(); - }); - - it("test_assumptions", async function () { - expect(await distributor.isKilled()).to.be.false; - expect(await distributor.emergencyReturn()).to.equal(bob.address); - }); - - it("test_kill", async function () { - await distributor.connect(alice).killMe(); - expect(await distributor.isKilled()).to.be.true; - }); - - it("test_multi_kill", async function () { - await distributor.connect(alice).killMe(); - await distributor.connect(alice).killMe(); - expect(await distributor.isKilled()).to.be.true; - }); - - it("test_killing_transfers_tokens", async function () { - await coinA._mintForTesting(distributor.address, 31337); - await distributor.connect(alice).killMe(); - - expect(await distributor.emergencyReturn()).to.equal(bob.address); - expect(await coinA.balanceOf(bob.address)).to.equal(31337); - }); - - it("test_multi_kill_token_transfer", async function () { - await coinA._mintForTesting(distributor.address, 10000); - await distributor.connect(alice).killMe(); - - await coinA._mintForTesting(distributor.address, 30000); - await distributor.connect(alice).killMe(); - - expect(await distributor.emergencyReturn()).to.equal(bob.address); - expect(await coinA.balanceOf(bob.address)).to.equal(40000); - }); - - for (let idx = 1; idx <= 2; idx++) { - it(`test_only_admin_for_account_index_${idx}`, async function () { - await expect(distributor.connect(accounts[idx]).killMe()).to.be - .reverted; - }); - - it(`test_cannot_claim_after_killed_for_account_index_${idx}`, async function () { - await distributor.connect(alice).killMe(); - await expect(distributor.connect(accounts[idx])["claim()"]()).to.be - .reverted; - }); - - it(`test_cannot_claim_for_after_killed_for_account_index_${idx}`, async function () { - await distributor.connect(alice).killMe(); - await expect( - distributor.connect(accounts[idx])["claim(address)"](alice.address) - ).to.be.reverted; - }); - - it(`test_cannot_claim_many_after_killed_for_account_index_${idx}`, async function () { - await distributor.connect(alice).killMe(); - await expect( - distributor - .connect(accounts[idx]) - .claimMany(new Array(20).fill(alice.address)) - ).to.be.reverted; - }); - } - }); -}); diff --git a/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts b/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts deleted file mode 100644 index 4e8f63f5..00000000 --- a/test/fork/unitary/GaugeController/gaugecontrollerAdmin.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let gaugeController: Contract; - let snapshot: SnapshotRestorer; - - const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gaugeController } = await deployContracts()); - await gaugeController.addType("none", TYPE_WEIGHTS[0]); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("GaugeController GaugecontrollerAdmin", function () { - it("test_commit_admin_only", async function () { - await expect( - gaugeController - .connect(accounts[1]) - .commitTransferOwnership(accounts[1].address) - ).to.be.revertedWith("admin only"); - }); - - it("test_apply_admin_only", async function () { - await expect( - gaugeController.connect(accounts[1]).applyTransferOwnership() - ).to.be.revertedWith("admin only"); - }); - - it("test_commit_transfer_ownership", async function () { - await gaugeController.commitTransferOwnership(accounts[1].address); - - expect(await gaugeController.admin()).to.equal( - await accounts[0].getAddress() - ); - expect(await gaugeController.futureAdmin()).to.equal(accounts[1].address); - }); - - it("test_apply_transfer_ownership", async function () { - await gaugeController.commitTransferOwnership(accounts[1].address); - await gaugeController.applyTransferOwnership(); - - expect(await gaugeController.admin()).to.equal(accounts[1].address); - }); - - it("test_apply_without_commit", async function () { - await expect(gaugeController.applyTransferOwnership()).to.be.revertedWith( - "admin not set" - ); - }); - }); -}); diff --git a/test/fork/unitary/GaugeController/gaugesWeights.test.ts b/test/fork/unitary/GaugeController/gaugesWeights.test.ts deleted file mode 100644 index 78fb97e3..00000000 --- a/test/fork/unitary/GaugeController/gaugesWeights.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let gaugeController: Contract; - let threeGauges: String[]; - let snapshot: SnapshotRestorer; - - const WEEK = Constants.WEEK; - const YEAR = Constants.YEAR; - const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - const GAUGE_WEIGHTS = Constants.GAUGE_WEIGHTS; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gaugeController, threeGauges } = await deployContracts()); - await gaugeController.addType("none", TYPE_WEIGHTS[0]); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - describe("GaugeController GaugesWeights", function () { - it("test_add_gauges", async function () { - await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - - expect(await gaugeController.gauges(0)).to.equal(threeGauges[0]); - expect(await gaugeController.gauges(1)).to.equal(threeGauges[1]); - }); - - it("test_n_gauges", async function () { - expect(await gaugeController.nGauges()).to.equal(0); - - await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - - expect(await gaugeController.nGauges()).to.equal(2); - }); - - it("test_n_gauges_same_gauge", async function () { - await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - await expect( - gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]) - ).to.be.revertedWith("cannot add the same gauge twice"); - }); - - it("test_n_gauge_types", async function () { - expect(await gaugeController.nGaugeTypes()).to.equal(1); - - await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); - - expect(await gaugeController.nGaugeTypes()).to.equal(2); - }); - - it("test_gauge_types", async function () { - await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); - await gaugeController.addGauge(threeGauges[0], 1, GAUGE_WEIGHTS[0]); - await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - - expect(await gaugeController.gaugeTypes(threeGauges[0])).to.equal(1); - expect(await gaugeController.gaugeTypes(threeGauges[1])).to.equal(0); - }); - - it("test_gauge_weight", async function () { - await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - - expect(await gaugeController.getGaugeWeight(threeGauges[0])).to.equal( - GAUGE_WEIGHTS[0] - ); - }); - - it("test_gauge_weight_as_zero", async function () { - await gaugeController.addGauge(threeGauges[0], 0, 0); - - expect(await gaugeController.getGaugeWeight(threeGauges[0])).to.equal(0); - }); - - it("test_set_gauge_weight", async function () { - await gaugeController.addGauge(threeGauges[0], 0, 0); - await gaugeController.changeGaugeWeight(threeGauges[0], GAUGE_WEIGHTS[0]); - await ethers.provider.send("evm_increaseTime", [WEEK.toNumber()]); - - expect(await gaugeController.getGaugeWeight(threeGauges[0])).to.equal( - GAUGE_WEIGHTS[0] - ); - }); - - it("test_type_weight", async function () { - await gaugeController.addType("Insurance", 0); - - expect(await gaugeController.getTypeWeight(0)).to.equal(TYPE_WEIGHTS[0]); - expect(await gaugeController.getTypeWeight(1)).to.equal(0); - }); - - it("test_change_type_weight", async function () { - await gaugeController.addType("Insurance", TYPE_WEIGHTS[0]); - await gaugeController.changeTypeWeight(0, TYPE_WEIGHTS[1]); - - expect(await gaugeController.getTypeWeight(0)).to.equal(TYPE_WEIGHTS[1]); - }); - - it("test_relative_weight_write", async function () { - await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); - await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - await gaugeController.addGauge(threeGauges[2], 1, GAUGE_WEIGHTS[2]); - await ethers.provider.send("evm_increaseTime", [YEAR.toNumber()]); - - const expectedWeight = TYPE_WEIGHTS[0] - .mul(GAUGE_WEIGHTS[0]) - .add(TYPE_WEIGHTS[0].mul(GAUGE_WEIGHTS[1])) - .add(TYPE_WEIGHTS[1].mul(GAUGE_WEIGHTS[2])); - - for (let i = 0; i < threeGauges.length; i++) { - const relativeWeight = await gaugeController.gaugeRelativeWeight( - threeGauges[i], - 0 - ); - expect(relativeWeight).to.equal( - GAUGE_WEIGHTS[i] - .mul(TYPE_WEIGHTS[Math.floor(i / 2)]) - .div(expectedWeight) - ); - } - }); - }); -}); diff --git a/test/fork/unitary/GaugeController/timestamps.test.ts b/test/fork/unitary/GaugeController/timestamps.test.ts deleted file mode 100644 index d57f4206..00000000 --- a/test/fork/unitary/GaugeController/timestamps.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let gaugeController: Contract; - - let snapshot: SnapshotRestorer; - - const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - const week = Constants.week; - const year = Constants.year; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gaugeController } = await deployContracts()); - await gaugeController.addType("none", TYPE_WEIGHTS[0]); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("GaugeController Timestamps", function () { - it("test_timestamps", async function () { - const currentTime = (await ethers.provider.getBlock("latest")).timestamp; - const expectedTime = Math.floor((currentTime + week) / week) * week; - expect(await gaugeController.timeTotal()).to.equal(expectedTime); - - for (let i = 0; i < 5; i++) { - await ethers.provider.send("evm_increaseTime", [ - Math.floor(1.1 * year), - ]); - - await gaugeController.checkpoint(); - - const newCurrentTime = (await ethers.provider.getBlock("latest")) - .timestamp; - const newExpectedTime = - Math.floor((newCurrentTime + week) / week) * week; - expect(await gaugeController.timeTotal()).to.equal(newExpectedTime); - } - }); - }); -}); diff --git a/test/fork/unitary/GaugeController/totalWeight.test.ts b/test/fork/unitary/GaugeController/totalWeight.test.ts deleted file mode 100644 index 0b55cbb9..00000000 --- a/test/fork/unitary/GaugeController/totalWeight.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let gaugeController: Contract; - let threeGauges: string[]; - let snapshot: SnapshotRestorer; - - const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - const GAUGE_WEIGHTS = Constants.GAUGE_WEIGHTS; - const ten_to_the_18 = Constants.ten_to_the_18; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gaugeController, threeGauges } = await deployContracts()); - await gaugeController.addType("none", TYPE_WEIGHTS[0]); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("GaugeController TotalWeight", function () { - it("test_total_weight", async function () { - await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - - expect(await gaugeController.getTotalWeight()).to.equal( - GAUGE_WEIGHTS[0].mul(TYPE_WEIGHTS[0]) - ); - }); - - it("test_change_type_weight", async function () { - await gaugeController.addGauge(threeGauges[0], 0, ten_to_the_18); - await gaugeController.changeTypeWeight(0, 31337); - - expect(await gaugeController.getTotalWeight()).to.equal( - ten_to_the_18.mul(31337) - ); - }); - - it("test_change_gauge_weight", async function () { - await gaugeController.addGauge(threeGauges[0], 0, ten_to_the_18); - await gaugeController.changeGaugeWeight(threeGauges[0], 31337); - - expect(await gaugeController.getTotalWeight()).to.equal( - TYPE_WEIGHTS[0].mul(31337) - ); - }); - - it("test_multiple", async function () { - await gaugeController.addType("Insurance", TYPE_WEIGHTS[1]); - await gaugeController.addGauge(threeGauges[0], 0, GAUGE_WEIGHTS[0]); - await gaugeController.addGauge(threeGauges[1], 0, GAUGE_WEIGHTS[1]); - await gaugeController.addGauge(threeGauges[2], 1, GAUGE_WEIGHTS[2]); - - const expectedTotalWeight = GAUGE_WEIGHTS[0] - .mul(TYPE_WEIGHTS[0]) - .add(GAUGE_WEIGHTS[1].mul(TYPE_WEIGHTS[0])) - .add(GAUGE_WEIGHTS[2].mul(TYPE_WEIGHTS[1])); - - expect(await gaugeController.getTotalWeight()).to.equal( - expectedTotalWeight - ); - }); - }); -}); diff --git a/test/fork/unitary/GaugeController/vote.test.ts b/test/fork/unitary/GaugeController/vote.test.ts deleted file mode 100644 index c6d004e8..00000000 --- a/test/fork/unitary/GaugeController/vote.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../Helper"; -import Constants from "../../Constants"; - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let gaugeController: Contract; - let threeGauges: String[]; - let votingEscrow: Contract; - let token: Contract; - - let snapshot: SnapshotRestorer; - - const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - const ten_to_the_18 = Constants.ten_to_the_18; - const ten_to_the_24 = Constants.ten_to_the_24; - const day = Constants.day; - const year = Constants.year; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gaugeController, threeGauges, votingEscrow, token } = - await deployContracts()); - - await gaugeController.addType("none", TYPE_WEIGHTS[0]); - await gaugeController.addType("Insurance", ten_to_the_18); - await gaugeController.addGauge(threeGauges[0], 0, 0); - await gaugeController.addGauge(threeGauges[1], 1, 0); - - await token.approve(votingEscrow.address, ten_to_the_24); - await votingEscrow.createLock( - ten_to_the_24, - (await ethers.provider.getBlock("latest")).timestamp + year - ); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("GaugeController vote", function () { - it("test_vote", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); - expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( - 10000 - ); - }); - - it("test_vote_partial", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[1], 1234); - expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( - 1234 - ); - }); - - it("test_vote_change", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[1], 1234); - - await expect( - gaugeController.voteForGaugeWeights(threeGauges[1], 42) - ).to.be.revertedWith("Cannot vote so often"); - - await ethers.provider.send("evm_increaseTime", [day * 10]); - await gaugeController.voteForGaugeWeights(threeGauges[1], 42); - - expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( - 42 - ); - }); - - it("test_vote_remove", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[1], 10000); - - await expect( - gaugeController.voteForGaugeWeights(threeGauges[1], 0) - ).to.be.revertedWith("Cannot vote so often"); - - await ethers.provider.send("evm_increaseTime", [day * 10]); - - await gaugeController.voteForGaugeWeights(threeGauges[1], 0); - - expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( - 0 - ); - }); - - it("test_vote_multiple", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[0], 4000); - await gaugeController.voteForGaugeWeights(threeGauges[1], 6000); - - expect(await gaugeController.voteUserPower(accounts[0].address)).to.equal( - 10000 - ); - }); - - it("test_vote_no_balance", async function () { - await expect( - gaugeController - .connect(accounts[1]) - .voteForGaugeWeights(threeGauges[0], 10000) - ).to.be.revertedWith("Your token lock expires too soon"); - }); - - it("test_vote_expired", async function () { - await ethers.provider.send("evm_increaseTime", [year * 2]); - - await expect( - gaugeController.voteForGaugeWeights(threeGauges[0], 10000) - ).to.be.revertedWith("Your token lock expires too soon"); - }); - - it("test_invalid_gauge_id", async function () { - await expect( - gaugeController.voteForGaugeWeights(threeGauges[2], 10000) - ).to.be.revertedWith("Gauge not added"); - }); - - it("test_over_user_weight", async function () { - await expect( - gaugeController.voteForGaugeWeights(threeGauges[0], 10001) - ).to.be.revertedWith("You used all your voting power"); - }); - - it("test_over_weight_multiple", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[0], 8000); - - await expect( - gaugeController.voteForGaugeWeights(threeGauges[1], 4000) - ).to.be.revertedWith("Used too much power"); - }); - - it("test_over_weight_adjust_existing", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[0], 6000); - await gaugeController.voteForGaugeWeights(threeGauges[1], 3000); - - await ethers.provider.send("evm_increaseTime", [day * 10]); - - await expect( - gaugeController.voteForGaugeWeights(threeGauges[0], 8000) - ).to.be.revertedWith("Used too much power"); - }); - }); -}); diff --git a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts b/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts deleted file mode 100644 index af433400..00000000 --- a/test/fork/unitary/GaugeController/voteWeightUnitary.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; - -describe("GaugeController", function () { - let accounts: SignerWithAddress[]; - let gaugeController: Contract; - let threeGauges: String[]; - let votingEscrow: Contract; - let token: Contract; - - let snapshot: SnapshotRestorer; - - const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - const ten_to_the_18 = Constants.ten_to_the_18; - const ten_to_the_24 = Constants.ten_to_the_24; - const year = Constants.year; - const day = Constants.day; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gaugeController, threeGauges, votingEscrow, token } = - await deployContracts()); - - await gaugeController.addType("none", TYPE_WEIGHTS[0]); - await gaugeController.addType("Insurance", ten_to_the_18); - await gaugeController.addGauge(threeGauges[0], 0, 0); - await gaugeController.addGauge(threeGauges[1], 1, 0); - - await token.approve(votingEscrow.address, ten_to_the_24); - await votingEscrow.createLock( - ten_to_the_24, - (await ethers.provider.getBlock("latest")).timestamp + year - ); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - describe("GaugeController voteWeightUnitary", function () { - it("test_no_immediate_effect_on_weight", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); - const weight = await gaugeController.gaugeRelativeWeight( - threeGauges[0], - 0 - ); - expect(weight).to.equal(0); - }); - - it("test_effect_on_following_period", async () => { - await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); - - await ethers.provider.send("evm_increaseTime", [day * 7]); - - await gaugeController.checkpointGauge(threeGauges[0]); - expect( - await gaugeController.gaugeRelativeWeight( - threeGauges[0], - ( - await ethers.provider.getBlock("latest") - ).timestamp - ) - ).to.equal(ten_to_the_18); - }); - - it("test_remove_vote_no_immediate_effect", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); - - await ethers.provider.send("evm_increaseTime", [day * 10]); - - await gaugeController.checkpointGauge(threeGauges[0]); - await gaugeController.voteForGaugeWeights(threeGauges[0], 0); - - expect( - await gaugeController.gaugeRelativeWeight(threeGauges[0], 0) - ).to.equal(ten_to_the_18); - }); - - it("test_remove_vote_means_no_weight", async function () { - await gaugeController.voteForGaugeWeights(threeGauges[0], 10000); - - await ethers.provider.send("evm_increaseTime", [day * 10]); - await ethers.provider.send("evm_mine", []); - - await gaugeController.checkpointGauge(threeGauges[0]); - expect( - await gaugeController.gaugeRelativeWeight(threeGauges[0], 0) - ).to.equal(ethers.utils.parseUnits("1", 18)); - - await gaugeController.voteForGaugeWeights(threeGauges[0], 0); - - await ethers.provider.send("evm_increaseTime", [day * 7]); - await ethers.provider.send("evm_mine", []); - - await gaugeController.checkpointGauge(threeGauges[0]); - expect( - await gaugeController.gaugeRelativeWeight(threeGauges[0], 0) - ).to.equal(0); - }); - }); -}); diff --git a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts b/test/fork/unitary/LiquidityGauge/checkpoint.test.ts deleted file mode 100644 index a7756157..00000000 --- a/test/fork/unitary/LiquidityGauge/checkpoint.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../Helper"; -import Constants from "../../Constants"; - -describe("LiquidityGauge checkpoint", function () { - let accounts: SignerWithAddress; - let gauges: Contract[]; - - let snapshot: SnapshotRestorer; - const year = Constants.year; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gauges } = await deployContracts()); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - it("test_user_checkpoint", async function () { - // Assuming `userCheckpoint` is a function on your contract - await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); - }); - - it("test_user_checkpoint_new_period", async function () { - await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); - - // Increase the time on the blockchain - await ethers.provider.send("evm_increaseTime", [year * 1.1]); - await ethers.provider.send("evm_mine"); // this one will actually mine a new block - - await gauges[0].connect(accounts[1]).userCheckpoint(accounts[1].address); - }); - - it("test_user_checkpoint_wrong_account", async function () { - // Expect the transaction to be reverted with the specified error message - await expect( - gauges[0].connect(accounts[1]).userCheckpoint(accounts[2].address) - ).to.be.revertedWith("dev: unauthorized"); - }); -}); diff --git a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts b/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts deleted file mode 100644 index 788607d2..00000000 --- a/test/fork/unitary/LiquidityGauge/depositWithdraw.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../Helper"; -import Constants from "../../Constants"; - -describe("LiquidityGauge depositWithdraw", function () { - let accounts: SignerWithAddress; - let mockLpToken: Contract; - let threeGauges: String[]; - let gauges: Contract[]; - - let snapshot: SnapshotRestorer; - - const ten_to_the_21 = Constants.ten_to_the_21; - - beforeEach(async () => { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ mockLpToken, threeGauges, gauges } = await deployContracts()); - await mockLpToken.approve(threeGauges[0], ten_to_the_21.mul("2")); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - it("should deposit tokens", async function () { - const balance = await mockLpToken.balanceOf(accounts[0].address); - const depositAmount = BigNumber.from(100000); - await gauges[0].deposit(100000, accounts[0].address, false); - - expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(depositAmount); - expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal( - balance.sub(depositAmount) - ); - expect(await gauges[0].totalSupply()).to.equal(depositAmount); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal( - depositAmount - ); - }); - - it("should handle zero deposit", async function () { - const balance = await mockLpToken.balanceOf(accounts[0].address); - await gauges[0].deposit(0, accounts[0].address, false); - - expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal( - BigNumber.from(0) - ); - expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance); - expect(await gauges[0].totalSupply()).to.equal(BigNumber.from(0)); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal( - BigNumber.from(0) - ); - }); - - it("should revert on deposit with insufficient balance", async function () { - await expect( - gauges[0].connect(accounts[1]).deposit(100000, accounts[0].address, false) - ).to.be.reverted; - }); - - it("should withdraw tokens", async function () { - const balance = await mockLpToken.balanceOf(accounts[0].address); - const depositAmount = BigNumber.from(100000); - - await gauges[0].deposit(depositAmount, accounts[0].address, false); - await gauges[0].withdraw(depositAmount, false); - - expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(0); - expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance); - expect(await gauges[0].totalSupply()).to.equal(0); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(0); - }); - - it("should handle zero withdrawal", async function () { - const balance = await mockLpToken.balanceOf(accounts[0].address); - const depositAmount = BigNumber.from(100000); - - await gauges[0].deposit(depositAmount, accounts[0].address, false); - await gauges[0].withdraw(0, false); - - expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(depositAmount); - expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal( - balance.sub(depositAmount) - ); - expect(await gauges[0].totalSupply()).to.equal(depositAmount); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal( - depositAmount - ); - }); - - it("should withdraw tokens after a new epoch", async function () { - const balance = await mockLpToken.balanceOf(accounts[0].address); - const depositAmount = BigNumber.from(100000); - - await gauges[0].deposit(depositAmount, accounts[0].address, false); - - await ethers.provider.send("evm_increaseTime", [86400 * 400]); - await ethers.provider.send("evm_mine"); - - await gauges[0].withdraw(depositAmount, false); - - expect(await mockLpToken.balanceOf(threeGauges[0])).to.equal(0); - expect(await mockLpToken.balanceOf(accounts[0].address)).to.equal(balance); - expect(await gauges[0].totalSupply()).to.equal(0); - expect(await gauges[0].balanceOf(accounts[0].address)).to.equal(0); - }); -}); diff --git a/test/fork/unitary/LiquidityGauge/kick.test.ts b/test/fork/unitary/LiquidityGauge/kick.test.ts deleted file mode 100644 index 955d391a..00000000 --- a/test/fork/unitary/LiquidityGauge/kick.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { Contract } from "ethers"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../Helper"; -import Constants from "../../Constants"; - -describe("LiquidityGauge kick", function () { - let accounts: SignerWithAddress[]; - let token: Contract; - let votingEscrow: Contract; - let mockLpToken: Contract; - let gauges: Contract[]; - - let snapshot: SnapshotRestorer; - - const MAX_UINT256 = Constants.MAX_UINT256; - const week = Constants.week; - const DEPOSIT_AMOUNT = Constants.ten_to_the_21; // 10^21 - const LOCK_AMOUNT = Constants.ten_to_the_20; // 10^20 - - beforeEach(async () => { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ token, votingEscrow, mockLpToken, gauges } = await deployContracts()); - await token.transfer(accounts[1].address, LOCK_AMOUNT); - await mockLpToken.transfer(accounts[1].address, DEPOSIT_AMOUNT); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - it("test kick functionality", async function () { - // Forward time by 2 weeks + 5 seconds - await ethers.provider.send("evm_increaseTime", [2 * week + 5]); - await ethers.provider.send("evm_mine"); - - // Alice approves tokens to voting escrow and creates a lock - await token.connect(accounts[1]).approve(votingEscrow.address, MAX_UINT256); - await votingEscrow - .connect(accounts[1]) - .createLock( - LOCK_AMOUNT, - (await ethers.provider.getBlock("latest")).timestamp + 4 * week - ); - - // Alice approves LP tokens to Gauge and deposits - await mockLpToken - .connect(accounts[1]) - .approve(gauges[0].address, MAX_UINT256); - await gauges[0] - .connect(accounts[1]) - .deposit(DEPOSIT_AMOUNT, accounts[1].address, false); - - // Check working balance of Alice in Gauge - expect(await gauges[0].workingBalances(accounts[1].address)).to.equal( - DEPOSIT_AMOUNT - ); - - // Forward time by 1 week - await ethers.provider.send("evm_increaseTime", [week]); - await ethers.provider.send("evm_mine"); - - // Bob tries to kick Alice but should fail because it's not allowed yet - await expect( - gauges[0].connect(accounts[1]).kick(accounts[1].address) - ).to.be.revertedWith("Not allowed"); - - // Forward time by 4 weeks - await ethers.provider.send("evm_increaseTime", [4 * week]); - await ethers.provider.send("evm_mine"); - - // Now Bob kicks Alice - await gauges[0].connect(accounts[1]).kick(accounts[1].address); - - // Check the working balance of Alice after kick - expect(await gauges[0].workingBalances(accounts[1].address)).to.equal( - LOCK_AMOUNT.mul(4) - ); - - // Trying to kick again should fail as it's not needed - await expect( - gauges[0].connect(accounts[1]).kick(accounts[1].address) - ).to.be.revertedWith("Not needed"); - }); -}); diff --git a/test/fork/unitary/Minter/Minter.test.ts b/test/fork/unitary/Minter/Minter.test.ts deleted file mode 100644 index aee0ab1d..00000000 --- a/test/fork/unitary/Minter/Minter.test.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { BigNumber, Contract } from "ethers"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { deployContracts } from "../../helper"; -import Constants from "../../Constants"; - -describe("Minter", function () { - let accounts: SignerWithAddress[]; - let minter: Contract; - let gaugeController: Contract; - let token: Contract; - let mockLpToken: Contract; - let threeGauges: String[]; - let gauges: Contract[]; - - let snapshot: SnapshotRestorer; - - const TYPE_WEIGHTS = Constants.TYPE_WEIGHTS; - const GAUGE_WEIGHTS = Constants.GAUGE_WEIGHTS; - const GAUGE_TYPES = [0, 0, 1]; - - const ten_to_the_18 = Constants.ten_to_the_18; - const ten_to_the_17 = Constants.ten_to_the_17; - const ZERO_ADDRESS = Constants.ZERO_ADDRESS; - const zero = Constants.zero; - const WEEK = Constants.WEEK; - const month = Constants.month; - const week = Constants.week; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - accounts = await ethers.getSigners(); - ({ gaugeController, minter, token, mockLpToken, threeGauges, gauges } = - await deployContracts()); - - // Set minter for the token - await token.setMinter(minter.address); - - // Skip to the start of a new epoch week - const currentWeek = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ) - .div(WEEK) - .mul(WEEK); - await ethers.provider.send("evm_setNextBlockTimestamp", [ - currentWeek.add(WEEK).toNumber(), - ]); - - // Add types and gauges - for (const weight of TYPE_WEIGHTS) { - await gaugeController.addType("Liquidity", weight); - } - for (let i = 0; i < 3; i++) { - await gaugeController.addGauge( - threeGauges[i], - GAUGE_TYPES[i], - GAUGE_WEIGHTS[i] - ); - } - for (const account of accounts) { - mockLpToken.transfer(account.address, ten_to_the_18); - } - for (let i = 1; i <= 3; i++) { - for (const gauge of threeGauges) { - await mockLpToken.connect(accounts[i]).approve(gauge, ten_to_the_18); - } - } - }); - - afterEach(async () => { - await snapshot.restore(); - }); - describe("Minter Behavior", function () { - // Test basic mint functionality - it("test_mint", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [month]); - - await minter.connect(accounts[1]).mint(threeGauges[0]); //gauge_address, msg.sender, mint - let expected = await gauges[0].integrateFraction(accounts[1].address); - - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await token.balanceOf(accounts[1].address)).to.equal(expected); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - expected - ); - }); - - // Test minting immediately after setup - it("test_mint_immediate", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_18, accounts[1].address, false); - - let t0 = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - let moment = t0.add(WEEK).div(WEEK).mul(WEEK).add("5"); - await ethers.provider.send("evm_setNextBlockTimestamp", [ - moment.toNumber(), - ]); - - //mint - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - "0" - ); - await minter.connect(accounts[1]).mint(threeGauges[0]); - - //check - let balance = await token.balanceOf(accounts[1].address); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - balance - ); - }); - - // Test multiple mint operations on the same gauge - it("test_mint_multiple_same_gauge", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_18, accounts[1].address, false); - await ethers.provider.send("evm_increaseTime", [month]); - await minter.connect(accounts[1]).mint(threeGauges[0]); - let balance = await token.balanceOf(accounts[1].address); - await ethers.provider.send("evm_increaseTime", [month]); - await minter.connect(accounts[1]).mint(threeGauges[0]); - let expected = await gauges[0].integrateFraction(accounts[1].address); - let final_balance = await token.balanceOf(accounts[1].address); - - expect(final_balance.gt(balance)).to.be.equal(true); //2nd mint success - expect(final_balance).to.equal(expected); //2nd mint works fine - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - expected - ); //tracks fine - }); - - // Test minting across multiple gauges - it("test_mint_multiple_gauges", async () => { - //setup - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - await gauges[1] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - await gauges[2] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [month]); - - //mint - for (let i = 0; i < 3; i++) { - await minter.connect(accounts[1]).mint(threeGauges[i]); - } - - //check - let total_minted = BigNumber.from("0"); - - for (let i = 0; i < 3; i++) { - let gauge = gauges[i]; - let minted = await minter.minted(accounts[1].address, gauge.address); - expect(minted).to.equal( - await gauge.integrateFraction(accounts[1].address) - ); - total_minted = total_minted.add(minted); - } - - expect(await token.balanceOf(accounts[1].address)).to.equal(total_minted); - }); - - // Test minting after withdrawing - it("test_mint_after_withdraw", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_18, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [week * 2]); - - await gauges[0].connect(accounts[1]).withdraw(ten_to_the_18, false); - await minter.connect(accounts[1]).mint(threeGauges[0]); - - expect( - (await token.balanceOf(accounts[1].address)).gt(BigNumber.from("0")) - ).to.equal(true); - }); - - // Test multiple mints after withdrawing - it("test_mint_multiple_after_withdraw", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_18, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [10]); - await gauges[0].connect(accounts[1]).withdraw(ten_to_the_18, false); - await minter.connect(accounts[1]).mint(threeGauges[0]); - - let balance = await token.balanceOf(accounts[1].address); - - await ethers.provider.send("evm_increaseTime", [10]); - await minter.connect(accounts[1]).mint(threeGauges[0]); - - expect(await token.balanceOf(accounts[1].address)).to.equal(balance); - }); - - // Test mint without any deposit - it("test_no_deposit", async () => { - await minter.connect(accounts[1]).mint(threeGauges[0]); - expect(await token.balanceOf(accounts[1].address)).to.equal(zero); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - zero - ); - }); - - // Test minting with the wrong gauge - it("test_mint_wrong_gauge", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_18, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [month]); - await minter.connect(accounts[1]).mint(threeGauges[1]); - - //check - expect(await token.balanceOf(accounts[1].address)).to.equal(zero); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - zero - ); - expect(await minter.minted(accounts[1].address, threeGauges[1])).to.equal( - zero - ); - }); - - // Test minting with an invalid gauge address - it("test_mint_not_a_gauge", async () => { - await expect(minter.mint(accounts[1].address)).to.revertedWith( - "dev: gauge is not added" - ); - }); - - // Test minting before inflation begins - it("test_mint_before_inflation_begins", async function () { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_18, accounts[1].address, false); - const startEpochTime = await token.startEpochTime(); - const currentTime = BigNumber.from( - (await ethers.provider.getBlock("latest")).timestamp - ); - const timeToSleep = startEpochTime.sub(currentTime).sub(5); - await ethers.provider.send("evm_increaseTime", [timeToSleep.toNumber()]); - - await minter.connect(accounts[1]).mint(threeGauges[0]); - - expect(await token.balanceOf(accounts[1].address)).to.equal( - BigNumber.from(0) - ); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - BigNumber.from(0) - ); - }); - - // Test mintMany function with multiple gauges - it("test_mintMany_function_multiple_gauges", async () => { - //setup - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - await gauges[1] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - await gauges[2] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [month]); - - let addresses = [ - threeGauges[0], - threeGauges[1], - threeGauges[2], - ZERO_ADDRESS, - ZERO_ADDRESS, - ZERO_ADDRESS, - ZERO_ADDRESS, - ZERO_ADDRESS, - ]; - await minter.connect(accounts[1]).mintMany(addresses); - - //check - let total_minted = BigNumber.from("0"); - - for (let i = 0; i < 3; i++) { - let gauge = gauges[i]; - let minted = await minter.minted(accounts[1].address, gauge.address); - expect(minted).to.equal( - await gauge.integrateFraction(accounts[1].address) - ); - total_minted = total_minted.add(minted); - } - - expect(await token.balanceOf(accounts[1].address)).to.equal(total_minted); - }); - - // Test toggling of the mint approval function - it("test_toggleApproveMint_function", async () => { - await minter.connect(accounts[1]).toggleApproveMint(accounts[2].address); - expect( - await minter.allowedToMintFor(accounts[2].address, accounts[1].address) - ).to.equal(true); - - await minter.connect(accounts[1]).toggleApproveMint(accounts[2].address); - expect( - await minter.allowedToMintFor(accounts[2].address, accounts[1].address) - ).to.equal(false); - }); - - // Test minting on behalf of another user - it("test_mintFor_function", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [month]); - - await minter.connect(accounts[1]).toggleApproveMint(accounts[2].address); - expect( - await minter.allowedToMintFor(accounts[2].address, accounts[1].address) - ).to.equal(true); - - await minter - .connect(accounts[2]) - .mintFor(threeGauges[0], accounts[1].address); - - let expected = await gauges[0].integrateFraction(accounts[1].address); - expect(expected.gt(BigNumber.from("0"))).to.be.equal(true); - expect(await token.balanceOf(accounts[1].address)).to.equal(expected); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - expected - ); - }); - - // Test mintFor function when not approved - it("test_mintForFail_function", async () => { - await gauges[0] - .connect(accounts[1]) - .deposit(ten_to_the_17, accounts[1].address, false); - - await ethers.provider.send("evm_increaseTime", [month]); - - expect( - await minter.allowedToMintFor(accounts[2].address, accounts[1].address) - ).to.equal(false); - - await minter - .connect(accounts[2]) - .mintFor(threeGauges[0], accounts[1].address); - - expect(await token.balanceOf(accounts[1].address)).to.equal(0); - expect(await minter.minted(accounts[1].address, threeGauges[0])).to.equal( - 0 - ); - }); - }); -}); diff --git a/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts b/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts deleted file mode 100644 index a9b3b039..00000000 --- a/test/fork/unitary/VotingEscrow/votingescrowAdmin.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { Contract } from "ethers"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; - -describe("VotingEscrow", () => { - let votingEscrow: Contract; - let token: Contract; - let accounts: SignerWithAddress[]; - let snapshot: SnapshotRestorer; - - beforeEach(async function () { - snapshot = await takeSnapshot(); - const CRV = await ethers.getContractFactory("CRV"); - const VotingEscrow = await ethers.getContractFactory("VotingEscrow"); - - token = await CRV.deploy(); - await token.deployed(); - - votingEscrow = await VotingEscrow.deploy( - token.address, - "Voting-escrowed token", - "vetoken", - "v1" - ); - await votingEscrow.deployed(); - - accounts = await ethers.getSigners(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - it("test_commit_admin_only", async function () { - await expect( - votingEscrow - .connect(accounts[1]) - .commitTransferOwnership(accounts[1].address) - ).to.be.revertedWith("admin only"); - }); - - it("test_apply_admin_only", async function () { - await expect( - votingEscrow.connect(accounts[1]).applyTransferOwnership() - ).to.be.revertedWith("admin only"); - }); - - it("test_commit_transfer_ownership", async function () { - await votingEscrow - .connect(accounts[0]) - .commitTransferOwnership(accounts[1].address); - - expect(await votingEscrow.admin()).to.equal(accounts[0].address); - expect(await votingEscrow.futureAdmin()).to.equal(accounts[1].address); - }); - - it("test_apply_transfer_ownership", async function () { - await votingEscrow - .connect(accounts[0]) - .commitTransferOwnership(accounts[1].address); - await votingEscrow.connect(accounts[0]).applyTransferOwnership(); - - expect(await votingEscrow.admin()).to.equal(accounts[1].address); - }); - - it("test_apply_without_commit", async function () { - await expect( - votingEscrow.connect(accounts[0]).applyTransferOwnership() - ).to.be.revertedWith("admin not set"); - }); -}); From 10edf2b6b3730521add6f90a05af2d382d7895ad Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 8 Dec 2023 11:47:15 +0900 Subject: [PATCH 117/412] Remove archive test code. --- test/archive/PriceFeed.test.ts | 758 --------------------------------- 1 file changed, 758 deletions(-) delete mode 100644 test/archive/PriceFeed.test.ts diff --git a/test/archive/PriceFeed.test.ts b/test/archive/PriceFeed.test.ts deleted file mode 100644 index cebeed03..00000000 --- a/test/archive/PriceFeed.test.ts +++ /dev/null @@ -1,758 +0,0 @@ -import { ethers } from "hardhat"; -import { FakeContract, smock } from "@defi-wonderland/smock"; -import chai, { expect } from "chai"; -import { Signer, BigNumber } from "ethers"; -import { - ChainLinkMock, - PriceFeedV3, - PriceFeedV3__factory, -} from "../../typechain"; -import { getProxy } from "../../src/testUtil"; -import { assert } from "console"; -import { getStatic } from "ethers/lib/utils"; -import { setProvider } from "../../src/deployUtil"; - -chai.use(smock.matchers); - -let feed: PriceFeedV3; -let accounts: Signer[]; -let ownerAddress: string; -let mockAggregatorV3EthUsd: FakeContract; -let mockAggregatorV3JpyUsd: FakeContract; -let mockRoundCount = 2; -let lastChainLinkAnswer; - -type ChainLinKNumberType = { - ethInUsd: number; - jpyInUsd: number; -}; -type PriceType = { - chainlink: ChainLinKNumberType; - tellor: number; -}; -type SilenceType = { - chainlink: ChainLinKNumberType; - tellor: number; -}; -type MockConf = { - price: PriceType; - silentFor: SilenceType; - resetFlag?: boolean; -}; -const CHAINLINK_DIGITS = 8; -const TELLOR_DIGITS = 6; -function assertChainlink(price, status, testData) { - expect(status).to.not.eq(1); - expect(status).to.not.eq(2); - expect(status).to.not.eq(3); - expect(price.toString().slice(0, 6)).to.eq( - `${ - testData.price.chainlink.ethInUsd / testData.price.chainlink.jpyInUsd - }`.slice(0, 6) - ); -} -function assertTellor(price, status, testData) { - expect(status).to.not.eq(0); - expect(status).to.not.eq(2); - expect(status).to.not.eq(4); - expect(price.toString().slice(0, 6)).to.eq( - `${testData.price.tellor}`.slice(0, 6) - ); -} -function assertTellorWithFrozenChainLink(price, status, lastGoodPrice) { - expect(status).to.eq(3); - expect(price.toString().slice(0, 6)).to.eq(`${lastGoodPrice}`.slice(0, 6)); -} - -function assertUnchange(price, status, lastGoodPrice) { - expect(status).to.not.eq(0); - expect(status).to.not.eq(3); - expect(price.toString().slice(0, 6)).to.eq(`${lastGoodPrice}`.slice(0, 6)); -} -function assertAdjusted( - price, - status, - isAdjusted, - lastGoodPrice, - MAX_PRICE_DIFFERENCE_FOR_TELLOR_ADJUSTMENT -) { - expect(status).to.eq(2); - expect(isAdjusted).to.be.true; - - let coef; - if (price.gt(lastGoodPrice)) { - coef = BigNumber.from(1e18 + "").add( - MAX_PRICE_DIFFERENCE_FOR_TELLOR_ADJUSTMENT - ); - } else if (price.lt(lastGoodPrice)) { - coef = BigNumber.from(1e18 + "").sub( - MAX_PRICE_DIFFERENCE_FOR_TELLOR_ADJUSTMENT - ); - } else { - throw new Error("Adjusted price can't be the same with lastGoodPrice."); - } - expect(price.toString().slice(0, 6)).to.eq( - `${lastGoodPrice.mul(coef).div(1e18 + "")}`.slice(0, 6) - ); -} - -async function setMocks(conf: MockConf) { - if (conf.resetFlag) { - mockRoundCount = 2; - lastChainLinkAnswer = undefined; - } - - let cPriceEthInUsd = BigNumber.from(conf.price.chainlink.ethInUsd).mul( - BigNumber.from(10).pow(CHAINLINK_DIGITS) - ); - if (conf.price.chainlink.ethInUsd == 0) { - cPriceEthInUsd = BigNumber.from(0); - } - let cPriceJpyInUsd = BigNumber.from( - conf.price.chainlink.jpyInUsd * 10 ** CHAINLINK_DIGITS - ); - if (conf.price.chainlink.jpyInUsd == 0) { - cPriceJpyInUsd = BigNumber.from(0); - } - let tPrice = BigNumber.from(conf.price.tellor).mul( - BigNumber.from(10).pow(TELLOR_DIGITS) - ); - if (conf.price.tellor == 0) { - tPrice = BigNumber.from(0); - } - let cDiffEthInUsd = conf.silentFor.chainlink.ethInUsd; // TIMEOUT = 14400 secs - let cDiffJpyInUsd = conf.silentFor.chainlink.jpyInUsd; - let tDiff = conf.silentFor.tellor; - - let now = Math.ceil(Date.now() / 1000); - if (feed) { - let block = await feed.provider.getBlock("latest"); - now = block.timestamp; - } - - mockAggregatorV3EthUsd.decimals.returns(CHAINLINK_DIGITS); // uint8 - mockAggregatorV3EthUsd.latestRoundData.returns([ - mockRoundCount, - cPriceEthInUsd, - now - cDiffEthInUsd /* unused */, - now - cDiffEthInUsd, - 2 /* unused */, - ]); // uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound - mockAggregatorV3EthUsd.getRoundData - .whenCalledWith(mockRoundCount - 1) - .returns([ - mockRoundCount - 1, - lastChainLinkAnswer ? lastChainLinkAnswer : cPriceEthInUsd, - now - cDiffEthInUsd /* unused */, - now - cDiffEthInUsd, - mockRoundCount - 1 /* unused */, - ]); // uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound - mockAggregatorV3JpyUsd.decimals.returns(CHAINLINK_DIGITS); // uint8 - mockAggregatorV3JpyUsd.latestRoundData.returns([ - mockRoundCount, - cPriceJpyInUsd, - now - cDiffJpyInUsd /* unused */, - now - cDiffJpyInUsd, - mockRoundCount /* unused */, - ]); // uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound - mockAggregatorV3JpyUsd.getRoundData - .whenCalledWith(mockRoundCount - 1) - .returns([ - mockRoundCount - 1, - cPriceJpyInUsd, - now - cDiffJpyInUsd /* unused */, - now - cDiffJpyInUsd, - mockRoundCount - 1 /* unused */, - ]); // uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound - - lastChainLinkAnswer = cPriceEthInUsd; - mockRoundCount++; -} -describe("PriceFeed", function () { - let lastMockInput; - beforeEach(async () => { - accounts = await ethers.getSigners(); - ownerAddress = await accounts[0].getAddress(); - // https://github.com/liquity/dev/blob/main/packages/contracts/contracts/Dependencies/AggregatorV3Interface.sol - mockAggregatorV3EthUsd = await smock.fake("ChainLinkMock"); - mockAggregatorV3JpyUsd = await smock.fake("ChainLinkMock"); - - lastMockInput = { - price: { - chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, - tellor: 351650, - }, - silentFor: { - chainlink: { ethInUsd: 7200, jpyInUsd: 7200 }, - tellor: 7200, - }, - resetFlag: true, - }; - // lastMockInput = { - // price: { - // chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, - // tellor: 351650, - // }, - // silentFor: { - // chainlink: { ethInUsd: 200, jpyInUsd: 200 }, - // tellor: 200, - // }, - // resetFlag: true, - // }; - await setMocks(lastMockInput); - // (accounts[0].provider).send("evm_increaseTime", [150]); - (accounts[0].provider).send("evm_increaseTime", [1200]); - (accounts[0].provider).send("evm_mine"); - - lastMockInput.silentFor.chainlink.ethInUsd = 1000; // Note: To avoid frozen response due to other depending specs - lastMockInput.silentFor.chainlink.jpyInUsd = 1000; - // lastMockInput.silentFor.chainlink.ethInUsd = 100; // Note: To avoid frozen response due to other depending specs - // lastMockInput.silentFor.chainlink.jpyInUsd = 100; - lastMockInput.resetFlag = false; - await setMocks(lastMockInput); - - feed = await getProxy( - "PriceFeed", - [mockAggregatorV3EthUsd.address, mockAggregatorV3JpyUsd.address], - 3 - ); - - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - - describe("constructor()", function () { - it(`should NOT revoke ownership`, async function () { - expect(await feed.governance()).to.eq(await feed.signer.getAddress()); - }); - }); - - describe("getPrice()", function () { - it(`should get chainlink data with a good chainlink and a good tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, // 362637 - tellor: 351640, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - it(`should get chainlink data with a good chainlink and a frozen tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { ethInUsd: 3220, jpyInUsd: 0.0091 }, // 362637 - tellor: 351648, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14401, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - it(`should get tellor data with a frozen chainlink and a good tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { ethInUsd: 3220, jpyInUsd: 0.0091 }, // 362637 - tellor: 351648, - }, - silentFor: { - chainlink: { ethInUsd: 14401, jpyInUsd: 14401 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - assertTellor( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - - it(`should get unchanged-but-tellor-data with a frozen chainlink and a frozen tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput.silentFor = { - chainlink: { ethInUsd: 14401, jpyInUsd: 14401 }, - tellor: 14401, // 14401 and tellor will be frozen - }; - await setMocks(lastMockInput); - assertTellorWithFrozenChainLink( - await feed.getPrice(), - await feed.getStatus(), - await feed.lastGoodPrice() - ); - }); - it(`should get chainlink data with a less-fructuated chainlink and a similar tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { - ethInUsd: lastMockInput.price.chainlink.ethInUsd * 1.5, - jpyInUsd: 0.0091, - }, - tellor: 351648 * 1.5, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - it(`should get chainlink data with a more-fructuated chainlink and a similar tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { - ethInUsd: lastMockInput.price.chainlink.ethInUsd * 2 + 1, - jpyInUsd: 0.0091, - }, - tellor: 351648 * 2 + 1, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - it(`should get tellor data with a more-fructuated chainlink and a stable tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { - ethInUsd: 3200 * 3 + 1, - jpyInUsd: 0.0091, - }, - tellor: 351648, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - // Note: max price route with usingTellor - assertTellor( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - - it(`should get tellor data with a less-fructuated chainlink and a stable tellor`, async function () { - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { - ethInUsd: lastMockInput.price.chainlink.ethInUsd * 1.5, - jpyInUsd: 0.0091, - }, - tellor: 351648, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - - it(`should get chainlink data with a stable chainlink and a less-than-50%-fructuated tellor`, async function () { - /* Because chainlink is to be priored whatever Tellor price is. */ - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { - ethInUsd: 3200, - jpyInUsd: 0.0091, - }, // 362637 - tellor: 362638 * 1.5, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - it(`should get chainlink data with a stable chainlink and a more-than-50%-fructuated tellor`, async function () { - /* Because chainlink is to be priored whatever Tellor price is. */ - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { - ethInUsd: 3200, - jpyInUsd: 0.0091, - }, // 362637 - tellor: 362637 * 2 + 1, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - - it(`should NOT use tellor data with an untrusted chainlink and a drastically surging tellor`, async function () { - // setMocks(zeroPrice) => Untrusted - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 0, jpyInUsd: 999999 }, // answer=0 - tellor: lastMockInput.price.tellor * 2 + 1, - }, - silentFor: { - chainlink: { ethInUsd: 14401, jpyInUsd: 14401 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - // getPrice - // assertAdjusted - assertAdjusted( - await feed.getPrice(), - await feed.getStatus(), - await feed.getIsAdjusted(), - await feed.lastGoodPrice(), - await feed.MAX_PRICE_DIFFERENCE_FOR_TELLOR_ADJUSTMENT() - ); - }); - - describe("Contect - recovery from untrusted feed", function () { - it(`should recover to use chainlink from an untrusted chainlink and a good tellor`, async function () { - // setMocks(zeroPrice) => Untrusted - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - - expect(await feed.getStatus()).to.eq(0); - expect(await feed.getPrice()).to.gt(0); - - lastMockInput = { - price: { - chainlink: { ethInUsd: 0, jpyInUsd: 999999 }, // answer=0 - tellor: 350000, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - // getPrice - // assertAdjusted - expect(await feed.getStatus()).to.eq(1); // usingTellorChainlinkUntrusted - - // setMocks([un]similarPrice) - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, // 362637 - tellor: 351650, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - - await setMocks(lastMockInput); // Note: If prevChainLinkResponse is broken, then _bothOraclesLiveAndUnbrokenAndSimilarPrice is not met the condition - await setMocks(lastMockInput); // Note: Need to remove broken prevChainLinkResponse to make _bothOraclesLiveAndUnbrokenAndSimilarPrice true - - // getPrice - // assertChainlink - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - - it(`should recover to use chainlink from a good chainlink and an untrusted tellor`, async function () { - // setMocks(zeroPrice) => Untrusted - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, // 36**** - tellor: 0, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - // getPrice - // assertUsingChainlinkTellorUntrusted - expect(await feed.getStatus()).to.eq(4); - - // setMocks([un]similarPrice) - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, // 362637 - tellor: 351648, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); // Note: If prevChainLinkResponse is broken, then _bothOraclesLiveAndUnbrokenAndSimilarPrice is not met the condition - await setMocks(lastMockInput); // Note: Need to remove broken prevChainLinkResponse to make _bothOraclesLiveAndUnbrokenAndSimilarPrice true - - // getPrice - // assertChainlink - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - it(`should recover to use chainlink from an untrusted chainlink and an untrusted tellor`, async function () { - // setMocks(zeroPrice) => Untrusted - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 0, jpyInUsd: 9999999 }, // 36**** - tellor: 0, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - // getPrice - // assertBothUntrusted - expect(await feed.getStatus()).to.eq(2); - expect(await feed.getIsAdjusted()).to.eq(false); - expect(await feed.getPrice()).to.eq(await feed.lastGoodPrice()); - - // setMocks([un]similarPrice) - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, // 362637 - tellor: 351648, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); // Note: If prevChainLinkResponse is broken, then _bothOraclesLiveAndUnbrokenAndSimilarPrice is not met the condition - await setMocks(lastMockInput); // Note: Need to remove broken prevChainLinkResponse to make _bothOraclesLiveAndUnbrokenAndSimilarPrice true - - // getPrice - // assertChainlink - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - - it(`should recover to use chainlink from an untrusted chainlink and an extremely low but working tellor`, async function () { - // setMocks(zeroPrice) => Untrusted - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 0, jpyInUsd: 9999999 }, // 36**** - tellor: 1, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); - - // getPrice - // assertAdjusted - assertAdjusted( - await feed.getPrice(), - await feed.getStatus(), - await feed.getIsAdjusted(), - await feed.lastGoodPrice(), - await feed.MAX_PRICE_DIFFERENCE_FOR_TELLOR_ADJUSTMENT() - ); - - // setMocks([un]similarPrice) - (feed.provider).send("evm_increaseTime", [3200]); - (feed.provider).send("evm_mine"); - lastMockInput = { - price: { - chainlink: { ethInUsd: 3200, jpyInUsd: 0.0091 }, // 362637 - tellor: 351648, - }, - silentFor: { - chainlink: { ethInUsd: 14400, jpyInUsd: 14400 }, - tellor: 14400, // 14401 and tellor will be frozen - }, - }; - await setMocks(lastMockInput); // Note: If prevChainLinkResponse is broken, then _bothOraclesLiveAndUnbrokenAndSimilarPrice is not met the condition - await setMocks(lastMockInput); // Note: Need to remove broken prevChainLinkResponse to make _bothOraclesLiveAndUnbrokenAndSimilarPrice true - - // getPrice - // assertChainlink - assertChainlink( - await feed.getPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - }); - }); - - describe("fetchPrice()", function () { - it(`succeeds to get dec18 price from ChainLink`, async function () { - let cPriceAtExecInEthUsd = 3201; - let cPriceAtExecInJpyUsd = 0.0091; - let tPriceAtExecInJpyUsd = 351649; - await setMocks({ - price: { - chainlink: { - ethInUsd: cPriceAtExecInEthUsd, - jpyInUsd: cPriceAtExecInJpyUsd, - }, - tellor: tPriceAtExecInJpyUsd, - }, - silentFor: { - chainlink: { ethInUsd: 7200, jpyInUsd: 7200 }, - tellor: 7200, - }, - }); - await (await feed.fetchPrice()).wait(); - const status = await feed.status(); - const lastGoodPrice = await feed.lastGoodPrice(); - const localPrice = BigNumber.from(cPriceAtExecInEthUsd) - .mul(BigNumber.from(10).pow(18 - CHAINLINK_DIGITS + CHAINLINK_DIGITS)) - .div(BigNumber.from(cPriceAtExecInJpyUsd * 10000)) - .mul(10000); - expect(status).to.eq(0); - expect(lastGoodPrice.toString().length).to.eq( - localPrice.toString().length - ); - expect(`${localPrice}`.slice(0, 6)).to.eq(`${lastGoodPrice}`.slice(0, 6)); - /* - enum Status { - chainlinkWorking, - usingTellorChainlinkUntrusted, - bothOraclesUntrusted, - usingTellorChainlinkFrozen, - usingChainlinkTellorUntrusted - } - */ - }); - - it(`succeeds to get price from Tellor because ChainLink is frozen`, async function () { - (feed.provider).send("evm_increaseTime", [7200]); - (feed.provider).send("evm_mine"); - - lastMockInput = { - price: { - chainlink: { - ethInUsd: 3202, - jpyInUsd: 0.0091, - }, - tellor: 351650, - }, - silentFor: { - chainlink: { ethInUsd: 14401, jpyInUsd: 14401 }, - tellor: 3600, - }, - }; - await setMocks(lastMockInput); - - await (await feed.fetchPrice()).wait(); - - assertTellor( - await feed.lastGoodPrice(), - await feed.getStatus(), - lastMockInput - ); - }); - }); -}); From de42d26fe161c5490dc003388896d1c8c2a9b2a7 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 8 Dec 2023 12:25:23 +0900 Subject: [PATCH 118/412] v1 test code upgraded to v1.5 --- test/param/version.ts | 4 +- test/unit/BurnCJPY.test.ts | 16 +++-- test/unit/CurrencyOS.test.ts | 2 +- test/unit/Events.test.ts | 7 +- test/unit/FlashLock.test.ts | 17 +++-- test/unit/MintCJPY.test.ts | 16 +++-- test/unit/PriceChangeAndRedemption.test.ts | 32 +++++----- test/unit/Yamato.test.ts | 74 ++++++++++++++++++---- test/v15/testHelpers.ts | 12 ++-- 9 files changed, 116 insertions(+), 64 deletions(-) diff --git a/test/param/version.ts b/test/param/version.ts index 54558933..5a018c87 100644 --- a/test/param/version.ts +++ b/test/param/version.ts @@ -30,11 +30,11 @@ export const contractVersion: Version = { veYMT: "veYMT", Yamato: "YamatoV4", YamatoBorrower: "YamatoBorrowerV2", - YamatoDepositor: "YamatoDepositorV2", + YamatoDepositor: "YamatoDepositorV3", YamatoRedeemer: "YamatoRedeemerV5", YamatoRepayer: "YamatoRepayerV3", YamatoSweeper: "YamatoSweeperV3", - YamatoWithdrawer: "YamatoWithdrawerV2", + YamatoWithdrawer: "YamatoWithdrawerV3", YMT: "YMT", YmtMinter: "YmtMinter", YmtOS: "YmtOS", diff --git a/test/unit/BurnCJPY.test.ts b/test/unit/BurnCJPY.test.ts index f4ae5a93..19db3b69 100644 --- a/test/unit/BurnCJPY.test.ts +++ b/test/unit/BurnCJPY.test.ts @@ -3,6 +3,7 @@ import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; @@ -10,7 +11,7 @@ import { toERC20 } from "../param/helper"; import { ChainLinkMock, PriceFeedV3, - FeePool, + FeePoolV2, CurrencyOS, CJPY, YamatoV4, @@ -29,7 +30,7 @@ import { ScoreRegistry, ChainLinkMock__factory, PriceFeedV3__factory, - FeePool__factory, + FeePoolV2__factory, CurrencyOS__factory, CJPY__factory, YamatoV4__factory, @@ -57,7 +58,7 @@ describe("burnCurrency :: contract Yamato", () => { let ChainLinkUsdJpy: ChainLinkMock; let PriceFeed: PriceFeedV3; let CJPY: CJPY; - let FeePool: FeePool; + let FeePool: FeePoolV2; let CurrencyOS: CurrencyOS; let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; @@ -121,9 +122,9 @@ describe("burnCurrency :: contract Yamato", () => { await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy( + FeePool = await getProxy( contractVersion["FeePool"], - [] + [await time.latest()] ); CurrencyOS = await getProxy( @@ -194,9 +195,10 @@ describe("burnCurrency :: contract Yamato", () => { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getProxy( + ScoreRegistry = await getLinkedProxy( contractVersion["ScoreRegistry"], - [YmtMinter.address, Yamato.address] + [YmtMinter.address, Yamato.address], + ["PledgeLib"] ); await ( diff --git a/test/unit/CurrencyOS.test.ts b/test/unit/CurrencyOS.test.ts index 7fdf84dc..8e937be0 100644 --- a/test/unit/CurrencyOS.test.ts +++ b/test/unit/CurrencyOS.test.ts @@ -21,7 +21,7 @@ import { YamatoSweeper, PriceFeedV3, CurrencyOS__factory, - FeePool__factory, + FeePoolV2__factory, } from "../../typechain"; import { getFakeProxy, getProxy } from "../../src/testUtil"; import { contractVersion } from "../param/version"; diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index c73b1daa..0e138b69 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -31,7 +31,7 @@ import { YamatoRedeemer__factory, YamatoSweeper__factory, YamatoDummy__factory, - FeePool__factory, + FeePoolV2__factory, Pool__factory, YMT__factory, VeYMT__factory, @@ -156,9 +156,10 @@ describe("story Events", function () { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getProxy( + ScoreRegistry = await getLinkedProxy( contractVersion["ScoreRegistry"], - [YmtMinter.address, yamato.address] + [YmtMinter.address, yamato.address], + ["PledgeLib"] ); await ( diff --git a/test/unit/FlashLock.test.ts b/test/unit/FlashLock.test.ts index f2b1e517..77a3d512 100644 --- a/test/unit/FlashLock.test.ts +++ b/test/unit/FlashLock.test.ts @@ -3,6 +3,7 @@ import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; @@ -10,7 +11,7 @@ import { toERC20 } from "../param/helper"; import { ChainLinkMock, PriceFeedV3, - FeePool, + FeePoolV2, CurrencyOS, CJPY, YamatoV4, @@ -30,7 +31,7 @@ import { SameBlockClient, ChainLinkMock__factory, PriceFeedV3__factory, - FeePool__factory, + FeePoolV2__factory, CurrencyOS__factory, CJPY__factory, YamatoV4__factory, @@ -59,7 +60,7 @@ describe("FlashLock :: contract Yamato", () => { let ChainLinkUsdJpy: ChainLinkMock; let PriceFeed: PriceFeedV3; let CJPY: CJPY; - let FeePool: FeePool; + let FeePool: FeePoolV2; let CurrencyOS: CurrencyOS; let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; @@ -124,9 +125,9 @@ describe("FlashLock :: contract Yamato", () => { await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy( + FeePool = await getProxy( contractVersion["FeePool"], - [] + [await time.latest()] ); CurrencyOS = await getProxy( @@ -197,9 +198,10 @@ describe("FlashLock :: contract Yamato", () => { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getProxy( + ScoreRegistry = await getLinkedProxy( contractVersion["ScoreRegistry"], - [YmtMinter.address, Yamato.address] + [YmtMinter.address, Yamato.address], + ["PledgeLib"] ); await ( @@ -215,6 +217,7 @@ describe("FlashLock :: contract Yamato", () => { ) ).wait(); await (await Yamato.setScoreRegistry(ScoreRegistry.address)).wait(); + await YMT.setMinter(YmtMinter.address); await (await CurrencyOS.addYamato(Yamato.address)).wait(); await (await CJPY.setCurrencyOS(CurrencyOS.address)).wait(); diff --git a/test/unit/MintCJPY.test.ts b/test/unit/MintCJPY.test.ts index 0b97a447..6d338c1d 100644 --- a/test/unit/MintCJPY.test.ts +++ b/test/unit/MintCJPY.test.ts @@ -3,6 +3,7 @@ import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; @@ -10,7 +11,7 @@ import { toERC20 } from "../param/helper"; import { ChainLinkMock, PriceFeedV3, - FeePool, + FeePoolV2, CurrencyOS, CJPY, YamatoV4, @@ -29,7 +30,7 @@ import { Pool, ChainLinkMock__factory, PriceFeedV3__factory, - FeePool__factory, + FeePoolV2__factory, CurrencyOS__factory, CJPY__factory, YamatoV4__factory, @@ -57,7 +58,7 @@ describe("MintCJPY :: contract Yamato", () => { let ChainLinkUsdJpy: ChainLinkMock; let PriceFeed: PriceFeedV3; let CJPY: CJPY; - let FeePool: FeePool; + let FeePool: FeePoolV2; let CurrencyOS: CurrencyOS; let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; @@ -120,9 +121,9 @@ describe("MintCJPY :: contract Yamato", () => { await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy( + FeePool = await getProxy( contractVersion["FeePool"], - [] + [await time.latest()] ); CurrencyOS = await getProxy( @@ -193,9 +194,10 @@ describe("MintCJPY :: contract Yamato", () => { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getProxy( + ScoreRegistry = await getLinkedProxy( contractVersion["ScoreRegistry"], - [YmtMinter.address, Yamato.address] + [YmtMinter.address, Yamato.address], + ["PledgeLib"] ); await ( diff --git a/test/unit/PriceChangeAndRedemption.test.ts b/test/unit/PriceChangeAndRedemption.test.ts index d7af5f48..f5e2cfff 100644 --- a/test/unit/PriceChangeAndRedemption.test.ts +++ b/test/unit/PriceChangeAndRedemption.test.ts @@ -3,6 +3,7 @@ import { smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber, Wallet } from "ethers"; import { + time, takeSnapshot, SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; @@ -10,7 +11,7 @@ import { toERC20 } from "../param/helper"; import { ChainLinkMock, PriceFeedV3, - FeePool, + FeePoolV2, CurrencyOS, CJPY, YamatoV4, @@ -39,7 +40,7 @@ import { YamatoRedeemer__factory, YamatoSweeper__factory, Pool__factory, - FeePool__factory, + FeePoolV2__factory, PriorityRegistryV6__factory, YMT__factory, VeYMT__factory, @@ -64,7 +65,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { let ChainLinkUsdJpy: ChainLinkMock; let PriceFeed: PriceFeedV3; let CJPY: CJPY; - let FeePool: FeePool; + let FeePool: FeePoolV2; let CurrencyOS: CurrencyOS; let Yamato: YamatoV4; let YamatoDepositor: YamatoDepositor; @@ -132,9 +133,9 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { await ethers.getContractFactory("CJPY") )).deploy(); - FeePool = await getProxy( + FeePool = await getProxy( contractVersion["FeePool"], - [] + [await time.latest()] ); CurrencyOS = await getProxy( @@ -209,9 +210,10 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getProxy( + ScoreRegistry = await getLinkedProxy( contractVersion["ScoreRegistry"], - [YmtMinter.address, Yamato.address] + [YmtMinter.address, Yamato.address], + ["PledgeLib"] ); await ( @@ -1398,7 +1400,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(90); }); - it("should redeem within 11m gas and sweep within 16m gas", async () => { + it("should redeem within 12m gas and sweep within 16m gas", async () => { let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); let ethBalanceBefore = await Yamato.provider.getBalance(redeemerAddr); @@ -1415,7 +1417,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(11000000); + expect(txReceipt1.gasUsed).to.be.lt(12000000); let sweepablePledge = await Yamato.getPledge( await PriorityRegistry.getRankedQueue( 0, @@ -1446,7 +1448,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(100); }); - it("should redeem within 11m gas and sweep within 16m gas", async () => { + it("should redeem within 12m gas and sweep within 16m gas", async () => { const price = await PriceFeed.getPrice(); let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); @@ -1464,7 +1466,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(11000000); + expect(txReceipt1.gasUsed).to.be.lt(12000000); expect( (await Yamato.getPledge(await accounts[1].getAddress())).coll @@ -1501,7 +1503,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(110); }); - it("should redeem within 11m gas and sweep within 16m gas", async () => { + it("should redeem within 12m gas and sweep within 16m gas", async () => { let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); let ethBalanceBefore = await Yamato.provider.getBalance(redeemerAddr); @@ -1518,7 +1520,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(11000000); + expect(txReceipt1.gasUsed).to.be.lt(12000000); let ethBalanceAfter = await Yamato.provider.getBalance(redeemerAddr); let redemptionReturn = ethBalanceAfter.sub(ethBalanceBefore); @@ -1545,7 +1547,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { beforeEach(async () => { await fillMinPledgesWithICR(120); }); - it("should redeem within 11m gas and sweep within 16m gas", async () => { + it("should redeem within 12m gas and sweep within 16m gas", async () => { let redeemerAddr = await accounts[0].getAddress(); let cjpyBalanceBefore = await CJPY.balanceOf(redeemerAddr); let ethBalanceBefore = await Yamato.provider.getBalance(redeemerAddr); @@ -1562,7 +1564,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { gasLimit: 30000000, }); let txReceipt1 = await tx1.wait(); - expect(txReceipt1.gasUsed).to.be.lt(11000000); + expect(txReceipt1.gasUsed).to.be.lt(12000000); let ethBalanceAfter = await Yamato.provider.getBalance(redeemerAddr); let redemptionReturn = ethBalanceAfter.sub(ethBalanceBefore); diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index 60a8def6..61575aa0 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -2,11 +2,16 @@ import { ethers } from "hardhat"; import { FakeContract, smock } from "@defi-wonderland/smock"; import chai, { expect } from "chai"; import { Signer, BigNumber } from "ethers"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; import { CJPY, CurrencyOS, Pool, - FeePool, + FeePoolV2, PriceFeedV3, PriorityRegistry, PriorityRegistry__factory, @@ -36,7 +41,7 @@ import { ScoreWeightController__factory, YmtMinter__factory, ScoreRegistry__factory, - FeePool__factory, + FeePoolV2__factory, Pool__factory, } from "../../typechain"; import { encode, toERC20 } from "../param/helper"; @@ -52,7 +57,7 @@ chai.use(smock.matchers); describe("contract Yamato - pure func quickier tests", function () { let mockPool: FakeContract; - let mockFeePool: FakeContract; + let mockFeePool: FakeContract; let mockFeed: FakeContract; let mockYMT: FakeContract; let mockCJPY: FakeContract; @@ -78,12 +83,14 @@ describe("contract Yamato - pure func quickier tests", function () { let accounts: Signer[]; let ownerAddress: string; + let snapshot: SnapshotRestorer; + before(async () => { accounts = await ethers.getSigners(); ownerAddress = await accounts[0].getAddress(); mockPool = await smock.fake("Pool"); - mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); mockYMT = await smock.fake("YMT"); mockCJPY = await smock.fake("CJPY"); @@ -163,9 +170,10 @@ describe("contract Yamato - pure func quickier tests", function () { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getProxy( + ScoreRegistry = await getLinkedProxy( contractVersion["ScoreRegistry"], - [YmtMinter.address, yamato.address] + [YmtMinter.address, yamato.address], + ["PledgeLib"] ); await ( @@ -214,6 +222,16 @@ describe("contract Yamato - pure func quickier tests", function () { mockPriorityRegistry.remove.returns(0); }); + beforeEach(async () => { + snapshot = await takeSnapshot(); + PRICE = BigNumber.from(260000).mul(1e18 + ""); + MCR = BigNumber.from(130); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("FR()", function () { /* Given ICR, get borrowing fee. */ it(`should be reverted for ICR 11000 pertenk`, async function () { @@ -278,7 +296,7 @@ describe("contract Yamato - pure func quickier tests", function () { describe("contract Yamato", function () { let mockPool: FakeContract; - let mockFeePool: FakeContract; + let mockFeePool: FakeContract; let mockFeed: FakeContract; let mockYMT: FakeContract; let mockCJPY: FakeContract; @@ -302,14 +320,14 @@ describe("contract Yamato", function () { let PRICE: BigNumber; let MCR: BigNumber; let accounts: Signer[]; - let ownerAddress: string; - beforeEach(async () => { + let snapshot: SnapshotRestorer; + + before(async () => { accounts = await ethers.getSigners(); - ownerAddress = await accounts[0].getAddress(); mockPool = await smock.fake("Pool"); - mockFeePool = await getFakeProxy(contractVersion["FeePool"]); + mockFeePool = await getFakeProxy(contractVersion["FeePool"]); mockFeed = await getFakeProxy(contractVersion["PriceFeed"]); mockYMT = await smock.fake("YMT"); mockCJPY = await smock.fake("CJPY"); @@ -389,9 +407,10 @@ describe("contract Yamato", function () { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getProxy( + ScoreRegistry = await getLinkedProxy( contractVersion["ScoreRegistry"], - [YmtMinter.address, yamato.address] + [YmtMinter.address, yamato.address], + ["PledgeLib"] ); await ( @@ -423,9 +442,31 @@ describe("contract Yamato", function () { await yamatoDummy.setPriorityRegistry(priorityRegistry.address) ).wait(); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + mockPool.depositRedemptionReserve.reset(); + mockPool.sweepReserve.reset(); + mockPool.sendETH.reset(); + mockPool.sendCurrency.reset(); + mockPool.useSweepReserve.reset(); + mockPool.useRedemptionReserve.reset(); + mockPool.redemptionReserve.reset(); + mockPool.depositSweepReserve.reset(); + mockFeed.fetchPrice.reset(); + mockFeed.lastGoodPrice.reset(); + mockFeed.getPrice.reset(); + mockCurrencyOS.mintCurrency.reset(); + mockCurrencyOS.burnCurrency.reset(); + mockPriorityRegistry.remove.reset(); + mockPriorityRegistry.upsert.reset(); + + mockCurrencyOS.priceFeed.returns(mockFeed.address); + mockCurrencyOS.feePool.returns(mockFeePool.address); + mockCurrencyOS.currency.returns(mockCJPY.address); PRICE = BigNumber.from(260000).mul(1e18 + ""); MCR = BigNumber.from(130); - mockCJPY.balanceOf.returns(PRICE.mul(1).mul(100).div(MCR)); mockPool.depositRedemptionReserve.returns(0); mockPool.depositSweepReserve.returns(0); @@ -439,6 +480,11 @@ describe("contract Yamato", function () { mockPriorityRegistry.upsert.returns(0); mockPriorityRegistry.remove.returns(0); }); + + afterEach(async () => { + await snapshot.restore(); + }); + describe("setPledge()", function () { beforeEach(async () => { await ( diff --git a/test/v15/testHelpers.ts b/test/v15/testHelpers.ts index ea5770e5..5d12f357 100644 --- a/test/v15/testHelpers.ts +++ b/test/v15/testHelpers.ts @@ -36,11 +36,7 @@ export function fee(amount: BigNumber): BigNumber { return amount.sub(amount.div(1000)); } -export function approx( - value: BigNumber, - target: BigNumber, - tol: BigNumber -): boolean { +export function approx(value: BigNumber, target: BigNumber, tol: BigNumber): boolean { if (value.isZero() && target.isZero()) { return true; } @@ -66,6 +62,6 @@ export function generateUniqueRandomNumbers( } export async function gasCostOf(tx) { - const receipt = await tx.wait(); - return receipt.gasUsed.mul(receipt.effectiveGasPrice); -} + const receipt = await tx.wait() + return receipt.gasUsed.mul(receipt.effectiveGasPrice) +} \ No newline at end of file From 70b7ea114c8e6cf7587b72498ed95763808c6f5e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 8 Dec 2023 12:27:08 +0900 Subject: [PATCH 119/412] Change directory of hardhat test code. --- hardhat.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index d4bbfbc3..dd4d6553 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -153,7 +153,8 @@ module.exports = { }, paths: { // tests: "./test/unit", - tests: "./test/v15", + // tests: "./test/v15", + tests: "./test", }, defender: { apiKey: process.env.DEFENDER_TEAM_API_KEY, From 2602746a3b362e45f2e644872b969e0231388561 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 00:32:13 +0900 Subject: [PATCH 120/412] Should correctly reduce mining parameters over 400 years test code added --- contracts/YMT.sol | 10 +-- .../integration/YMT/mintIntegration.test.ts | 61 ++++++++++++++++++- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/contracts/YMT.sol b/contracts/YMT.sol index 033d07bb..e2fdcb8a 100644 --- a/contracts/YMT.sol +++ b/contracts/YMT.sol @@ -26,10 +26,10 @@ contract YMT is ERC20Permit { uint256 constant YEAR = 365 days; // Supply parameters - uint256 constant INITIAL_SUPPLY = 450_000_000; - uint256 constant INITIAL_RATE = (55_000_000 * 10 ** 18) / YEAR; // leading to 45% premine + uint256 constant INITIAL_SUPPLY = 450_000_000 * 10 ** 18; + uint256 constant INITIAL_RATE = (55_000_000 * 10 ** 18) / YEAR; uint256 constant RATE_REDUCTION_TIME = YEAR; - uint256 constant RATE_REDUCTION_COEFFICIENT = 1_111_111_111_111_111_111; // 10 * 1e18 / 9 + uint256 constant RATE_REDUCTION_COEFFICIENT = 1_111_111_111_111_111_111; uint256 constant RATE_DENOMINATOR = 10 ** 18; uint256 constant INFLATION_DELAY = 1 days; @@ -41,7 +41,7 @@ contract YMT is ERC20Permit { uint256 startEpochSupply; constructor() ERC20Permit("Yamato") ERC20("Yamato", "YMT") { - uint256 initSupply = INITIAL_SUPPLY * 10 ** decimals(); + uint256 initSupply = INITIAL_SUPPLY; _mint(msg.sender, initSupply); admin = msg.sender; @@ -64,7 +64,7 @@ contract YMT is ERC20Permit { startEpochTime += RATE_REDUCTION_TIME; miningEpoch += 1; - if (_rate == 0) { + if (_rate == 0 && miningEpoch < 1) { _rate = INITIAL_RATE; } else { _startEpochSupply += _rate * RATE_REDUCTION_TIME; diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index 313c9303..5f690c18 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -9,8 +9,11 @@ import { import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { YMT, YMT__factory } from "../../../../typechain"; import Constants from "../../Constants"; +import { approxEqual } from "../../testHelpers"; const year = Constants.year; +const day = Constants.day; +const ten_to_the_18 = Constants.ten_to_the_18; describe("YMT", function () { let accounts: SignerWithAddress[]; @@ -21,7 +24,7 @@ describe("YMT", function () { accounts = await ethers.getSigners(); YMT = await (await ethers.getContractFactory("YMT")).deploy(); - await time.increase(86401); + await time.increase(day); await YMT.updateMiningParameters(); }); @@ -96,4 +99,60 @@ describe("YMT", function () { } }); }); + + describe("YMT Long-Term Mining Parameters", function () { + // 400年間にわたるマイニングパラメータの変化とトークンmintをテスト + it.only("should correctly reduce mining parameters over 400 years", async function () { + const initialRate = await YMT.rate(); + await YMT.setMinter(accounts[0].address); + let currentRate = initialRate; + let totalMinted = BigNumber.from(0); + + // 400年間、毎年マイニングパラメータを更新 + for (let i = 0; i < 400; i++) { + // 次の年に時間を進める + await time.increase(year); + + // マイニングパラメータを更新 + await YMT.updateMiningParameters(); + + // 新しいレートを取得 + let newRate = await YMT.rate(); + + // レートが減少したことを確認 + if(Number(newRate) > 0){ + expect(newRate).to.be.below(currentRate); + } + + // 総供給量が一定の限界に達したかどうかを確認 + const totalSupply = await YMT.totalSupply(); + const availableSupply = await YMT.availableSupply(); + + // トークンの発行量を計算してmint + let yearMinted = currentRate.mul(year); + await YMT.mint(accounts[1].address, availableSupply.sub(totalSupply)); + totalMinted = totalMinted.add(yearMinted); + + expect(availableSupply).to.be.lte( + BigNumber.from("1000000000").mul(ten_to_the_18) + ); + expect(totalSupply).to.be.lte( + BigNumber.from("1000000000").mul(ten_to_the_18) + ); + + currentRate = newRate; + } + + // トークンの総発行量が最大限界に達していることを確認 + const finalTotalSupply = await YMT.totalSupply(); + expect( + approxEqual( + finalTotalSupply, + BigNumber.from("1000000000").mul(ten_to_the_18), + ten_to_the_18 + ) + ).to.be.true; + + }); + }); }); From a9894d9ec5b34bf22efc2d78102dcba3237c3828 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 11:32:51 +0900 Subject: [PATCH 121/412] Helper function for the approxEqual function. --- test/unit/Events.test.ts | 5 ++++- test/unit/Yamato.test.ts | 1 - test/v15/testHelpers.ts | 20 ++++++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index 0e138b69..12abca3e 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -156,7 +156,10 @@ describe("story Events", function () { [YMT.address, ScoreWeightController.address] ); - ScoreRegistry = await getLinkedProxy( + ScoreRegistry = await getLinkedProxy< + ScoreRegistry, + ScoreRegistry__factory + >( contractVersion["ScoreRegistry"], [YmtMinter.address, yamato.address], ["PledgeLib"] diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index 61575aa0..b9f7f5ac 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -441,7 +441,6 @@ describe("contract Yamato", function () { await ( await yamatoDummy.setPriorityRegistry(priorityRegistry.address) ).wait(); - }); beforeEach(async () => { diff --git a/test/v15/testHelpers.ts b/test/v15/testHelpers.ts index 5d12f357..60c9eca9 100644 --- a/test/v15/testHelpers.ts +++ b/test/v15/testHelpers.ts @@ -36,7 +36,11 @@ export function fee(amount: BigNumber): BigNumber { return amount.sub(amount.div(1000)); } -export function approx(value: BigNumber, target: BigNumber, tol: BigNumber): boolean { +export function approx( + value: BigNumber, + target: BigNumber, + tol: BigNumber +): boolean { if (value.isZero() && target.isZero()) { return true; } @@ -48,6 +52,14 @@ export function approx(value: BigNumber, target: BigNumber, tol: BigNumber): boo return ratio.lte(tol); } +export function approxEqual(actual, expected, tolerance) { + // 差の絶対値を計算 + const diff = actual.sub(expected).abs(); + + // 差が許容誤差以下であるかどうかを確認 + return diff.lte(tolerance); +} + export function generateUniqueRandomNumbers( count: number, min: number, @@ -62,6 +74,6 @@ export function generateUniqueRandomNumbers( } export async function gasCostOf(tx) { - const receipt = await tx.wait() - return receipt.gasUsed.mul(receipt.effectiveGasPrice) -} \ No newline at end of file + const receipt = await tx.wait(); + return receipt.gasUsed.mul(receipt.effectiveGasPrice); +} From 4d743bddd48cc561dc0f700a9bcb5f0c8ef944d2 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 21:10:44 +0900 Subject: [PATCH 122/412] Add YMT token vesting contract. --- contracts/Interfaces/IYMT.sol | 2 + contracts/YMT.sol | 15 ++-- contracts/YmtVesting.sol | 137 ++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 contracts/YmtVesting.sol diff --git a/contracts/Interfaces/IYMT.sol b/contracts/Interfaces/IYMT.sol index 02f330a0..63439d48 100644 --- a/contracts/Interfaces/IYMT.sol +++ b/contracts/Interfaces/IYMT.sol @@ -35,4 +35,6 @@ interface IYMT { function rate() external view returns (uint256); function futureEpochTimeWrite() external returns (uint256); + + function startTime() external view returns (uint256); } diff --git a/contracts/YMT.sol b/contracts/YMT.sol index e2fdcb8a..be9ecd3f 100644 --- a/contracts/YMT.sol +++ b/contracts/YMT.sol @@ -26,7 +26,8 @@ contract YMT is ERC20Permit { uint256 constant YEAR = 365 days; // Supply parameters - uint256 constant INITIAL_SUPPLY = 450_000_000 * 10 ** 18; + uint256 constant INITIAL_SUPPLY = 200_000_000 * 10 ** 18; + uint256 constant VESTING_SUPPLY = 250_000_000 * 10 ** 18; uint256 constant INITIAL_RATE = (55_000_000 * 10 ** 18) / YEAR; uint256 constant RATE_REDUCTION_TIME = YEAR; uint256 constant RATE_REDUCTION_COEFFICIENT = 1_111_111_111_111_111_111; @@ -39,10 +40,13 @@ contract YMT is ERC20Permit { uint256 public rate; uint256 startEpochSupply; + uint256 public startTime; - constructor() ERC20Permit("Yamato") ERC20("Yamato", "YMT") { - uint256 initSupply = INITIAL_SUPPLY; - _mint(msg.sender, initSupply); + constructor( + address ymtVestingAddr + ) ERC20Permit("Yamato") ERC20("Yamato", "YMT") { + _mint(msg.sender, INITIAL_SUPPLY); + _mint(ymtVestingAddr, VESTING_SUPPLY); admin = msg.sender; @@ -50,9 +54,10 @@ contract YMT is ERC20Permit { block.timestamp + INFLATION_DELAY - RATE_REDUCTION_TIME; + startTime = block.timestamp; miningEpoch = -1; rate = 0; - startEpochSupply = initSupply; + startEpochSupply = INITIAL_SUPPLY; } // @dev Update mining rate and supply at the start of the epoch diff --git a/contracts/YmtVesting.sol b/contracts/YmtVesting.sol new file mode 100644 index 00000000..4fc0a5de --- /dev/null +++ b/contracts/YmtVesting.sol @@ -0,0 +1,137 @@ +pragma solidity 0.8.4; + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) + */ + +import "./Interfaces/IYMT.sol"; + +/** + * @title YmtVesting + * @dev This contract manages the vesting of YMT tokens. + */ +contract YmtVesting { + // Events + event YmtAddressSet(address ymt); + event AdminAddressSet(address admin); + + // Constants + uint256 private constant YEAR = 365 days; + uint256 private constant VESTING_AMOUNT = 100_000_000 * 10 ** 18; // 100,000,000 YMT + uint256 private constant LINEAR_DISTRIBUTION_DURATION = 5 * YEAR; + uint256 private constant LINEAR_DISTRIBUTION_RATE = + VESTING_AMOUNT / LINEAR_DISTRIBUTION_DURATION; + + // State variables + address public ymtTokenAddress; + address public contractAdmin; + uint256 public totalLinearDistributionClaimed; + bool public isClaimed; + + // Vesting mapping + mapping(address => uint256) public vestingAmounts; + mapping(address => uint256) public claimedAmounts; + + // Constructor + constructor() { + contractAdmin = msg.sender; + } + + /** + * @notice Sets a new admin for the contract. + * @param newAdmin The address of the new admin. + */ + function setAdmin(address newAdmin) external onlyAdmin { + require(newAdmin != address(0), "Invalid admin address"); + contractAdmin = newAdmin; + emit AdminAddressSet(newAdmin); + } + + /** + * @notice Sets the YMT token address. + * @param ymtToken The address of the YMT token. + */ + function setYmtToken(address ymtToken) external onlyAdmin { + require(ymtToken != address(0), "Invalid YMT token address"); + ymtTokenAddress = ymtToken; + emit YmtAddressSet(ymtToken); + } + + /** + * @notice Sets the claim amount for a user. + * @param user The address of the user. + * @param amount The amount of tokens to be claimed. + */ + function setClaimAmount(address user, uint256 amount) external onlyAdmin { + require(user != address(0), "Invalid user address"); + vestingAmounts[user] = amount; + } + + /** + * @notice Allows the admin to claim tokens from five-year linear distribution. + * @dev Transfers claimable tokens to the admin address. + */ + function claimV1RetroactiveRewards() external { + require(vestingAmounts[msg.sender] > 0, "No tokens to claim"); + uint256 distributionStart = IYMT(ymtTokenAddress).startTime(); + uint256 timeElapsed = block.timestamp - distributionStart; + uint256 claimableAmount; + if (block.timestamp >= distributionStart + YEAR) { + claimableAmount = vestingAmounts[msg.sender]; + } else { + claimableAmount = timeElapsed * (vestingAmounts[msg.sender] / YEAR); + } + uint256 availableToClaim = claimableAmount - claimedAmounts[msg.sender]; + require(availableToClaim > 0, "No tokens available to claim"); + IYMT(ymtTokenAddress).transfer(msg.sender, availableToClaim); + claimedAmounts[msg.sender] += availableToClaim; + } + + /** + * @notice Allows the admin to claim tokens from five-year linear distribution. + * @dev Transfers claimable tokens to the admin address. + */ + function claimFiveYearVestingTokens() external onlyAdmin { + require( + totalLinearDistributionClaimed < VESTING_AMOUNT, + "All tokens have already been claimed" + ); + + uint256 distributionStart = IYMT(ymtTokenAddress).startTime(); + uint256 timeElapsed = block.timestamp - distributionStart; + uint256 claimableAmount; + if ( + block.timestamp >= distributionStart + LINEAR_DISTRIBUTION_DURATION + ) { + claimableAmount = VESTING_AMOUNT; + } else { + claimableAmount = timeElapsed * LINEAR_DISTRIBUTION_RATE; + } + uint256 availableToClaim = claimableAmount - + totalLinearDistributionClaimed; + IYMT(ymtTokenAddress).transfer(contractAdmin, availableToClaim); + totalLinearDistributionClaimed += availableToClaim; + } + + /** + * @notice Allows the admin to claim tokens from two-year vesting period. + * @dev Transfers tokens to the admin address if within the two-year period. + */ + function claimTwoYearVestingTokens() external onlyAdmin { + uint256 distributionStart = IYMT(ymtTokenAddress).startTime(); + require( + distributionStart + 2 * YEAR < block.timestamp, + "Distribution period has ended" + ); + require(!isClaimed, "All tokens have already been claimed"); + IYMT(ymtTokenAddress).transfer(contractAdmin, VESTING_AMOUNT); + isClaimed = true; + } + + // Modifier + modifier onlyAdmin() { + require(msg.sender == contractAdmin, "Caller is not the admin"); + _; + } +} From 168fbc65d91c325931390d87c417ce3347143e28 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 21:11:37 +0900 Subject: [PATCH 123/412] Add YmtVesting test code. --- .../integration/YMT/mintIntegration.test.ts | 5 +- .../integration/YmtVesting/vesting.test.ts | 82 +++++++++++ .../claimFiveYearVestingTokens.test.ts | 138 ++++++++++++++++++ .../claimTwoYearVestingTokens.test.ts | 94 ++++++++++++ .../claimV1RetroactiveRewards.test.ts | 108 ++++++++++++++ 5 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 test/v15/integration/YmtVesting/vesting.test.ts create mode 100644 test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts create mode 100644 test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts create mode 100644 test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index 5f690c18..5913daf0 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -102,7 +102,7 @@ describe("YMT", function () { describe("YMT Long-Term Mining Parameters", function () { // 400年間にわたるマイニングパラメータの変化とトークンmintをテスト - it.only("should correctly reduce mining parameters over 400 years", async function () { + it("should correctly reduce mining parameters over 400 years", async function () { const initialRate = await YMT.rate(); await YMT.setMinter(accounts[0].address); let currentRate = initialRate; @@ -120,7 +120,7 @@ describe("YMT", function () { let newRate = await YMT.rate(); // レートが減少したことを確認 - if(Number(newRate) > 0){ + if (Number(newRate) > 0) { expect(newRate).to.be.below(currentRate); } @@ -152,7 +152,6 @@ describe("YMT", function () { ten_to_the_18 ) ).to.be.true; - }); }); }); diff --git a/test/v15/integration/YmtVesting/vesting.test.ts b/test/v15/integration/YmtVesting/vesting.test.ts new file mode 100644 index 00000000..7d661854 --- /dev/null +++ b/test/v15/integration/YmtVesting/vesting.test.ts @@ -0,0 +1,82 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; +import Constants from "../../Constants"; + +const year = Constants.year; +const ten_to_the_18 = Constants.ten_to_the_18; + +describe("YmtVesting Integration Tests", function () { + let accounts: SignerWithAddress[]; + let YMT: YMT; + let YmtVesting: YmtVesting; + let snapshot: SnapshotRestorer; + + before(async function () { + accounts = await ethers.getSigners(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); + await YmtVesting.setYmtToken(YMT.address); + await YmtVesting.setAdmin(accounts[1].address); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + it("should claim all tokens and empty the contract balance", async function () { + const initialBalance = await YMT.balanceOf(YmtVesting.address); + expect(initialBalance).to.equal( + BigNumber.from("250000000").mul(ten_to_the_18) + ); + await time.increase(year); + for (let i = 2; i <= 6; i++) { + await YmtVesting.connect(accounts[1]).setClaimAmount( + accounts[i].address, + BigNumber.from("10000000").mul(ten_to_the_18) + ); + await YmtVesting.connect(accounts[i]).claimV1RetroactiveRewards(); + expect(await YMT.balanceOf(accounts[i].address)).to.equal( + BigNumber.from("10000000").mul(ten_to_the_18) + ); + } + + await time.increase(year * 2); + await YmtVesting.connect(accounts[1]).claimTwoYearVestingTokens(); + expect(await YMT.balanceOf(accounts[1].address)).to.equal( + BigNumber.from("100000000").mul(ten_to_the_18) + ); + + await time.increase(3 * year); + await YmtVesting.connect(accounts[1]).claimFiveYearVestingTokens(); + expect(await YMT.balanceOf(accounts[1].address)).to.equal( + BigNumber.from("200000000").mul(ten_to_the_18) + ); + + // Check final balance of YMT in the contract + const finalContractBalance = await YMT.balanceOf(YmtVesting.address); + expect(finalContractBalance).to.equal( + BigNumber.from(0) + ); + }); +}); diff --git a/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts b/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts new file mode 100644 index 00000000..b7341d2a --- /dev/null +++ b/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts @@ -0,0 +1,138 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; +import Constants from "../../Constants"; +import { approx } from "../../testHelpers"; + +const year = Constants.year; +const YEAR = Constants.YEAR; +const zero = Constants.zero; +const ten_to_the_18 = Constants.ten_to_the_18; +const LINEAR_DISTRIBUTION_DURATION = YEAR.mul(5); +const TOTAL_LINEAR_DISTRIBUTION = BigNumber.from(100000000).mul(ten_to_the_18); +const LINEAR_DISTRIBUTION_RATE = TOTAL_LINEAR_DISTRIBUTION.div( + LINEAR_DISTRIBUTION_DURATION +); + +describe("YmtVesting", function () { + let accounts: SignerWithAddress[]; + let YMT: YMT; + let YmtVesting: YmtVesting; + let snapshot: SnapshotRestorer; + + before(async function () { + accounts = await ethers.getSigners(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); + await YmtVesting.setYmtToken(YMT.address); + await YmtVesting.setAdmin(accounts[1].address); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YmtVesting claimFiveYearVestingTokens Tests", function () { + // 正しい量のトークンが一定時間経過後にミントされるかテスト + it("should mint correct amount of tokens after a year", async function () { + const startTime = await YMT.startTime(); + + await ethers.provider.send("evm_setAutomine", [false]); + await time.increaseTo(startTime.add(YEAR)); + await YmtVesting.connect(accounts[1]).claimFiveYearVestingTokens(); + const expectedMintAmount = TOTAL_LINEAR_DISTRIBUTION.div(5); + + await ethers.provider.send("evm_mine", []); + await ethers.provider.send("evm_setAutomine", [true]); + const newTotalSupply = await YMT.balanceOf(accounts[1].address); + + expect(approx(newTotalSupply, expectedMintAmount, ten_to_the_18)).to.be + .true; + }); + + // 5年間、毎年claimFiveYearVestingTokensを実行し、全トークンがミントされるかテスト + it("should mint all tokens over five years", async function () { + const initialBalance = zero; + for (let i = 0; i < 5; i++) { + await time.increase(year); + await YmtVesting.connect(accounts[1]).claimFiveYearVestingTokens(); + expect(await YMT.balanceOf(accounts[1].address)).to.equal( + initialBalance.add(await YmtVesting.totalLinearDistributionClaimed()) + ); + } + const balance = await YMT.balanceOf(accounts[1].address); + expect(balance).to.equal(initialBalance.add(TOTAL_LINEAR_DISTRIBUTION)); + }); + + // 全トークンがミントされた後、claimFiveYearVestingTokensが失敗することをテスト + it("should fail to mint using claimFiveYearVestingTokens after all tokens are minted", async function () { + // 線形配布期間を超える時間を進める + await time.increase(LINEAR_DISTRIBUTION_DURATION.add(1)); + await YmtVesting.connect(accounts[1]).claimFiveYearVestingTokens(); + await time.increase(year); + // すべてのトークンがミントされた後にclaimFiveYearVestingTokensを実行すると失敗するはず + await expect( + YmtVesting.connect(accounts[1]).claimFiveYearVestingTokens() + ).to.be.revertedWith("All tokens have already been claimed"); + }); + + // 一定期間経過後にミントされるトークン量が期待通りかテスト + it("should mint the expected amount of tokens at various time intervals", async function () { + const intervals = [ + YEAR, + YEAR.mul(2), + YEAR.mul(3), + YEAR.mul(4), + LINEAR_DISTRIBUTION_DURATION, + ]; + const initialTotalSupply = await YMT.balanceOf(accounts[1].address); + + for (let i = 0; i < intervals.length; i++) { + const interval = intervals[i]; + await time.increase(YEAR); + await YmtVesting.connect(accounts[1]).claimFiveYearVestingTokens(); + + const expectedMintAmount = LINEAR_DISTRIBUTION_RATE.mul(interval); + const newTotalSupply = await YMT.balanceOf(accounts[1].address); + + expect( + approx( + newTotalSupply, + initialTotalSupply.add(expectedMintAmount), + ten_to_the_18 + ) + ).to.be.true; + } + }); + + // admin以外がトークンをクレームしようとすると失敗することを確認。 + it("should fail to claim tokens by a non-admin", async function () { + await time.increase(LINEAR_DISTRIBUTION_DURATION); + + // Non-admin attempts to claim the tokens + await expect( + YmtVesting.connect(accounts[2]).claimFiveYearVestingTokens() + ).to.be.revertedWith("Caller is not the admin"); + }); + }); +}); diff --git a/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts b/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts new file mode 100644 index 00000000..eada94e8 --- /dev/null +++ b/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts @@ -0,0 +1,94 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; +import Constants from "../../Constants"; + +const YEAR = Constants.YEAR; +const twoYears = YEAR.mul(2); +const TOTAL_LINEAR_DISTRIBUTION = BigNumber.from(100000000).mul( + Constants.ten_to_the_18 +); + +describe("YmtVesting", function () { + let accounts: SignerWithAddress[]; + let YMT: YMT; + let YmtVesting: YmtVesting; + let snapshot: SnapshotRestorer; + + before(async function () { + accounts = await ethers.getSigners(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); + await YmtVesting.setYmtToken(YMT.address); + await YmtVesting.setAdmin(accounts[1].address); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YmtVesting claimTwoYearVestingTokens Tests", function () { + // 2年後にadminが全トークンをクレームできることを確認。 + it("should allow admin to claim all tokens after two years", async function () { + await time.increase(twoYears); + + // Admin claims the tokens + await YmtVesting.connect(accounts[1]).claimTwoYearVestingTokens(); + const balance = await YMT.balanceOf(accounts[1].address); + + // Assert that the admin received the total distribution amount + expect(balance).to.equal(TOTAL_LINEAR_DISTRIBUTION); + }); + + // 2年間の期間が終了する前にトークンをクレームしようとすると失敗することを確認。 + it("should fail to claim tokens if period has not ended", async function () { + await time.increase(YEAR.mul("1")); // Less than two years + + // Attempt to claim tokens before period ends + await expect( + YmtVesting.connect(accounts[1]).claimTwoYearVestingTokens() + ).to.be.revertedWith("Distribution period has ended"); + }); + + // 一度トークンをクレームした後、再度クレームしようとすると失敗することを確認。 + it("should fail to claim tokens again after claiming once", async function () { + await time.increase(twoYears); + await YmtVesting.connect(accounts[1]).claimTwoYearVestingTokens(); + + // Attempt to claim tokens again + await expect( + YmtVesting.connect(accounts[1]).claimTwoYearVestingTokens() + ).to.be.revertedWith("All tokens have already been claimed"); + }); + + // admin以外がトークンをクレームしようとすると失敗することを確認。 + it("should fail to claim tokens by a non-admin", async function () { + await time.increase(twoYears); + + // Non-admin attempts to claim the tokens + await expect( + YmtVesting.connect(accounts[2]).claimTwoYearVestingTokens() + ).to.be.revertedWith("Caller is not the admin"); + }); + }); +}); diff --git a/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts b/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts new file mode 100644 index 00000000..c7681f0e --- /dev/null +++ b/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts @@ -0,0 +1,108 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { + time, + takeSnapshot, + SnapshotRestorer, +} from "@nomicfoundation/hardhat-network-helpers"; +import { BigNumber } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; +import Constants from "../../Constants"; +import { approx } from "../../testHelpers"; + +const YEAR = Constants.YEAR; +const ten_to_the_18 = Constants.ten_to_the_18; + +describe("YmtVesting", function () { + let accounts: SignerWithAddress[]; + let YMT: YMT; + let YmtVesting: YmtVesting; + let snapshot: SnapshotRestorer; + + before(async function () { + accounts = await ethers.getSigners(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); + await YmtVesting.setYmtToken(YMT.address); + await YmtVesting.setAdmin(accounts[1].address); + }); + + beforeEach(async () => { + snapshot = await takeSnapshot(); + }); + + afterEach(async () => { + await snapshot.restore(); + }); + + describe("YmtVesting claimV1RetroactiveRewards Tests", function () { + // 特定のアドレスが正しい量のトークンを請求できるかテスト + it("should allow a user to claim the correct amount of tokens", async function () { + const user = accounts[2].address; + const claimAmount = BigNumber.from(5000000).mul(ten_to_the_18); + + // Set claim amount for user + await YmtVesting.connect(accounts[1]).setClaimAmount(user, claimAmount); + + // Simulate time passage + await time.increase(YEAR); + + // Claim tokens + await YmtVesting.connect(accounts[2]).claimV1RetroactiveRewards(); + + // Check user's balance + const userBalance = await YMT.balanceOf(user); + expect(userBalance).to.equal(claimAmount); + }); + + // 時間経過に伴い、正しい量のトークンが請求できるかテスト + it("should allow claiming the correct amount of tokens over time", async function () { + const user = accounts[2].address; + const claimAmount = BigNumber.from(2000000).mul(ten_to_the_18); + + await YmtVesting.connect(accounts[1]).setClaimAmount(user, claimAmount); + await time.increase(YEAR.div(2)); // Increase time by half a year + + await YmtVesting.connect(accounts[2]).claimV1RetroactiveRewards(); + + const userBalance = await YMT.balanceOf(user); + const expectedBalance = claimAmount.div(2); + expect(approx(userBalance, expectedBalance, ten_to_the_18)).to.be.true; + }); + + // トークン請求量を超える請求は失敗するかテスト + it("should fail to claim more than the allocated amount", async function () { + const user = accounts[2].address; + const claimAmount = BigNumber.from(1000000).mul(ten_to_the_18); + + await YmtVesting.connect(accounts[1]).setClaimAmount(user, claimAmount); + await time.increase(YEAR); // Full year passes + + // First claim is successful + await YmtVesting.connect(accounts[2]).claimV1RetroactiveRewards(); + + // Second claim attempt should fail + await expect( + YmtVesting.connect(accounts[2]).claimV1RetroactiveRewards() + ).to.be.revertedWith("No tokens available to claim"); + }); + + // 請求可能なトークンがない場合に失敗するかテスト + it("should fail to claim when there are no tokens to claim", async function () { + // Attempt to claim tokens before the full year has passed + await expect( + YmtVesting.connect(accounts[1]).claimV1RetroactiveRewards() + ).to.be.revertedWith("No tokens to claim"); + }); + }); +}); From 4f9f2bd2d517f225817433d73602bda610ee2054 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 21:44:49 +0900 Subject: [PATCH 124/412] Fixed startEpochSupply in YMT. --- contracts/YMT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/YMT.sol b/contracts/YMT.sol index be9ecd3f..2d36e758 100644 --- a/contracts/YMT.sol +++ b/contracts/YMT.sol @@ -57,7 +57,7 @@ contract YMT is ERC20Permit { startTime = block.timestamp; miningEpoch = -1; rate = 0; - startEpochSupply = INITIAL_SUPPLY; + startEpochSupply = INITIAL_SUPPLY + VESTING_SUPPLY; } // @dev Update mining rate and supply at the start of the epoch From fe1a9550d106311bb444f06638b429f86d15c879 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 22:04:36 +0900 Subject: [PATCH 125/412] YmtVesting deployments added to existing test code. --- test/unit/BurnCJPY.test.ts | 9 ++++++++- test/unit/Events.test.ts | 9 +++++++-- test/unit/FlashLock.test.ts | 9 ++++++++- test/unit/MintCJPY.test.ts | 9 ++++++++- test/unit/PriceChangeAndRedemption.test.ts | 9 ++++++++- test/unit/Yamato.test.ts | 16 ++++++++++++++-- .../v15/integration/FeePool/checkpointTs.test.ts | 9 ++++++++- .../FeePool/distributefFeesStateful.test.ts | 11 ++++++++++- .../ScoreRegistry/scoreRegistry.test.ts | 9 ++++++++- test/v15/integration/YMT/mintIntegration.test.ts | 9 +++++++-- .../integration/YMT/mintableInTimeframe.test.ts | 9 +++++++-- .../v15/integration/YmtMinter/components.test.ts | 9 ++++++++- .../YmtMinter/minterIntegration.test.ts | 9 ++++++++- test/v15/integration/YmtVesting/vesting.test.ts | 4 ++-- test/v15/integration/veYMT/votingEscrow.test.ts | 9 ++++++++- .../veYMT/zeroBalanceAtUnlockTime.test.ts | 9 ++++++++- test/v15/unitary/FeePool/checkpoints.test.ts | 9 ++++++++- test/v15/unitary/FeePool/claimMany.test.ts | 9 ++++++++- test/v15/unitary/FeePool/feePool.test.ts | 9 ++++++++- test/v15/unitary/FeePool/killFeeDistro.test.ts | 9 ++++++++- .../v15/unitary/ScoreRegistry/checkpoint.test.ts | 9 ++++++++- test/v15/unitary/ScoreRegistry/kick.test.ts | 9 ++++++++- test/v15/unitary/YMT/burn.test.ts | 9 +++++++-- test/v15/unitary/YMT/epochTimeSupply.test.ts | 9 +++++++-- test/v15/unitary/YMT/inflationDelay.test.ts | 9 +++++++-- test/v15/unitary/YMT/mint.test.ts | 9 +++++++-- test/v15/unitary/YMT/setters.test.ts | 9 +++++++-- test/v15/unitary/YmtMinter/Minter.test.ts | 9 ++++++++- .../claimFiveYearVestingTokens.test.ts | 4 ++-- .../YmtVesting/claimTwoYearVestingTokens.test.ts | 4 ++-- .../YmtVesting/claimV1RetroactiveRewards.test.ts | 4 ++-- test/v15/unitary/veYMT/votingescrowAdmin.test.ts | 9 ++++++++- 32 files changed, 232 insertions(+), 45 deletions(-) diff --git a/test/unit/BurnCJPY.test.ts b/test/unit/BurnCJPY.test.ts index 19db3b69..3c1d67b2 100644 --- a/test/unit/BurnCJPY.test.ts +++ b/test/unit/BurnCJPY.test.ts @@ -23,6 +23,7 @@ import { YamatoSweeper, PriorityRegistry, Pool, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -42,6 +43,7 @@ import { YamatoSweeper__factory, Pool__factory, PriorityRegistry__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -73,6 +75,7 @@ describe("burnCurrency :: contract Yamato", () => { let YmtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; @@ -179,7 +182,11 @@ describe("burnCurrency :: contract Yamato", () => { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index 12abca3e..6b0afe61 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -18,6 +18,7 @@ import { YamatoRedeemer, YamatoSweeper, YamatoDummy, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -33,6 +34,7 @@ import { YamatoDummy__factory, FeePoolV2__factory, Pool__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -66,6 +68,7 @@ describe("story Events", function () { let YmtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let ScoreWeightController: ScoreWeightController; let PRICE: BigNumber; let MCR: BigNumber; @@ -138,9 +141,11 @@ describe("story Events", function () { contractVersion["PriorityRegistry"] ); - YMT = await (( - await ethers.getContractFactory("YMT") + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/FlashLock.test.ts b/test/unit/FlashLock.test.ts index 77a3d512..1de028ae 100644 --- a/test/unit/FlashLock.test.ts +++ b/test/unit/FlashLock.test.ts @@ -22,6 +22,7 @@ import { YamatoRedeemer, YamatoSweeper, PriorityRegistry, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -43,6 +44,7 @@ import { YamatoSweeper__factory, Pool__factory, PriorityRegistry__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -75,6 +77,7 @@ describe("FlashLock :: contract Yamato", () => { let YmtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; @@ -182,7 +185,11 @@ describe("FlashLock :: contract Yamato", () => { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/MintCJPY.test.ts b/test/unit/MintCJPY.test.ts index 6d338c1d..8e843cdc 100644 --- a/test/unit/MintCJPY.test.ts +++ b/test/unit/MintCJPY.test.ts @@ -22,6 +22,7 @@ import { YamatoRedeemer, YamatoSweeper, PriorityRegistry, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -42,6 +43,7 @@ import { YamatoSweeper__factory, Pool__factory, PriorityRegistry__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -73,6 +75,7 @@ describe("MintCJPY :: contract Yamato", () => { let YmtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; @@ -178,7 +181,11 @@ describe("MintCJPY :: contract Yamato", () => { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/PriceChangeAndRedemption.test.ts b/test/unit/PriceChangeAndRedemption.test.ts index f5e2cfff..a9baaa8b 100644 --- a/test/unit/PriceChangeAndRedemption.test.ts +++ b/test/unit/PriceChangeAndRedemption.test.ts @@ -22,6 +22,7 @@ import { YamatoRedeemer, YamatoSweeper, PriorityRegistryV6, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -42,6 +43,7 @@ import { Pool__factory, FeePoolV2__factory, PriorityRegistryV6__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -80,6 +82,7 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { let YmtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let ScoreWeightController: ScoreWeightController; let accounts: Signer[]; let ownerAddress: string; @@ -194,7 +197,11 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { PriorityRegistryV6__factory >(contractVersion["PriorityRegistry"], [Yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index b9f7f5ac..add03e01 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -23,6 +23,7 @@ import { YamatoRedeemer, YamatoSweeper, YamatoDummy, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -36,6 +37,7 @@ import { YamatoRedeemer__factory, YamatoSweeper__factory, YamatoDummy__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -75,6 +77,7 @@ describe("contract Yamato - pure func quickier tests", function () { let YmtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let ScoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; @@ -154,7 +157,11 @@ describe("contract Yamato - pure func quickier tests", function () { contractVersion["PriorityRegistry"] ); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") @@ -316,6 +323,7 @@ describe("contract Yamato", function () { let YmtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let ScoreWeightController: ScoreWeightController; let PRICE: BigNumber; let MCR: BigNumber; @@ -391,7 +399,11 @@ describe("contract Yamato", function () { contractVersion["PriorityRegistry"] ); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v15/integration/FeePool/checkpointTs.test.ts index 0c32d229..be69f144 100644 --- a/test/v15/integration/FeePool/checkpointTs.test.ts +++ b/test/v15/integration/FeePool/checkpointTs.test.ts @@ -9,7 +9,9 @@ import { import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { FeePool, + YmtVesting, FeePool__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, } from "../../../../typechain"; @@ -27,12 +29,17 @@ describe("FeePoolV2", function () { let YMT: Contract; let veYMT: Contract; let feePool: Contract; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index e7544bce..50efde8a 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -9,7 +9,11 @@ import { import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { FeePool, + YmtVesting, + YMT, + VeYMT, FeePool__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, } from "../../../../typechain"; @@ -41,6 +45,7 @@ describe("FeePoolV2", function () { let veYMT: Contract; let feePool: Contract; let YMT: Contract; + let YmtVesting: YmtVesting; let lockedUntil: { [key: string]: number } = {}; let fees: { [key: number]: BigNumber } = {}; // timestamp -> amount @@ -57,7 +62,11 @@ describe("FeePoolV2", function () { userClaims = {}; totalFees = ethers.utils.parseEther("1"); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts index 39f176ed..cc92850e 100644 --- a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts +++ b/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts @@ -23,6 +23,7 @@ import { YamatoWithdrawer, YamatoRedeemer, YamatoSweeper, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -37,6 +38,7 @@ import { YamatoWithdrawer__factory, YamatoRedeemer__factory, YamatoSweeper__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -80,6 +82,7 @@ describe("ScoreRegistry", function () { let ymtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let scoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; @@ -146,7 +149,11 @@ describe("ScoreRegistry", function () { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v15/integration/YMT/mintIntegration.test.ts index 5913daf0..48151496 100644 --- a/test/v15/integration/YMT/mintIntegration.test.ts +++ b/test/v15/integration/YMT/mintIntegration.test.ts @@ -7,7 +7,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory } from "../../../../typechain"; +import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; import Constants from "../../Constants"; import { approxEqual } from "../../testHelpers"; @@ -18,11 +18,16 @@ const ten_to_the_18 = Constants.ten_to_the_18; describe("YMT", function () { let accounts: SignerWithAddress[]; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); await time.increase(day); await YMT.updateMiningParameters(); diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v15/integration/YMT/mintableInTimeframe.test.ts index 7e14ed3e..93f8c370 100644 --- a/test/v15/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v15/integration/YMT/mintableInTimeframe.test.ts @@ -7,7 +7,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory } from "../../../../typechain"; +import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; import Constants from "../../Constants"; const year = Constants.year; @@ -23,11 +23,16 @@ const INITIAL_SUPPLY = Constants.INITIAL_SUPPLY; describe("YMT", function () { let accounts: SignerWithAddress[]; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); await time.increase(BigNumber.from(86401)); await YMT.updateMiningParameters(); diff --git a/test/v15/integration/YmtMinter/components.test.ts b/test/v15/integration/YmtMinter/components.test.ts index 80994145..18744e2f 100644 --- a/test/v15/integration/YmtMinter/components.test.ts +++ b/test/v15/integration/YmtMinter/components.test.ts @@ -23,6 +23,7 @@ import { YamatoWithdrawer, YamatoRedeemer, YamatoSweeper, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -37,6 +38,7 @@ import { YamatoWithdrawer__factory, YamatoRedeemer__factory, YamatoSweeper__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -78,6 +80,7 @@ describe("YmtMinter components", function () { let ymtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let scoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; @@ -144,7 +147,11 @@ describe("YmtMinter components", function () { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/integration/YmtMinter/minterIntegration.test.ts b/test/v15/integration/YmtMinter/minterIntegration.test.ts index 500e66f4..475f165d 100644 --- a/test/v15/integration/YmtMinter/minterIntegration.test.ts +++ b/test/v15/integration/YmtMinter/minterIntegration.test.ts @@ -23,6 +23,7 @@ import { YamatoWithdrawer, YamatoRedeemer, YamatoSweeper, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -37,6 +38,7 @@ import { YamatoWithdrawer__factory, YamatoRedeemer__factory, YamatoSweeper__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -73,6 +75,7 @@ describe("YmtMinter integration", function () { let ymtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let scoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; @@ -137,7 +140,11 @@ describe("YmtMinter integration", function () { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/integration/YmtVesting/vesting.test.ts b/test/v15/integration/YmtVesting/vesting.test.ts index 7d661854..834b7d9e 100644 --- a/test/v15/integration/YmtVesting/vesting.test.ts +++ b/test/v15/integration/YmtVesting/vesting.test.ts @@ -8,10 +8,10 @@ import { import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - YMT, - YMT__factory, YmtVesting, + YMT, YmtVesting__factory, + YMT__factory, } from "../../../../typechain"; import Constants from "../../Constants"; diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v15/integration/veYMT/votingEscrow.test.ts index 914950fb..b5ab9199 100644 --- a/test/v15/integration/veYMT/votingEscrow.test.ts +++ b/test/v15/integration/veYMT/votingEscrow.test.ts @@ -8,7 +8,9 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { + YmtVesting, YMT, + YmtVesting__factory, YMT__factory, VeYMT, VeYMT__factory, @@ -57,6 +59,7 @@ describe("veYMT", function () { let alice, bob: SignerWithAddress; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let t0: number; let wTotal, wAlice, wBob: BigNumber; let snapshot: SnapshotRestorer; @@ -64,7 +67,11 @@ describe("veYMT", function () { before(async function () { [alice, bob] = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts index e8230c0a..800e6d32 100644 --- a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts +++ b/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts @@ -7,7 +7,9 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { + YmtVesting, YMT, + YmtVesting__factory, YMT__factory, VeYMT, VeYMT__factory, @@ -21,12 +23,17 @@ describe("veYMT", function () { let accounts: SignerWithAddress[]; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async () => { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/FeePool/checkpoints.test.ts b/test/v15/unitary/FeePool/checkpoints.test.ts index 595fc5fc..0bd170a8 100644 --- a/test/v15/unitary/FeePool/checkpoints.test.ts +++ b/test/v15/unitary/FeePool/checkpoints.test.ts @@ -9,7 +9,9 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { FeePoolV2, FeePoolV2__factory, + YmtVesting, YMT, + YmtVesting__factory, YMT__factory, VeYMT, VeYMT__factory, @@ -28,12 +30,17 @@ describe("FeePoolV2", () => { let feePool: FeePoolV2; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { [alice] = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/FeePool/claimMany.test.ts b/test/v15/unitary/FeePool/claimMany.test.ts index f4587ed1..72848370 100644 --- a/test/v15/unitary/FeePool/claimMany.test.ts +++ b/test/v15/unitary/FeePool/claimMany.test.ts @@ -9,7 +9,9 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { FeePoolV2, FeePoolV2__factory, + YmtVesting, YMT, + YmtVesting__factory, YMT__factory, VeYMT, VeYMT__factory, @@ -29,12 +31,17 @@ describe("FeePoolV2", () => { let feePool: FeePoolV2; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { [alice, bob, charlie] = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/FeePool/feePool.test.ts b/test/v15/unitary/FeePool/feePool.test.ts index f7a8c5c2..bddb2141 100644 --- a/test/v15/unitary/FeePool/feePool.test.ts +++ b/test/v15/unitary/FeePool/feePool.test.ts @@ -9,7 +9,9 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { FeePoolV2, FeePoolV2__factory, + YmtVesting, YMT, + YmtVesting__factory, YMT__factory, VeYMT, VeYMT__factory, @@ -30,12 +32,17 @@ describe("FeePoolV2", () => { let feePool: FeePoolV2; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { [alice, bob, charlie] = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/FeePool/killFeeDistro.test.ts b/test/v15/unitary/FeePool/killFeeDistro.test.ts index bc2291a0..0812f096 100644 --- a/test/v15/unitary/FeePool/killFeeDistro.test.ts +++ b/test/v15/unitary/FeePool/killFeeDistro.test.ts @@ -9,7 +9,9 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { FeePoolV2, FeePoolV2__factory, + YmtVesting, YMT, + YmtVesting__factory, YMT__factory, VeYMT, VeYMT__factory, @@ -23,12 +25,17 @@ describe("FeePoolV2", () => { let feePool: FeePoolV2; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { [alice, bob, charlie] = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts index e269822d..f6553d02 100644 --- a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts +++ b/test/v15/unitary/ScoreRegistry/checkpoint.test.ts @@ -23,6 +23,7 @@ import { YamatoWithdrawer, YamatoRedeemer, YamatoSweeper, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -37,6 +38,7 @@ import { YamatoWithdrawer__factory, YamatoRedeemer__factory, YamatoSweeper__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -72,6 +74,7 @@ describe("ScoreRegistry checkpoint", function () { let ymtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let scoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; @@ -138,7 +141,11 @@ describe("ScoreRegistry checkpoint", function () { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/ScoreRegistry/kick.test.ts b/test/v15/unitary/ScoreRegistry/kick.test.ts index a690332e..da57c094 100644 --- a/test/v15/unitary/ScoreRegistry/kick.test.ts +++ b/test/v15/unitary/ScoreRegistry/kick.test.ts @@ -23,6 +23,7 @@ import { YamatoWithdrawer, YamatoRedeemer, YamatoSweeper, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -37,6 +38,7 @@ import { YamatoWithdrawer__factory, YamatoRedeemer__factory, YamatoSweeper__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -75,6 +77,7 @@ describe("ScoreRegistry kick", function () { let ymtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let scoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; @@ -141,7 +144,11 @@ describe("ScoreRegistry kick", function () { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/YMT/burn.test.ts b/test/v15/unitary/YMT/burn.test.ts index de55ff06..ab97bfc2 100644 --- a/test/v15/unitary/YMT/burn.test.ts +++ b/test/v15/unitary/YMT/burn.test.ts @@ -6,16 +6,21 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory } from "../../../../typechain"; +import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; describe("YMT", function () { let accounts: SignerWithAddress[]; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); }); beforeEach(async () => { diff --git a/test/v15/unitary/YMT/epochTimeSupply.test.ts b/test/v15/unitary/YMT/epochTimeSupply.test.ts index bfb09c9a..f6f2aa9b 100644 --- a/test/v15/unitary/YMT/epochTimeSupply.test.ts +++ b/test/v15/unitary/YMT/epochTimeSupply.test.ts @@ -6,7 +6,7 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; -import { YMT, YMT__factory } from "../../../../typechain"; +import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -15,10 +15,15 @@ const YEAR = Constants.YEAR; describe("YMT", function () { let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); }); beforeEach(async () => { diff --git a/test/v15/unitary/YMT/inflationDelay.test.ts b/test/v15/unitary/YMT/inflationDelay.test.ts index 339e07e8..9cf68024 100644 --- a/test/v15/unitary/YMT/inflationDelay.test.ts +++ b/test/v15/unitary/YMT/inflationDelay.test.ts @@ -6,17 +6,22 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; -import { YMT, YMT__factory } from "../../../../typechain"; +import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; import Constants from "../../Constants"; const YEAR = Constants.YEAR; describe("YMT", function () { let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); }); beforeEach(async () => { diff --git a/test/v15/unitary/YMT/mint.test.ts b/test/v15/unitary/YMT/mint.test.ts index e1630b08..cae12a98 100644 --- a/test/v15/unitary/YMT/mint.test.ts +++ b/test/v15/unitary/YMT/mint.test.ts @@ -7,7 +7,7 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory } from "../../../../typechain"; +import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -16,11 +16,16 @@ const ZERO_ADDRESS = Constants.ZERO_ADDRESS; describe("YMT", function () { let accounts: SignerWithAddress[]; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); await time.increase(86401); await YMT.updateMiningParameters(); }); diff --git a/test/v15/unitary/YMT/setters.test.ts b/test/v15/unitary/YMT/setters.test.ts index 8e7d1333..ef97823d 100644 --- a/test/v15/unitary/YMT/setters.test.ts +++ b/test/v15/unitary/YMT/setters.test.ts @@ -5,16 +5,21 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory } from "../../../../typechain"; +import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; describe("YMT", function () { let accounts: SignerWithAddress[]; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); }); beforeEach(async () => { diff --git a/test/v15/unitary/YmtMinter/Minter.test.ts b/test/v15/unitary/YmtMinter/Minter.test.ts index c11ef2f8..652900ae 100644 --- a/test/v15/unitary/YmtMinter/Minter.test.ts +++ b/test/v15/unitary/YmtMinter/Minter.test.ts @@ -23,6 +23,7 @@ import { YamatoWithdrawer, YamatoRedeemer, YamatoSweeper, + YmtVesting, YMT, VeYMT, ScoreWeightController, @@ -37,6 +38,7 @@ import { YamatoWithdrawer__factory, YamatoRedeemer__factory, YamatoSweeper__factory, + YmtVesting__factory, YMT__factory, VeYMT__factory, ScoreWeightController__factory, @@ -77,6 +79,7 @@ describe("YmtMinter", function () { let ymtMinter: YmtMinter; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let scoreWeightController: ScoreWeightController; let pool: Pool; let priorityRegistry: PriorityRegistry; @@ -143,7 +146,11 @@ describe("YmtMinter", function () { PriorityRegistry__factory >(contractVersion["PriorityRegistry"], [yamato.address], ["PledgeLib"]); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts b/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts index b7341d2a..683b1df2 100644 --- a/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts +++ b/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts @@ -8,10 +8,10 @@ import { import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - YMT, - YMT__factory, YmtVesting, + YMT, YmtVesting__factory, + YMT__factory, } from "../../../../typechain"; import Constants from "../../Constants"; import { approx } from "../../testHelpers"; diff --git a/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts b/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts index eada94e8..4a7d5249 100644 --- a/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts +++ b/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts @@ -8,10 +8,10 @@ import { import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - YMT, - YMT__factory, YmtVesting, + YMT, YmtVesting__factory, + YMT__factory, } from "../../../../typechain"; import Constants from "../../Constants"; diff --git a/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts b/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts index c7681f0e..4ef951ef 100644 --- a/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts +++ b/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts @@ -8,10 +8,10 @@ import { import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { - YMT, - YMT__factory, YmtVesting, + YMT, YmtVesting__factory, + YMT__factory, } from "../../../../typechain"; import Constants from "../../Constants"; import { approx } from "../../testHelpers"; diff --git a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts index 8f3d3e52..a874bab1 100644 --- a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts +++ b/test/v15/unitary/veYMT/votingescrowAdmin.test.ts @@ -6,7 +6,9 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { + YmtVesting, YMT, + YmtVesting__factory, YMT__factory, VeYMT, VeYMT__factory, @@ -16,12 +18,17 @@ describe("veYMT", () => { let accounts: SignerWithAddress[]; let veYMT: VeYMT; let YMT: YMT; + let YmtVesting: YmtVesting; let snapshot: SnapshotRestorer; before(async function () { accounts = await ethers.getSigners(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(); + YmtVesting = await (( + await ethers.getContractFactory("YmtVesting") + )).deploy(); + + YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); veYMT = await (( await ethers.getContractFactory("veYMT") From 75812a68cceedb39f625b0ae95aed2e7245abc3e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 22:40:26 +0900 Subject: [PATCH 126/412] Comment out console output of test code. --- .../FeePool/distributefFeesStateful.test.ts | 68 ++++++------- .../integration/YmtMinter/components.test.ts | 96 +++++++++---------- .../YmtMinter/minterIntegration.test.ts | 2 +- .../veYMT/depositWithdrawVoting.test.ts | 16 ++-- .../integration/veYMT/votingEscrow.test.ts | 20 ++-- 5 files changed, 101 insertions(+), 101 deletions(-) diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v15/integration/FeePool/distributefFeesStateful.test.ts index 50efde8a..561a1843 100644 --- a/test/v15/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v15/integration/FeePool/distributefFeesStateful.test.ts @@ -159,12 +159,12 @@ describe("FeePoolV2", function () { stWeeks = stWeeks || getRandomWeeks(); stTime = stTime || getRandomsTime(); - console.log(` - ruleNewLock --- - stAcct: ${ - stAcct.address - }, stAmount: ${stAmount.toString()}, stWeeks: ${stWeeks.toString()}, stTime: ${stTime.toString()} - `); + // console.log(` + // ruleNewLock --- + // stAcct: ${ + // stAcct.address + // }, stAmount: ${stAmount.toString()}, stWeeks: ${stWeeks.toString()}, stTime: ${stTime.toString()} + // `); stTime.gt(0) && (await time.increase(stTime)); @@ -198,11 +198,11 @@ describe("FeePoolV2", function () { stWeeks = stWeeks || getRandomWeeks(); stTime = stTime || getRandomsTime(); - console.log(` - ruleExtendLock --- stAmount ${ - stAcct.address - }, stAmount: ${stWeeks.toString()}, stTime: ${stTime.toString()} - `); + // console.log(` + // ruleExtendLock --- stAmount ${ + // stAcct.address + // }, stAmount: ${stWeeks.toString()}, stTime: ${stTime.toString()} + // `); stTime.gt(0) && (await time.increase(stTime)); @@ -225,7 +225,7 @@ describe("FeePoolV2", function () { stAmount?: BigNumber, stTime?: BigNumber ) { - console.log("ruleIncreaseLockAmount"); + // console.log("ruleIncreaseLockAmount"); /* Increase the amount of an existing user lock. @@ -244,11 +244,11 @@ describe("FeePoolV2", function () { stAmount = getRandomAmounts(); stTime = getRandomsTime(); - console.log(` - ruleIncreaseLockAmount --- stAmount ${ - stAcct.address - }, stAmount: ${stAmount.toString()}, stTime: ${stTime.toString()} - `); + // console.log(` + // ruleIncreaseLockAmount --- stAmount ${ + // stAcct.address + // }, stAmount: ${stAmount.toString()}, stTime: ${stTime.toString()} + // `); stTime.gt(0) && (await time.increase(stTime)); @@ -258,7 +258,7 @@ describe("FeePoolV2", function () { } async function ruleClaimFees(stAcct?: SignerWithAddress, stTime?: BigNumber) { - console.log("ruleClaimFees"); + // console.log("ruleClaimFees"); /* Claim fees for a user. @@ -272,9 +272,9 @@ describe("FeePoolV2", function () { stAcct = accounts[getRandomAccountNum()]; stTime = getRandomsTime(); - console.log(` - ruleClaimFees --- stAmount ${stAcct.address}, stTime: ${stTime.toString()} - `); + // console.log(` + // ruleClaimFees --- stAmount ${stAcct.address}, stTime: ${stTime.toString()} + // `); stTime.gt(0) && (await time.increase(stTime)); @@ -301,7 +301,7 @@ describe("FeePoolV2", function () { } async function ruleTransferFees(stAmount?: BigNumber, stTime?: BigNumber) { - console.log("ruleTransferFees"); + // console.log("ruleTransferFees"); /* Transfer fees into the feePool and make a checkpoint. @@ -318,9 +318,9 @@ describe("FeePoolV2", function () { stAmount = stAmount || getRandomAmounts(); stTime = stTime || getRandomsTime(); - console.log(` - ruleTransferFees --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} - `); + // console.log(` + // ruleTransferFees --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} + // `); stTime.gt(0) && (await time.increase(stTime)); @@ -343,7 +343,7 @@ describe("FeePoolV2", function () { stAmount?: BigNumber, stTime?: BigNumber ) { - console.log("ruleTransferFeesWithoutCheckpoint"); + // console.log("ruleTransferFeesWithoutCheckpoint"); /* Transfer fees into the feePool without checkpointing. @@ -358,9 +358,9 @@ describe("FeePoolV2", function () { stAmount = stAmount || getRandomAmounts(); stTime = stTime || getRandomsTime(); - console.log(` - ruleTransferFeesWithoutCheckpoint --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} - `); + // console.log(` + // ruleTransferFeesWithoutCheckpoint --- stAmount ${stAmount.toString()}, stTime: ${stTime.toString()} + // `); stTime.gt(0) && (await time.increase(stTime)); @@ -378,7 +378,7 @@ describe("FeePoolV2", function () { /* Claim fees for all accounts and verify that only dust remains. */ - console.log("teardown----"); + // console.log("teardown----"); if (!(await feePool.canCheckpointToken())) { //if no token checkpoint occured, add 100,000 tokens prior to teardown await ruleTransferFees( @@ -497,18 +497,18 @@ describe("FeePoolV2", function () { // initialize_transfer_fees: This is equivalent to `rule_transfer_fees` to make it more likely that claimable fees are available from the start of the test. const initializerSeed = Math.random(); if (initializerSeed < 0.2) { - console.log("0.2"); + // console.log("0.2"); await ruleNewLock(); await ruleTransferFees(); } else if (initializerSeed < 0.4) { - console.log("0.4"); + // console.log("0.4"); await ruleTransferFees(); await ruleNewLock(); } else if (initializerSeed < 0.6) { - console.log("0.6"); + // console.log("0.6"); await ruleNewLock(); } else if (initializerSeed < 0.8) { - console.log("0.8"); + // console.log("0.8"); await ruleTransferFees(); } diff --git a/test/v15/integration/YmtMinter/components.test.ts b/test/v15/integration/YmtMinter/components.test.ts index 18744e2f..e0d7ba66 100644 --- a/test/v15/integration/YmtMinter/components.test.ts +++ b/test/v15/integration/YmtMinter/components.test.ts @@ -256,30 +256,30 @@ describe("YmtMinter components", function () { const weight3 = Math.floor(weight2 + (durations[2] - durations[1]) * 3); const totalWeight = weight1 + weight2 + weight3; - console.log( - `Total minted: ${totalMinted.toString()}, Total Weight: ${totalWeight.toString()}` - ); - console.log( - `Balance 1: ${balances[0]} (${balances[0] - .mul(ten_to_the_20) - .div(totalMinted)}) Weight 1: ${weight1.toString()} (${ - (100 * weight1) / totalWeight - }%)` - ); - console.log( - `Balance 2: ${balances[1]} (${balances[1] - .mul(ten_to_the_20) - .div(totalMinted)}) Weight 2: ${weight2.toString()} (${ - (100 * weight2) / totalWeight - }%)` - ); - console.log( - `Balance 3: ${balances[2]} (${balances[2] - .mul(ten_to_the_20) - .div(totalMinted)}) Weight 3: ${weight3.toString()} (${ - (100 * weight3) / totalWeight - }%)` - ); + // console.log( + // `Total minted: ${totalMinted.toString()}, Total Weight: ${totalWeight.toString()}` + // ); + // console.log( + // `Balance 1: ${balances[0]} (${balances[0] + // .mul(ten_to_the_20) + // .div(totalMinted)}) Weight 1: ${weight1.toString()} (${ + // (100 * weight1) / totalWeight + // }%)` + // ); + // console.log( + // `Balance 2: ${balances[1]} (${balances[1] + // .mul(ten_to_the_20) + // .div(totalMinted)}) Weight 2: ${weight2.toString()} (${ + // (100 * weight2) / totalWeight + // }%)` + // ); + // console.log( + // `Balance 3: ${balances[2]} (${balances[2] + // .mul(ten_to_the_20) + // .div(totalMinted)}) Weight 3: ${weight3.toString()} (${ + // (100 * weight3) / totalWeight + // }%)` + // ); expect( approx( @@ -336,30 +336,30 @@ describe("YmtMinter components", function () { zero ); - console.log( - `Total deposited: ${totalDeposited.toString()}, Total minted: ${totalMinted.toString()}` - ); - console.log( - `Balance 1: ${balances[0]} (${balances[0] - .mul(ten_to_the_20) - .div(totalMinted)}) Deposited 1: ${stAmounts[0].toString()} (${ - (100 * stAmounts[0]) / totalDeposited - }%)` - ); - console.log( - `Balance 2: ${balances[1]} (${balances[1] - .mul(ten_to_the_20) - .div(totalMinted)}) Deposited 2: ${stAmounts[1].toString()} (${ - (100 * stAmounts[1]) / totalDeposited - }%)` - ); - console.log( - `Balance 3: ${balances[2]} (${balances[2] - .mul(ten_to_the_20) - .div(totalMinted)}) Deposited 3: ${stAmounts[2].toString()} (${ - (100 * stAmounts[2]) / totalDeposited - }%)` - ); + // console.log( + // `Total deposited: ${totalDeposited.toString()}, Total minted: ${totalMinted.toString()}` + // ); + // console.log( + // `Balance 1: ${balances[0]} (${balances[0] + // .mul(ten_to_the_20) + // .div(totalMinted)}) Deposited 1: ${stAmounts[0].toString()} (${ + // (100 * stAmounts[0]) / totalDeposited + // }%)` + // ); + // console.log( + // `Balance 2: ${balances[1]} (${balances[1] + // .mul(ten_to_the_20) + // .div(totalMinted)}) Deposited 2: ${stAmounts[1].toString()} (${ + // (100 * stAmounts[1]) / totalDeposited + // }%)` + // ); + // console.log( + // `Balance 3: ${balances[2]} (${balances[2] + // .mul(ten_to_the_20) + // .div(totalMinted)}) Deposited 3: ${stAmounts[2].toString()} (${ + // (100 * stAmounts[2]) / totalDeposited + // }%)` + // ); expect( approx( diff --git a/test/v15/integration/YmtMinter/minterIntegration.test.ts b/test/v15/integration/YmtMinter/minterIntegration.test.ts index 475f165d..3966d1a6 100644 --- a/test/v15/integration/YmtMinter/minterIntegration.test.ts +++ b/test/v15/integration/YmtMinter/minterIntegration.test.ts @@ -282,7 +282,7 @@ describe("YmtMinter integration", function () { for (let i = 0; i < 5; i++) { await ymtMinter.connect(accounts[i + 1]).mint(scoreRegistry.address); const token = await YMT.balanceOf(accounts[i + 1].address); - console.log("tokenBalance:", Number(token)); + // console.log("tokenBalance:", Number(token)); tokens.push(token); } for (let i = 0; i < 5; i++) { diff --git a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts index de7d90af..1d64103f 100644 --- a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts +++ b/test/v15/integration/veYMT/depositWithdrawVoting.test.ts @@ -83,7 +83,7 @@ describe("veYMT", function () { //--------------------------------------------- randomly excuted functions -----------------------------------------------------------// async function ruleCreateLock() { - console.log("ruleCreateLock"); + // console.log("ruleCreateLock"); //stAccount let rdm = Math.floor(Math.random() * 10); //0~9 integer @@ -131,7 +131,7 @@ describe("veYMT", function () { } async function ruleIncreaseAmount() { - console.log("ruleIncreaseAmount"); + // console.log("ruleIncreaseAmount"); //stAccount let rdm = Math.floor(Math.random() * 10); //0~9 integer @@ -173,7 +173,7 @@ describe("veYMT", function () { } async function ruleIncreaseUnlockTime() { - console.log("ruleIncreaseUnlockTime"); + // console.log("ruleIncreaseUnlockTime"); //stAccount let rdm = Math.floor(Math.random() * 10); //0~9 integer @@ -218,7 +218,7 @@ describe("veYMT", function () { } async function ruleWithdraw() { - console.log("ruleWithdraw"); + // console.log("ruleWithdraw"); // Withdraw tokens from the voting escrow. //stAccount @@ -241,7 +241,7 @@ describe("veYMT", function () { } async function ruleCheckpoint() { - console.log("ruleCheckpoint"); + // console.log("ruleCheckpoint"); //stAccount let rdm = Math.floor(Math.random() * 10); //0~9 integer @@ -252,16 +252,16 @@ describe("veYMT", function () { } async function ruleAdvanceTime() { - console.log("ruleAdvanceTime"); + // console.log("ruleAdvanceTime"); let stSleepDuration = Math.floor(Math.random() * 3) + 1; //1~4 await time.increase(WEEK.mul(stSleepDuration).toNumber()); if (stSleepDuration == 1) { - console.log("Time advanced"); + // console.log("Time advanced"); } else { - console.log("Time advanced"); + // console.log("Time advanced"); } } diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v15/integration/veYMT/votingEscrow.test.ts index b5ab9199..b61d8304 100644 --- a/test/v15/integration/veYMT/votingEscrow.test.ts +++ b/test/v15/integration/veYMT/votingEscrow.test.ts @@ -519,34 +519,34 @@ describe("veYMT", function () { for (let i = initialBlock; i < latestBlock; i++) { let a = await veYMT.balanceOfAt(alice.address, `${i}`); let b = await veYMT.balanceOfAt(bob.address, `${i}`); - console.log(`Block ${i}: a: ${a.toString()} b: ${b.toString()}`); + // console.log(`Block ${i}: a: ${a.toString()} b: ${b.toString()}`); } const epoch = (await veYMT.epoch()).toNumber(); const aliceEpoch = (await veYMT.userPointEpoch(alice.address)).toNumber(); const bobEpoch = (await veYMT.userPointEpoch(bob.address)).toNumber(); for (let i = 0; i <= epoch; i++) { let p = await veYMT.pointHistory(i); - console.log( - `epoch: ${i}: bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` - ); + // console.log( + // `epoch: ${i}: bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + // ); } for (let i = 0; i <= aliceEpoch; i++) { let p = await veYMT.userPointHistory(alice.address, i); let balanceAt = p.blk.isZero() ? 0 : await veYMT.balanceOfAt(alice.address, p.blk.toNumber()); - console.log( - `alice epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` - ); + // console.log( + // `alice epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + // ); } for (let i = 0; i < bobEpoch; i++) { let p = await veYMT.userPointHistory(bob.address, i); let balanceAt = p.blk.isZero() ? 0 : await veYMT.balanceOfAt(bob.address, p.blk.toNumber()); - console.log( - `bob epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` - ); + // console.log( + // `bob epoch: ${i}: balanceAt: ${balanceAt.toString()} bias: ${p.bias.toString()} slope: ${p.slope.toString()} ts: ${p.ts.toString()} blk: ${p.blk.toString()}` + // ); } } }); From 14d896d62405e95c79a6e5be4c86d95ecff5ae20 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 22:55:11 +0900 Subject: [PATCH 127/412] Change the amount of burn --- test/v15/unitary/YMT/burn.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/v15/unitary/YMT/burn.test.ts b/test/v15/unitary/YMT/burn.test.ts index ab97bfc2..94a67ed9 100644 --- a/test/v15/unitary/YMT/burn.test.ts +++ b/test/v15/unitary/YMT/burn.test.ts @@ -7,6 +7,9 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import Constants from "../../Constants"; + +const ten_to_the_18 = Constants.ten_to_the_18; describe("YMT", function () { let accounts: SignerWithAddress[]; @@ -60,12 +63,12 @@ describe("YMT", function () { // トークン供給の全量を正しくburnさせるテスト it("should correctly burn the entire token supply", async function () { - const initialSupply: BigNumber = await YMT.totalSupply(); + const balance: BigNumber = await YMT.balanceOf(accounts[0].address) - await YMT.burn(initialSupply); + await YMT.burn(balance); expect(await YMT.balanceOf(accounts[0].address)).to.equal(0); - expect(await YMT.totalSupply()).to.equal(0); + expect(await YMT.totalSupply()).to.equal(BigNumber.from(250000000).mul(ten_to_the_18)); }); // 総供給量を超える量をburnさせようとした場合にリバートするテスト From 2466f42c5602206417428771816026fdaa9c0b70 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Mon, 11 Dec 2023 23:00:50 +0900 Subject: [PATCH 128/412] Change the directory name of the test code. --- hardhat.config.ts | 2 +- test/{v15 => v1.5}/Constants.ts | 0 test/{v15 => v1.5}/integration/FeePool/checkpointTs.test.ts | 0 .../integration/FeePool/distributefFeesStateful.test.ts | 0 .../integration/ScoreRegistry/scoreRegistry.test.ts | 0 test/{v15 => v1.5}/integration/YMT/mintIntegration.test.ts | 0 test/{v15 => v1.5}/integration/YMT/mintableInTimeframe.test.ts | 0 test/{v15 => v1.5}/integration/YmtMinter/components.test.ts | 0 .../integration/YmtMinter/minterIntegration.test.ts | 0 test/{v15 => v1.5}/integration/YmtVesting/vesting.test.ts | 0 .../integration/veYMT/depositWithdrawVoting.test.ts | 0 test/{v15 => v1.5}/integration/veYMT/votingEscrow.test.ts | 0 .../integration/veYMT/zeroBalanceAtUnlockTime.test.ts | 0 test/{v15 => v1.5}/testHelpers.ts | 0 test/{v15 => v1.5}/unitary/FeePool/checkpoints.test.ts | 0 test/{v15 => v1.5}/unitary/FeePool/claimMany.test.ts | 0 test/{v15 => v1.5}/unitary/FeePool/feePool.test.ts | 0 test/{v15 => v1.5}/unitary/FeePool/killFeeDistro.test.ts | 0 test/{v15 => v1.5}/unitary/ScoreRegistry/checkpoint.test.ts | 0 test/{v15 => v1.5}/unitary/ScoreRegistry/kick.test.ts | 0 test/{v15 => v1.5}/unitary/YMT/burn.test.ts | 0 test/{v15 => v1.5}/unitary/YMT/epochTimeSupply.test.ts | 0 test/{v15 => v1.5}/unitary/YMT/inflationDelay.test.ts | 0 test/{v15 => v1.5}/unitary/YMT/mint.test.ts | 0 test/{v15 => v1.5}/unitary/YMT/setters.test.ts | 0 test/{v15 => v1.5}/unitary/YmtMinter/Minter.test.ts | 0 .../unitary/YmtVesting/claimFiveYearVestingTokens.test.ts | 0 .../unitary/YmtVesting/claimTwoYearVestingTokens.test.ts | 0 .../unitary/YmtVesting/claimV1RetroactiveRewards.test.ts | 0 test/{v15 => v1.5}/unitary/veYMT/votingescrowAdmin.test.ts | 0 30 files changed, 1 insertion(+), 1 deletion(-) rename test/{v15 => v1.5}/Constants.ts (100%) rename test/{v15 => v1.5}/integration/FeePool/checkpointTs.test.ts (100%) rename test/{v15 => v1.5}/integration/FeePool/distributefFeesStateful.test.ts (100%) rename test/{v15 => v1.5}/integration/ScoreRegistry/scoreRegistry.test.ts (100%) rename test/{v15 => v1.5}/integration/YMT/mintIntegration.test.ts (100%) rename test/{v15 => v1.5}/integration/YMT/mintableInTimeframe.test.ts (100%) rename test/{v15 => v1.5}/integration/YmtMinter/components.test.ts (100%) rename test/{v15 => v1.5}/integration/YmtMinter/minterIntegration.test.ts (100%) rename test/{v15 => v1.5}/integration/YmtVesting/vesting.test.ts (100%) rename test/{v15 => v1.5}/integration/veYMT/depositWithdrawVoting.test.ts (100%) rename test/{v15 => v1.5}/integration/veYMT/votingEscrow.test.ts (100%) rename test/{v15 => v1.5}/integration/veYMT/zeroBalanceAtUnlockTime.test.ts (100%) rename test/{v15 => v1.5}/testHelpers.ts (100%) rename test/{v15 => v1.5}/unitary/FeePool/checkpoints.test.ts (100%) rename test/{v15 => v1.5}/unitary/FeePool/claimMany.test.ts (100%) rename test/{v15 => v1.5}/unitary/FeePool/feePool.test.ts (100%) rename test/{v15 => v1.5}/unitary/FeePool/killFeeDistro.test.ts (100%) rename test/{v15 => v1.5}/unitary/ScoreRegistry/checkpoint.test.ts (100%) rename test/{v15 => v1.5}/unitary/ScoreRegistry/kick.test.ts (100%) rename test/{v15 => v1.5}/unitary/YMT/burn.test.ts (100%) rename test/{v15 => v1.5}/unitary/YMT/epochTimeSupply.test.ts (100%) rename test/{v15 => v1.5}/unitary/YMT/inflationDelay.test.ts (100%) rename test/{v15 => v1.5}/unitary/YMT/mint.test.ts (100%) rename test/{v15 => v1.5}/unitary/YMT/setters.test.ts (100%) rename test/{v15 => v1.5}/unitary/YmtMinter/Minter.test.ts (100%) rename test/{v15 => v1.5}/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts (100%) rename test/{v15 => v1.5}/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts (100%) rename test/{v15 => v1.5}/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts (100%) rename test/{v15 => v1.5}/unitary/veYMT/votingescrowAdmin.test.ts (100%) diff --git a/hardhat.config.ts b/hardhat.config.ts index dd4d6553..5e64b9a0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -153,7 +153,7 @@ module.exports = { }, paths: { // tests: "./test/unit", - // tests: "./test/v15", + // tests: "./test/v1.5", tests: "./test", }, defender: { diff --git a/test/v15/Constants.ts b/test/v1.5/Constants.ts similarity index 100% rename from test/v15/Constants.ts rename to test/v1.5/Constants.ts diff --git a/test/v15/integration/FeePool/checkpointTs.test.ts b/test/v1.5/integration/FeePool/checkpointTs.test.ts similarity index 100% rename from test/v15/integration/FeePool/checkpointTs.test.ts rename to test/v1.5/integration/FeePool/checkpointTs.test.ts diff --git a/test/v15/integration/FeePool/distributefFeesStateful.test.ts b/test/v1.5/integration/FeePool/distributefFeesStateful.test.ts similarity index 100% rename from test/v15/integration/FeePool/distributefFeesStateful.test.ts rename to test/v1.5/integration/FeePool/distributefFeesStateful.test.ts diff --git a/test/v15/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v1.5/integration/ScoreRegistry/scoreRegistry.test.ts similarity index 100% rename from test/v15/integration/ScoreRegistry/scoreRegistry.test.ts rename to test/v1.5/integration/ScoreRegistry/scoreRegistry.test.ts diff --git a/test/v15/integration/YMT/mintIntegration.test.ts b/test/v1.5/integration/YMT/mintIntegration.test.ts similarity index 100% rename from test/v15/integration/YMT/mintIntegration.test.ts rename to test/v1.5/integration/YMT/mintIntegration.test.ts diff --git a/test/v15/integration/YMT/mintableInTimeframe.test.ts b/test/v1.5/integration/YMT/mintableInTimeframe.test.ts similarity index 100% rename from test/v15/integration/YMT/mintableInTimeframe.test.ts rename to test/v1.5/integration/YMT/mintableInTimeframe.test.ts diff --git a/test/v15/integration/YmtMinter/components.test.ts b/test/v1.5/integration/YmtMinter/components.test.ts similarity index 100% rename from test/v15/integration/YmtMinter/components.test.ts rename to test/v1.5/integration/YmtMinter/components.test.ts diff --git a/test/v15/integration/YmtMinter/minterIntegration.test.ts b/test/v1.5/integration/YmtMinter/minterIntegration.test.ts similarity index 100% rename from test/v15/integration/YmtMinter/minterIntegration.test.ts rename to test/v1.5/integration/YmtMinter/minterIntegration.test.ts diff --git a/test/v15/integration/YmtVesting/vesting.test.ts b/test/v1.5/integration/YmtVesting/vesting.test.ts similarity index 100% rename from test/v15/integration/YmtVesting/vesting.test.ts rename to test/v1.5/integration/YmtVesting/vesting.test.ts diff --git a/test/v15/integration/veYMT/depositWithdrawVoting.test.ts b/test/v1.5/integration/veYMT/depositWithdrawVoting.test.ts similarity index 100% rename from test/v15/integration/veYMT/depositWithdrawVoting.test.ts rename to test/v1.5/integration/veYMT/depositWithdrawVoting.test.ts diff --git a/test/v15/integration/veYMT/votingEscrow.test.ts b/test/v1.5/integration/veYMT/votingEscrow.test.ts similarity index 100% rename from test/v15/integration/veYMT/votingEscrow.test.ts rename to test/v1.5/integration/veYMT/votingEscrow.test.ts diff --git a/test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts b/test/v1.5/integration/veYMT/zeroBalanceAtUnlockTime.test.ts similarity index 100% rename from test/v15/integration/veYMT/zeroBalanceAtUnlockTime.test.ts rename to test/v1.5/integration/veYMT/zeroBalanceAtUnlockTime.test.ts diff --git a/test/v15/testHelpers.ts b/test/v1.5/testHelpers.ts similarity index 100% rename from test/v15/testHelpers.ts rename to test/v1.5/testHelpers.ts diff --git a/test/v15/unitary/FeePool/checkpoints.test.ts b/test/v1.5/unitary/FeePool/checkpoints.test.ts similarity index 100% rename from test/v15/unitary/FeePool/checkpoints.test.ts rename to test/v1.5/unitary/FeePool/checkpoints.test.ts diff --git a/test/v15/unitary/FeePool/claimMany.test.ts b/test/v1.5/unitary/FeePool/claimMany.test.ts similarity index 100% rename from test/v15/unitary/FeePool/claimMany.test.ts rename to test/v1.5/unitary/FeePool/claimMany.test.ts diff --git a/test/v15/unitary/FeePool/feePool.test.ts b/test/v1.5/unitary/FeePool/feePool.test.ts similarity index 100% rename from test/v15/unitary/FeePool/feePool.test.ts rename to test/v1.5/unitary/FeePool/feePool.test.ts diff --git a/test/v15/unitary/FeePool/killFeeDistro.test.ts b/test/v1.5/unitary/FeePool/killFeeDistro.test.ts similarity index 100% rename from test/v15/unitary/FeePool/killFeeDistro.test.ts rename to test/v1.5/unitary/FeePool/killFeeDistro.test.ts diff --git a/test/v15/unitary/ScoreRegistry/checkpoint.test.ts b/test/v1.5/unitary/ScoreRegistry/checkpoint.test.ts similarity index 100% rename from test/v15/unitary/ScoreRegistry/checkpoint.test.ts rename to test/v1.5/unitary/ScoreRegistry/checkpoint.test.ts diff --git a/test/v15/unitary/ScoreRegistry/kick.test.ts b/test/v1.5/unitary/ScoreRegistry/kick.test.ts similarity index 100% rename from test/v15/unitary/ScoreRegistry/kick.test.ts rename to test/v1.5/unitary/ScoreRegistry/kick.test.ts diff --git a/test/v15/unitary/YMT/burn.test.ts b/test/v1.5/unitary/YMT/burn.test.ts similarity index 100% rename from test/v15/unitary/YMT/burn.test.ts rename to test/v1.5/unitary/YMT/burn.test.ts diff --git a/test/v15/unitary/YMT/epochTimeSupply.test.ts b/test/v1.5/unitary/YMT/epochTimeSupply.test.ts similarity index 100% rename from test/v15/unitary/YMT/epochTimeSupply.test.ts rename to test/v1.5/unitary/YMT/epochTimeSupply.test.ts diff --git a/test/v15/unitary/YMT/inflationDelay.test.ts b/test/v1.5/unitary/YMT/inflationDelay.test.ts similarity index 100% rename from test/v15/unitary/YMT/inflationDelay.test.ts rename to test/v1.5/unitary/YMT/inflationDelay.test.ts diff --git a/test/v15/unitary/YMT/mint.test.ts b/test/v1.5/unitary/YMT/mint.test.ts similarity index 100% rename from test/v15/unitary/YMT/mint.test.ts rename to test/v1.5/unitary/YMT/mint.test.ts diff --git a/test/v15/unitary/YMT/setters.test.ts b/test/v1.5/unitary/YMT/setters.test.ts similarity index 100% rename from test/v15/unitary/YMT/setters.test.ts rename to test/v1.5/unitary/YMT/setters.test.ts diff --git a/test/v15/unitary/YmtMinter/Minter.test.ts b/test/v1.5/unitary/YmtMinter/Minter.test.ts similarity index 100% rename from test/v15/unitary/YmtMinter/Minter.test.ts rename to test/v1.5/unitary/YmtMinter/Minter.test.ts diff --git a/test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts b/test/v1.5/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts similarity index 100% rename from test/v15/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts rename to test/v1.5/unitary/YmtVesting/claimFiveYearVestingTokens.test.ts diff --git a/test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts b/test/v1.5/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts similarity index 100% rename from test/v15/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts rename to test/v1.5/unitary/YmtVesting/claimTwoYearVestingTokens.test.ts diff --git a/test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts b/test/v1.5/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts similarity index 100% rename from test/v15/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts rename to test/v1.5/unitary/YmtVesting/claimV1RetroactiveRewards.test.ts diff --git a/test/v15/unitary/veYMT/votingescrowAdmin.test.ts b/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts similarity index 100% rename from test/v15/unitary/veYMT/votingescrowAdmin.test.ts rename to test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts From ea894d32bb3b16b52b8a4b169e4ee2e10777dca1 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 12 Dec 2023 11:34:13 +0900 Subject: [PATCH 129/412] code format --- test/unit/BurnCJPY.test.ts | 4 +++- test/unit/Events.test.ts | 6 ++++-- test/unit/FlashLock.test.ts | 4 +++- test/unit/MintCJPY.test.ts | 4 +++- test/unit/PriceChangeAndRedemption.test.ts | 4 +++- test/unit/Yamato.test.ts | 8 ++++++-- .../integration/FeePool/checkpointTs.test.ts | 4 +++- .../FeePool/distributefFeesStateful.test.ts | 6 ++++-- .../ScoreRegistry/scoreRegistry.test.ts | 4 +++- .../integration/YMT/mintIntegration.test.ts | 11 +++++++++-- .../integration/YMT/mintableInTimeframe.test.ts | 11 +++++++++-- .../integration/YmtMinter/components.test.ts | 4 +++- .../YmtMinter/minterIntegration.test.ts | 4 +++- .../v1.5/integration/YmtVesting/vesting.test.ts | 4 +--- .../v1.5/integration/veYMT/votingEscrow.test.ts | 4 +++- .../veYMT/zeroBalanceAtUnlockTime.test.ts | 4 +++- test/v1.5/unitary/FeePool/checkpoints.test.ts | 4 +++- test/v1.5/unitary/FeePool/claimMany.test.ts | 4 +++- test/v1.5/unitary/FeePool/feePool.test.ts | 4 +++- test/v1.5/unitary/FeePool/killFeeDistro.test.ts | 4 +++- .../unitary/ScoreRegistry/checkpoint.test.ts | 4 +++- test/v1.5/unitary/ScoreRegistry/kick.test.ts | 4 +++- test/v1.5/unitary/YMT/burn.test.ts | 17 +++++++++++++---- test/v1.5/unitary/YMT/epochTimeSupply.test.ts | 11 +++++++++-- test/v1.5/unitary/YMT/inflationDelay.test.ts | 11 +++++++++-- test/v1.5/unitary/YMT/mint.test.ts | 11 +++++++++-- test/v1.5/unitary/YMT/setters.test.ts | 11 +++++++++-- test/v1.5/unitary/YmtMinter/Minter.test.ts | 4 +++- .../unitary/veYMT/votingescrowAdmin.test.ts | 4 +++- 29 files changed, 136 insertions(+), 43 deletions(-) diff --git a/test/unit/BurnCJPY.test.ts b/test/unit/BurnCJPY.test.ts index 3c1d67b2..80c2fd87 100644 --- a/test/unit/BurnCJPY.test.ts +++ b/test/unit/BurnCJPY.test.ts @@ -186,7 +186,9 @@ describe("burnCurrency :: contract Yamato", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/Events.test.ts b/test/unit/Events.test.ts index 6b0afe61..151920a2 100644 --- a/test/unit/Events.test.ts +++ b/test/unit/Events.test.ts @@ -144,8 +144,10 @@ describe("story Events", function () { YmtVesting = await (( await ethers.getContractFactory("YmtVesting") )).deploy(); - - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/FlashLock.test.ts b/test/unit/FlashLock.test.ts index 1de028ae..395cde81 100644 --- a/test/unit/FlashLock.test.ts +++ b/test/unit/FlashLock.test.ts @@ -189,7 +189,9 @@ describe("FlashLock :: contract Yamato", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/MintCJPY.test.ts b/test/unit/MintCJPY.test.ts index 8e843cdc..53eb3778 100644 --- a/test/unit/MintCJPY.test.ts +++ b/test/unit/MintCJPY.test.ts @@ -185,7 +185,9 @@ describe("MintCJPY :: contract Yamato", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/PriceChangeAndRedemption.test.ts b/test/unit/PriceChangeAndRedemption.test.ts index a9baaa8b..5a652c2a 100644 --- a/test/unit/PriceChangeAndRedemption.test.ts +++ b/test/unit/PriceChangeAndRedemption.test.ts @@ -201,7 +201,9 @@ describe("PriceChangeAndRedemption :: contract Yamato", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/unit/Yamato.test.ts b/test/unit/Yamato.test.ts index add03e01..c6481d6e 100644 --- a/test/unit/Yamato.test.ts +++ b/test/unit/Yamato.test.ts @@ -161,7 +161,9 @@ describe("contract Yamato - pure func quickier tests", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") @@ -403,7 +405,9 @@ describe("contract Yamato", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/integration/FeePool/checkpointTs.test.ts b/test/v1.5/integration/FeePool/checkpointTs.test.ts index be69f144..4d27bc43 100644 --- a/test/v1.5/integration/FeePool/checkpointTs.test.ts +++ b/test/v1.5/integration/FeePool/checkpointTs.test.ts @@ -39,7 +39,9 @@ describe("FeePoolV2", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/integration/FeePool/distributefFeesStateful.test.ts b/test/v1.5/integration/FeePool/distributefFeesStateful.test.ts index 561a1843..a8d5b045 100644 --- a/test/v1.5/integration/FeePool/distributefFeesStateful.test.ts +++ b/test/v1.5/integration/FeePool/distributefFeesStateful.test.ts @@ -66,7 +66,9 @@ describe("FeePoolV2", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") @@ -160,7 +162,7 @@ describe("FeePoolV2", function () { stTime = stTime || getRandomsTime(); // console.log(` - // ruleNewLock --- + // ruleNewLock --- // stAcct: ${ // stAcct.address // }, stAmount: ${stAmount.toString()}, stWeeks: ${stWeeks.toString()}, stTime: ${stTime.toString()} diff --git a/test/v1.5/integration/ScoreRegistry/scoreRegistry.test.ts b/test/v1.5/integration/ScoreRegistry/scoreRegistry.test.ts index cc92850e..2cadbf17 100644 --- a/test/v1.5/integration/ScoreRegistry/scoreRegistry.test.ts +++ b/test/v1.5/integration/ScoreRegistry/scoreRegistry.test.ts @@ -153,7 +153,9 @@ describe("ScoreRegistry", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/integration/YMT/mintIntegration.test.ts b/test/v1.5/integration/YMT/mintIntegration.test.ts index 48151496..b91995eb 100644 --- a/test/v1.5/integration/YMT/mintIntegration.test.ts +++ b/test/v1.5/integration/YMT/mintIntegration.test.ts @@ -7,7 +7,12 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; import { approxEqual } from "../../testHelpers"; @@ -27,7 +32,9 @@ describe("YMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); await time.increase(day); await YMT.updateMiningParameters(); diff --git a/test/v1.5/integration/YMT/mintableInTimeframe.test.ts b/test/v1.5/integration/YMT/mintableInTimeframe.test.ts index 93f8c370..8e79cba1 100644 --- a/test/v1.5/integration/YMT/mintableInTimeframe.test.ts +++ b/test/v1.5/integration/YMT/mintableInTimeframe.test.ts @@ -7,7 +7,12 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const year = Constants.year; @@ -32,7 +37,9 @@ describe("YMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); await time.increase(BigNumber.from(86401)); await YMT.updateMiningParameters(); diff --git a/test/v1.5/integration/YmtMinter/components.test.ts b/test/v1.5/integration/YmtMinter/components.test.ts index e0d7ba66..240ee764 100644 --- a/test/v1.5/integration/YmtMinter/components.test.ts +++ b/test/v1.5/integration/YmtMinter/components.test.ts @@ -151,7 +151,9 @@ describe("YmtMinter components", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/integration/YmtMinter/minterIntegration.test.ts b/test/v1.5/integration/YmtMinter/minterIntegration.test.ts index 3966d1a6..4f3a4aef 100644 --- a/test/v1.5/integration/YmtMinter/minterIntegration.test.ts +++ b/test/v1.5/integration/YmtMinter/minterIntegration.test.ts @@ -144,7 +144,9 @@ describe("YmtMinter integration", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/integration/YmtVesting/vesting.test.ts b/test/v1.5/integration/YmtVesting/vesting.test.ts index 834b7d9e..881897a5 100644 --- a/test/v1.5/integration/YmtVesting/vesting.test.ts +++ b/test/v1.5/integration/YmtVesting/vesting.test.ts @@ -75,8 +75,6 @@ describe("YmtVesting Integration Tests", function () { // Check final balance of YMT in the contract const finalContractBalance = await YMT.balanceOf(YmtVesting.address); - expect(finalContractBalance).to.equal( - BigNumber.from(0) - ); + expect(finalContractBalance).to.equal(BigNumber.from(0)); }); }); diff --git a/test/v1.5/integration/veYMT/votingEscrow.test.ts b/test/v1.5/integration/veYMT/votingEscrow.test.ts index b61d8304..f6b5ec5e 100644 --- a/test/v1.5/integration/veYMT/votingEscrow.test.ts +++ b/test/v1.5/integration/veYMT/votingEscrow.test.ts @@ -71,7 +71,9 @@ describe("veYMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/integration/veYMT/zeroBalanceAtUnlockTime.test.ts b/test/v1.5/integration/veYMT/zeroBalanceAtUnlockTime.test.ts index 800e6d32..77138033 100644 --- a/test/v1.5/integration/veYMT/zeroBalanceAtUnlockTime.test.ts +++ b/test/v1.5/integration/veYMT/zeroBalanceAtUnlockTime.test.ts @@ -33,7 +33,9 @@ describe("veYMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/FeePool/checkpoints.test.ts b/test/v1.5/unitary/FeePool/checkpoints.test.ts index 0bd170a8..88663735 100644 --- a/test/v1.5/unitary/FeePool/checkpoints.test.ts +++ b/test/v1.5/unitary/FeePool/checkpoints.test.ts @@ -40,7 +40,9 @@ describe("FeePoolV2", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/FeePool/claimMany.test.ts b/test/v1.5/unitary/FeePool/claimMany.test.ts index 72848370..49b63dd8 100644 --- a/test/v1.5/unitary/FeePool/claimMany.test.ts +++ b/test/v1.5/unitary/FeePool/claimMany.test.ts @@ -41,7 +41,9 @@ describe("FeePoolV2", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/FeePool/feePool.test.ts b/test/v1.5/unitary/FeePool/feePool.test.ts index bddb2141..99185191 100644 --- a/test/v1.5/unitary/FeePool/feePool.test.ts +++ b/test/v1.5/unitary/FeePool/feePool.test.ts @@ -42,7 +42,9 @@ describe("FeePoolV2", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/FeePool/killFeeDistro.test.ts b/test/v1.5/unitary/FeePool/killFeeDistro.test.ts index 0812f096..387b10dc 100644 --- a/test/v1.5/unitary/FeePool/killFeeDistro.test.ts +++ b/test/v1.5/unitary/FeePool/killFeeDistro.test.ts @@ -35,7 +35,9 @@ describe("FeePoolV2", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/ScoreRegistry/checkpoint.test.ts b/test/v1.5/unitary/ScoreRegistry/checkpoint.test.ts index f6553d02..02ec19f1 100644 --- a/test/v1.5/unitary/ScoreRegistry/checkpoint.test.ts +++ b/test/v1.5/unitary/ScoreRegistry/checkpoint.test.ts @@ -145,7 +145,9 @@ describe("ScoreRegistry checkpoint", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/ScoreRegistry/kick.test.ts b/test/v1.5/unitary/ScoreRegistry/kick.test.ts index da57c094..7c8e561f 100644 --- a/test/v1.5/unitary/ScoreRegistry/kick.test.ts +++ b/test/v1.5/unitary/ScoreRegistry/kick.test.ts @@ -148,7 +148,9 @@ describe("ScoreRegistry kick", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/YMT/burn.test.ts b/test/v1.5/unitary/YMT/burn.test.ts index 94a67ed9..0204ca33 100644 --- a/test/v1.5/unitary/YMT/burn.test.ts +++ b/test/v1.5/unitary/YMT/burn.test.ts @@ -6,7 +6,12 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const ten_to_the_18 = Constants.ten_to_the_18; @@ -23,7 +28,9 @@ describe("YMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); }); beforeEach(async () => { @@ -63,12 +70,14 @@ describe("YMT", function () { // トークン供給の全量を正しくburnさせるテスト it("should correctly burn the entire token supply", async function () { - const balance: BigNumber = await YMT.balanceOf(accounts[0].address) + const balance: BigNumber = await YMT.balanceOf(accounts[0].address); await YMT.burn(balance); expect(await YMT.balanceOf(accounts[0].address)).to.equal(0); - expect(await YMT.totalSupply()).to.equal(BigNumber.from(250000000).mul(ten_to_the_18)); + expect(await YMT.totalSupply()).to.equal( + BigNumber.from(250000000).mul(ten_to_the_18) + ); }); // 総供給量を超える量をburnさせようとした場合にリバートするテスト diff --git a/test/v1.5/unitary/YMT/epochTimeSupply.test.ts b/test/v1.5/unitary/YMT/epochTimeSupply.test.ts index f6f2aa9b..ba0131a4 100644 --- a/test/v1.5/unitary/YMT/epochTimeSupply.test.ts +++ b/test/v1.5/unitary/YMT/epochTimeSupply.test.ts @@ -6,7 +6,12 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; -import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -23,7 +28,9 @@ describe("YMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); }); beforeEach(async () => { diff --git a/test/v1.5/unitary/YMT/inflationDelay.test.ts b/test/v1.5/unitary/YMT/inflationDelay.test.ts index 9cf68024..1668a937 100644 --- a/test/v1.5/unitary/YMT/inflationDelay.test.ts +++ b/test/v1.5/unitary/YMT/inflationDelay.test.ts @@ -6,7 +6,12 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; -import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const YEAR = Constants.YEAR; @@ -21,7 +26,9 @@ describe("YMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); }); beforeEach(async () => { diff --git a/test/v1.5/unitary/YMT/mint.test.ts b/test/v1.5/unitary/YMT/mint.test.ts index cae12a98..6eae4754 100644 --- a/test/v1.5/unitary/YMT/mint.test.ts +++ b/test/v1.5/unitary/YMT/mint.test.ts @@ -7,7 +7,12 @@ import { } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; import Constants from "../../Constants"; const week = Constants.week; @@ -25,7 +30,9 @@ describe("YMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); await time.increase(86401); await YMT.updateMiningParameters(); }); diff --git a/test/v1.5/unitary/YMT/setters.test.ts b/test/v1.5/unitary/YMT/setters.test.ts index ef97823d..c1354ee8 100644 --- a/test/v1.5/unitary/YMT/setters.test.ts +++ b/test/v1.5/unitary/YMT/setters.test.ts @@ -5,7 +5,12 @@ import { SnapshotRestorer, } from "@nomicfoundation/hardhat-network-helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { YMT, YMT__factory, YmtVesting, YmtVesting__factory } from "../../../../typechain"; +import { + YMT, + YMT__factory, + YmtVesting, + YmtVesting__factory, +} from "../../../../typechain"; describe("YMT", function () { let accounts: SignerWithAddress[]; @@ -19,7 +24,9 @@ describe("YMT", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); }); beforeEach(async () => { diff --git a/test/v1.5/unitary/YmtMinter/Minter.test.ts b/test/v1.5/unitary/YmtMinter/Minter.test.ts index 652900ae..9071cbf7 100644 --- a/test/v1.5/unitary/YmtMinter/Minter.test.ts +++ b/test/v1.5/unitary/YmtMinter/Minter.test.ts @@ -150,7 +150,9 @@ describe("YmtMinter", function () { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") diff --git a/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts b/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts index a874bab1..4e97d2e9 100644 --- a/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts +++ b/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts @@ -28,7 +28,9 @@ describe("veYMT", () => { await ethers.getContractFactory("YmtVesting") )).deploy(); - YMT = await (await ethers.getContractFactory("YMT")).deploy(YmtVesting.address); + YMT = await (await ethers.getContractFactory("YMT")).deploy( + YmtVesting.address + ); veYMT = await (( await ethers.getContractFactory("veYMT") From e2a6d08115166202b3d013d7a599c1f225c11ec5 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 12 Dec 2023 16:41:33 +0900 Subject: [PATCH 130/412] Remove unwanted modifiers. --- contracts/FeePoolV2.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/contracts/FeePoolV2.sol b/contracts/FeePoolV2.sol index 65d76112..4285a60b 100644 --- a/contracts/FeePoolV2.sol +++ b/contracts/FeePoolV2.sol @@ -20,8 +20,6 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { string constant VEYMT_SLOT_ID = "deps.veYMT"; - mapping(address => bool) protocolWhitelist; - uint256 public startTime; uint256 public timeCursor; mapping(address => uint256) public timeCursorOf; @@ -582,11 +580,6 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { } } - modifier onlyProtocols() { - require(protocolWhitelist[msg.sender], "You are not in the whitelist"); - _; - } - receive() external payable { emit Received(msg.sender, msg.value); } From 8551de80af3b624f94553d56d157a7b91fa303f1 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Tue, 12 Dec 2023 16:52:24 +0900 Subject: [PATCH 131/412] Refactoring function comments in contracts. --- contracts/FeePoolV2.sol | 16 +++--- contracts/ScoreRegistry.sol | 49 ++++++++++++++++++ contracts/ScoreWeightController.sol | 48 +++++++++--------- contracts/YMT.sol | 78 ++++++++++++++++++----------- contracts/YmtMinter.sol | 34 ++++++------- contracts/YmtVesting.sol | 6 +-- contracts/veYMT.sol | 48 +++++++++--------- 7 files changed, 173 insertions(+), 106 deletions(-) diff --git a/contracts/FeePoolV2.sol b/contracts/FeePoolV2.sol index 4285a60b..10447465 100644 --- a/contracts/FeePoolV2.sol +++ b/contracts/FeePoolV2.sol @@ -46,7 +46,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { event Received(address sender, uint256 value); event VeYMTSet(address sender, address veYMT); - /*** + /** * @param startTime_ Epoch time for fee distribution to start */ function initialize(uint256 startTime_) public initializer { @@ -105,7 +105,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { emit CheckpointToken(block.timestamp, _toDistribute); } - /*** + /** * @notice Update the token checkpoint * @dev Calculates the total number of tokens to be distributed in a given week. During setup for the initial distribution this function is only callable @@ -182,7 +182,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { return _min; } - /*** + /** * @notice Get the veYMT balance for `user_` at `timestamp_` * @param user_ Address to query balance for * @param timestamp_ Epoch time @@ -238,7 +238,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { timeCursor = _t; } - /*** + /** * @notice Update the veYMT total supply checkpoint * @dev The checkpoint is also updated by the first claimant each new epoch week. This function may be called independently of a claim, to reduce claiming gas costs. */ @@ -398,7 +398,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { return _toDistribute; } - /*** + /** * @notice Claim fees for `msg.sender` * @dev Each call to claim look at a maximum of 50 user veYMT points. For accounts with many veYMT related actions, this function @@ -438,7 +438,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { return _amount; } - /*** + /** * @notice Claim fees for `addr_` * @dev Each call to claim look at a maximum of 50 user veYMT points. For accounts with many veYMT related actions, this function @@ -532,7 +532,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { return true; } - /*** + /** * @notice Toggle permission for checkpointing by any account */ function toggleAllowCheckpointToken() external onlyGovernance { @@ -552,7 +552,7 @@ contract FeePoolV2 is IFeePoolV2, UUPSBase, ReentrancyGuardUpgradeable { require(success, "Transfer failed"); } - /*** + /** * @notice Recover native token from this contract * @dev Tokens are sent to the emergency return address. * @return bool success diff --git a/contracts/ScoreRegistry.sol b/contracts/ScoreRegistry.sol index 2b4b3154..e7e46344 100644 --- a/contracts/ScoreRegistry.sol +++ b/contracts/ScoreRegistry.sol @@ -73,6 +73,11 @@ contract ScoreRegistry is YamatoAction { mapping(int128 => uint256) public periodTimestamp; mapping(int128 => uint256) public integrateInvSupply; + /** + * @notice Initializes the contract with given parameters. + * @param ymtMinterAddr The address of the YMT minter. + * @param yamatoAddr The address of the Yamato contract. + */ function initialize( address ymtMinterAddr, address yamatoAddr @@ -107,10 +112,18 @@ contract ScoreRegistry is YamatoAction { futureEpochTime = IYMT(ymtAddr).futureEpochTimeWrite(); } + /** + * @notice Updates the checkpoint for a specified address. + * @param addr The address to update the checkpoint for. + */ function checkpoint(address addr) public onlyYamato { _checkpoint(addr); } + /** + * @notice Internal function to update the checkpoint for a specified address. + * @param addr The address to update the checkpoint for. + */ function _checkpoint(address addr) private { CheckPointParameters memory _st; @@ -198,6 +211,13 @@ contract ScoreRegistry is YamatoAction { integrateCheckpointOf[addr] = block.timestamp; } + /** + * @notice Updates the score limit for a user based on their collateral ratio and total debt. + * @param addr_ The address of the user. + * @param debt_ The user's debt amount. + * @param totalDebt_ The total debt amount in the system. + * @param collateralRatio_ The user's collateral ratio. + */ function updateScoreLimit( address addr_, uint256 debt_, @@ -207,6 +227,13 @@ contract ScoreRegistry is YamatoAction { _updateScoreLimit(addr_, debt_, totalDebt_, collateralRatio_); } + /** + * @notice Internal function to calculate and update score limits for a user. + * @param addr_ The address of the user. + * @param debt_ The user's debt amount. + * @param totalDebt_ The total debt amount in the system. + * @param collateralRatio_ The user's collateral ratio. + */ function _updateScoreLimit( address addr_, uint256 debt_, @@ -248,6 +275,11 @@ contract ScoreRegistry is YamatoAction { ); } + /** + * @notice Calculates the coefficient based on the collateral ratio. + * @param collateralRatio_ The collateral ratio to calculate the coefficient for. + * @return The calculated coefficient. + */ function calculateCoefficient( uint256 collateralRatio_ ) internal pure returns (uint256) { @@ -258,6 +290,11 @@ contract ScoreRegistry is YamatoAction { return 0; } + /** + * @notice Allows a user to checkpoint their score. + * @param addr_ The address of the user. + * @return True if the operation was successful. + */ function userCheckpoint(address addr_) external returns (bool) { require( msg.sender == addr_ || msg.sender == ymtMinter(), @@ -272,6 +309,10 @@ contract ScoreRegistry is YamatoAction { return true; } + /** + * @notice Kicks a user for abusing their boost, resetting their score limit. + * @param addr_ The address of the user to kick. + */ function kick(address addr_) external { uint256 _tLast = integrateCheckpointOf[addr_]; uint256 _tVe = IveYMT(veYMT()).userPointHistoryTs( @@ -298,10 +339,18 @@ contract ScoreRegistry is YamatoAction { _updateScoreLimit(addr_, _balance, _totalSupply, _collateralRatio); } + /** + * @notice Sets the killed status of the contract. + * @param isKilled_ The status to set. + */ function setKilled(bool isKilled_) external onlyGovernance { isKilled = isKilled_; } + /** + * @notice Gets the timestamp of the last checkpoint. + * @return The timestamp of the last checkpoint. + */ function integrateCheckpoint() external view returns (uint256) { return periodTimestamp[period]; } diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 566f22f9..247c72c5 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -5,9 +5,9 @@ pragma solidity 0.8.4; * Copyright (C) 2023 Yamato Protocol (DeFiGeek Community Japan) */ -/*** - *@title Score Controller - *@notice Controls liquidity scores and the issuance of token through the scores +/** + * @title Score Controller + * @notice Controls liquidity scores and the issuance of token through the scores */ //dao-contracts @@ -27,10 +27,10 @@ contract ScoreWeightController is UUPSBase { // Needed for enumeration mapping(address => int128) public scores; - /*** - *@notice Contract constructor - *@param _token `Token` contract address - *@param _votingEscrow `VotingEscrow` contract address + /** + * @notice Contract constructor + * @param ymtAddr `Token` contract address + * @param veYmtAddr `VotingEscrow` contract address */ function initialize(address ymtAddr, address veYmtAddr) public initializer { require(ymtAddr != address(0)); @@ -45,10 +45,10 @@ contract ScoreWeightController is UUPSBase { } } - /*** - *@notice Add Currency `addr` of type `score_type` with weight `weight` - *@param addr_ Score address - *@param scoreType_ Score type + /** + * @notice Add Currency `addr` of type `score_type` with weight `weight` + * @param addr_ Score address + * @param weight_ Score type */ function addCurrency( address addr_, @@ -63,28 +63,28 @@ contract ScoreWeightController is UUPSBase { emit NewScore(addr_, weight_); } - /*** + /** * @notice Checkpoint to fill data common for all scores */ function checkpoint() external { // Add to V2.0 } - /*** - *@notice Checkpoint to fill data for both a specific score and common for all scores - *@param addr_ Score address + /** + * @notice Checkpoint to fill data for both a specific score and common for all scores + * @param addr_ Score address */ function checkpointScore(address addr_) external { // Add to V2.0 } - /*** - *@notice Get Score relative weight (not more than 1.0) normalized to 1e18 + /** + * @notice Get Score relative weight (not more than 1.0) normalized to 1e18 * (e.g. 1.0 == 1e18). Inflation which will be received by it is * inflation_rate * relative_weight / 1e18 - *@param addr_ Score address - *@param time_ Relative weight at the specified timestamp in the past or present - *@return Value of relative weight normalized to 1e18 + * @param addr_ Score address + * @param time_ Relative weight at the specified timestamp in the past or present + * @return Value of relative weight normalized to 1e18 */ function scoreRelativeWeight( address addr_, @@ -93,10 +93,10 @@ contract ScoreWeightController is UUPSBase { return MULTIPLIER; } - /*** - *@notice Change weight of score `addr` to `weight` - *@param addr_ `ScoreController` contract address - *@param weight_ New Score weight + /** + * @notice Change weight of score `addr` to `weight` + * @param addr_ `ScoreController` contract address + * @param weight_ New Score weight */ function changeScoreWeight( address addr_, diff --git a/contracts/YMT.sol b/contracts/YMT.sol index 2d36e758..c97217e0 100644 --- a/contracts/YMT.sol +++ b/contracts/YMT.sol @@ -60,8 +60,10 @@ contract YMT is ERC20Permit { startEpochSupply = INITIAL_SUPPLY + VESTING_SUPPLY; } - // @dev Update mining rate and supply at the start of the epoch - // Any modifying mining call must also call this + /** + * @dev Update mining rate and supply at the start of the epoch + * Any modifying mining call must also call this + */ function _updateMiningParameters() internal { uint256 _rate = rate; uint256 _startEpochSupply = startEpochSupply; @@ -82,9 +84,11 @@ contract YMT is ERC20Permit { emit UpdateMiningParameters(block.timestamp, _rate, _startEpochSupply); } - // @notice Update mining rate and supply at the start of the epoch - // @dev Callable by any address, but only once per epoch - // Total supply becomes slightly larger if(this function is called late + /** + * @notice Update mining rate and supply at the start of the epoch + * @dev Callable by any address, but only once per epoch + * Total supply becomes slightly larger if(this function is called late + */ function updateMiningParameters() external { require( block.timestamp >= startEpochTime + RATE_REDUCTION_TIME, @@ -93,9 +97,11 @@ contract YMT is ERC20Permit { _updateMiningParameters(); } - // @notice Get timestamp of the current mining epoch start - // while simultaneously updating mining parameters - // @return Timestamp of the epoch + /** + * @notice Get timestamp of the current mining epoch start + * while simultaneously updating mining parameters + * @return Timestamp of the epoch + */ function startEpochTimeWrite() external returns (uint256) { uint256 _startEpochTime = startEpochTime; if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { @@ -106,9 +112,11 @@ contract YMT is ERC20Permit { } } - // @notice Get timestamp of the next mining epoch start - // while simultaneously updating mining parameters - // @return Timestamp of the next epoch + /** + * @notice Get timestamp of the next mining epoch start + * while simultaneously updating mining parameters + * @return Timestamp of the next epoch + */ function futureEpochTimeWrite() external returns (uint256) { uint256 _startEpochTime = startEpochTime; if (block.timestamp >= _startEpochTime + RATE_REDUCTION_TIME) { @@ -128,10 +136,12 @@ contract YMT is ERC20Permit { return _availableSupply(); } - // @notice How much supply is mintable from start timestamp till end timestamp - // @param start Start of the time interval (timestamp) - // @param end End of the time interval (timestamp) - // @return Tokens mintable from `start` till `end` + /** + * @notice How much supply is mintable from start timestamp till end timestamp + * @param start Start of the time interval (timestamp) + * @param end End of the time interval (timestamp) + * @return Tokens mintable from `start` till `end` + */ function mintableInTimeframe( uint256 start, uint256 end @@ -188,9 +198,11 @@ contract YMT is ERC20Permit { return to_mint; } - // @notice Set the ymtMinter address - // @dev Only callable once, when ymtMinter has not yet been set - // @param _ymtMinter Address of the ymtMinter + /** + * @notice Set the ymtMinter address + * @dev Only callable once, when ymtMinter has not yet been set + * @param _ymtMinter Address of the ymtMinter + */ function setMinter(address _ymtMinter) external onlyAdmin { require( _ymtMinter != address(0), @@ -200,19 +212,23 @@ contract YMT is ERC20Permit { emit SetMinter(_ymtMinter); } - // @notice Set the new admin. - // @dev After all is set up, admin only can change the token name - // @param _admin New admin address + /** + * @notice Set the new admin. + * @dev After all is set up, admin only can change the token name + * @param _admin New admin address + */ function setAdmin(address _admin) external onlyAdmin { admin = _admin; emit SetAdmin(_admin); } - // @notice Mint `_value` tokens and assign them to `_to` - // @dev Emits a Transfer event originating from 0x00 - // @param _to The account that will receive the created tokens - // @param _value The amount that will be created - // @return bool success + /** + * @notice Mint `_value` tokens and assign them to `_to` + * @dev Emits a Transfer event originating from 0x00 + * @param _to The account that will receive the created tokens + * @param _value The amount that will be created + * @return bool success + */ function mint(address _to, uint256 _value) external returns (bool) { require(msg.sender == ymtMinter, "dev: ymtMinter only"); // dev: ymtMinter only require(_to != address(0), "dev: zero address"); // dev: zero address @@ -230,10 +246,12 @@ contract YMT is ERC20Permit { return true; } - // @notice Burn `_value` tokens belonging to `msg.sender` - // @dev Emits a Transfer event with a destination of 0x00 - // @param _value The amount that will be burned - // @return bool success + /** + * @notice Burn `_value` tokens belonging to `msg.sender` + * @dev Emits a Transfer event with a destination of 0x00 + * @param _value The amount that will be burned + * @return bool success + */ function burn(uint256 _value) external returns (bool) { _burn(msg.sender, _value); return true; diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index cd4d1cfa..97bd5b0c 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -15,8 +15,8 @@ import "./Dependencies/UUPSBase.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -/*** - *@title Token Minter +/** + * @title Token Minter */ contract YmtMinter is @@ -71,18 +71,18 @@ contract YmtMinter is } } - /*** - *@notice Mint everything which belongs to `msg.sender` and send to them - *@param scoreAddr_ `LiquidityScore` address to get mintable amount from + /** + * @notice Mint everything which belongs to `msg.sender` and send to them + * @param scoreAddr_ `ScoreRegistry` address to get mintable amount from */ function mint(address scoreAddr_) external nonReentrant { _mintFor(scoreAddr_, msg.sender); } - /*** - *@notice Mint everything which belongs to `msg.sender` across multiple scores - *@param scoreAddrs_ List of `LiquidityScore` addresses - *@dev address[8]: 8 has randomly decided and has no meaning. + /** + * @notice Mint everything which belongs to `msg.sender` across multiple scores + * @param scoreAddrs_ List of `ScoreRegistry` addresses + * @dev address[8]: 8 has randomly decided and has no meaning. */ // function mintMany(address[8] memory scoreAddrs_) external nonReentrant { // for (uint256 i; i < 8; ) { @@ -96,11 +96,11 @@ contract YmtMinter is // } // } - /*** - *@notice Mint tokens for `for_` - *@dev Only possible when `msg.sender` has been approved via `toggle_approve_mint` - *@param scoreAddr_ `LiquidityScore` address to get mintable amount from - *@param for_ Address to mint to + /** + * @notice Mint tokens for `for_` + * @dev Only possible when `msg.sender` has been approved via `toggle_approve_mint` + * @param scoreAddr_ `ScoreRegistry` address to get mintable amount from + * @param for_ Address to mint to */ function mintFor(address scoreAddr_, address for_) external nonReentrant { if (allowedToMintFor[msg.sender][for_]) { @@ -108,9 +108,9 @@ contract YmtMinter is } } - /*** - *@notice allow `mintingUser` to mint for `msg.sender` - *@param mintingUser_ Address to toggle permission for + /** + * @notice allow `mintingUser` to mint for `msg.sender` + * @param mintingUser_ Address to toggle permission for */ function toggleApproveMint(address mintingUser_) external { allowedToMintFor[mintingUser_][msg.sender] = !allowedToMintFor[ diff --git a/contracts/YmtVesting.sol b/contracts/YmtVesting.sol index 4fc0a5de..7245927b 100644 --- a/contracts/YmtVesting.sol +++ b/contracts/YmtVesting.sol @@ -69,8 +69,8 @@ contract YmtVesting { } /** - * @notice Allows the admin to claim tokens from five-year linear distribution. - * @dev Transfers claimable tokens to the admin address. + * @notice Allows users to claim their V1 Retroactive Rewards based on a one-year linear vesting schedule. + * @dev Calculates the claimable amount based on the time elapsed since the distribution start, then transfers the tokens to the user's address. Any unclaimed amount from the previous claims is considered. */ function claimV1RetroactiveRewards() external { require(vestingAmounts[msg.sender] > 0, "No tokens to claim"); @@ -116,7 +116,7 @@ contract YmtVesting { /** * @notice Allows the admin to claim tokens from two-year vesting period. - * @dev Transfers tokens to the admin address if within the two-year period. + * @dev Transfers claimable tokens to the admin address. */ function claimTwoYearVestingTokens() external onlyAdmin { uint256 distributionStart = IYMT(ymtTokenAddress).startTime(); diff --git a/contracts/veYMT.sol b/contracts/veYMT.sol index 59d6ae25..86e4f076 100644 --- a/contracts/veYMT.sol +++ b/contracts/veYMT.sol @@ -90,7 +90,7 @@ contract veYMT is ReentrancyGuard { address public admin; address public futureAdmin; - /*** + /** * @notice Contract constructor * @param tokenAddr_ `YMT` token address */ @@ -108,16 +108,16 @@ contract veYMT is ReentrancyGuard { symbol = "veYMT"; } - /*** + /** * @notice Transfer ownership of VotingEscrow contract to `addr_` - * @param addr Address to have ownership transferred to + * @param addr_ Address to have ownership transferred to */ function commitTransferOwnership(address addr_) external onlyAdmin { futureAdmin = addr_; emit CommitOwnership(addr_); } - /*** + /** * @notice Apply ownership transfer */ function applyTransferOwnership() external onlyAdmin { @@ -127,7 +127,7 @@ contract veYMT is ReentrancyGuard { emit ApplyOwnership(_admin); } - /*** + /** * @notice Get the most recently recorded rate of voting power decrease for `addr` * @param addr_ Address of the user wallet * @return Value of the slope @@ -137,7 +137,7 @@ contract veYMT is ReentrancyGuard { return userPointHistory[addr_][_uEpoch].slope; } - /*** + /** * @notice Get the timestamp for checkpoint `idx_` for `addr_` * @param addr_ User wallet address * @param idx_ User epoch number @@ -150,7 +150,7 @@ contract veYMT is ReentrancyGuard { return userPointHistory[addr_][idx_].ts; } - /*** + /** * @notice Get timestamp when `addr_`'s lock finishes * @param addr_ User wallet * @return Epoch time of the lock end @@ -159,7 +159,7 @@ contract veYMT is ReentrancyGuard { return locked[addr_].end; } - /*** + /** * @notice Record global and per-user data to checkpoint * @param addr_ User's wallet address. No user checkpoint if 0x0 * @param oldLocked_ Pevious locked amount / end lock time for the user @@ -356,7 +356,7 @@ contract veYMT is ReentrancyGuard { } } - /*** + /** * @notice Deposit and lock tokens for a user * @param addr_ User's wallet address * @param value_ Amount to deposit @@ -406,7 +406,7 @@ contract veYMT is ReentrancyGuard { emit Supply(_supplyBefore, _supplyBefore + value_); } - /*** + /** * @notice Record global data to checkpoint */ function checkpoint() external { @@ -415,7 +415,7 @@ contract veYMT is ReentrancyGuard { _checkpoint(address(0), _old, _new); } - /*** + /** * @notice Deposit `_value` tokens for `_addr` and add to the lock * @dev Anyone (even a smart contract) can deposit for someone else, but cannot extend their locktime and deposit for a brand new user @@ -435,7 +435,7 @@ contract veYMT is ReentrancyGuard { _depositFor(addr_, value_, 0, locked[addr_], DEPOSIT_FOR_TYPE); } - /*** + /** * @notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time` * @param value_ Amount to deposit * @param unlockTime_ Epoch time when tokens unlock, rounded down to whole weeks @@ -467,7 +467,7 @@ contract veYMT is ReentrancyGuard { ); } - /*** + /** * @notice Deposit `_value` additional tokens for `msg.sender` without modifying the unlock time * @param value_ Amount of tokens to deposit and add to the lock @@ -485,7 +485,7 @@ contract veYMT is ReentrancyGuard { _depositFor(msg.sender, value_, 0, _locked, INCREASE_LOCK_AMOUNT); } - /*** + /** * @notice Extend the unlock time for `msg.sender` to `_unlock_time` * @param unlockTime_ New epoch time for unlocking */ @@ -516,7 +516,7 @@ contract veYMT is ReentrancyGuard { ); } - /*** + /** * @notice Withdraw all tokens for `msg.sender` * @dev Only possible if the lock has expired */ @@ -554,7 +554,7 @@ contract veYMT is ReentrancyGuard { // They measure the weights for the purpose of voting, so they don't represent // real coins. - /*** + /** * @notice Binary search to estimate epoch for block number * @param block_ Block to find * @param maxEpoch_ Don't go beyond this epoch @@ -584,7 +584,7 @@ contract veYMT is ReentrancyGuard { return _min; } - /*** + /** * @notice Get the current voting power for `msg.sender` * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility * @param addr_ User wallet address @@ -610,7 +610,7 @@ contract veYMT is ReentrancyGuard { } } - /*** + /** * @notice Get the current voting power for `msg.sender` * @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility * @param addr_ User wallet address @@ -632,7 +632,7 @@ contract veYMT is ReentrancyGuard { } } - /*** + /** * @notice Measure voting power of `addr` at block height `_block` * @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime * @param addr_ User's wallet address @@ -697,7 +697,7 @@ contract veYMT is ReentrancyGuard { } } - /*** + /** * @notice Calculate total voting power at some point in the past * @param point_ The point (bias/slope) to start search from * @param t_ Time to calculate the total voting power at @@ -739,7 +739,7 @@ contract veYMT is ReentrancyGuard { return uint256(int256(_lastPoint.bias)); } - /*** + /** * @notice Calculate total voting power * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility * @param t_ Time to calculate the total voting power at @@ -751,7 +751,7 @@ contract veYMT is ReentrancyGuard { return supplyAt(_lastPoint, t_); } - /*** + /** * @notice Calculate total voting power * @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility * @return Total voting power @@ -762,7 +762,7 @@ contract veYMT is ReentrancyGuard { return supplyAt(_lastPoint, block.timestamp); } - /*** + /** * @notice Calculate total voting power at some point in the past * @param block_ Block to calculate the total voting power at * @return Total voting power at `_block` @@ -796,7 +796,7 @@ contract veYMT is ReentrancyGuard { return supplyAt(_point, _point.ts + _dt); } - /*** + /** * @dev Dummy method required for Aragon compatibility */ function changeController(address newController) external { From 0cc6922eee9465c3fecc96c9c4bc8284f2c8e119 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 13 Dec 2023 16:40:27 +0900 Subject: [PATCH 132/412] Updated documentation. --- doc/v1.5/Controller/index.md | 296 ----------------------- doc/v1.5/FeePoolV2/index.md | 129 ++++++++++ doc/v1.5/Gauge/index.md | 198 ---------------- doc/v1.5/GaugeController/class.md | 68 ------ doc/v1.5/Minter/index.md | 93 -------- doc/v1.5/ScoreRegistry/index.md | 136 +++++++++++ doc/v1.5/ScoreWeightController/index.md | 80 +++++++ doc/v1.5/YMT/index.md | 215 +++++++---------- doc/v1.5/YmtMinter/index.md | 83 +++++++ doc/v1.5/veYMT/index.md | 303 +++++++++--------------- 10 files changed, 620 insertions(+), 981 deletions(-) delete mode 100644 doc/v1.5/Controller/index.md create mode 100644 doc/v1.5/FeePoolV2/index.md delete mode 100644 doc/v1.5/Gauge/index.md delete mode 100644 doc/v1.5/GaugeController/class.md delete mode 100644 doc/v1.5/Minter/index.md create mode 100644 doc/v1.5/ScoreRegistry/index.md create mode 100644 doc/v1.5/ScoreWeightController/index.md create mode 100644 doc/v1.5/YmtMinter/index.md diff --git a/doc/v1.5/Controller/index.md b/doc/v1.5/Controller/index.md deleted file mode 100644 index e3bfe2e6..00000000 --- a/doc/v1.5/Controller/index.md +++ /dev/null @@ -1,296 +0,0 @@ -## 定数 - -### `WEEK: constant(uint256) = 604800` - -- **説明**: 1 週間の秒数。現在の値は 604800 秒です。 - -### `WEIGHT_VOTE_DELAY: constant(uint256) = 10 * 86400` - -- **説明**: 重みの投票を変更することができるのは 10 日に 1 回だけです。現在の値は 10 日(864,000 秒)です。 - -### `MULTIPLIER: constant(uint256) = 10 ** 18` - -- **説明**: 多数の固定小数点数を扱うための乗数。現在の値は 10 の 18 乗です。 - ---- - -## 変数 - -### `admin: public(address)` - -- **説明**: 管理者のアドレス。このアドレスはスマートコントラクトに許可されています。 - -### `future_admin: public(address)` - -- **説明**: 未来の管理者のアドレス。所有権の移転が計画されています。 - -### `token: public(address)` - -- **説明**: CRV トークンのアドレス。 - -### `voting_escrow: public(address)` - -- **説明**: 投票エスクローのアドレス。 - -### `n_gauge_types: public(int128)` - -- **説明**: 使用可能なゲージの種類の数。 - -### `n_gauges: public(int128)` - -- **説明**: 現在のゲージの数。 - -### `gauge_type_names: public(HashMap[int128, String[64]])` - -- **説明**: 各ゲージの種類の名前を格納するマップ。 - -### `gauges: public(address[1000000000])` - -- **説明**: ゲージのアドレスを格納するリスト。 - -### `gauge_types_: HashMap[address, int128]` - -- **説明**: 各ゲージアドレスとそのタイプを関連付けるマップ。 - -### `vote_user_slopes: public(HashMap[address, HashMap[address, VotedSlope]])` - -- **説明**: ユーザーの各ゲージアドレスに対する投票傾斜を格納するマップ。 - -### `vote_user_power: public(HashMap[address, uint256])` - -- **説明**: ユーザーが使用する総投票パワー。 - -### `last_user_vote: public(HashMap[address, HashMap[address, uint256]])` - -- **説明**: 各ゲージアドレスの最後のユーザー投票のタイムスタンプ。 - -### `points_weight: public(HashMap[address, HashMap[uint256, Point]])` - -- **説明**: 各ゲージアドレスと時間に対する点数の重みを格納するマップ。 - -### `changes_weight: HashMap[address, HashMap[uint256, uint256]]` - -- **説明**: 各ゲージアドレスと時間に対する重みの変更を格納するマップ。 - -### `time_weight: public(HashMap[address, uint256])` - -- **説明**: 各ゲージアドレスの最後のスケジュールされた時間。 - -### `points_sum: public(HashMap[int128, HashMap[uint256, Point]])` - -- **説明**: ゲージタイプと時間に対する点の合計を格納するマップ。 - -### `changes_sum: HashMap[int128, HashMap[uint256, uint256]]` - -- **説明**: ゲージタイプと時間に対する合計の変更を格納するマップ。 - -### `time_sum: public(uint256[1000000000])` - -- **説明**: ゲージタイプの最後のスケジュールされた時間。 - -### `points_total: public(HashMap[uint256, uint256])` - -- **説明**: 時間ごとの総重みを格納するマップ。 - -### `time_total: public(uint256)` - -- **説明**: 総重みの最後のスケジュールされた時間。 - ---- - -## イベント - -### `CommitOwnership` - -- **説明**: 所有権のコミットイベント。 -- **パラメータ**: - - `admin`: 管理者のアドレス。 - -### `ApplyOwnership` - -- **説明**: 所有権の適用イベント。 -- **パラメータ**: - - `admin`: 管理者のアドレス。 - -### `AddType` - -- **説明**: 新しいタイプの追加イベント。 -- **パラメータ**: - - `name`: タイプの名前。 - - `type_id`: タイプ ID。 - -### `NewTypeWeight` - -- **説明**: 新しいタイプの重みイベント。 -- **パラメータ**: - - `type_id`: タイプ ID。 - - `time`: タイムスタンプ。 - - `weight`: 重み。 - - `total_weight`: 総重量。 - -### `NewGaugeWeight` - -- **説明**: 新しいゲージの重みイベント。 -- **パラメータ**: - - `gauge_address`: ゲージのアドレス。 - - `time`: タイムスタンプ。 - - `weight`: 重み。 - - `total_weight`: 総重量。 - -### `VoteForGauge` - -- **説明**: ゲージへの投票イベント。 -- **パラメータ**: - - `time`: タイムスタンプ。 - - `user`: ユーザーのアドレス。 - - `gauge_addr`: ゲージのアドレス。 - - `weight`: 重み。 - -### `NewGauge` - -- **説明**: 新しいゲージのイベント。 -- **パラメータ**: - - `addr`: ゲージのアドレス。 - - `gauge_type`: ゲージのタイプ。 - - `weight`: 重み。 - ---- - -## 関数 - -### `__init__(_token: address, _voting_escrow: address)` - -- **説明**: コントラクトの初期化関数。初期設定を行います。 -- **パラメータ**: - - `_token`: `ERC20CRV` コントラクトのアドレス。 - - `_voting_escrow`: `VotingEscrow` コントラクトのアドレス。 - -### `commit_transfer_ownership(addr: address)` - -- **説明**: GaugeController の所有権を`addr`に転送するためのコミット関数。 -- **パラメータ**: - - `addr`: 所有権が転送されるアドレス。 - -### `apply_transfer_ownership()` - -- **説明**: 保留中の所有権転送を適用する関数。 - -### `gauge_types(_addr: address) -> int128` - -- **説明**: 指定したアドレスのゲージタイプを取得する関数。 -- **パラメータ**: - - `_addr`: ゲージのアドレス。 - -### `_get_type_weight(gauge_type: int128) -> uint256` - -- **説明**: 歴史的なタイプの重みを計算し、未チェックインの週に記入します。また、次の週のタイプの重みを返します。 -- **パラメータ**: - - `gauge_type`: ゲージタイプの ID。 - -### `_get_sum(gauge_type: int128) -> uint256` - -- **説明**: 同じタイプのゲージの重みの合計を計算し、未チェックインの週に記入します。また、次の週の合計を返します。 -- **パラメータ**: - - `gauge_type`: ゲージタイプの ID。 - -### `_get_total() -> uint256` - -- **説明**: 歴史的な全体の重みを計算し、未チェックインの週に記入します。また、次の週の合計を返します。 - -### `_get_weight(gauge_addr: address) -> uint256` - -- **説明**: 歴史的なゲージの重みを計算し、未チェックインの週に記入します。また、次の週のゲージの重みを返します。 -- **パラメータ**: - - `gauge_addr`: ゲージのアドレス。 - -### `add_gauge(addr: address, gauge_type: int128, weight: uint256 = 0)` - -- **説明**: 指定されたタイプと重みでゲージを追加します。 -- **パラメータ**: - - `addr`: ゲージのアドレス。 - - `gauge_type`: ゲージタイプ。 - - `weight`: ゲージの重み。 - -### `checkpoint()` - -- **説明**: すべてのゲージに共通のデータを記入するためのチェックポイント。 - -### `checkpoint_gauge(addr: address)` - -- **説明**: 特定のゲージとすべてのゲージに共通のデータを記入するためのチェックポイント。 -- **パラメータ**: - - `addr`: ゲージのアドレス。 - -### `_gauge_relative_weight(addr: address, time: uint256) -> uint256` - -- **説明**: 指定されたタイムスタンプでのゲージの相対重み(1.0 を超えない)を取得します。 -- **パラメータ**: - - `addr`: ゲージのアドレス。 - - `time`: 指定したタイムスタンプ。 - -### `gauge_relative_weight(addr: address, time: uint256 = block.timestamp) -> uint256` - -- **説明**: 指定されたタイムスタンプでのゲージの相対重みを取得します。 -- **パラメータ**: - - `addr`: ゲージのアドレス。 - - `time`: 指定したタイムスタンプ。 - -### `gauge_relative_weight_write(addr: address, time: uint256 = block.timestamp) -> uint256` - -- **説明**: - -指定されたタイムスタンプでのゲージの相対重みを書き込みます。 - -- **パラメータ**: - - `addr`: ゲージのアドレス。 - - `time`: 指定したタイムスタンプ。 - -### `add_type(_name: String[64], weight: uint256 = 0)` - -- **説明**: ゲージタイプを追加します。ゲージタイプ名 `_name` と重み `weight` を指定して追加します。 -- **パラメータ**: - - `_name`: ゲージタイプの名前。 - - `weight`: ゲージタイプの重み。 - -### `change_type_weight(type_id: int128, weight: uint256)` - -- **説明**: ゲージタイプの重みを変更します。 -- **パラメータ**: - - `type_id`: ゲージタイプ ID。 - - `weight`: 新しいゲージの重み。 - -### `change_gauge_weight(addr: address, weight: uint256)` - -- **説明**: ゲージの重みを変更します。 -- **パラメータ**: - - `addr`: `GaugeController` コントラクトのアドレス。 - - `weight`: 新しいゲージの重み。 - -### `vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256)` - -- **説明**: プールの重みを変更するための投票パワーを割り当てます。 -- **パラメータ**: - - `_gauge_addr`: 投票対象のゲージアドレス。 - - `_user_weight`: ゲージの重み (bps 単位、0.01%の単位)。 - -### `get_gauge_weight(addr: address) -> uint256` - -- **説明**: 現在のゲージの重みを取得します。 -- **パラメータ**: - - `addr`: ゲージのアドレス。 - -### `get_type_weight(type_id: int128) -> uint256` - -- **説明**: 現在のタイプの重みを取得します。 -- **パラメータ**: - - `type_id`: タイプ ID。 - -### `get_total_weight() -> uint256` - -- **説明**: 現在の合計 (タイプごとの重み付け) 重みを取得します。 - -### `get_weights_sum_per_type(type_id: int128) -> uint256` - -- **説明**: タイプごとのゲージの重みの合計を取得します。 -- **パラメータ**: - - `type_id`: タイプ ID。 diff --git a/doc/v1.5/FeePoolV2/index.md b/doc/v1.5/FeePoolV2/index.md new file mode 100644 index 00000000..7e81b69d --- /dev/null +++ b/doc/v1.5/FeePoolV2/index.md @@ -0,0 +1,129 @@ +# Fee Pool V2 + +## 概要 + +Fee Pool V2は、手数料プールの管理と手数料の分配を行うコントラクトです。veYMT(投票エスクローYMT)トークンの所有者に対して、プール内のトークンを分配します。 + +--- + +## イベント + +### `ToggleAllowCheckpointToken`: + +- **説明**: トークンのチェックポイント許可が切り替えられたときにトリガーされるイベント。 +- **パラメータ**: + - `toggleFlag`: 切り替えのフラグ。 + +### `CheckpointToken`: + +- **説明**: トークンのチェックポイントが更新されたときにトリガーされるイベント。 +- **パラメータ**: + - `time`: チェックポイントのタイムスタンプ。 + - `tokens`: 分配されたトークンの量。 + +### `Claimed`: + +- **説明**: 手数料が請求されたときにトリガーされるイベント。 +- **パラメータ**: + - `recipient`: 受取人のアドレス。 + - `amount`: 請求された手数料の量。 + - `claimEpoch`: 請求されたエポック。 + - `maxEpoch`: 最大エポック。 + +### `Received`: + +- **説明**: トークンがコントラクトに送られたときにトリガーされるイベント。 +- **パラメータ**: + - `sender`: 送信者のアドレス。 + - `value`: 送信されたトークンの量。 + +### `VeYMTSet`: + +- **説明**: veYMTのアドレスが設定されたときにトリガーされるイベント。 +- **パラメータ**: + - `sender`: 設定を行ったアドレス。 + - `veYMT`: veYMTのアドレス。 + +--- + +## 変数 + +### `startTime: public(uint256)` + +- **説明**: 手数料分配の開始時間。 + +### `timeCursor: public(uint256)` + +- **説明**: 現在の時間カーソル。 + +### `lastTokenTime: public(uint256)` + +- **説明**: 最後のトークン時間。 + +### `tokensPerWeek: public(mapping(uint256 => uint256))` + +- **説明**: 週ごとのトークン分配量。 + +### `tokenLastBalance: public(uint256)` + +- **説明**: 最後のトークン残高。 + +### `veSupply: public(mapping(uint256 => uint256))` + +- **説明**: 週ごとのveYMTの総供給量。 + +### `canCheckpointToken: public(bool)` + +- **説明**: トークンのチェックポイントを任意のアカウントが行えるかどうかのフラグ。 + +### `isKilled: public(bool)` + +- **説明**: コントラクトが停止されたかどうかを示すフラグ。 + +--- + +## 関数 + +### `initialize(startTime_: uint256)` + +- **説明**: コントラクトの初期化を行います。 +- **パラメータ**: + - `startTime_`: 手数料分配の開始時間。 + +### `checkpointToken()` + +- **説明**: トークンのチェックポイントを更新します。 + +### `claim()` + +- **説明**: `msg.sender`の手数料を請求します。 + +### `claim(addr_: address)` + +- **説明**: 指定されたアドレスの手数料を請求します。 +- **パラメータ**: + - `addr_`: 請求するアドレス。 + +### `claimMany(receivers_: address[])` + +- **説明**: 複数のアドレスの手数料を一括で請求します。 +- **パラメータ**: + - `receivers_`: 請求するアドレスの配列。 + +### `toggleAllowCheckpointToken()` + +- **説明**: トークンのチェックポイント許可を切り替えます。 + +### `killMe()` + +- **説明**: コントラクトを停止し、残高をガバナンスアドレスに送ります。 + +### `recoverBalance()` + +- **説明**: コントラクトからネイティブトークンを回収します。 + +### `setVeYMT(_veymt: address)` + +- **説明**: veYMTのアドレスを設定します。 +- **パラメータ**: + - `_veymt`: veYMTのアドレス。 diff --git a/doc/v1.5/Gauge/index.md b/doc/v1.5/Gauge/index.md deleted file mode 100644 index 5db08149..00000000 --- a/doc/v1.5/Gauge/index.md +++ /dev/null @@ -1,198 +0,0 @@ -# Gauge - -## 概要 - -Gauge は、Yamato Protocol によって提供される報酬ゲージを計算・保存するコントラクトです。 - ---- - -## 定数 - -### `VERSION: constant(String[8]) = "v1"` - -- **説明**: コントラクトのバージョン情報。 - -### `MAX_REWARDS` - -- **説明**: 同時に追跡できる報酬の最大数。現在は 8 です。 - -### `TOKENLESS_PRODUCTION` - -- **説明**: トークンなしでの生産可能な最大値。現在の値は 40 です。 - -### `WEEK` - -- **説明**: 1 週間の秒数。現在の値は 604800 秒です。 - -### 各アドレス定数 (`MINTER`, `YMT`, `veYMT`) - -- **説明**: 特定の役割を持つ外部コントラクトのアドレスを定義しています。 - ---- - -## 変数 - -### `NAME` - -- **説明**: ゲージの名前。 - -### `TOKEN_ADDRESS` - -- **説明**: 対象となるトークンのアドレス。 - -### `nonces` - -- **説明**: 各アドレスの nonce のマッピング。 - -### `future_epoch_time` - -- **説明**: 未来のエポック時間。 - -### `balanceOf` - -- **説明**: 各アドレスのバランスのマッピング。 - -### `totalSupply` - -- **説明**: 合計供給量。 - -### `working_balances` - -- **説明**: 各アドレスの動作中のバランス。 - -### `working_supply` - -- **説明**: 動作中の供給量。 - -### `reward_count`, `reward_tokens`, `reward_data` - -- **説明**: 外部報酬の追跡のための変数。 - -### `rewards_receiver` - -- **説明**: 請求者からデフォルトの報酬受取人へのマッピング。 - -### `reward_integral_for` - -- **説明**: 報酬トークンに関する請求アドレスの積分のマッピング。 - -### `claim_data` - -- **説明**: ユーザーに関する請求データのマッピング。 - -### `admin`, `future_admin`, `is_killed` - -- **説明**: 管理者関連の変数。 - -### `integrate_inv_supply_of`, `integrate_checkpoint_of`, `integrate_fraction` - -- **説明**: 供給や報酬に関する積分の計算のための変数。 - -### `inflation_rate` - -- **説明**: インフレ率。 - -### `period`, `period_timestamp`, `integrate_inv_supply` - -- **説明**: インフレ率の変化や供給の積分に関する変数。 - ---- - -## イベント - -### `UpdateLimit` - -- **説明**: ユーザーの情報が更新された際に発火。 -- 各変数はユーザーの情報や更新後の情報を示します。 - -### その他のイベント(`CommitOwnership`, `ApplyOwnership`) - -- **説明**: 各イベントはゲージの管理やトークンの移転、許可の変更に関連する操作を示します。 - ---- - -## 関数 - -### `__init__(_lp_token: address, _admin: address)` - -- **説明**: コントラクトの初期化関数。初期設定を行います。 -- **パラメータ**: - - `_lp_token`: 流動性プールトークンのアドレス。 - - `_admin`: 管理者のアドレス。 - -### `integrate_checkpoint() -> uint256` - -- **説明**: 現在の期間のタイムスタンプを取得する関数。 - -### `_update_liquidity_limit(addr: address, l: uint256, L: uint256)` - -- **説明**: ユーザーの流動性の限界を更新する内部関数。CRV トークンの量に基づいてワーキングバランスを計算します。 -- **パラメータ**: - - `addr`: ユーザーのアドレス。 - - `l`: ユーザーの流動性の量。 - - `L`: 全体の流動性の量。 - -### `user_checkpoint(addr: address) -> bool` - -- **説明**: ユーザー`addr`のチェックポイントを記録します。 -- **パラメータ**: - - `addr`: チェックポイントを記録するユーザーのアドレス。 - -### `claimable_tokens(addr: address) -> uint256` - -- **説明**: チェックポイントを更新して、ユーザー`addr`に対して請求可能なトークンの数を取得します。 -- **パラメータ**: - - `addr`: 請求可能なトークンの数を取得するユーザーのアドレス。 - -### `claimed_reward(_addr: address, _token: address) -> uint256` - -- **説明**: ユーザー`_addr`が既に請求した報酬トークンの数を取得します。 -- **パラメータ**: - - `_addr`: 既に請求した報酬トークンの数を取得するユーザーのアドレス。 - - `_token`: 既に請求した報酬トークンのアドレス。 - -### `claimable_reward(_user: address, _reward_token: address) -> uint256` - -- **説明**: ユーザー`_user`に対して請求可能な報酬トークンの数を取得します。 -- **パラメータ**: - - `_user`: 請求可能な報酬トークンの数を取得するユーザーのアドレス。 - - `_reward_token`: 請求可能な報酬トークンのアドレス。 - -### `set_rewards_receiver(_receiver: address)` - -- **説明**: 呼び出し元のデフォルトの報酬受信者を設定します。 -- **パラメータ**: - - `_receiver`: `claim_rewards`で報酬を受け取るアドレス。 - -### `claim_rewards(_addr: address = msg.sender, _receiver: address = ZERO_ADDRESS)` - -- **説明**: `_addr`の利用可能な報酬トークンを請求します。 -- **パラメータ**: - - `_addr`: 報酬を請求するアドレス。 - - `_receiver`: 報酬を転送するアドレス。ZERO_ADDRESS に設定されている場合、呼び出し元のデフォルトの報酬受信者を使用します。 - -### `kick(addr: address)` - -- **説明**: そのブーストを乱用している`addr`をキックします。 -- **パラメータ**: - - `addr`: キックするアドレス。 - -### `set_killed(_is_killed: bool)` - -- **説明**: このコントラクトの`killed`ステータスを設定します。`killed`としてマークされた場合、このゲージは常にレート 0 を返すため、CRV を生成することはできません。 -- **パラメータ**: - - `_is_killed`: 設定する`killed`のステータス。 - -### `commit_transfer_ownership(addr: address)` - -- **説明**: `Gauge`の所有権を`addr`に転送します。 -- **パラメータ**: - - `addr`: 所有権を転送するアドレス。 - -### `accept_transfer_ownership()` - -- **説明**: 所有権転送を受け入れます。 - -### `version() -> String[8]` - -- **説明**: このゲージのバージョンを取得します。 diff --git a/doc/v1.5/GaugeController/class.md b/doc/v1.5/GaugeController/class.md deleted file mode 100644 index 50beaf3a..00000000 --- a/doc/v1.5/GaugeController/class.md +++ /dev/null @@ -1,68 +0,0 @@ -# GaugeController - -```mermaid -classDiagram - - class GaugeController { - -admin: address - -future_admin: address - -token: address - -voting_escrow: address - -n_gauge_types: int128 - -n_gauges: int128 - -gauge_type_names: HashMap[int128, String[64]] - -gauges: address[1000000000] - -gauge_types_: HashMap[address, int128] - -vote_user_slopes: HashMap[address, HashMap[address, VotedSlope]] - -vote_user_power: HashMap[address, uint256] - -last_user_vote: HashMap[address, HashMap[address, uint256]] - -points_weight: HashMap[address, HashMap[uint256, Point]] - -changes_weight: HashMap[address, HashMap[uint256, uint256]] - -time_weight: HashMap[address, uint256] - -points_sum: HashMap[int128, HashMap[uint256, Point]] - -changes_sum: HashMap[int128, HashMap[uint256, uint256]] - -time_sum: uint256[1000000000] - -points_total: HashMap[uint256, uint256] - -time_total: uint256 - -points_type_weight: HashMap[int128, HashMap[uint256, uint256]] - -time_type_weight: uint256[1000000000] - +__init__(_token: address, _voting_escrow: address) - +commit_transfer_ownership(addr: address) - +apply_transfer_ownership() - +gauge_types(_addr: address): int128 - +add_gauge(addr: address, gauge_type: int128, weight: uint256) - +checkpoint() - +checkpoint_gauge(addr: address) - +gauge_relative_weight(addr: address, time: uint256): uint256 - +add_type(_name: String[64], weight: uint256) - +change_type_weight(type_id: int128, weight: uint256) - +change_gauge_weight(addr: address, weight: uint256) - +vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256) - +get_gauge_weight(addr: address): uint256 - +get_type_weight(type_id: int128): uint256 - +get_total_weight(): uint256 - +get_weights_sum_per_type(type_id: int128): uint256 - } - - class Point { - bias: uint256 - slope: uint256 - } - - class VotedSlope { - slope: uint256 - power: uint256 - end: uint256 - } - - class VotingEscrow { - +get_last_user_slope(addr: address): int128 - +locked__end(addr: address): uint256 - } - - GaugeController --|> Point - GaugeController --|> VotedSlope - GaugeController --|> VotingEscrow: Uses > - - -``` diff --git a/doc/v1.5/Minter/index.md b/doc/v1.5/Minter/index.md deleted file mode 100644 index fee31692..00000000 --- a/doc/v1.5/Minter/index.md +++ /dev/null @@ -1,93 +0,0 @@ -# Token Minter - -## 概要 - -Token Minter は、特定のゲージからトークンを mint するためのコントラクトです。YMT トークンを mint する機能を提供します。また、ユーザーが他のユーザーに代わって mint する権限を与えることもできます。 - ---- - -## インターフェース - -### `Gauge`: - -- **説明**: 流動性のゲージから情報を取得するためのインターフェース。 -- **関数**: - - `integrate_fraction(addr: address) -> uint256`: 指定したアドレスの積分値を取得する。 - - `user_checkpoint(addr: address) -> bool`: ユーザーチェックポイントを更新する。 - -### `YMT`: - -- **説明**: トークンを mint するための ERC20 トークンインターフェース。 -- **関数**: - - `mint(_to: address, _value: uint256) -> bool`: トークンを mint する。 - ---- - -## イベント - -### `Minted`: - -- **説明**: トークンが mint されたときにトリガーされるイベント。 -- **パラメータ**: - - `recipient`: 受取人のアドレス。 - - `gauge`: 使用されたゲージのアドレス。 - - `minted`: mint されたトークンの量。 - ---- - -## 変数 - -### `token: public(address)` - -- **説明**: mint する MERC20 トークンのアドレス。 - -### `minted: public(HashMap[address, HashMap[address, uint256]])` - -- **説明**: すでに mint されたトークンの量を追跡するためのマップ。 - -### `allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]])` - -- **説明**: あるユーザーが別のユーザーに代わってトークンを mint することを許可するためのマップ。 - ---- - -## 関数 - -### `__init__(_token: address, _controller: address)` - -- **説明**: コントラクトの初期化関数。トークンとコントローラーのアドレスを設定する。 -- **パラメータ**: - - `_token`: MERC20 トークンのアドレス。 - - `_controller`: GaugeController のアドレス。 - -### `_mint_for(gauge_addr: address, _for: address)` - -- **説明**: 内部関数。指定したゲージからトークンを mint する。 -- **パラメータ**: - - `gauge_addr`: ゲージのアドレス。 - - `_for`: トークンを受け取るアドレス。 - -### `mint(gauge_addr: address)` - -- **説明**: `msg.sender`に属するすべてのトークンを mint して送信する。 -- **パラメータ**: - - `gauge_addr`: ゲージのアドレス。 - -### `mint_many(gauge_addrs: address[8])` - -- **説明**: 複数のゲージから`msg.sender`に属するすべてのトークンを mint する。 -- **パラメータ**: - - `gauge_addrs`: ゲージのアドレスのリスト。 - -### `mint_for(gauge_addr: address, _for: address)` - -- **説明**: `_for`のためのトークンを mint する。`msg.sender`が承認されている場合のみ可能。 -- **パラメータ**: - - `gauge_addr`: ゲージのアドレス。 - - `_for`: トークンを受け取るアドレス。 - -### `toggle_approve_mint(minting_user: address)` - -- **説明**: `minting_user`が`msg.sender`の代わりにトークンを mint することを許可または禁止する。 -- **パラメータ**: - - `minting_user`: 許可または禁止を切り替えるユーザーのアドレス。 diff --git a/doc/v1.5/ScoreRegistry/index.md b/doc/v1.5/ScoreRegistry/index.md new file mode 100644 index 00000000..b18f6de3 --- /dev/null +++ b/doc/v1.5/ScoreRegistry/index.md @@ -0,0 +1,136 @@ +# Score Registry + +## 概要 + +`ScoreRegistry` コントラクトは、Yamato Protocolの一部として、ユーザーの作業バランス(スコアリミット)を計算し、YMTトークンの適切な配分を実現するために使用されます。この計算は、ユーザーの担保比率と総借入額に基づいて行われます。 + +--- + +## イベント + +### `UpdateScoreLimit`: + +- **説明**: ユーザーのスコア制限が更新されたときにトリガーされるイベント。 +- **パラメータ**: + - `user`: ユーザーのアドレス。 + - `originalBalance`: 元のバランス。 + - `originalSupply`: 元の供給量。 + - `collateralRatio`: 担保比率。 + - `workingBalance`: 労働バランス。 + - `workingSupply`: 労働供給。 + +--- + +## 変数 + +### `isKilled: public(bool)` + +- **説明**: コントラクトが停止されているかどうかを示すフラグ。 + +### `futureEpochTime: public(uint256)` + +- **説明**: 次のエポックの開始時刻。 + +### `inflationRate: public(uint256)` + +- **説明**: インフレーション率。 + +### `workingBalances: public(mapping(address => uint256))` + +- **説明**: 各アドレスの労働バランス。 + +### `workingSupply: public(uint256)` + +- **説明**: 総労働供給量。 + +### `integrateFraction: public(mapping(address => uint256))` + +- **説明**: ユーザーごとの積分値。 + +--- + +## 関数 + +### `initialize(ymtMinterAddr: address, yamatoAddr: address)` + +- **説明**: コントラクトを初期化します。 +- **パラメータ**: + - `ymtMinterAddr`: YMTミンターのアドレス。 + - `yamatoAddr`: Yamatoコントラクトのアドレス。 + +### `checkpoint(addr: address)` + +- **説明**: 指定されたアドレスのチェックポイントを更新します。 +- **パラメータ**: + - `addr`: 更新するアドレス。 +- **アクセス制限**: Yamatoの関連コントラクトのみ。 + +### `updateScoreLimit(addr_: address, debt_: uint256, totalDebt_: uint256, collateralRatio_: uint256)` + +- **説明**: ユーザーのスコアを更新します。 +- **パラメータ**: + - `addr_`: ユーザーのアドレス。 + - `debt_`: ユーザーの借金額。 + - `totalDebt_`: システムの総借金額。 + - `collateralRatio_`: 担保比率。 +- **アクセス制限**: Yamatoの関連コントラクトのみ。 + +### `userCheckpoint(addr_: address)` + +- **説明**: ユーザーのチェックポイントとスコアを更新します。 +- **パラメータ**: + - `addr_`: ユーザーのアドレス。 + +### `kick(addr_: address)` + +- **説明**: ユーザーをキックし、スコア制限をリセットします。 +- **パラメータ**: + - `addr_`: キックするユーザーのアドレス。 + +### `setKilled(isKilled_: bool)` + +- **説明**: コントラクトの停止状態を設定します。管理者のみ実行可能。 +- **パラメータ**: + - `isKilled_`: 停止状態。 +- **アクセス制限**: 管理者のみ。 + +### `integrateCheckpoint()` + +- **説明**: 最後のチェックポイントのタイムスタンプを取得します。 + +--- + +## コントラクト詳細 + +### スコア計算式 + +ユーザーの作業バランスは以下の式によって計算されます: + +$$ +\text{workingSupply} = \min\left( \text{自債務}, \left( \text{自債務} \times 0.4 \right) + \left( 0.6 \times \frac{\text{総債務} \times \text{自veYMT}}{\text{総veYMT}} \right) \right) / \text{担保係数} +$$ + + +ここで、 +- **自債務**: ユーザーのCJPY借入額。 +- **総債務**: システム全体のCJPY総借入額。 +- **自veYMT**: ユーザーのveYMTトークンのバランス。 +- **総veYMT**: veYMTトークンの総供給量。 +- **担保係数**: ユーザーの担保比率に基づく係数。250%以上で2.5、200%以上で2.0、150%以上で1.5、130%以上で1.0、それ以外は0。 + + +この計算により、ユーザーがYMTトークンの配分を受け取る際の基準となる`workingSupply`(作業バランス)が決定されます。 + +### 積分計算の説明 + +`integrateInvSupply` は、特定の期間にわたって計算される積分値です。具体的には以下の式で表されます: + +$$ \text{integrateInvSupply} = \int_{\text{開始時刻}}^{\text{終了時刻}} \frac{\text{rate}(t) \times \text{weight}(t)}{\text{workingSupply}(t)} \, dt $$ + +ここで、 +- t は時間を表します。 +- weight(t) は時間 t におけるScoreRegistryの相対的な重み。 +- rate(t) は時間 t におけるマイニングレートです。 +- workingSupply(t) は時間 t におけるシステム全体の作業バランスの総和です。 + +この積分は、特定の期間(通常は週単位)にわたって計算され、新たな `integrateInvSupply` の値は、その期間にわたる平均マイニングレートと作業供給の比率を反映します。 diff --git a/doc/v1.5/ScoreWeightController/index.md b/doc/v1.5/ScoreWeightController/index.md new file mode 100644 index 00000000..033ecf4a --- /dev/null +++ b/doc/v1.5/ScoreWeightController/index.md @@ -0,0 +1,80 @@ +# Score Weight Controller + +## 概要 + +Score Weight Controllerは、流動性スコアを管理し、これらのスコアを通じてトークンの発行を制御するコントラクトです。 + +--- + +## イベント + +### `NewScore`: + +- **説明**: 新しいスコアが追加されたときにトリガーされるイベント。 +- **パラメータ**: + - `addr`: スコアのアドレス。 + - `weight`: スコアの重み。 + +--- + +## 変数 + +### `nScores: public(int128)` + +- **説明**: スコアの総数。 + +### `scores: public(mapping(address => int128))` + +- **説明**: アドレスに関連付けられたスコアのマッピング。 + +--- + +## 関数 + +### `initialize(ymtAddr: address, veYmtAddr: address)` + +- **説明**: コントラクトの初期化関数。YMTトークンとveYMTコントラクトのアドレスを設定します。 +- **パラメータ**: + - `ymtAddr`: YMTトークンのアドレス。 + - `veYmtAddr`: veYMTコントラクトのアドレス。 + +### `addCurrency(addr_: address, weight_: uint256)` + +- **説明**: 指定されたアドレスのスコアを追加し、その重みを設定します。 +- **パラメータ**: + - `addr_`: スコアのアドレス。 + - `weight_`: スコアの重み。 +- **アクセス制限**: 管理者のみ。 + +### `checkpoint()` + +- **説明**: すべてのスコアに共通のデータを記録するためのチェックポイント関数。 + +### `checkpointScore(addr_: address)` + +- **説明**: 特定のスコアおよびすべてのスコアに共通のデータを記録するためのチェックポイント関数。 +- **パラメータ**: + - `addr_`: スコアのアドレス。 + +### `scoreRelativeWeight(addr_: address, time_: uint256)` + +- **説明**: 指定されたスコアの相対重みを取得します。 +- **パラメータ**: + - `addr_`: スコアのアドレス。 + - `time_`: 相対重みを計算するための時刻。 + +### `changeScoreWeight(addr_: address, weight_: uint256)` + +- **説明**: 指定されたスコアの重みを変更します。 +- **パラメータ**: + - `addr_`: スコアのアドレス。 + - `weight_`: 新しいスコアの重み。 +- **アクセス制限**: 管理者のみ。 + +### `YMT()` + +- **説明**: YMTトークンのアドレスを返します。 + +### `veYMT()` + +- **説明**: veYMTコントラクトのアドレスを返します。 diff --git a/doc/v1.5/YMT/index.md b/doc/v1.5/YMT/index.md index 18618a46..b8965352 100644 --- a/doc/v1.5/YMT/index.md +++ b/doc/v1.5/YMT/index.md @@ -2,205 +2,158 @@ ## 概要 -YMT Token は、piecewise-linear mining supply を持つ ERC20 トークンです。このトークンのサプライは線形に増加しており、マイニングパラメータは定期的に更新される。初期供給は一定であり、その後の供給レートは時間の経過とともに減少していく。 +YMT Tokenはインフレーションを伴う分配型トークンであり、時間経過とともに減少する発行率を特徴とします。このトークンはERC20規格に準拠し、ERC20Permitを拡張しています。 --- -## 定数 - -### `YEAR: constant(uint256) = 86400 * 365` - -- **説明**: 1 年の秒数を表す定数。具体的には、1 日の秒数(86400 秒)に 365 を掛けた値。 - -### `INITIAL_SUPPLY: constant(uint256) ` - -- **説明**: トークンの初期供給量。 - -### `INITIAL_RATE: constant(uint256)` - -- **説明**: 初期のマイニングレート。 - -### `RATE_REDUCTION_TIME: constant(uint256) = YEAR` - -- **説明**: マイニングレートが減少する時間の間隔。 +## イベント -### `RATE_REDUCTION_COEFFICIENT: constant(uint256)` +### `UpdateMiningParameters`: -- **説明**: マイニングレートの減少係数。 +- **説明**: マイニングパラメータ(時刻、レート、供給量)が更新されたときにトリガーされるイベント。 +- **パラメータ**: + - `time`: 更新された時刻。 + - `rate`: 新しいマイニングレート。 + - `supply`: 更新された供給量。 -### `RATE_DENOMINATOR: constant(uint256) = 10 ** 18` +### `SetMinter`: -- **説明**: レートの分母として使用される定数。 +- **説明**: ymtMinterアドレスが設定されたときに発生するイベント。 +- **パラメータ**: + - `ymtMinter`: ymtMinterのアドレス。 -### `INFLATION_DELAY: constant(uint256) = 86400` +### `SetAdmin`: -- **説明**: インフレーションが適用されるまでの遅延時間(秒)。 +- **説明**: 管理者アドレスが設定または更新されたときに発生するイベント。 +- **パラメータ**: + - `admin`: 新しい管理者のアドレス。 --- ## 変数 -### `name: public(String[64])` - -- **説明**: トークンの名前。 - -### `symbol: public(String[32])` - -- **説明**: トークンのシンボル。 - -### `decimals: public(uint256)` - -- **説明**: トークンの小数点以下の桁数。 - -### `balanceOf: public(HashMap[address, uint256])` +### `ymtMinter: public(address)` -- **説明**: 各アドレスのトークン残高を保持するマップ。 - -### `allowances: HashMap[address, HashMap[address, uint256]]` - -- **説明**: トークンの所有者が他のユーザーに許可したトークンの移動量。 - -### `total_supply: uint256` - -- **説明**: 現在のトークンの総供給量。 - -### `minter: public(address)` - -- **説明**: トークンを mint できるアドレス。 +- **説明**: トークンを発行(mint)するためのアドレス。 ### `admin: public(address)` -- **説明**: このスマートコントラクトの管理者アドレス。 +- **説明**: 管理者のアドレス。 -### `mining_epoch: public(int128)` +### `miningEpoch: public(int128)` - **説明**: 現在のマイニングエポック。 -### `start_epoch_time: public(uint256)` +### `startEpochTime: public(uint256)` -- **説明**: 現在のエポックの開始タイムスタンプ。 +- **説明**: 現在のエポックの開始時刻。 ### `rate: public(uint256)` - **説明**: 現在のマイニングレート。 -### `start_epoch_supply: uint256` +### `startEpochSupply: public(uint256)` -- **説明**: エポック開始時の供給量。 +- **説明**: 現在のエポックの開始時点での供給量。 --- -## イベント - -### Transfer: +## 関数 -- \_from: indexed(address) -- \_to: indexed(address) -- \_value: uint256 +### `constructor(ymtVestingAddr: address)` -### Approval: +- **説明**: コントラクトのコンストラクタ。初期供給とベスティング供給を設定します。 +- **パラメータ**: + - `ymtVestingAddr`: ベスティング供給の受取先アドレス。 -- \_owner: indexed(address) -- \_spender: indexed(address) -- \_value: uint256 +### `updateMiningParameters()` -### UpdateMiningParameters: +- **説明**: マイニングパラメータを更新します。 -- time: uint256 -- rate: uint256 -- supply: uint256 +### `startEpochTimeWrite()` -### SetMinter: +- **説明**: 現在のマイニングエポックの開始時刻を取得し、必要に応じてマイニングパラメータを更新します。 -- minter: address +### `futureEpochTimeWrite()` -### SetAdmin: +- **説明**: 次のマイニングエポックの開始時刻を取得し、必要に応じてマイニングパラメータを更新します。 -- admin: address +### `availableSupply()` ---- +- **説明**: 現在存在しているトークンの総数を返します。 -## 関数 +### `mintableInTimeframe(start: uint256, end: uint256)` -### `__init__(_name: String[64], _symbol: String[32], _decimals: uint256)` +- **説明**: 指定された時間枠で発行可能なトークンの量を計算します。 +- **パラメータ**: + - `start`: 時間枠の開始時刻。 + - `end`: 時間枠の終了時刻。 -- **説明**:コントラクトのコンストラクタ。初期設定を行います。 -- **パラメータ**: - - `_name`:トークンの名前。 - - `_symbol`:トークンのシンボル。 - - `_decimals`:トークンの小数点以下の桁数。 +### `setMinter(_ymtMinter: address)` -### `_update_mining_parameters()` +- **説明**: ymtMinterアドレスを設定します。 +- **パラメータ**: + - `_ymtMinter`: 新しいymtMinterのアドレス。 +- **アクセス制限**: 管理者のみ。 -- **説明**:採掘のレートと供給をエポックの開始時に更新します。 -- **制限**:内部関数。 -### `update_mining_parameters()` +### `setAdmin(_admin: address)` -- **説明**:エポックの開始時に採掘のレートと供給を更新します。 +- **説明**: 新しい管理者を設定します。 +- **パラメータ**: + - `_admin`: 新しい管理者のアドレス。 +- **アクセス制限**: 管理者のみ。 -### `start_epoch_time_write() -> uint256` +### `mint(_to: address, _value: uint256)` -- **説明**:現在の採掘エポックの開始タイムスタンプを取得し、同時に採掘パラメータを更新します。 +- **説明**: 指定されたアドレスにトークンを発行(mint)します。 +- **パラメータ**: + - `_to`: トークンの受取人アドレス。 + - `_value`: 発行するトークンの量。 +- **アクセス制限**: ymtMinterコントラクトのみ。 -### `future_epoch_time_write() -> uint256` +### `burn(_value: uint256)` -- **説明**:次の採掘エポックの開始タイムスタンプを取得し、同時に採掘パラメータを更新します。 +- **説明**: `msg.sender`が所有するトークンを焼却(burn)します。 +- **パラメータ**: + - `_value`: 焼却するトークンの量。 -### `_available_supply() -> uint256` -- **説明**:存在するトークンの数(請求済みまたは未請求)を返します。 -- **制限**:内部関数。 +以下は、提供されたSolidityコード「YMT Token」のためのドキュメントです。このドキュメントは、非技術者が理解できるように作成され、コントラクトの主要な機能とメカニズムを詳細に説明します。 -### `available_supply() -> uint256` - -- **説明**:存在するトークンの数(請求済みまたは未請求)を返します。 - -### `mintable_in_timeframe(start: uint256, end: uint256) -> uint256` +--- -- **説明**:指定されたタイムフレームで発行可能なトークンの量を計算します。 -- **パラメータ**: - - `start`:タイムフレームの開始タイムスタンプ。 - - `end`:タイムフレームの終了タイムスタンプ。 +## コントラクトの詳細 -### `set_minter(_minter: address)` -- **説明**:minter のアドレスを設定します。 -- **パラメータ**: - - `_minter`:minter のアドレス。 +### トークンのインフレーションとマイニング -### `set_admin(_admin: address)` +YMTトークンのマイニングレートは、毎年10%の割合で減少します。この減少は、レート減少係数によって決定されます。具体的な減少割合を計算するには、初期レート、レート減少係数、およびレート分母を用いて、毎年の新しいレートを計算します。 -- **説明**:新しい管理者のアドレスを設定します。 -- **パラメータ**: - - `_admin`:新しい管理者のアドレス。 +#### マイニングレートの減少計算 -### `totalSupply() -> uint256` +- **初期レート**: 年間`55,000,000` YMT。 +- **レート減少係数**: `1_111_111_111_111_111_111`。 +- **レート分母**: `10^18`。 -- **説明**:存在するトークンの総量を返します。 +#### マイニングレートの減少計算 -### `allowance(_owner: address, _spender: address) -> uint256` +新しいマイニングレートの計算式は次の通りです: -- **説明**:\_owner が\_spender に許可したトークンの量を返します。 -- **パラメータ**: - - `_owner`:資金の所有者のアドレス。 - - `_spender`:資金を使うアドレス。 +$$ +\text{新レート} = \frac{\text{現レート} \times \text{レート減少係数}}{\text{レート分母}} +$$ -### `transfer(_to: address, _value: uint256) -> bool` +この式を用いて、各エポックにおける新しいマイニングレートを計算することができます。 -- **説明**:`msg.sender`から`_to`へのトークンの転送。 -- **パラメータ**: - - `_to`:受取人のアドレス。 - - `_value`:転送するトークンの量。 +#### 具体的な減少割合の計算 -### `transferFrom(_from: address, _to: address, _value: uint256) -> bool` +例として、最初の数年のレート減少を計算は以下の通りです。 -- **説明**:`_from`から`_to`へのトークンの転送。 -- **パラメータ**: - - `_from`:送信者のアドレス。 - - `_to`:受取人のアドレス。 - - `_value`:転送するトークンの量。 +1. **年1**: 初期レートは`55,000,000` YMT/年。 +2. **年2**: 新レートは`55,000,000 * 1_111_111_111_111_111_111 / 10^18 = 49,166,666.667` YMT/年。 +3. **年3**: 新レートは`49,166,666.667 * 1_111_111_111_111_111_111 / 10^18 = 43,703,703.704` YMT/年。 +4. **年4**: 新レートは`43,703,703.704 * 1_111_111_111_111_111_111 / 10^18 = 39,259,259.259` YMT/年。 -### `approve(_spender: address, _value: uint256) -> bool` +この計算により、毎年レートは約10%ずつ減少していくことがわかります。この減少は、マイニングレートを年々減少させ、トークンのインフレーションを時間と共に制御するために設計されています。 -- **説明**:`msg.sender`が`_spender`にトークンを使わせ diff --git a/doc/v1.5/YmtMinter/index.md b/doc/v1.5/YmtMinter/index.md new file mode 100644 index 00000000..bad02895 --- /dev/null +++ b/doc/v1.5/YmtMinter/index.md @@ -0,0 +1,83 @@ +# YMT Minter + +## 概要 + +Token Minter は、特定のスコアレジストリからトークンを mint するためのコントラクトです。YMT トークンを mint する機能を提供します。また、ユーザーが他のユーザーに代わって mint する権限を与えることもできます。 + +--- + +## インターフェース + +### `IScoreRegistry`: + +- **説明**: スコアレジストリから情報を取得するためのインターフェース。 +- **関数**: + - `integrateFraction(addr: address) -> uint256`: 指定したアドレスの積分値を取得する。 + - `userCheckpoint(addr: address) -> bool`: ユーザーチェックポイントを更新する。 + +### `IYMT`: + +- **説明**: トークンを mint するための ERC20 トークンインターフェース。 +- **関数**: + - `mint(_to: address, _amount: uint256) -> bool`: トークンを mint する。 + +--- + +## イベント + +### `Minted`: + +- **説明**: トークンが mint されたときにトリガーされるイベント。 +- **パラメータ**: + - `recipient`: 受取人のアドレス。 + - `score`: 使用されたスコアレジストリのアドレス。 + - `minted`: mint されたトークンの量。 + +--- + +## 変数 + +### `minted: public(mapping(address => mapping(address => uint256)))` + +- **説明**: すでに mint されたトークンの量を追跡するためのマップ。 + +### `allowedToMintFor: public(mapping(address => mapping(address => bool)))` + +- **説明**: あるユーザーが別のユーザーに代わってトークンを mint することを許可するためのマップ。 + +--- + +## 関数 + +### `initialize(_ymtAddr: address, _scoreWeightControllerAddr: address)` + +- **説明**: コントラクトの初期化関数。YMTトークンとスコアウェイトコントローラーのアドレスを設定する。 +- **パラメータ**: + - `_ymtAddr`: YMTトークンのアドレス。 + - `_scoreWeightControllerAddr`: スコアウェイトコントローラーのアドレス。 + +### `_mintFor(scoreAddr: address, _for: address)` + +- **説明**: 内部関数。指定したスコアレジストリからトークンを mint する。 +- **パラメータ**: + - `scoreAddr`: スコアレジストリのアドレス。 + - `_for`: トークンを受け取るアドレス。 + +### `mint(scoreAddr: address)` + +- **説明**: `msg.sender`に属するすべてのトークンを mint して送信する。 +- **パラメータ**: + - `scoreAddr`: スコアレジストリのアドレス。 + +### `mintFor(scoreAddr: address, _for: address)` + +- **説明**: `_for`のためのトークンを mint する。`msg.sender`が承認されている場合のみ可能。 +- **パラメータ**: + - `scoreAddr`: スコアレジストリのアドレス。 + - `_for`: トークンを受け取るアドレス。 + +### `toggleApproveMint(mintingUser: address)` + +- **説明**: `mintingUser`が`msg.sender`の代わりにトークンを mint することを許可または禁止する。 +- **パラメータ**: + - `mintingUser`: 許可または禁止を切り替えるユーザーのアドレス。 diff --git a/doc/v1.5/veYMT/index.md b/doc/v1.5/veYMT/index.md index fbdf43a7..1ddddd7d 100644 --- a/doc/v1.5/veYMT/index.md +++ b/doc/v1.5/veYMT/index.md @@ -1,9 +1,8 @@ -# veYMT (VotingEscrow) +# veYMT Token ## 概要 -YMT トークンをロックし、移転不可の veYMT トークンを発行する。 -ロック期間は最大 4 年間、最小単位は 1 週間で、veYMT はロック後の時間経過により線形に減衰する。1YMT を 4 年間ロックすると 1veYMT が発行される。 +veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20互換のトークンです。ただし、このトークンは`transfer()`機能を持ちません。投票用のエスクロートークンとして機能し、ロックされたトークンには時間に応じた重みが付与されます。 #### 参考 @@ -11,266 +10,180 @@ YMT トークンをロックし、移転不可の veYMT トークンを発行す - [Curve VotingEscrow Contract](https://curve.readthedocs.io/dao-vecrv.html) - [The Curve DAO: Liquidity Gauges and Minting CRV](https://curve.readthedocs.io/dao-gauges.html) - [LiquidityGaugeV6 Contract](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/LiquidityGauge.vy) +以下は、提供されたSolidityコード「veYMT Token」のためのドキュメントです。このドキュメントは、以前の「Token Minter」ドキュメントのフォーマットに従って作成されています。 -## Struct +--- -### Point +## インターフェース -veYMT のある時点での状態を格納するための構造体 +### `IYMT`: -- bias(int128) - - veYMT の残高 -- slope(int128) - - veYMT の減り方を表す傾き。ロック額 / 最大ロック期間 -- ts(uint256) - - タイムスタンプ -- blk(uint256) - - ブロック高 +- **説明**: YMTトークンのインターフェース。veYMTトークンはこのトークンを基にしています。 +- **関数**: + - トークンに関連する標準的なERC20関数群。 -### LockedBalance +--- -ロックの情報を格納するための構造体 +## イベント -- amount(int128) - - ロック量 -- end(uint256) - - ロックが終了する時点のタイムスタンプ +### `CommitOwnership`: -## 定数 +- **説明**: 新しい管理者に所有権を委譲する際に発生します。 +- **パラメータ**: + - `admin`: 新しい管理者のアドレス。 -### `DEPOSIT_FOR_TYPE(int128)` +### `ApplyOwnership`: -イベント識別用 +- **説明**: 所有権の移転が適用されたときにトリガーされるイベント。 +- **パラメータ**: + - `admin`: 新しい管理者のアドレス。 -### `CREATE_LOCK_TYPE(int128)` +### `Deposit`: -イベント識別用 +- **説明**: ユーザーがトークンをデポジットした際に発生します。 +- **パラメータ**: + - `provider`: デポジットしたユーザーのアドレス。 + - `value`: デポジットされたトークンの量。 + - `locktime`: ロックされる時間。 + - `ts`: タイムスタンプ。 -### `INCREASE_LOCK_AMOUNT(int128)` +### `Withdraw`: -イベント識別用 +- **説明**: ユーザーがトークンを引き出した際に発生します。 +- **パラメータ**: + - `provider`: 引き出したユーザーのアドレス。 + - `value`: 引き出されたトークンの量。 + - `ts`: タイムスタンプ。 -### `INCREASE_UNLOCK_TIME(int128)` +### `Supply`: -イベント識別用 +- **説明**: 供給量が変更されたときにトリガーされるイベント。 +- **パラメータ**: + - `prevSupply`: 変更前の供給量。 + - `supply`: 新しい供給量。 -### `WEEK(uint256)` +--- -1 週間(7 \* 86400) +## 変数 -### `MAXTIME(uint256)` +### `locked: public(mapping(address => LockedBalance))` -4 年間(4 \* 365 \* 86400) +- **説明**: ユーザーごとのロックされたバランスを記録します。 -### `MULTIPLIER(uint256)` - -除算時の丸め誤差防止に使用する定数 (10^18) +### `epoch: public(uint256)` -## プロパティ +- **説明**: 現在のエポック番号。 ### `token: public(address)` -ロック対象のトークンアドレス(YMT のコントラクトアドレス) +- **説明**: veYMTトークンが基づいているYMTトークンのアドレス。 ### `supply: public(uint256)` -ロック対象トークンの総ロック量。デポジット、引き出し時に変化 - -### `locked: public(address => LockedBalance)` - -ユーザごとのトークンロック情報(量、終了タイムスタンプ)を格納 - -### `epoch: public(uint256)` - -全てのユーザのアクションごとにインクリメントするグローバルなインデックス - -### `point_history: public(Point[])` - -veYMT のグローバルな状態を epoch ごとに記録する配列 - -### `user_point_history: public(address => Point[])` - -veYMT のユーザごとの状態を user epoch ごとに記録する配列 - -### `user_point_epoch: public(address => uint256)` - -各ユーザのアクションごとにインクリメントするローカルなインデックス - -### `slope_changes: public(uint256 => uint128)` - -ある時点で予定されている slope の変化を記録する。ユーザのデポジットやロック期間変更時に更新される。週単位のタイムスタンプ(WEEK の倍数)がキーになり、ユーザのアクション時に該当する slope の変化がある場合は Point の slope にこの変化を適用する。 - -### `controller: public(address)` - -Aragon 互換性のため - -### `transfersEnabled: public(bool)` - -Aragon 互換性のため - -### `name: public(string)` - -ve トークン名 - -### `symbol: public(string)` - -ve トークンシンボル - -### `version: public(string)` - -ve トークンバージョン - -### `decimals: public(uint256)` - -ve トークンデシマル - -### `future_smart_wallet_checker: public(address)` - -Checker for whitelisted (smart contract) wallets which are allowed to deposit -The goal is to prevent tokenizing the escrow - -### `smart_wallet_checker: public(address)` - -Checker for whitelisted (smart contract) wallets which are allowed to deposit -The goal is to prevent tokenizing the escrow - -Curve の VotingEscrow では現在下記コントラクトが登録されている -https://etherscan.io/address/0xca719728Ef172d0961768581fdF35CB116e0B7a4#readContract - -### `admin: public(address)` - -管理者アドレス - -### `future_admin: public(address)` - -次期管理者アドレス - -# 機能 - -### `**init**(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]) external` - -初期化 - -- admin に msg.sender を設定 -- token に token_addr を設定 -- point_history[0].blk に block.number を設定 -- point_history[0].ts に block.timestamp を設定 -- controller に msg.sender を設定 -- transfersEnabled に True を設定 -- decimals に token の decimals と同じ値を設定 -- name に\_name を設定 -- symbol に\_symbol を設定 -- version に\_version を設定 - -### `commit_transfer_ownership(addr: address) external` - -- 次期管理者アドレスを設定 -- 管理者のみ - -### `apply_transfer_ownership() external` - -- 管理者アドレスに次期管理者アドレスを設定 -- 管理者のみ +- **説明**: 総供給量。 -### `commit_smart_wallet_checker(addr: address) external` +--- -- 次期スマートウォレットチェッカーを設定 -- 管理者のみ +## 関数 -### `apply_smart_wallet_checker() external` +### `constructor(tokenAddr_: address)` -- スマートウォレットチェッカーに次期スマートウォレットチェッカーを設定 -- 管理者のみ +- **説明**: コントラクトのコンストラクタ。YMTトークンのアドレスを設定します。 +- **パラメータ**: + - `tokenAddr_`: YMTトークンのアドレス。 -### `assert_not_contract(addr: address) internal` +### `commitTransferOwnership(addr_: address)` -- スマートウォレットチェッカーを使用して対象アドレスがホワイトリストされたスマコンかどうかチェックする +- **説明**: 新しい管理者に所有権を委譲します。 +- **パラメータ**: + - `addr_`: 新しい管理者のアドレス。 +- **アクセス制限**: 管理者のみ。 -### `get_last_user_slope(addr: address) -> int128 external view` +### `applyTransferOwnership()` -- 指定アドレスの最新の slope を返す +- **説明**: 所有権の移転を適用します。 +- **アクセス制限**: 管理者のみ。 -### `user_point_history__ts(_addr: address, _idx: uint256) -> uint256 external view` +### `depositFor(addr_: address, value_: uint256)` -- 指定アドレスの指定インデックス(user epoch)のタイムスタンプを返す +- **説明**: 指定したアドレスのユーザーのためにトークンをデポジットし、ロックします。 +- **パラメータ**: + - `addr_`: トークンをデポジットするユーザーのアドレス。 + - `value_`: デポジットするトークンの量。 -### `locked__end(_addr: address) -> uint256 external view` +### `createLock(value_: uint256, unlockTime_: uint256)` -- 指定アドレスのロック終了時点タイムスタンプを返す +- **説明**: 新しいロックを作成し、トークンをデポジットします。 +- **パラメータ**: + - `value_`: デポジットするトークンの量。 + - `unlockTime_`: トークンのロックを解除する時間(エポック秒)。 -### `_checkpoint(addr: address, old_locked: LockedBalance, new_locked: LockedBalance) internal` +### `increaseAmount(value_: uint256)` -各ユーザアクションごとにコールされ、ポイント履歴、報酬情報を更新する +- **説明**: 既存のロックにトークンを追加します。 +- **パラメータ**: + - `value_`: 追加するトークンの量。 -- addr が ZERO_ADDRESS でない場合 - - addr の新旧 slope と bias を計算 - - slope の変化(slope_changes)を計算 - - ユーザのポイント履歴を更新 - - ユーザごとの報酬の計算をする - - integrate_inv_supply_of - - integrate_checkpoint_of - - integrate_fraction -- ポイント履歴の最後の時点から 最大 255 週分の履歴を作成する。255 週以上の期間に渡って履歴がない場合(=ユーザ操作がない場合)は正しい計算ができなくなる -- integrate_inv_supply を更新する +### `increaseUnlockTime(unlockTime_: uint256)` -### `_deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128) internal` +- **説明**: ロックの解除時間を延長します。 +- **パラメータ**: + - `unlockTime_`: 新しいロック解除時間(エポック秒)。 -- 任意の addr に代わって YMT を任意の量ロックする +### `withdraw()` -### `checkpoint() external` +- **説明**: ロックが解除されたトークンを引き出します。 -- \_checkpoint を呼び、veYMT のグローバルな状態を更新する -### `deposit_for(_addr: address, _value: uint256) external` +以下は、提供されたSolidityコード「veYMT Token」の追加的なドキュメント記述です。このセクションは、コントラクトの仕様をより深く理解するのに役立ちます。 -- \_deposit_for を呼び、任意の addr に代わって YMT を任意の量ロックする -- 既存のロックがない場合はリバート +--- -### `create_lock(_value: uint256, _unlock_time: uint256) external` +## コントラクトの詳細 -- 新規にロックを作成する -- 既存のロックがある場合はリバート +### veYMT コントラクト: -### `increase_amount(_value: uint256) external` +- **目的**: veYMTコントラクトは、YMTトークンをロックして、ロック期間に応じた投票権を提供する機能を実装しています。ロックされたトークンは、所定の期間が経過するまで引き出すことができません。 -- ロック量を増額する +### ロックメカニズム: -### `increase_unlock_time(_unlock_time: uint256) external` +- **ロックの仕組み**: ユーザーはYMTトークンをveYMTコントラクトに預けることで、ロックされたトークンに基づく投票権を獲得します。ロック期間はユーザーによって設定され、最大で4年間です。 +veYMTトークンにおける投票権の変化は、ロックされたトークンの量とロック期間に基づいて計算されます。この計算には「バイアス」と「スロープ」という二つの概念が関係します。 -- ロック期間を延長する +### バイアスとスロープ -### `withdraw() external` +- **バイアス (Bias)**: ある時点でのユーザーの投票権の量を示します。これは、ロックされたトークンの量とロック期間に比例します。 +- **スロープ (Slope)**: バイアスが時間とともにどのように減少するかを示す値です。これは、ロックされたトークンの量に依存し、ロック期間が経過するにつれて減少します。 -- ロック期間が終了した YMT を引き出す +## 投票権の計算 -### `find_block_epoch(_block: uint256, max_epoch: uint256) -> uint256 internal view` +投票権の計算においては、次の数式が用いられます: -- 指定したブロック高に一番近い epoch を検索して返す +$$ \text{バイアス} = \text{スロープ} \times (\text{ロック終了時刻} - \text{現在時刻}) $$ -### `balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256 external view` +ここで、 +- **ロック終了時刻**: ユーザーが設定したトークンのロックが解除される時刻。 +- **現在時刻**: 現在の時刻。 -- 指定したアドレスの指定したタイムスタンプ時点での veYMT 残高を返す -- \_t が最後に記録されたユーザのポイント履歴より前の場合は失敗する -### `balanceOfAt(addr: address, _block: uint256) -> uint256 external view` +### 例 -- 指定したアドレスの指定したブロック高時点での veYMT 残高を返す +例えば、ユーザーが100 veYMTトークンを4年間ロックするとします。この場合、スロープはロック量を最大ロック期間で割った値になります。 -### `supply_at(point: Point, t: uint256) -> uint256 internal view` +$$ \text{スロープ} = \frac{100 \text{ veYMT}}{4 \times 365 \times 24 \times 3600 \text{ 秒}} $$ -- 指定したポイントを起点に指定したタイムスタンプ時点での 総 veYMT 残高を返す -- 255 週以上ポイントが記録されていない状況の場合は正しい計算ができなくなる -### `totalSupply(t: uint256 = block.timestamp) -> uint256 external view` +ロックが開始された時点でのバイアスは、スロープにロック期間を掛けたものに等しくなります。したがって、バイアスは初め100 veYMTとなりますが、時間が経過するにつれて徐々に減少していきます。 -- 最後に記録されたポイントを起点に、指定したタイムスタンプ時点の総 veYMT 残高を返す +### ロック期間の影響 -### `totalSupplyAt(_block: uint256) -> uint256 external view` +- **長期ロックの利点**: 長期にわたってトークンをロックするほど、初期のバイアスは大きくなり、投票権の量も多くなります。 +- **時間経過とともの減少**: しかし、時間が経過するにつれて、スロープによりバイアスは減少し、投票権も徐々に減少していきます。 -- 指定したブロック高時点での総 veYMT 残高を返す +このようにveYMTトークンでは、ロック期間が長いほど、初期の投票権は多くなりますが、その権利は時間とともに減少していきます。このメカニズムにより、ユーザーは長期的なコミットメントを通じてより大きな投票権を得ることができますが、ロック期間が経過するにつれてその影響力は減少していきます。 -### `changeController(_newController: address) external` +### ユーザーインタラクション: -- controller のみ -- controller を変更する -- Aragon 互換性のためのダミー関数 +- **デポジット**: ユーザーは特定の量のYMTトークンをデポジットし、特定の期間でロックすることができます。 +- **ロックの増加**: ユーザーは既存のロックにトークンを追加するか、ロック期間を延長することが可能です。 +- **引き出し**: ロック期間が終了した後、ユーザーはロックされたトークンを引き出すことができます。 From b0b8c14de058d5d7627ec8640115ace191264e7b Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 13 Dec 2023 16:41:08 +0900 Subject: [PATCH 133/412] Corrected comment on contract name. --- contracts/ScoreWeightController.sol | 2 +- contracts/YmtMinter.sol | 2 +- contracts/YmtVesting.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/ScoreWeightController.sol b/contracts/ScoreWeightController.sol index 247c72c5..4fbc70f8 100644 --- a/contracts/ScoreWeightController.sol +++ b/contracts/ScoreWeightController.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.4; */ /** - * @title Score Controller + * @title Score Weight Controller * @notice Controls liquidity scores and the issuance of token through the scores */ diff --git a/contracts/YmtMinter.sol b/contracts/YmtMinter.sol index 97bd5b0c..bb31e6b3 100644 --- a/contracts/YmtMinter.sol +++ b/contracts/YmtMinter.sol @@ -16,7 +16,7 @@ import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable. import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; /** - * @title Token Minter + * @title YMT Minter */ contract YmtMinter is diff --git a/contracts/YmtVesting.sol b/contracts/YmtVesting.sol index 7245927b..05601e46 100644 --- a/contracts/YmtVesting.sol +++ b/contracts/YmtVesting.sol @@ -8,7 +8,7 @@ pragma solidity 0.8.4; import "./Interfaces/IYMT.sol"; /** - * @title YmtVesting + * @title YMT Vesting * @dev This contract manages the vesting of YMT tokens. */ contract YmtVesting { From 0e50be707f34b30440eddb64f21c2fa19982e2bb Mon Sep 17 00:00:00 2001 From: naizo10 Date: Wed, 13 Dec 2023 16:43:02 +0900 Subject: [PATCH 134/412] code format --- doc/v1.5/FeePoolV2/index.md | 12 ++++----- doc/v1.5/ScoreRegistry/index.md | 30 +++++++++++----------- doc/v1.5/ScoreWeightController/index.md | 12 ++++----- doc/v1.5/YMT/index.md | 30 ++++++++++------------ doc/v1.5/YmtMinter/index.md | 4 +-- doc/v1.5/veYMT/index.md | 34 ++++++++++++------------- 6 files changed, 58 insertions(+), 64 deletions(-) diff --git a/doc/v1.5/FeePoolV2/index.md b/doc/v1.5/FeePoolV2/index.md index 7e81b69d..70ada39f 100644 --- a/doc/v1.5/FeePoolV2/index.md +++ b/doc/v1.5/FeePoolV2/index.md @@ -2,7 +2,7 @@ ## 概要 -Fee Pool V2は、手数料プールの管理と手数料の分配を行うコントラクトです。veYMT(投票エスクローYMT)トークンの所有者に対して、プール内のトークンを分配します。 +Fee Pool V2 は、手数料プールの管理と手数料の分配を行うコントラクトです。veYMT(投票エスクロー YMT)トークンの所有者に対して、プール内のトークンを分配します。 --- @@ -39,10 +39,10 @@ Fee Pool V2は、手数料プールの管理と手数料の分配を行うコン ### `VeYMTSet`: -- **説明**: veYMTのアドレスが設定されたときにトリガーされるイベント。 +- **説明**: veYMT のアドレスが設定されたときにトリガーされるイベント。 - **パラメータ**: - `sender`: 設定を行ったアドレス。 - - `veYMT`: veYMTのアドレス。 + - `veYMT`: veYMT のアドレス。 --- @@ -70,7 +70,7 @@ Fee Pool V2は、手数料プールの管理と手数料の分配を行うコン ### `veSupply: public(mapping(uint256 => uint256))` -- **説明**: 週ごとのveYMTの総供給量。 +- **説明**: 週ごとの veYMT の総供給量。 ### `canCheckpointToken: public(bool)` @@ -124,6 +124,6 @@ Fee Pool V2は、手数料プールの管理と手数料の分配を行うコン ### `setVeYMT(_veymt: address)` -- **説明**: veYMTのアドレスを設定します。 +- **説明**: veYMT のアドレスを設定します。 - **パラメータ**: - - `_veymt`: veYMTのアドレス。 + - `_veymt`: veYMT のアドレス。 diff --git a/doc/v1.5/ScoreRegistry/index.md b/doc/v1.5/ScoreRegistry/index.md index b18f6de3..007df0c2 100644 --- a/doc/v1.5/ScoreRegistry/index.md +++ b/doc/v1.5/ScoreRegistry/index.md @@ -2,7 +2,7 @@ ## 概要 -`ScoreRegistry` コントラクトは、Yamato Protocolの一部として、ユーザーの作業バランス(スコアリミット)を計算し、YMTトークンの適切な配分を実現するために使用されます。この計算は、ユーザーの担保比率と総借入額に基づいて行われます。 +`ScoreRegistry` コントラクトは、Yamato Protocol の一部として、ユーザーの作業バランス(スコアリミット)を計算し、YMT トークンの適切な配分を実現するために使用されます。この計算は、ユーザーの担保比率と総借入額に基づいて行われます。 --- @@ -55,15 +55,15 @@ - **説明**: コントラクトを初期化します。 - **パラメータ**: - - `ymtMinterAddr`: YMTミンターのアドレス。 - - `yamatoAddr`: Yamatoコントラクトのアドレス。 + - `ymtMinterAddr`: YMT ミンターのアドレス。 + - `yamatoAddr`: Yamato コントラクトのアドレス。 ### `checkpoint(addr: address)` - **説明**: 指定されたアドレスのチェックポイントを更新します。 - **パラメータ**: - `addr`: 更新するアドレス。 -- **アクセス制限**: Yamatoの関連コントラクトのみ。 +- **アクセス制限**: Yamato の関連コントラクトのみ。 ### `updateScoreLimit(addr_: address, debt_: uint256, totalDebt_: uint256, collateralRatio_: uint256)` @@ -73,7 +73,7 @@ - `debt_`: ユーザーの借金額。 - `totalDebt_`: システムの総借金額。 - `collateralRatio_`: 担保比率。 -- **アクセス制限**: Yamatoの関連コントラクトのみ。 +- **アクセス制限**: Yamato の関連コントラクトのみ。 ### `userCheckpoint(addr_: address)` @@ -110,26 +110,26 @@ $$ \text{workingSupply} = \min\left( \text{自債務}, \left( \text{自債務} \times 0.4 \right) + \left( 0.6 \times \frac{\text{総債務} \times \text{自veYMT}}{\text{総veYMT}} \right) \right) / \text{担保係数} $$ - ここで、 -- **自債務**: ユーザーのCJPY借入額。 -- **総債務**: システム全体のCJPY総借入額。 -- **自veYMT**: ユーザーのveYMTトークンのバランス。 -- **総veYMT**: veYMTトークンの総供給量。 -- **担保係数**: ユーザーの担保比率に基づく係数。250%以上で2.5、200%以上で2.0、150%以上で1.5、130%以上で1.0、それ以外は0。 +- **自債務**: ユーザーの CJPY 借入額。 +- **総債務**: システム全体の CJPY 総借入額。 +- **自 veYMT**: ユーザーの veYMT トークンのバランス。 +- **総 veYMT**: veYMT トークンの総供給量。 +- **担保係数**: ユーザーの担保比率に基づく係数。250%以上で 2.5、200%以上で 2.0、150%以上で 1.5、130%以上で 1.0、それ以外は 0。 -この計算により、ユーザーがYMTトークンの配分を受け取る際の基準となる`workingSupply`(作業バランス)が決定されます。 +この計算により、ユーザーが YMT トークンの配分を受け取る際の基準となる`workingSupply`(作業バランス)が決定されます。 ### 積分計算の説明 `integrateInvSupply` は、特定の期間にわたって計算される積分値です。具体的には以下の式で表されます: -$$ \text{integrateInvSupply} = \int_{\text{開始時刻}}^{\text{終了時刻}} \frac{\text{rate}(t) \times \text{weight}(t)}{\text{workingSupply}(t)} \, dt $$ +$$ \text{integrateInvSupply} = \int\_{\text{開始時刻}}^{\text{終了時刻}} \frac{\text{rate}(t) \times \text{weight}(t)}{\text{workingSupply}(t)} \, dt $$ ここで、 -- t は時間を表します。 -- weight(t) は時間 t におけるScoreRegistryの相対的な重み。 + +- t は時間を表します。 +- weight(t) は時間 t における ScoreRegistry の相対的な重み。 - rate(t) は時間 t におけるマイニングレートです。 - workingSupply(t) は時間 t におけるシステム全体の作業バランスの総和です。 diff --git a/doc/v1.5/ScoreWeightController/index.md b/doc/v1.5/ScoreWeightController/index.md index 033ecf4a..0ccb20fd 100644 --- a/doc/v1.5/ScoreWeightController/index.md +++ b/doc/v1.5/ScoreWeightController/index.md @@ -2,7 +2,7 @@ ## 概要 -Score Weight Controllerは、流動性スコアを管理し、これらのスコアを通じてトークンの発行を制御するコントラクトです。 +Score Weight Controller は、流動性スコアを管理し、これらのスコアを通じてトークンの発行を制御するコントラクトです。 --- @@ -33,10 +33,10 @@ Score Weight Controllerは、流動性スコアを管理し、これらのスコ ### `initialize(ymtAddr: address, veYmtAddr: address)` -- **説明**: コントラクトの初期化関数。YMTトークンとveYMTコントラクトのアドレスを設定します。 +- **説明**: コントラクトの初期化関数。YMT トークンと veYMT コントラクトのアドレスを設定します。 - **パラメータ**: - - `ymtAddr`: YMTトークンのアドレス。 - - `veYmtAddr`: veYMTコントラクトのアドレス。 + - `ymtAddr`: YMT トークンのアドレス。 + - `veYmtAddr`: veYMT コントラクトのアドレス。 ### `addCurrency(addr_: address, weight_: uint256)` @@ -73,8 +73,8 @@ Score Weight Controllerは、流動性スコアを管理し、これらのスコ ### `YMT()` -- **説明**: YMTトークンのアドレスを返します。 +- **説明**: YMT トークンのアドレスを返します。 ### `veYMT()` -- **説明**: veYMTコントラクトのアドレスを返します。 +- **説明**: veYMT コントラクトのアドレスを返します。 diff --git a/doc/v1.5/YMT/index.md b/doc/v1.5/YMT/index.md index b8965352..0ae06029 100644 --- a/doc/v1.5/YMT/index.md +++ b/doc/v1.5/YMT/index.md @@ -2,7 +2,7 @@ ## 概要 -YMT Tokenはインフレーションを伴う分配型トークンであり、時間経過とともに減少する発行率を特徴とします。このトークンはERC20規格に準拠し、ERC20Permitを拡張しています。 +YMT Token はインフレーションを伴う分配型トークンであり、時間経過とともに減少する発行率を特徴とします。このトークンは ERC20 規格に準拠し、ERC20Permit を拡張しています。 --- @@ -18,9 +18,9 @@ YMT Tokenはインフレーションを伴う分配型トークンであり、 ### `SetMinter`: -- **説明**: ymtMinterアドレスが設定されたときに発生するイベント。 +- **説明**: ymtMinter アドレスが設定されたときに発生するイベント。 - **パラメータ**: - - `ymtMinter`: ymtMinterのアドレス。 + - `ymtMinter`: ymtMinter のアドレス。 ### `SetAdmin`: @@ -91,12 +91,11 @@ YMT Tokenはインフレーションを伴う分配型トークンであり、 ### `setMinter(_ymtMinter: address)` -- **説明**: ymtMinterアドレスを設定します。 +- **説明**: ymtMinter アドレスを設定します。 - **パラメータ**: - - `_ymtMinter`: 新しいymtMinterのアドレス。 + - `_ymtMinter`: 新しい ymtMinter のアドレス。 - **アクセス制限**: 管理者のみ。 - ### `setAdmin(_admin: address)` - **説明**: 新しい管理者を設定します。 @@ -110,7 +109,7 @@ YMT Tokenはインフレーションを伴う分配型トークンであり、 - **パラメータ**: - `_to`: トークンの受取人アドレス。 - `_value`: 発行するトークンの量。 -- **アクセス制限**: ymtMinterコントラクトのみ。 +- **アクセス制限**: ymtMinter コントラクトのみ。 ### `burn(_value: uint256)` @@ -118,17 +117,15 @@ YMT Tokenはインフレーションを伴う分配型トークンであり、 - **パラメータ**: - `_value`: 焼却するトークンの量。 - -以下は、提供されたSolidityコード「YMT Token」のためのドキュメントです。このドキュメントは、非技術者が理解できるように作成され、コントラクトの主要な機能とメカニズムを詳細に説明します。 +以下は、提供された Solidity コード「YMT Token」のためのドキュメントです。このドキュメントは、非技術者が理解できるように作成され、コントラクトの主要な機能とメカニズムを詳細に説明します。 --- ## コントラクトの詳細 - ### トークンのインフレーションとマイニング -YMTトークンのマイニングレートは、毎年10%の割合で減少します。この減少は、レート減少係数によって決定されます。具体的な減少割合を計算するには、初期レート、レート減少係数、およびレート分母を用いて、毎年の新しいレートを計算します。 +YMT トークンのマイニングレートは、毎年 10%の割合で減少します。この減少は、レート減少係数によって決定されます。具体的な減少割合を計算するには、初期レート、レート減少係数、およびレート分母を用いて、毎年の新しいレートを計算します。 #### マイニングレートの減少計算 @@ -150,10 +147,9 @@ $$ 例として、最初の数年のレート減少を計算は以下の通りです。 -1. **年1**: 初期レートは`55,000,000` YMT/年。 -2. **年2**: 新レートは`55,000,000 * 1_111_111_111_111_111_111 / 10^18 = 49,166,666.667` YMT/年。 -3. **年3**: 新レートは`49,166,666.667 * 1_111_111_111_111_111_111 / 10^18 = 43,703,703.704` YMT/年。 -4. **年4**: 新レートは`43,703,703.704 * 1_111_111_111_111_111_111 / 10^18 = 39,259,259.259` YMT/年。 - -この計算により、毎年レートは約10%ずつ減少していくことがわかります。この減少は、マイニングレートを年々減少させ、トークンのインフレーションを時間と共に制御するために設計されています。 +1. **年 1**: 初期レートは`55,000,000` YMT/年。 +2. **年 2**: 新レートは`55,000,000 * 1_111_111_111_111_111_111 / 10^18 = 49,166,666.667` YMT/年。 +3. **年 3**: 新レートは`49,166,666.667 * 1_111_111_111_111_111_111 / 10^18 = 43,703,703.704` YMT/年。 +4. **年 4**: 新レートは`43,703,703.704 * 1_111_111_111_111_111_111 / 10^18 = 39,259,259.259` YMT/年。 +この計算により、毎年レートは約 10%ずつ減少していくことがわかります。この減少は、マイニングレートを年々減少させ、トークンのインフレーションを時間と共に制御するために設計されています。 diff --git a/doc/v1.5/YmtMinter/index.md b/doc/v1.5/YmtMinter/index.md index bad02895..65dd86d9 100644 --- a/doc/v1.5/YmtMinter/index.md +++ b/doc/v1.5/YmtMinter/index.md @@ -51,9 +51,9 @@ Token Minter は、特定のスコアレジストリからトークンを mint ### `initialize(_ymtAddr: address, _scoreWeightControllerAddr: address)` -- **説明**: コントラクトの初期化関数。YMTトークンとスコアウェイトコントローラーのアドレスを設定する。 +- **説明**: コントラクトの初期化関数。YMT トークンとスコアウェイトコントローラーのアドレスを設定する。 - **パラメータ**: - - `_ymtAddr`: YMTトークンのアドレス。 + - `_ymtAddr`: YMT トークンのアドレス。 - `_scoreWeightControllerAddr`: スコアウェイトコントローラーのアドレス。 ### `_mintFor(scoreAddr: address, _for: address)` diff --git a/doc/v1.5/veYMT/index.md b/doc/v1.5/veYMT/index.md index 1ddddd7d..3693352e 100644 --- a/doc/v1.5/veYMT/index.md +++ b/doc/v1.5/veYMT/index.md @@ -2,7 +2,7 @@ ## 概要 -veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20互換のトークンです。ただし、このトークンは`transfer()`機能を持ちません。投票用のエスクロートークンとして機能し、ロックされたトークンには時間に応じた重みが付与されます。 +veYMT Token は、YMT トークンをロックすることで得られる、ERC-20 互換のトークンです。ただし、このトークンは`transfer()`機能を持ちません。投票用のエスクロートークンとして機能し、ロックされたトークンには時間に応じた重みが付与されます。 #### 参考 @@ -10,7 +10,7 @@ veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20 - [Curve VotingEscrow Contract](https://curve.readthedocs.io/dao-vecrv.html) - [The Curve DAO: Liquidity Gauges and Minting CRV](https://curve.readthedocs.io/dao-gauges.html) - [LiquidityGaugeV6 Contract](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/LiquidityGauge.vy) -以下は、提供されたSolidityコード「veYMT Token」のためのドキュメントです。このドキュメントは、以前の「Token Minter」ドキュメントのフォーマットに従って作成されています。 + 以下は、提供された Solidity コード「veYMT Token」のためのドキュメントです。このドキュメントは、以前の「Token Minter」ドキュメントのフォーマットに従って作成されています。 --- @@ -18,9 +18,9 @@ veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20 ### `IYMT`: -- **説明**: YMTトークンのインターフェース。veYMTトークンはこのトークンを基にしています。 +- **説明**: YMT トークンのインターフェース。veYMT トークンはこのトークンを基にしています。 - **関数**: - - トークンに関連する標準的なERC20関数群。 + - トークンに関連する標準的な ERC20 関数群。 --- @@ -76,7 +76,7 @@ veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20 ### `token: public(address)` -- **説明**: veYMTトークンが基づいているYMTトークンのアドレス。 +- **説明**: veYMT トークンが基づいている YMT トークンのアドレス。 ### `supply: public(uint256)` @@ -88,9 +88,9 @@ veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20 ### `constructor(tokenAddr_: address)` -- **説明**: コントラクトのコンストラクタ。YMTトークンのアドレスを設定します。 +- **説明**: コントラクトのコンストラクタ。YMT トークンのアドレスを設定します。 - **パラメータ**: - - `tokenAddr_`: YMTトークンのアドレス。 + - `tokenAddr_`: YMT トークンのアドレス。 ### `commitTransferOwnership(addr_: address)` @@ -134,8 +134,7 @@ veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20 - **説明**: ロックが解除されたトークンを引き出します。 - -以下は、提供されたSolidityコード「veYMT Token」の追加的なドキュメント記述です。このセクションは、コントラクトの仕様をより深く理解するのに役立ちます。 +以下は、提供された Solidity コード「veYMT Token」の追加的なドキュメント記述です。このセクションは、コントラクトの仕様をより深く理解するのに役立ちます。 --- @@ -143,12 +142,12 @@ veYMT Tokenは、YMTトークンをロックすることで得られる、ERC-20 ### veYMT コントラクト: -- **目的**: veYMTコントラクトは、YMTトークンをロックして、ロック期間に応じた投票権を提供する機能を実装しています。ロックされたトークンは、所定の期間が経過するまで引き出すことができません。 +- **目的**: veYMT コントラクトは、YMT トークンをロックして、ロック期間に応じた投票権を提供する機能を実装しています。ロックされたトークンは、所定の期間が経過するまで引き出すことができません。 ### ロックメカニズム: -- **ロックの仕組み**: ユーザーはYMTトークンをveYMTコントラクトに預けることで、ロックされたトークンに基づく投票権を獲得します。ロック期間はユーザーによって設定され、最大で4年間です。 -veYMTトークンにおける投票権の変化は、ロックされたトークンの量とロック期間に基づいて計算されます。この計算には「バイアス」と「スロープ」という二つの概念が関係します。 +- **ロックの仕組み**: ユーザーは YMT トークンを veYMT コントラクトに預けることで、ロックされたトークンに基づく投票権を獲得します。ロック期間はユーザーによって設定され、最大で 4 年間です。 + veYMT トークンにおける投票権の変化は、ロックされたトークンの量とロック期間に基づいて計算されます。この計算には「バイアス」と「スロープ」という二つの概念が関係します。 ### バイアスとスロープ @@ -162,28 +161,27 @@ veYMTトークンにおける投票権の変化は、ロックされたトーク $$ \text{バイアス} = \text{スロープ} \times (\text{ロック終了時刻} - \text{現在時刻}) $$ ここで、 + - **ロック終了時刻**: ユーザーが設定したトークンのロックが解除される時刻。 - **現在時刻**: 現在の時刻。 - ### 例 -例えば、ユーザーが100 veYMTトークンを4年間ロックするとします。この場合、スロープはロック量を最大ロック期間で割った値になります。 +例えば、ユーザーが 100 veYMT トークンを 4 年間ロックするとします。この場合、スロープはロック量を最大ロック期間で割った値になります。 $$ \text{スロープ} = \frac{100 \text{ veYMT}}{4 \times 365 \times 24 \times 3600 \text{ 秒}} $$ - -ロックが開始された時点でのバイアスは、スロープにロック期間を掛けたものに等しくなります。したがって、バイアスは初め100 veYMTとなりますが、時間が経過するにつれて徐々に減少していきます。 +ロックが開始された時点でのバイアスは、スロープにロック期間を掛けたものに等しくなります。したがって、バイアスは初め 100 veYMT となりますが、時間が経過するにつれて徐々に減少していきます。 ### ロック期間の影響 - **長期ロックの利点**: 長期にわたってトークンをロックするほど、初期のバイアスは大きくなり、投票権の量も多くなります。 - **時間経過とともの減少**: しかし、時間が経過するにつれて、スロープによりバイアスは減少し、投票権も徐々に減少していきます。 -このようにveYMTトークンでは、ロック期間が長いほど、初期の投票権は多くなりますが、その権利は時間とともに減少していきます。このメカニズムにより、ユーザーは長期的なコミットメントを通じてより大きな投票権を得ることができますが、ロック期間が経過するにつれてその影響力は減少していきます。 +このように veYMT トークンでは、ロック期間が長いほど、初期の投票権は多くなりますが、その権利は時間とともに減少していきます。このメカニズムにより、ユーザーは長期的なコミットメントを通じてより大きな投票権を得ることができますが、ロック期間が経過するにつれてその影響力は減少していきます。 ### ユーザーインタラクション: -- **デポジット**: ユーザーは特定の量のYMTトークンをデポジットし、特定の期間でロックすることができます。 +- **デポジット**: ユーザーは特定の量の YMT トークンをデポジットし、特定の期間でロックすることができます。 - **ロックの増加**: ユーザーは既存のロックにトークンを追加するか、ロック期間を延長することが可能です。 - **引き出し**: ロック期間が終了した後、ユーザーはロックされたトークンを引き出すことができます。 From 4a9251d3bf991f9963e7c870fe8384553d29dc5c Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 14 Dec 2023 13:04:58 +0900 Subject: [PATCH 135/412] Removed veYMT admin function. --- contracts/veYMT.sol | 28 ---------------------------- doc/v1.5/veYMT/index.md | 25 ------------------------- 2 files changed, 53 deletions(-) diff --git a/contracts/veYMT.sol b/contracts/veYMT.sol index 86e4f076..68a39928 100644 --- a/contracts/veYMT.sol +++ b/contracts/veYMT.sol @@ -50,8 +50,6 @@ contract veYMT is ReentrancyGuard { uint128 private constant INCREASE_LOCK_AMOUNT = 2; uint128 private constant INCREASE_UNLOCK_TIME = 3; - event CommitOwnership(address admin); - event ApplyOwnership(address admin); event Deposit( address indexed provider, uint256 value, @@ -87,7 +85,6 @@ contract veYMT is ReentrancyGuard { string public symbol; uint8 public decimals; - address public admin; address public futureAdmin; /** @@ -95,7 +92,6 @@ contract veYMT is ReentrancyGuard { * @param tokenAddr_ `YMT` token address */ constructor(address tokenAddr_) { - admin = msg.sender; token = tokenAddr_; pointHistory[0].blk = block.number; pointHistory[0].ts = block.timestamp; @@ -108,25 +104,6 @@ contract veYMT is ReentrancyGuard { symbol = "veYMT"; } - /** - * @notice Transfer ownership of VotingEscrow contract to `addr_` - * @param addr_ Address to have ownership transferred to - */ - function commitTransferOwnership(address addr_) external onlyAdmin { - futureAdmin = addr_; - emit CommitOwnership(addr_); - } - - /** - * @notice Apply ownership transfer - */ - function applyTransferOwnership() external onlyAdmin { - address _admin = futureAdmin; - require(_admin != address(0), "admin not set"); - admin = _admin; - emit ApplyOwnership(_admin); - } - /** * @notice Get the most recently recorded rate of voting power decrease for `addr` * @param addr_ Address of the user wallet @@ -806,9 +783,4 @@ contract veYMT is ReentrancyGuard { ); controller = newController; } - - modifier onlyAdmin() { - require(admin == msg.sender, "admin only"); - _; - } } diff --git a/doc/v1.5/veYMT/index.md b/doc/v1.5/veYMT/index.md index 3693352e..8da2a5d6 100644 --- a/doc/v1.5/veYMT/index.md +++ b/doc/v1.5/veYMT/index.md @@ -10,7 +10,6 @@ veYMT Token は、YMT トークンをロックすることで得られる、ERC- - [Curve VotingEscrow Contract](https://curve.readthedocs.io/dao-vecrv.html) - [The Curve DAO: Liquidity Gauges and Minting CRV](https://curve.readthedocs.io/dao-gauges.html) - [LiquidityGaugeV6 Contract](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/LiquidityGauge.vy) - 以下は、提供された Solidity コード「veYMT Token」のためのドキュメントです。このドキュメントは、以前の「Token Minter」ドキュメントのフォーマットに従って作成されています。 --- @@ -26,18 +25,6 @@ veYMT Token は、YMT トークンをロックすることで得られる、ERC- ## イベント -### `CommitOwnership`: - -- **説明**: 新しい管理者に所有権を委譲する際に発生します。 -- **パラメータ**: - - `admin`: 新しい管理者のアドレス。 - -### `ApplyOwnership`: - -- **説明**: 所有権の移転が適用されたときにトリガーされるイベント。 -- **パラメータ**: - - `admin`: 新しい管理者のアドレス。 - ### `Deposit`: - **説明**: ユーザーがトークンをデポジットした際に発生します。 @@ -92,18 +79,6 @@ veYMT Token は、YMT トークンをロックすることで得られる、ERC- - **パラメータ**: - `tokenAddr_`: YMT トークンのアドレス。 -### `commitTransferOwnership(addr_: address)` - -- **説明**: 新しい管理者に所有権を委譲します。 -- **パラメータ**: - - `addr_`: 新しい管理者のアドレス。 -- **アクセス制限**: 管理者のみ。 - -### `applyTransferOwnership()` - -- **説明**: 所有権の移転を適用します。 -- **アクセス制限**: 管理者のみ。 - ### `depositFor(addr_: address, value_: uint256)` - **説明**: 指定したアドレスのユーザーのためにトークンをデポジットし、ロックします。 From 826eb845b8313a51f4671f4488658cfb3022c487 Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 14 Dec 2023 13:05:44 +0900 Subject: [PATCH 136/412] Add access restrictions to FeePoolV2 documentation. --- doc/v1.5/FeePoolV2/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/v1.5/FeePoolV2/index.md b/doc/v1.5/FeePoolV2/index.md index 70ada39f..a24e19fb 100644 --- a/doc/v1.5/FeePoolV2/index.md +++ b/doc/v1.5/FeePoolV2/index.md @@ -113,17 +113,21 @@ Fee Pool V2 は、手数料プールの管理と手数料の分配を行うコ ### `toggleAllowCheckpointToken()` - **説明**: トークンのチェックポイント許可を切り替えます。 +- **アクセス制限**: 管理者のみ。 ### `killMe()` - **説明**: コントラクトを停止し、残高をガバナンスアドレスに送ります。 +- **アクセス制限**: 管理者のみ。 ### `recoverBalance()` - **説明**: コントラクトからネイティブトークンを回収します。 +- **アクセス制限**: 管理者のみ。 ### `setVeYMT(_veymt: address)` - **説明**: veYMT のアドレスを設定します。 - **パラメータ**: - `_veymt`: veYMT のアドレス。 +- **アクセス制限**: 管理者のみ。 From bb4dd264c2a0a1bf0655d8fed921a234f9ec30ec Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 14 Dec 2023 14:12:28 +0900 Subject: [PATCH 137/412] Added to the v1.5 documentation. --- doc/v1.5/FeePoolV2/index.md | 35 ++++-- doc/v1.5/ScoreRegistry/index.md | 49 +++++++-- doc/v1.5/ScoreWeightController/index.md | 8 +- doc/v1.5/YMT/index.md | 6 +- doc/v1.5/YmtMinter/index.md | 37 +++---- doc/v1.5/veYMT/index.md | 135 ++++++++++++++++++++---- 6 files changed, 205 insertions(+), 65 deletions(-) diff --git a/doc/v1.5/FeePoolV2/index.md b/doc/v1.5/FeePoolV2/index.md index a24e19fb..ddb4aace 100644 --- a/doc/v1.5/FeePoolV2/index.md +++ b/doc/v1.5/FeePoolV2/index.md @@ -50,35 +50,43 @@ Fee Pool V2 は、手数料プールの管理と手数料の分配を行うコ ### `startTime: public(uint256)` -- **説明**: 手数料分配の開始時間。 +- **説明**: 手数料配分が開始されるエポックタイム。この時間以降、手数料が配分され始めます。 + +### `timeCursorOf: mapping(address => uint256)` + +- **説明**: 各ユーザーの時間カーソルを記録するマッピング。ユーザーが最後に報酬を請求した時点での時間を示します。 + +### `userEpochOf: mapping(address => uint256)` + +- **説明**: 各ユーザーの現在のエポックを記録するマッピング。これにより、ユーザーがトークン報酬を請求する際の計算に必要なデータが提供されます。 ### `timeCursor: public(uint256)` -- **説明**: 現在の時間カーソル。 +- **説明**: 現在の時間カーソル。これは手数料計算における時間の進行を追跡するために使用されます。 ### `lastTokenTime: public(uint256)` -- **説明**: 最後のトークン時間。 +- **説明**: 最後にトークンが処理された時間。この時間は、トークンの分配を効率的に管理するために使用されます。 ### `tokensPerWeek: public(mapping(uint256 => uint256))` -- **説明**: 週ごとのトークン分配量。 +- **説明**: 各週に分配されるトークンの量を記録するマッピング。これは、週ごとの分配量を管理するために使用されます。 ### `tokenLastBalance: public(uint256)` -- **説明**: 最後のトークン残高。 +- **説明**: 最後に記録されたトークンの残高。これは、新しいトークンが追加された際の分配計算に使用されます。 ### `veSupply: public(mapping(uint256 => uint256))` -- **説明**: 週ごとの veYMT の総供給量。 +- **説明**: 各週の veYMT の総供給量を記録するマッピング。これは、週ごとの veYMT の供給を追跡するために使用されます。 ### `canCheckpointToken: public(bool)` -- **説明**: トークンのチェックポイントを任意のアカウントが行えるかどうかのフラグ。 +- **説明**: トークンのチェックポイントを任意のアカウントが実行できるかどうかを示すブール値。これは、コントラクトの柔軟性を高めるために使用されます。 ### `isKilled: public(bool)` -- **説明**: コントラクトが停止されたかどうかを示すフラグ。 +- **説明**: コントラクトが停止されたかどうかを示すブール値。この値が `true` の場合、コントラクトの主要機能は使用不可になります。 --- @@ -94,6 +102,17 @@ Fee Pool V2 は、手数料プールの管理と手数料の分配を行うコ - **説明**: トークンのチェックポイントを更新します。 +### `veForAt(user_: address, timestamp_: uint256)` + +- **説明**: 特定のタイムスタンプ時点でのユーザーの veYMT バランスを取得します。 +- **パラメータ**: + - `user_`: バランスを確認するユーザーのアドレス。 + - `timestamp_`: バランスを確認するタイムスタンプ。 + +### `checkpointTotalSupply()` + +- **説明**: veYMT の総供給量のチェックポイントを更新します。 + ### `claim()` - **説明**: `msg.sender`の手数料を請求します。 diff --git a/doc/v1.5/ScoreRegistry/index.md b/doc/v1.5/ScoreRegistry/index.md index 007df0c2..bd87a3d4 100644 --- a/doc/v1.5/ScoreRegistry/index.md +++ b/doc/v1.5/ScoreRegistry/index.md @@ -25,27 +25,47 @@ ### `isKilled: public(bool)` -- **説明**: コントラクトが停止されているかどうかを示すフラグ。 +- **説明**: このフラグは、コントラクトが「停止」状態にあるかどうかを示します。`isKilled`が`true`の場合、コントラクトの主要な機能が無効になり、使用できなくなります。 ### `futureEpochTime: public(uint256)` -- **説明**: 次のエポックの開始時刻。 +- **説明**: これは、コントラクトの次のエポック(期間)がいつ始まるかを示すタイムスタンプです。 ### `inflationRate: public(uint256)` -- **説明**: インフレーション率。 +- **説明**: システムにおけるインフレーション(通貨膨張)率を表す値です。 ### `workingBalances: public(mapping(address => uint256))` -- **説明**: 各アドレスの労働バランス。 +- **説明**: 各ユーザーの「労働バランス」を保持するマッピングです。 ### `workingSupply: public(uint256)` -- **説明**: 総労働供給量。 +- **説明**: システム全体の総労働供給量を示す値です。 + +### `integrateInvSupplyOf: public(mapping(address => uint256))` + +- **説明**: 各アドレスごとに、最後のチェックポイントから現在までの平均インフレーション率を積分した値を保持します。 + +### `integrateCheckpointOf: public(mapping(address => uint256))` + +- **説明**: 各アドレスの最後のチェックポイントのタイムスタンプを保持します。 ### `integrateFraction: public(mapping(address => uint256))` -- **説明**: ユーザーごとの積分値。 +- **説明**: 各ユーザーが特定の期間において受け取るべき報酬の量を積分値として保持するマッピングです。 + +### `period: public(int128)` + +- **説明**: 現在の期間(エポック)を示すカウンターです。 + +### `periodTimestamp: public(mapping(int128 => uint256))` + +- **説明**: 各期間(エポック)の開始タイムスタンプを保持するマッピングです。 + +### `integrateInvSupply: public(mapping(int128 => uint256))` + +- **説明**: 各期間(エポック)における総供給量に対するインフレーション率の積分値を保持するマッピングです。 --- @@ -80,6 +100,7 @@ - **説明**: ユーザーのチェックポイントとスコアを更新します。 - **パラメータ**: - `addr_`: ユーザーのアドレス。 +- **アクセス制限**: 自分のアドレスまたは、YmtMinter コントラクトのみ ### `kick(addr_: address)` @@ -98,6 +119,22 @@ - **説明**: 最後のチェックポイントのタイムスタンプを取得します。 +### `YMT()` + +- **説明**: YMT トークンのアドレスを返します。 + +### `veYMT()` + +- **説明**: veYMT コントラクトのアドレスを返します。 + +### `ymtMinter()` + +- **説明**: YmtMinter コントラクトのアドレスを返します。 + +### `scoreWeightController()` + +- **説明**: ScoreWeightController コントラクトのアドレスを返します。 + --- ## コントラクト詳細 diff --git a/doc/v1.5/ScoreWeightController/index.md b/doc/v1.5/ScoreWeightController/index.md index 0ccb20fd..c6e0b4c8 100644 --- a/doc/v1.5/ScoreWeightController/index.md +++ b/doc/v1.5/ScoreWeightController/index.md @@ -48,24 +48,24 @@ Score Weight Controller は、流動性スコアを管理し、これらのス ### `checkpoint()` -- **説明**: すべてのスコアに共通のデータを記録するためのチェックポイント関数。 +- **説明**: すべてのスコアに共通のデータを記録するためのチェックポイント関数。(V2.0 で実装予定) ### `checkpointScore(addr_: address)` -- **説明**: 特定のスコアおよびすべてのスコアに共通のデータを記録するためのチェックポイント関数。 +- **説明**: 特定のスコアおよびすべてのスコアに共通のデータを記録するためのチェックポイント関数。(V2.0 で実装予定) - **パラメータ**: - `addr_`: スコアのアドレス。 ### `scoreRelativeWeight(addr_: address, time_: uint256)` -- **説明**: 指定されたスコアの相対重みを取得します。 +- **説明**: 指定されたスコアの相対重みを取得します。(V2.0 で実装予定。V1.5 では一定の値を返します。) - **パラメータ**: - `addr_`: スコアのアドレス。 - `time_`: 相対重みを計算するための時刻。 ### `changeScoreWeight(addr_: address, weight_: uint256)` -- **説明**: 指定されたスコアの重みを変更します。 +- **説明**: 指定されたスコアの重みを変更します。(V2.0 で実装予定) - **パラメータ**: - `addr_`: スコアのアドレス。 - `weight_`: 新しいスコアの重み。 diff --git a/doc/v1.5/YMT/index.md b/doc/v1.5/YMT/index.md index 0ae06029..6f91545c 100644 --- a/doc/v1.5/YMT/index.md +++ b/doc/v1.5/YMT/index.md @@ -56,6 +56,10 @@ YMT Token はインフレーションを伴う分配型トークンであり、 - **説明**: 現在のエポックの開始時点での供給量。 +### `startTime: public(uint256)` + +- **説明**: デプロイした時の時刻を保存。 + --- ## 関数 @@ -117,8 +121,6 @@ YMT Token はインフレーションを伴う分配型トークンであり、 - **パラメータ**: - `_value`: 焼却するトークンの量。 -以下は、提供された Solidity コード「YMT Token」のためのドキュメントです。このドキュメントは、非技術者が理解できるように作成され、コントラクトの主要な機能とメカニズムを詳細に説明します。 - --- ## コントラクトの詳細 diff --git a/doc/v1.5/YmtMinter/index.md b/doc/v1.5/YmtMinter/index.md index 65dd86d9..eab11261 100644 --- a/doc/v1.5/YmtMinter/index.md +++ b/doc/v1.5/YmtMinter/index.md @@ -6,23 +6,6 @@ Token Minter は、特定のスコアレジストリからトークンを mint --- -## インターフェース - -### `IScoreRegistry`: - -- **説明**: スコアレジストリから情報を取得するためのインターフェース。 -- **関数**: - - `integrateFraction(addr: address) -> uint256`: 指定したアドレスの積分値を取得する。 - - `userCheckpoint(addr: address) -> bool`: ユーザーチェックポイントを更新する。 - -### `IYMT`: - -- **説明**: トークンを mint するための ERC20 トークンインターフェース。 -- **関数**: - - `mint(_to: address, _amount: uint256) -> bool`: トークンを mint する。 - ---- - ## イベント ### `Minted`: @@ -56,13 +39,6 @@ Token Minter は、特定のスコアレジストリからトークンを mint - `_ymtAddr`: YMT トークンのアドレス。 - `_scoreWeightControllerAddr`: スコアウェイトコントローラーのアドレス。 -### `_mintFor(scoreAddr: address, _for: address)` - -- **説明**: 内部関数。指定したスコアレジストリからトークンを mint する。 -- **パラメータ**: - - `scoreAddr`: スコアレジストリのアドレス。 - - `_for`: トークンを受け取るアドレス。 - ### `mint(scoreAddr: address)` - **説明**: `msg.sender`に属するすべてのトークンを mint して送信する。 @@ -81,3 +57,16 @@ Token Minter は、特定のスコアレジストリからトークンを mint - **説明**: `mintingUser`が`msg.sender`の代わりにトークンを mint することを許可または禁止する。 - **パラメータ**: - `mintingUser`: 許可または禁止を切り替えるユーザーのアドレス。 + +### `toggle()` + +- **説明**: コントラクトの状態を一時停止または再開します。この関数は、コントラクトの管理者(governance)のみが呼び出すことができます。コントラクトが一時停止中の場合、この関数はコントラクトを再開します。逆に、コントラクトがアクティブな状態の場合、一時停止します。一時停止中は、コントラクトの主要な機能が使用不可になります。 +- **アクセス制限**: 管理者のみ。 + +### `YMT()` + +- **説明**: YMT トークンのアドレスを返します。 + +### `scoreWeightController()` + +- **説明**: ScoreWeightController コントラクトのアドレスを返します。 diff --git a/doc/v1.5/veYMT/index.md b/doc/v1.5/veYMT/index.md index 8da2a5d6..73d1651b 100644 --- a/doc/v1.5/veYMT/index.md +++ b/doc/v1.5/veYMT/index.md @@ -13,16 +13,6 @@ veYMT Token は、YMT トークンをロックすることで得られる、ERC- --- -## インターフェース - -### `IYMT`: - -- **説明**: YMT トークンのインターフェース。veYMT トークンはこのトークンを基にしています。 -- **関数**: - - トークンに関連する標準的な ERC20 関数群。 - ---- - ## イベント ### `Deposit`: @@ -55,19 +45,59 @@ veYMT Token は、YMT トークンをロックすることで得られる、ERC- ### `locked: public(mapping(address => LockedBalance))` -- **説明**: ユーザーごとのロックされたバランスを記録します。 +- **説明**: 各ユーザーのロックされたバランス(`LockedBalance`構造体)を保持するマッピングです。これには、ロックされたトークンの量(`amount`)と、ロックが終了する時間(`end`)が含まれます。 ### `epoch: public(uint256)` -- **説明**: 現在のエポック番号。 +- **説明**: コントラクトの現在のエポック(期間)番号を示します。エポックは、システムの状態を追跡するための時間単位で、特定のイベントや変更が記録されるタイミングを表します。 ### `token: public(address)` -- **説明**: veYMT トークンが基づいている YMT トークンのアドレス。 +- **説明**: veYMT トークンが基づく YMT トークンのアドレスを示します。これは、veYMT トークンが YMT トークンをロックするために使用される基本通貨のアドレスです。 ### `supply: public(uint256)` -- **説明**: 総供給量。 +- **説明**: コントラクトにおける総供給量を示します。この値は、すべてのユーザーがロックしたトークンの総量を表し、veYMT の総発行量と見なすことができます。 + +### `pointHistory: public(mapping(uint256 => Point))` + +- **説明**: 各エポックにおけるシステムの状態(`Point`構造体)を追跡するためのマッピングです。`Point`には、その時点のバイアス(`bias`)、傾斜(`slope`)、タイムスタンプ(`ts`)、ブロック番号(`blk`)が含まれます。 + +### `userPointHistory: public(mapping(address => mapping(uint256 => Point)))` + +- **説明**: 各ユーザーごとに、異なるエポックでのユーザーの状態(`Point`構造体)を追跡するための二重マッピングです。これにより、ユーザーが過去にどのような投票力を持っていたかを把握することができます。 + +### `userPointEpoch: public(mapping(address => uint256))` + +- **説明**: 各ユーザーの現在のエポック番号を保持するマッピングです。これは、ユーザーの最新の状態が記録されたエポックを示します。 + +### `slopeChanges: public(mapping(uint256 => int128))` + +- **説明**: 未来の特定の時点で予定されている傾斜(`slope`)の変化を追跡するマッピングです。この値は、投票力の変化を表す際に使用されます。 + +### `controller: public(address)` + +- **説明**: コントラクトのコントローラー(管理者)のアドレスです。コントローラーは、コントラクトの特定の機能を管理する責任を持ちます。 + +### `transfersEnabled: public(bool)` + +- **説明**: トークンの転送が有効かどうかを示すブール値です。Aragon との互換性のために含まれていますが、veYMT トークンでは転送は基本的に行われません。 + +### `name: public(string)` + +- **説明**: トークンの名前です。「Voting-escrowed Yamato」となります。 + +### `symbol: public(string)` + +- **説明**: トークンのシンボルです。「veYMT」となります。 + +### `decimals: public(uint8)` + +- **説明**: トークンの小数点以下の桁数です。YMT トークンの小数点以下の桁数に基づいて設定されます。 + +### `futureAdmin: public(address)` + +- **説明**: 将来の管理者のアドレスです。これは、所有権の移転プロセス中に使用される一時的な変数です。所有権が正式に移転されると、`controller`が更新されます。 --- @@ -75,41 +105,104 @@ veYMT Token は、YMT トークンをロックすることで得られる、ERC- ### `constructor(tokenAddr_: address)` -- **説明**: コントラクトのコンストラクタ。YMT トークンのアドレスを設定します。 +- **説明**: veYMT コントラクトのコンストラクタです。YMT トークンのアドレスを受け取り、コントラクトの初期設定を行います。 - **パラメータ**: - `tokenAddr_`: YMT トークンのアドレス。 +### `getLastUserSlope(addr_: address)` + +- **説明**: 指定されたアドレスのユーザーの最新の投票力の減少率を取得します。 +- **パラメータ**: + - `addr_`: ユーザーのウォレットアドレス。 + +### `userPointHistoryTs(addr_: address, idx_: uint256)` + +- **説明**: 指定されたアドレスのユーザーの特定のチェックポイントのタイムスタンプを取得します。 +- **パラメータ**: + - `addr_`: ユーザーのウォレットアドレス。 + - `idx_`: ユーザーのエポック番号。 + +### `lockedEnd(addr_: address)` + +- **説明**: 指定されたアドレスのユーザーのロックが終了するタイムスタンプを取得します。 +- **パラメータ**: + - `addr_`: ユーザーのウォレットアドレス。 + +### `checkpoint()` + +- **説明**: グローバルデータをチェックポイントとして記録します。この関数は外部から呼び出されることを意図しています。 + ### `depositFor(addr_: address, value_: uint256)` -- **説明**: 指定したアドレスのユーザーのためにトークンをデポジットし、ロックします。 +- **説明**: 指定されたアドレスのユーザーのためにトークンをデポジットし、ロックします。この関数は外部から呼び出されることを意図しています。 - **パラメータ**: - `addr_`: トークンをデポジットするユーザーのアドレス。 - `value_`: デポジットするトークンの量。 ### `createLock(value_: uint256, unlockTime_: uint256)` -- **説明**: 新しいロックを作成し、トークンをデポジットします。 +- **説明**: 新しいロックを作成し、トークンをデポジットします。ロックは指定された時間まで有効です。 - **パラメータ**: - `value_`: デポジットするトークンの量。 - `unlockTime_`: トークンのロックを解除する時間(エポック秒)。 ### `increaseAmount(value_: uint256)` -- **説明**: 既存のロックにトークンを追加します。 +- **説明**: 既存のロックにトークンを追加します。ロックの期限は変更されません。 - **パラメータ**: - `value_`: 追加するトークンの量。 ### `increaseUnlockTime(unlockTime_: uint256)` -- **説明**: ロックの解除時間を延長します。 +- **説明**: ロックの解除時間を延長します。トークンの量は変更されません。 - **パラメータ**: - `unlockTime_`: 新しいロック解除時間(エポック秒)。 ### `withdraw()` -- **説明**: ロックが解除されたトークンを引き出します。 +- **説明**: ロックが解除されたトークンを引き出します。この関数は、ロックの期限が満了した後にのみ呼び出すことができます。 + +### `balanceOf(addr_: address, t_: uint256)` + +- **説明**: 指定された時点での指定されたアドレスのユーザーの現在の投票力を取得します。 +- **パラメータ**: + - `addr_`: ユーザーのウォレットアドレス。 + - `t_`: 投票力を取得するエポック時間。 + +### `balanceOf(addr_: address)` + +- **説明**: 現在時点での指定されたアドレスのユーザーの投票力を取得します。 +- **パラメータ**: + - `addr_`: ユーザーのウォレットアドレス。 -以下は、提供された Solidity コード「veYMT Token」の追加的なドキュメント記述です。このセクションは、コントラクトの仕様をより深く理解するのに役立ちます。 +### `balanceOfAt(addr_: address, block_: uint256)` + +- **説明**: 特定のブロック高さでの指定されたアドレスのユーザーの投票力を測定します。 +- **パラメータ**: + - `addr_`: ユーザーのウォレットアドレス。 + - `block_`: 投票力を計算するブロック番号。 + +### `totalSupply(t_: uint256)` + +- **説明**: 指定された時点での総投票力を計算します。 +- **パラメータ**: + - `t_`: 総投票力を計算するエポック時間。 + +### `totalSupply()` + +- **説明**: 現在時点での総投票力を計算します。 + +### `totalSupplyAt(block_: uint256)` + +- **説明**: 過去の特定のブロックでの総投票力を計算します。 +- **パラメータ**: + - `block_`: 総投票力を計算するブロック番号。 + +### `changeController(newController: address)` + +- **説明**: コントローラーを変更します。この機能は、現在のコントローラーのみが呼び出すことができます。 +- **パラメータ**: + - `newController`: 新しいコントローラーのアドレス。 --- From 1367c8194b3ce440acc9f981cb3a3c908a9ab27e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Thu, 14 Dec 2023 15:24:05 +0900 Subject: [PATCH 138/412] Removed veYMT admin test code. --- .../unitary/veYMT/votingescrowAdmin.test.ts | 84 ------------------- 1 file changed, 84 deletions(-) delete mode 100644 test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts diff --git a/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts b/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts deleted file mode 100644 index 4e97d2e9..00000000 --- a/test/v1.5/unitary/veYMT/votingescrowAdmin.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { - takeSnapshot, - SnapshotRestorer, -} from "@nomicfoundation/hardhat-network-helpers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { - YmtVesting, - YMT, - YmtVesting__factory, - YMT__factory, - VeYMT, - VeYMT__factory, -} from "../../../../typechain"; - -describe("veYMT", () => { - let accounts: SignerWithAddress[]; - let veYMT: VeYMT; - let YMT: YMT; - let YmtVesting: YmtVesting; - let snapshot: SnapshotRestorer; - - before(async function () { - accounts = await ethers.getSigners(); - - YmtVesting = await (( - await ethers.getContractFactory("YmtVesting") - )).deploy(); - - YMT = await (await ethers.getContractFactory("YMT")).deploy( - YmtVesting.address - ); - - veYMT = await (( - await ethers.getContractFactory("veYMT") - )).deploy(YMT.address); - }); - - beforeEach(async () => { - snapshot = await takeSnapshot(); - }); - - afterEach(async () => { - await snapshot.restore(); - }); - - // 管理者権限がない場合のapplyTransferOwnership関数テスト - it("Commit ownership transfer by non-admin", async function () { - await expect( - veYMT.connect(accounts[1]).commitTransferOwnership(accounts[1].address) - ).to.be.revertedWith("admin only"); - }); - - // 管理者権限がない場合のapplyTransferOwnership関数テスト - it("Apply ownership transfer by non-admin", async function () { - await expect( - veYMT.connect(accounts[1]).applyTransferOwnership() - ).to.be.revertedWith("admin only"); - }); - - // commitTransferOwnershipテスト - it("Successful commitment of transfer ownership", async function () { - await veYMT.commitTransferOwnership(accounts[1].address); - - expect(await veYMT.admin()).to.equal(accounts[0].address); - expect(await veYMT.futureAdmin()).to.equal(accounts[1].address); - }); - - // commitTransferOwnershipテスト - it("Successful application of transfer ownership", async function () { - await veYMT.commitTransferOwnership(accounts[1].address); - await veYMT.applyTransferOwnership(); - - expect(await veYMT.admin()).to.equal(accounts[1].address); - }); - - // コミットなしでのapplyTransferOwnership関数テスト - it("Apply transfer ownership without commit", async function () { - await expect(veYMT.applyTransferOwnership()).to.be.revertedWith( - "admin not set" - ); - }); -}); From f01832f4acb2bd00c439cce99f6476e22f47345e Mon Sep 17 00:00:00 2001 From: naizo10 Date: Fri, 15 Dec 2023 13:17:29 +0900 Subject: [PATCH 139/412] A use case diagram for veYMT was developed. --- doc/v1.5/veYMT/usecase.md | 228 ++++++++++++++++++++----------------- doc/v1.5/veYMT/usecase.png | Bin 0 -> 566914 bytes 2 files changed, 125 insertions(+), 103 deletions(-) create mode 100644 doc/v1.5/veYMT/usecase.png diff --git a/doc/v1.5/veYMT/usecase.md b/doc/v1.5/veYMT/usecase.md index 5495236b..dd48ca57 100644 --- a/doc/v1.5/veYMT/usecase.md +++ b/doc/v1.5/veYMT/usecase.md @@ -1,116 +1,95 @@ ```mermaid graph LR classDef transparent fill:none,stroke:none; - owner{{オーナー}} + + %%======= action ======= + holder{{YMTホルダー}} - yamato{{Yamato操作}} user{{ユーザ}} + keeper{{管理者やスクリプト実行}} - create_lock[YMT をロックする] - increase_amount[YMT ロック量を増額する] - increase_unlock_time[YMT ロック期間を延長する] - withdraw[YMTを引き出す] - uck[ユーザのポイント履歴を更新] - ck[全体のポイント履歴を更新] - ve_total_supply[ポイント履歴を取得] + %% Yamato + user ---> yamato_action + user ---> yamato_action_eth - mint[Tokenをmint] - rate[インフレーションレートの取得] - future_epoch_time_write[次回のインフレーションレート変更タイムスタンプ取得] - update_rate[インフレーションレート更新] - update_minted[ユーザのミント済みYMTトークン額を更新] - minted[ユーザのミント済みYMTトークン額を取得] - claim_YMT[YMTの報酬をクレーム] + %% ScoreRegistry + user ---> |v1→v1.5の初回実行する必要あり| userCheckpoint + keeper ---> kick + + %% veYMT + user --- ck + user ---> holder + holder ---> create_lock + holder ---> increase_amount + holder ---> increase_unlock_time + holder ---> withdraw - change_admin[管理者を変更する] - change_smchecker[スマートウォレットチェッカーを変更する] + %% YmtMinter + user ---> claim_YMT + %%======= ScoreRegistry ======= - claimable_tokens[ + integrate_fraction[ YMTのトータルmint額を取得 integrate_fraction ] - user_checkpoint[ + userCheckpoint[ ユーザーの報酬を更新 - user_checkpoint + userCheckpoint ] + checkpoint[ + チェックポイントを更新 + checkpoint + ] + updateScoreLimit[ + スコアを更新 + updateScoreLimit + ] + kick[ + ブースト切れのユーザーをリセット + kick + ] + + subgraph ScoreRegistry + integrate_fraction + userCheckpoint + checkpoint + updateScoreLimit + kick + end - gauge_relative_weight[ゲージの重みを取得する] - - collateral_ratio[預入額を取得] - balance_issued[CJPYの発行額を取得] - first_checkpoint[チェックポイントを更新する] - depositor - borrower - repayer - withdrawer - redeemer - sweeper + userCheckpoint -.->|include| checkpoint + userCheckpoint -.->|include| updateScoreLimit + kick -.->|include| checkpoint + kick -.->|include| updateScoreLimit - create_lock -.->|include| uck - increase_amount -.->|include| uck - increase_unlock_time -.->|include| uck - withdraw -.->|include| uck - uck -.->|include| ck - user --- ck - holder --- create_lock - holder --- increase_amount - holder --- increase_unlock_time - holder --- withdraw + userCheckpoint -.->|view| collateral_ratio + userCheckpoint -.->|view| balance_issued + checkpoint -.->|view| gauge_relative_weight - user ---> holder - user --- claim_YMT - user ---|View関数として実行| claimable_tokens + checkpoint -.->|include| integrate_fraction + checkpoint -.->|include| future_epoch_time_write + updateScoreLimit -.->|view| ve_total_supply + checkpoint -.->|view| rate - claimable_tokens -.->|include| claim_YMT - claim_YMT -.->|include| mint - claim_YMT -.-|view| minted - claim_YMT -.->|include| update_minted + %%======= ScoreWeightController ======= - future_epoch_time_write -.->|include| update_rate + gauge_relative_weight[ゲージの重みを取得する] - user ---> |v1→v1.5の初回実行する必要あり| first_checkpoint - user_checkpoint -.->|include| claimable_tokens - ve_total_supply -.-|view| user_checkpoint - rate -.-|view| user_checkpoint - future_epoch_time_write -.-|include| user_checkpoint - - owner --- change_admin - owner --- change_smchecker - - user --- yamato - yamato --- depositor - yamato --- borrower - yamato --- repayer - yamato --- withdrawer - yamato --- redeemer - yamato --- sweeper - - depositor -.->|include| user_checkpoint - borrower -.->|include| user_checkpoint - repayer -.->|include| user_checkpoint - withdrawer -.->|include| user_checkpoint - redeemer -.->|include| user_checkpoint - sweeper -.->|include| user_checkpoint - first_checkpoint -.->|include| user_checkpoint - collateral_ratio -.-|view| user_checkpoint - balance_issued -.-|view| user_checkpoint - gauge_relative_weight -.-|view| user_checkpoint - - subgraph Admin[ ] - direction LR + subgraph ScoreWeightController + gauge_relative_weight end - class Admin transparent + %%======= veYMT ======= - subgraph Gauge - claimable_tokens - user_checkpoint - end - subgraph Gauge Controller - gauge_relative_weight - end + create_lock[YMT をロックする] + increase_amount[YMT ロック量を増額する] + increase_unlock_time[YMT ロック期間を延長する] + withdraw[YMTを引き出す] + uck[ユーザのポイント履歴を更新] + ck[全体のポイント履歴を更新] + ve_total_supply[ve残高を取得] subgraph veYMT ve_total_supply @@ -121,36 +100,79 @@ graph LR withdraw uck ck - change_admin - change_smchecker end - subgraph YMT - direction LR - mint - rate - future_epoch_time_write - update_rate - end + create_lock -.->|include| uck + increase_amount -.->|include| uck + increase_unlock_time -.->|include| uck + withdraw -.->|include| uck + uck -.->|include| ck + + %%======= YmtMinter ======= + + claim_YMT[YMTの報酬をクレーム] - subgraph Minter + subgraph YmtMinter direction LR claim_YMT minted update_minted end - subgraph Yamato - collateral_ratio - balance_issued - first_checkpoint + claim_YMT -.->|view| integrate_fraction + + claim_YMT -.->|include| mint + claim_YMT -.-|view| minted + claim_YMT -.->|include| update_minted + + %%======= Yamato ======= + + collateral_ratio[預入額を取得] + balance_issued[CJPYの発行額を取得] + + yamato_action_eth[ depositor + withdrawer + ※CJPY発行がある場合のみ + ] + yamato_action[ borrower repayer - withdrawer redeemer sweeper + ※毎回 + ] + + subgraph Yamato + collateral_ratio + balance_issued + yamato_action_eth + yamato_action end + yamato_action_eth -.->|include| checkpoint + yamato_action -.->|include| checkpoint + + yamato_action_eth -.->|include| updateScoreLimit + yamato_action -.->|include| updateScoreLimit + + %%======= YMT ======= + + mint[Tokenをmint] + rate[インフレーションレートの取得] + future_epoch_time_write[次回のインフレーションレート変更タイムスタンプ取得] + update_rate[インフレーションレート更新] + update_minted[ユーザのミント済みYMTトークン額を更新] + minted[ユーザのミント済みYMTトークン額を取得] + + subgraph YMT + direction LR + mint + rate + future_epoch_time_write + update_rate + end + + future_epoch_time_write -.->|include| update_rate ``` diff --git a/doc/v1.5/veYMT/usecase.png b/doc/v1.5/veYMT/usecase.png new file mode 100644 index 0000000000000000000000000000000000000000..4bd26d5e86a9b744b3c35c061c4464b977b24caa GIT binary patch literal 566914 zcmeFZ2T)UO*ER|$qM)Kum1;#wKm?^XQBV)N^Ka?Y6Q zrjewx9@O9OX5EZALN;^EL`3@yENX=JU`OpupMx?{BOgz)$BsO+R7gnnJo8!mU^=-s zmxJiYIkGRlFJsX)%GX9-R~{>3T;6}}*&CPPFf{s;Jo8tYJI6VWe^y^Qc?k6?WVJZr zP+kP9%G&^omdh|#0lBb?fgxKcuOK39+VX8LYyHc&A$4B&zxe&298hXL_&WE}{kw~N zG_DzoEZbG>uH2SG?Y8AH@0SJD)|OFV#1E&I9M%b9XdljHYJA!n+lnajO}a z7_I8Cxn`B1#gN!oAuSoBFERd+3}@2=8`hW6E3e++{qv;EO0Qd4}AT<@GZkpTy-ejP1*Le8Pvtp@{Q3+*&cKHBBt~xLPU*H2C_o(OnCYfSNkll8h! z%4_kDG$-X4U&Cs>e{v!&9;1G0^4|60p%d3mJiJnHoX58@{`M5vJ(``vu|E9QwB8#| zNehz&$|BykUZTEGBdpDG)pxSyaKs_1>f?)QqK8_ZtqfDR(&&?G)hPbDuyMwNt%^qJ zSNI|`b^OhX4DZ+*Pt?igs(wlly?5qzfUZB!wa|pLyGK$q?=j6>z3u1zeDhiTht!AS zeJAFg;N%rQluq4Q)pa~ncEaGP?KPKpL;_V?!W4p;T7kyY$F4p{P%z@$H|jg5lUN;? z9Zq$;D)m44*`wMdmq(i}<7^i#i@)-xtj}S_>n1F@Ha&(|PUpifYEnn}9%q#pvr>fz z^48APv(*_?4}3oIOM2v%t3d^9kc#64)5yxw_=fnl+cs?blGl@X*-K|c>7E>tyvG0j z-Q$;Sw@c)h&)hl2@an>KEtCC%&r5u?<*YfKPjDO`dUNxf=UY1Ncjq`Wxinq}g=D=x za5L`9?FSB*{VSgFokpDII9>c^A!O+8MS4>|s`Ei_m3yzHChC8XeDCotp;6~+?zpS{ z>{!}Z)R^tp$}p}kEMJAj!@o{`4R4BjhdE8W?GtaDQLQ$7F+(FoO*Ek``%=8uLEVEXfM^!w<@s{OlzYnKWb?)&yon1Rt7$SbXrv1)CYhCKh z2Cxm*3r-lO>NW~$molAS8~!< zR&SOM%+~zamEx>5moIKGpeLo_8SS3GX?moSW_h4lqa1)haB`pOFhEHUkOzc01j`riBA!O-G*c-m|dZ`;6oq})2C zL1fFvAU7j8*otJaKeKG0|f(x(fSMZ-|M3Tq66ciQzCPem+#Cdvq!4k8Bi8dPFG%txZlid zo@$P4wvDQpz)gIe(3lY6Gp3=U@u@TPA9RawQ(1Yok~X=f_ZAizCKk39woj8s(?=7p z*{qp8$o4f5D7;cU-ly3+n48dp@5#)!%0=c^ zn~tlj&v)I`ekD*A(M)m)@pf{=w<>a_c4 z(GB;5?w37?VbE>6)eG7ph$2{#n{ zpZP~UmAfS2id-B$7V`SrYolwq?@zzic;EWj`m^Q>r|0T5;V-(Mqh2lsTnuz&wmilC z$~yRMuk;7AZ!iAw zV7f6;E8LP#Nzd7~blbjVIbm5HPF9s_ma2Xa=Brr;Vd47_P1QoLb+Oy} z1mZ@;2dlcSy74-r+a>3_y>jsHM_zn|z0z~P&31d;rp}hZcCLS-Dl_R$E?>6wV`LjH ziEWkbhQ#24e$~n&{FCLK@|Wf3%0DvrFu=->xF8005-*o7+lE?bjIw7+MXv`ejL%gT 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 *4O-1&&|5Hi^RnmG9Fuw?PaGsE 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%0A1Cyc!+dg@ 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 z zrFuk(d~tMmy5Nlj4*yZZhqw~^iJH%Jo_P{8c0v9EuxU`cPE@0LMvMn$*ui_ti!0~R zRzFF45SeEYH9dOut2X(CGJr<|^sThoto}bmFfg{2;}6y`htW#wTaKx9{gx4*@AVP4 z1xU}N~x-Y?50YPX;FI6YdrN94$_eK`H?PCS^`A*AI{0 z4QJ!wi>aWD$$jF3ZB3RB& zV@Vu-3b?O+Zj#s-7X5H=S0-kfQJjkt9h0&mYz27dd^l!-RUXH7Tfv?rKpv0Sffs9kGl^UT|op+dDEU-R_iT^ zekVtLqC&|2yD@|_O%Vbbyz~`5#ozNi`dmasecaT!NnayZM&gL4vnOr> zhS?spuN7agY1GT($FP*#s6|^Tf7VyhX>66vpWkA*b=-vxf$ZWucBCbq?d#pZ{%M+z z@ZU}#qDWwww3f9~X+)e1;o>CakW=NsHzwSzq2N)!4Od+*$nmLz8$ETXwyu7+v-?y) zA{g-^#j8jjVo|{Sa>M5N6Ev%oq{m0^8p96cEZFvN~fQg z^5OMC1zXSmBkMcBv26eMBNd@Yk%T8RyAZOIRYqpn6d4&I*)t?$WY5T6*<@3;?2%0< zd++W4xvBT}ec%6aI2^g3qvv_<>%OkfJkRq(S>9OYgp{fHEREi^O}VvWU8Ym%KKs^? z@ReBAF6aqTY3y6CR5*@;_wunBZc4?gDc`m1pj0`}vo3~oaw*wt z&Ie{+2Yd^oXay!~S3sS*q+pKgbRSFj{M!B|bbTVl=PZq>?I3PSm>`{6xnqy9KKf`) zw?+7gb9)b(EyOFdBt@m1Gc9dw_4EYV^*(_Py6ET6xMTws;BpB%#xYdC$_Z%2@{ODl_qd};cW47w+=70pKFb5&ZnNT(-ANu_yi*%yf zLpUP%&?+$6`vk55A~O&%usjJk<{4x10{<@a$ZX%*U#m|qnxOL_oK-jI8_o3kA~@1E zjia;4Z}TtsUMzRkU6lUF7(RXUlH+hQGA|BcgDr(QtK-If%i%aU~qUze(M&cAZb~ljaw= z0BsFiecm(x697-`u?L>9I~sBDeeuS=hp``MN0-D3EnCJbV0>Go&4D|SMz#YmlQZ>~ z%R=hUWY2B(wrpjb^&#oc+SSO*?%ikkDhyBd^^mdZ5f8p#n^jP+e1bok>)8RvAdam) z8d!mA3InxaFL!+y8!phk+=*zC97uVb=(W&l&4#7}rBbpP6>7E>+UW4@cm_!%uBSBa zd^d-|PGI-+)#xDi$`Cg9Q2*~(f+Z`<`Y2!OlP;(MUQ$JKH5K6@RGdb;YYK_#IKI-yNAb^6(-63tQiUW%gPRNUzH&fR+PgqXe8mgJw5FNL zBTKU0&tg`MS~Ol~ELKm*1auuf`c zaY>)CW%97r@x19IVo{-r7kzK9rr2uC?Q1qF?GO3n-7mvsX7Xnvf3EaYW>}io$(<^l zIMt$~`MIu=f%0*xf^utwCxP#&P3_57N|h#j^`ABkfmupDwRiXT1GIVD_Ai07$HuhM zc<83`sKXOzLn8?YegfHuwV$&iVOQ8*z$lCQY=`u7s0}!BScdI=x;%O*MJ+WTK`OBB zkaylVcT=;%oO!cXALf=ux}8Q*}(T!L~0$IiNBaIt)wKZ<<HvP4XpR2R-Oiy*d?{m_H`mHy}+5GAg!5-qbI{8{x ziseZ~6xIbN&BNR2jO@nUN`M4?Huv6$b(QFFo=tgs)Pa4N%eqgdqe^>WZi!~EQrA{< zV}jmL`_b-FmdEKnN)a!r;BwF%&wLL zxM3E}oHs$t@^v-?DuJJhdwcn!mu#S?SHXlJ$9c>iRdO1SR;bfNDpG#SgG7HYQzqV_ z{rxg_3l-}4a5+06D<9JvEc?H`QNe8ltn9d%KYs@OT5P7vEKD{G1wS1CRpf!~xr2Cs*i<>Jq}AgrQDI(?X(m zaaesMBulaH%P40+rzAxY0ApZMwj%H?pXY_nap|^2P!rJF8+x#7Eu(A>DjCbJ2|6AQ z-F@JH>yy;AY|9oE0`Y?awp;3t+B2s0q+>#QoA2%}DY@1&D5mHS#}HO!PHip@ipfPb z_oy&O3A~*1I%;Cz;Bo{SduNG=MClTowrl%$jVE!iFfskRk}eh)b_X?fB@JwGw}Mz} z6Diy6$sS=(;nQ>Vex$T(ldT1>nU%^Kd^PiG=lBntzHF-JZ9TfmsZscHP}k3n%lRM> zJw|+Zcf&Y#k1o)GA5dHz-g`j*ON2Pxd})NQqY<4?>hW_XtHq%l?b4STPdJI^TnVbk z_)sOyw|a$Le;0IT$v=VatFGo+s1+vMk^s+T3?T-YBAFYFT8_Fcw?_jvOUI_3iti?l z8s-b^I=os5puTZ8So=6XvPT-VUe#>!2KgO+5^w&MJUmAc`DDeLt7(>)IcC0!L=B88Mn#Z;wIa zT4yZAUnA9hXlj@c82m`^ybe8>#{k!ul8j}fw#NtvGa3z*6x=}?Jr+L$_jjl(urNk) z3`5~GQZ`ihmvyws?OMzjkzCVKS@mX_pZPpk7+gv-XD!SOnSlNiNvooO6~Nmj?yeRd`m;@qO0bV&2(U|e|9^jnF@mmsoOa8=jsJ=EDX#@ z5c9nV+F$Kz>nt`Tvf&dsUn+If87qlvy41Tx(qF3QH>^?l^z;swCjXuaDf_8m{O+tJ zZz3k$t%A9BjgdQBKMIXT7^Ar@@|mwNIM8K<3EC~>{cxE5MMun|n{PCf-SIKrsV7CT zOV`25sC~kV@KAD{`xk4!@0XW@%nL13s}G(JxnW&mapW!5p}|*RJX%V4PfPmUvJE@v z4)Lmd*aJ|NJpyQUNsl(^&8HQ~GgGg6KpzocdAvWEaf>wsXjl)&Im z%z>jpov)zx7r*u?H3m0u>^LA0#)H}&WrMpeGDHm~fL#%fr14SUs3b8YX4iRlUBGeZ z^Mb5R!x@V=KncduS;(qH6y=08zG}C&|9lT%neWdDeie6N)XCTAiuX#+th6+pt0d>6 z2kgj{ymA1Igl_Jqde-_wX3%hQn$JPHhHfm{At{@~(nf7dBX z+>gm9JQbIV0^5Rx5y3PTiMYo`SFi>kbA01!hNAxx@cf7X>DXHH#}mMuVc;iI05T}|J7;O;nHMKa zz^YeBVAdKDa{M@X8I~PeqjCpzgnm>>TZNwg-ShzT(na#_Z_N)64!6iU-NdM1Wk^c| zv73HSOMg$y!*ThJ>3Q@U1k*;37rSt#sy!|QtMgd(#~wm|Er6FMMP6cu^*OmRUDcxL znr0S_t>VKlJNupHGz=B`AST+-mb5dMm+(^_7o;UT440FRaHBZb-P%&BKYxYsCV5ye zK2*(U)l02Hp-pF-VSLRzOOQR#hYwJENXzp#5abt)o4Y@bX7GOimixl&RqmW6UWTrs z;i$97{k%lhN;MPX;GeCETy;XOYGzVRU{N#c2vwPv114p- z^hupm*v}!d5F)_Q1m@|ry-hsdx*?Ay<#@|>=!o{Ae%%(#D7h=f^QWJ-R-UY((-;AB zQf8-(OqvoJS+#d9rtCDVJp=)eL1kBHWd1Ef%M>71ey`J@i@s_(*_;bd=J4BwA@u`^ zH@40T13gBiQ2z!?7|x#IkIdjR*hD1-U$;gao;-vWu(5Wia`dmk2Z2KaWWmP~mo-8c z9(m0-jr%{fd(7<$xLVo2$kmJ(ywWtwYX&wuM}9!{AdXuQxPC4NgD#XK=qJKi%dqiR~wMBK+ZlmbfT`{ zYongjS;zC}7to~6%f>&JdO&vdCW8D&7z3G@x9uh+l?yGxCvN#UwrCwXt@p}VsHR>q9^c7Js2(g1_WF(dtFgOA z@$eEHHM^~azKATT1s)w6A zR_D^E$tfzGKB`9W1dF;UJ*aw??^P9`bOW55D`atqkuk> zXEDEh^!zR92Iw)Vs)RikFHpGMuLxN3g}&cgny;~bK%-9_sOP^Q)96(XY}_`W{($vNT8HM@%YpnE0}pqbn6qx> zP#4byDEzHnku#b2?MH*_^BvWXIbI&x@4O!7?p4?qWJ;Aw%ofccU1RsJ~0Yu0&rB83{D=ktpR4Yexn=TyoKjm44B-}LfF@9kL*I?us^p-R{ zguSB5tmOG?%aeh`I?((){2?meJL+5WofG_qVr@=_cImWqT@jux54V?A!)nxcgBW=tzLy&ndy4ZvMt(8zJhUlt6Pp}}x@#_;6DRV!5bNw*=jX^F zTgghYMcSod7XLejkM9~UKD};tbI%b%s(Ck8K-x65-jd|DUa2KY6g=EF|0b#92ha-p z7mszjjOLkCzgQmq)Q3TU`_d?Yc6OF2vw3cW`@#B0;g2>V(dQPXLe^s`xr&{n z`H?&^8v8xtt1reCkfnWntj{J#Ein*${jJ~Cu@bKsS$V3w&WG47`(!{bm3{Uz_Ffy8 z>+m7UzcoWtF7=rRPtc7?vd!xBU^kuj9nJJqcha7GiTMO(Y2XTmkU2l_G?C^I(gr%T zk|I9Fkp68byJf-EvAIWc8Wh>!vIhfQCPfj+X0n-{DVjm`o}=8)ymS(Kl(aizXIm7q zi0591k3w?P1nu(!(;%GVYHw8xV!@Kr8Jt$Fi6$eD=&Vme7B!4OP#9bwfO~&CccQB13H{}^E`z6k#97j_N?wb z9qps&0JKTRBLEQ!4Zi@X0IEd^mi`hUbu8MG_3(i^cYH-h{I7vg)}Dw@HNP<}x_w?> zbOq52f%F?uO7^puL`DMwKCy3sbim3V4lu8|q;Ov>u{WZO?4pac0%g~J)Mg%;S_MDF zRW{M%E!T?_15_RzlQZ{4P*iC1{gQ&Fz@tIBW4Y}Z=B1@WWv?aH{o;{>vpU(r03eL> z@~1-L`73rSoHMDORGO&s_@B%BKI`UjvrT}7h(#U7b}hOY#3D*<p4g;3Ru|;e zqhL?s%cFSFe^4f|$2%&azJu!e3`}pTayiZVd>|iBDoPxH9z(YSH+{o`l}<1Qn7jbl z-m9i4J8_MFvy^W%U`_h=xdtC$s6;&Pcm+Z|p~Ah*;}?n4%`IX0yyY4s5F$sy-2rs+ zG=M|%25zphBMd$UijJq#g*aS4%;~oN$(D#ljy&6mfTfC8nU}{t)EQu)QN{{<*<2VUr|_=vy!0S? zpg<(%%=@c%9)g4S!;5bFdi)&^5s<}^rncxYxc#^s3co&H*(N`A>eSJZ(@u!QU-9qx z=rh&|y+7U(sfoA$T*RX$H=vkIJA zi|=0xr=rx;t=QnbCAQhWy4cvav-yPr=7vpT-vC3345;=8nMz6ENyY)Y^2`H}b2^L# zxKh`QeDX{$TIE^gI>ig{N-*^6v)-11MpgvtbFXw9(g_SSyi;g+rnk_KcG{c(TWr3* zyrR9jwPlq(5?_isg$_SID1PGea3H=cV-`G;G-$_>!eEAvpkJ)};|@NZ0*JFU1@mx9 zg>il0cig}FA?=%k@efAjhOMusZNU{4%~iSopuV--t4YTp;Ageee?RhzQI54XoqR%U zrg}+=Eig@lVIw9ES}nE*rOGpn5YoffGUZ1%9?bvro|rYo#b@Wh%C>p5-iVl#$08O2 ziVhd2!%u(%8O$}v{~e0~r7K}*{w07uP)eI7n_f$t>X(;1)B5ek$laYnGsGA7N1>HF zti6?v<~8_CBg2B}eCl66MVDQ~@v=+1?X|PN;@woqzj*-g*;yly=y_9?(*cCVG*D!` zR0)FRv|$lyQ`vR*MazKQrUD89b2wy_XHJ&!Dmq>^PqV_Ocx2_YS_N!Y-G0RwymRI{jlDiz=OY2~B?`UzLd7s7 zgx)15D{G0f7xX3EVPO0NK(VrZ_5y~Yv^g8E)UM=|ReG{wtETujCMoz)3MK&dRixT52Pa5 zWsR9Na@|^^_zb%?JHl2lFHhFGT-<)8^0Q4NiO+|C_CmqV4DH5UoblK4tbJ#X7wH!D zDYQ~LTqD& zmh4_+VyUww2a?vPAYGC76$=o|$ z?f25B&@JQJ@OIB3wqo;!OsdeC-{yxa6Ji^DqH`iZ61sVjN7cJ4-+qbUL&-a<;o;#* z{OnLV`L`f!kj!j%uv=iR34@#p7Jo305+yEzSVJo7s``UXYGD+M&o-}s5QUd`8o0l@q^}MoK3f{6dUQv>HZrc1BbNy$VHWVV|l}!klH8RdJ$4qfs z%_dymO+MJ66`s#rv#1hB_76%3j;bFXKj7Q{EXB!!oI*dMIEA)X>HpI?{nGSsI%hDW zqMYY>VK-8R^@Xlbuh5&)e&hm4&<8rJSVQgSx%Lx6q2Q`}*A0QiDt(cR*YpA5RrX^G z@0^1vSY?v2Blz-oE8N83eO8}DPQs2dliJ$YNCeRY8K9A*Xq4Mt#>0zM>#wxmi7O#d zs`?0ep!BMR8QfOe%TKB8JL{;?U#G6Yeq?vtxBgwl{B0dl&Xo5W6dIwdDnu zU=M>ThM-eG$~imy#?3phbjJG~I52Z^ojh+!X~exk;ThA|T)LVblzt@qRSN_2kAGAxBbNUHT`RE2sv>y47hpM>QN= zNPKvuDdJALBY%ny7_qJ* zvx%TvX~1$$YkQcAAXoma$_L39d>DYC?{S8 zlcrwu)sKhDMbGB|Vt)=9B=w^uj_(|mzwc-px8-!KZG|t}3Sdtg(8OGrPkd3IE3DsU ztS`rjW&7udM}@=nR^HO9s$rNQA>#%T3t5@?z2y*p9L?>*si(t#6LpnM7w18VbDvgmUTeG9W7B6{&}ma3$WY2oVH-cLU#KFK|zf950oNtF!(3H!{Z$3U30hd zgpA@Jw!<)6q$)3-rPT^$9gihbu-{&G0qlR4G_W^o*JtE^AI?ln+=uSllgznQ zerk$Ju-^e+zHok4&J*?QUAUpeQZtd&G`>$M=GR%LRd=OZ$*kECqhy(46}TH6sa?qi z(D5N$5(icFRHsVEG>s+IFX9+Zy_uTH;d#2sWlRc~=%}AX7{Xj5QBwD)00u6iDn;nh z|3U$9b_tnB2Q1|ddm*~5A3s-~8%%S^pHRYhW~%!E1cKzxTWW>2@nQaPHMc@x@g@U= z?*8L?{=VRcykI6oM}$Hd4Cyt8eG(l2t&5HG z_ndc0cuxABeIj$;qN)#hOjV1>lI~rOJ&kNqEphY+XFibbfd1Fraz6@g%@T{$3KkNV zGxJcdNQ8kC^z{~ICPZI0{3)`v_dr{-p|>}b$~*YoVLni2&kUE_^_rk>e%$Bd!Yb{A za-F}U-j=o73YN}<TA1#VO(QpN3*iB z3SeZbNxvqH5>9Ox*~?FIm-+(zjcMQK?g}zcC=u2$72ia=HxYpGJS+OQ*-xGZ zB>`B7hCJ5_By4a#z|=>#oVf@6)nKkCg?G$~jSTUXs+cpQL5WUgl=Ni|GPf?hAP%Pa zu>#eHez^FRm;eoDk!qn)>ZXWNGE)?=TOgzDen3ieFZkUUb&bRDj|=mueZ_^?JiQMy z)>h+OJuajg?@xtcoW;E=2#D;sV3@Z)>0N}($A?x#wdwYM;$`Y;O(gD_jd zMaccUEDVoTP&TU~ycdMo2`T>P>p8zm6AfTk+6UonW^81X#pf8{km7^vOcb+bg(Pt6 z6zbs=6HRT<2Q$c@ee~!WtI^e+ymw?{MHt*`-`=tBJM04xC?61l^`w}#urIXq(N{@k zp-qk3A%>qr^!(iRM$~PnxVd2EoAh|G>a}KESQ`G~_b`|-wUF~=zYvVdz*4)ucVgR7 zc#IGzx{A0G{Q3K@8%C6H@v=RzNtn$aqx1EDo`SM!G7S9a0)OjzJt?7jVYIT6dkd(B z?VvH}%IM)L3`Bng3f!l+-;3ah`CdmO{Bz#3>EIKLy-0kHw^%jatt;8y*=iV*P>4v7 zXWYiUo@?44=sr9$GLjFm_(6V=?Ds7(o2(9@sBVDpivf+Y^FnGpXf?_La6>%TA^a^@ zA&gE6n{11D1SeiLdH#l0@9t$bT}j)$&CiA2V&_nJoF-}yqv@~k*{{C?USNvt&~iOF z9l3zOqbRO>-*@cp9!GbKQnAn6&npy45(m#bJy+>OAy}1yaeghX8b&syFM9G+7Oj8Z z`$g$EI+SO{;;_5kn4&ILd9nd;#K9$=mOfvC@t=$O%Tbkly)e(F+nT_lSy9As`Ta;chGU9?=g{zQqv3(={Vjs%(D z{|P#@@!^~2Ok$T8u)4t9^e!+@X)39zUzm4xcFy#VgJl<&%kYluhq-`n7+qbD9-*N2 z&u+V}PbvoU1U8I~{$%LR1&|k-7=KG4KaOz%1+~yhKZrgxM6(0EQzWSs6>_d{a}n%i zR=-Kj@`kG}{MX}5l`C%QV6OK8#v0<9dZ|z*KgM4i zjMrjE+%gClX0-xxQApb3Oi4HwpRkEoN+xx*y8k&097xhCWfp(?&u0E>M2Jvu9b1z1 ztv)}R8DZod!;q3#kd(6k`-oy$1Tm_IS3s!1?ozL%a05n@>BFaEbYn|PkH9oy?Vv~f zHAvXVQVF4CaginDB6i`Lk6>=i5yeqG-v??;;=o5r0%C24P%Hc9ZVL$|#l09I)Uvkg z*&qQ;{t|~h4ykYsCbxhw9%|nL=m#qj3D~Z`gP8!5(5Q%akNN=mOsJnF7;y3*K;z7E^m`~L%N2ShfAI2B zph|YWfcN~#Gb!RgvP!$d+qVB%j_=5BU6quLcO_U}U5x{!?Dju-!z9R|mRFYau&3tOs-A~)$=jtLDy*9cDI zvNsndgFVu^QgOL z`1{_z#Dv`YK50cOT|E@;2#YW`|AePiaftJ|DXnn!IGU6~f$Ao^#j=(Ij*EH%l4U?p z-P^{q_xAl(XoW-1t={FWQ5a;#wdb$?!v6kQ+&HL~wd)?1Kim4qM@Vcp1BM;yX#PtF zd+gpKF2{!ig~Be2$i3~(l}XOyIE3fw3)!aPXE_lpf3d?&lBhh~+YFma!cbgF>3DKF zxO8i=1Ehu;j;cNIcz#qEtFErECpbHwy2fY8SZ=*SQM1Z*9QdMs4j|@nVW)ojKan=$ zBe196*+hIUNF&Ec0Yp}~TRC;kM!q9KzbNtQ#b|9SBTP)dw!gOb6%Vh&7OD%~{Q&Xm zgpJ`!=UM%Cfz4on#iDM!I6kIglpx&r@KUJ$j@99}GXr^6S^RFB3j?xVzZ!!{rhupR z020#cSmX7{09fk?`K&qv{w?$Ux0$*T_qu0;wCmu#encu&&1 z+k%YDn}vD6LEZ#e9{MX^xV$NwY1=EEoajN~twamLw)JyzJ&_kLQlBjEH(@Bu+!_;l zikwzcYxKp%MRxmX8Hej%mWDpjMct9`29Rz$oT>tK-Z#!&c&3nS=`;QUmT=*0#=0FaoeQt8Oo5^;y&{he5@KW@hj>QI~Q#Gj6nKrVV@ zCX+0*nWtNaOTii|I$RZ45&E@0)7b$&U(*3rm(ktPi|;~iiI*p-vr`GW>rMgdm5<9}@D z6H2(A-ojN!Lhk}(`p)^gd5XoLylYSCcB`C=9Gm*B;@jgW5yS)*j^CCrkB`5k6k5S$8*G? zC}ZyCn<2c=U0+gefM0ve^sP)l6$DS`?sbdn=u8ou$@BfW9R-HH`jhiVM|{-*<&VNp zXd5%bs){!IHCSFCzo2s6eEin~2@cm1SQi{g6g_s|0+U8)??*?6y#TTKzF#f<)JZo= z-CSaB7tW}(z<6;;w9;}w4X2}cZHjWp6ne+v{0z~c9rW$>C-jp=nL^fLt%XkXKXH|o zCLHX+PyrRrn=XLYNFh%HXZ8ppNSxfzj=OiZ0F~NZ3}3fXEukGZKwke`tqr0)o7M+I z<@7;z?UVYOwLdmO3-MtsqV8{yzqORm=>lkz#ph4i0lu@`&-E-03O`pOirXO~gjqEy zrdqTCqk=`zD=j;+ux4qZt^=Cg_bX26;XaC8y(l09rt?YonCSa!&sPq;POsh6C}Qyg z<4fDKM+iE`c$*J9Q-<{m)+bv(d825o1zM#zYe4jM<9>78E)$P~^(y zi-s8oi4)!H=JLq^dHEVr?dgvN!uvtIyu1pZAP?`nCvE5Z*CN!zHDa= zSw7M?&PBD3^-c^dn&ol0P6vrw%4+@8lrCIX_V}%1$uG8vZe^;BW3d^kf`qgh%YZ7sCACBv07ZmwMjc88;=&0qFFNUrH=%NZv;sPMt0=N6 zJ=>hE0n`suW^TQ(3U->1+_( ze-e8(?!U7cXbD4++pdNB#GQ^ z?$1QkP-Y8Hr_msH7EHWb*Ib5*%{w60oZ0muc{~@PlL}N}e5g^>zc7w{c5)c%k$8Th zl@v*w|4DTbLm&wThUAq-I|C0vLtE+>4;O<<1_US(k@7qK0NZFU}+aMpA&E!I}AFf#=u7X3%@g8JL zJ^U23Q0~*P0I%c&MSZ$uCRdhLAm%rJ z`}8T!$&FqRg%Imp+_{pWd8F zW!9^*ZFyEg0D;x=A&w8yDJHcCTQk)+y0Od1)s zp`Ic!6u!aQj*gD_vt}7eIVtwV=2X~iUyS;N{J6A9;lok-$SQdc=3(|xb1d&oD3a*Z z1bJW>Ce;sXXV~Nqry%` z!4wB0tO`oLbbv{K=AaG?(@@Qp1b~qqy?O~dpVMI!@T{&G-Ho~xBls4FSRcADhXb2s(TkXkCYsCa8;!PKb=-0*DbK9&!bqM6$J)2cl%mz-P6_rl zYiXlr#11w|%Ac+9ecQk+S`#X}urMeOdAL5s(c>e?r6UEWdy_BL`jN+6AmbK>9<)&> z&9#%5>9PS0-`z3h`Y4Aub2Q#Dn)bn(^d}5i)Rzd?!ra z5Qd(3zU;;5(s1BCw7<01e&`KtHqpM@(&^ZKCmTESK5XpXaKrbPp(@OU0X^`|7QHR# zq01#-AXPXA<<|#*xlNS7%NR8Jrt{SD$nij2tPF$R-y8seuE{37a?8TP>_Jr zp(LQ9X>IZ?aIW8SewIhDSGu)Ccb0`8Uu%X z!F^352|+?WFXsa^mbyH}?upTOY{#u=+ef5(_2lcn8VOYI_*kCoQ5>*Cmaft`C1?I^ z>wad#iMQ%Yy^QjK%~5&k3SB3 z5ky0zpoA*-Mj79vK`5wrZBqFKX39B?sJDWWgrQq=gHL?wz11T@5I570=}D%V!Y83K z)bIFgR;jMslo1r5tz>xd0nEWAwsZIk5pNkW0y92PsJ(3S?Y;ymp^1C@U$R61SrNnq zdfyuZH=1GuDw|MrLe@4ccxe0JNH*3)hw(m?>xA~zTv#C&aAbeM*}=kq*U;j`@aQ9d z50SCbCr_F5p!s`9r!QFbq+Gz&ZFsUKf>p&}I~Ir5ygN>s@_#lyL{A^V-xlim5|X+J zLZLYY^p9pI9f5)=G`|4=I)$`R(0b_riA3UO%27BZE|6SzS_fVRyap#tv9ibCi0l;- zom`*}Le81XJA!xQ0{k0uU1C|&;F#F04(%PtNj9lbaT9=sYOgrhTNuR8H<3q&xGqVS zQq-Rpq^np4fLtIlkfokZc!f?atW~o}5Sx%WUpzVm10%}QS+zT*`vaRF6*xdBoESh~ zcXye}E}oGQ^6&HNKPx{0zD*8>_jKDxRgnc}!8A6uMl0gRBvk8Mz%UiyT+*U2+kS6} z?F_SWU>kuqO=A^rdSGTnbpJxJwBsX!WGm0d^F_={JyX_Z{SRAfK9N45T_MN3`raA* zTz8=qNwAI9?t9{2hWC3ns%}CPI)%_>xWs{HnQs21b611{+oEpTd0Xd?9sRpB=}0V` zW-VJxNq{Go0JofWgZ)MKJ1EIsnKy;Gm%^iF+#LV`mkVW9f5YTKFh+^uTm?<-49%f9 z5$pt`9s{itcaEY$YBs{>rjRoQv-r*v(F?Qe@C0D?6l}BT4CzqW2k88N>}g~oSU?pz-(H)J;o zVMKxA#lYN6orn9=^=6#?TcP~3tfq=^y>zx&d+5`p<>h4= zI_pF&5NZ#)FKbHl;GrhIqC|rf%}|ZvO~&1r1YXY9UqVUDi)^0C)}-JdZZj$X_Pg}m z&sQ%)nnH@DkRfyLE=xVYp)%DgrP-7*likmG_iC0qeqI>wXvNU+72bA-lJw{P*DwpD z*s(Pi_{T{|QgCD9r(ns;24Ie8! zK|v;9y&Svy1jds!g|pEj{+}>HhXs>S3);;1rUDMT3dQM>mZhlDd*7|D8*IT?f|6V6 zvuj&h$sF@4pKD}`5TUHODWxr&*XtSOX1jO1=tBBR&+B0?9K z^diIKNG7rApBiQG69iJ;qNBX!^DAxczs89^@qi#a4jqs;#6JHRfRld|;UT+MKlgTb zHi1%^_3c_8l2G03uJ4*RxPl+pjsO}m{VepHA3?PT%TBw9LIE9`cgdiYJ^PzVG1t$} z@BiWlOz6>XRtaZcL`*$CWqYQv>Th}mWHy=)RB=gj&YKk|V!oSB*YhJ=pWk_fK*#u| z#N5^h{3Xprl(!#1*~;RO^gaaxw()b9LzoUT4N@ZbZ&CeIa%<>o+ELHPe9z2!n|jEg zVJf*e>k#`(QhsDqkczsAFZC3E8!B-M5(aX~Nv%<4!rrenG%S|eZy1NxtuDDjDVh>s zKa9!&!ls9lEhLx5tYKFNr>{fGJ`G6T-8;~u9jMSiz4Jlpc_6Hta)m|Cv+bE}<2#D1 zgIulOXR&8%F@=VHhBB#&!t@{6tkX~-OoRbh#3}&BIMCePOq`y7#!`31={@;N{Tv58 zdDr`dDAbbbs+xZ-nW$&?{+H3Br}u-6^_!O}yS#~5-gn+vKRMi0Z^09OxnacqUk`Z+ z@sOSdb*O3yEfDPw;;}LrD0$5w70yas^8y62Qd+&I!Q2175mMW@OduY`b-M*rtg~VX zM%mfRXJBl=N$_|I6MnxcDT^D@C(Sro@6H+HuwNQFlr4x$8iido@L1sWd!eQqHHwkIeJI+243l z00S8q26nebn{d@he;z#yx%ugOyu;4Y{p1`88ue`0Ckn6vMhB}E| z)DVzBCg;@E+y4m~UnBJ`^@^0?uUPrdt-eA#x=Td50ANl_cw$i6xC$vcNwn6Ddgv|* zF?t8$Q=(esl2SuzzdjsF#5)NXQFvkS<}RLE$BiW+TBKA*U*nsjA{hn%4Mp6yr+K}! zEQP)fw?CR|T+*!hBwZ*0H4{b6Xk zcynuWA>X0iHx_&1Cg#;JK{AdE{h^c(Bh?~|qI}=u6g;8yE>p?x5Olie-9T-D8lh1b z$?6w;SJE14xDzo0YuPYDn6LN*TQJ|j28U4#nQv-~+pG3N>igM|kR=|d3dmE~+@*c9 z0kDP-@A^e}S+J$4YM5*{do$>uw8W90ZbN68`RD`18K&m|V1V;268IU~NFGsi!u)ym zzqW|tMT47yXZGY0r-8)9Zac43fT~Gx5c$*C%s_(aIa86(f0pWRj+_apo@H;e-TNUN zbeJAVQEWivE4ne)dvV2!&_1vT6b@kWcMe4qySH<~D-D?Wq)`Z`^tb>^PV(oyP;jW~ z*G^(b8q_mA<#%?I&BWzepq88pc*dqP^1e%uczoowiHoU!yMjd}5%PX^MaZpIBD6Cv z%lBK|(jD8YvdCny#E3gD{ELK^2Fg|20`Q{U^U27_*1@Q-IRCgT82D5(E;Pmy$Q;_cgZmip)xWDm3E@Cotb=n*#Q7M~8MjVWkab`X(aJ!8fQ@qeMMx~I7^H4Q7jNAAc#4>}~ z+M&Hc08mNWQW6jlsNN(XJ?RBxlY!~@Tu*rCB+~iEbrjJcPUF?c?H6oVFvrn7f4!VS z=Hi2VS%1>==Pq1Nd9ertZKUYY{8)yu45J3$FtO2RmEV411)8yApD!TLUU)OE8}K&K zrgFEbz{Pvx;e9f^OZXEg3m(Gn+1$_E|00EPe_YaTK)BoFgQzSPxa}6KcYCWT_>@7| zYY_#8Gf+XopS=%Y4MbwU8)>g=j+FgcSdi*UdgLb)U33*ALqssDEd6n6+>@8Hxy9ULeT^rBRwsb!JcH$EgEnTn=>XzQo4M`@2T;+?-!Xa%F@) zb(B$iVDPnABqG@9Aet1%a*@KW^(BoIMje75J;?6Bn9tpgCG*~(o?Kqa9#`-))j&^` zOQIS*cX9pwDPQhRuvjus9Pjz&9Sj8B*<0-|k3(yhTac*my|0qF{@?PF5CD(uZaSM@ z!#I19ETe|3)N(P-Y@*&BQ@C^QVUtGjjUI@V@V<{H{KjgaINcKd0w%tW7D*^q-021c zAK2}mn8{$N!-HZSM-Z zGc%eXBb+9&ipOKAdEkxTPIz719o{@i;BdS8+3R?Ulhmgk=}1kq5F?l}yxZs$at;7o z*LcuI#&;92)-R4=H|LwKJUh6~5#@@2(x!kXJHXN>FLF!Kz3cMK#$^tET~6d*e#|T?YJZ&#rjGSJZT!;C#f% zGfcZi9uA8YoETX`DpVzMzh5?W2ufM-Lz z_W>72l(#eHDUtj#of$b`5V^m?y%3K@;G#2cGB`0eZij!)>-*=#cs9Kl0{j)=L(oAl zmP%+Gj;?YEarZg&QM~DDp$?GXNYREoI&rk_{mx|B2TSNX>>0f~y$1!IYR4OyrdQF& zA2VnErqGDFW5>}rPAhhCCu(78N-Q)>&20Ts?9s`0*F`D_k}@}sK^7;M{{6ya2?kIO z41hR)UJ$0z-GBWyT)>H+T1PA#Mzwc<{E`|`RJ0hh25e>r3esoYxxRY78~&i~F#n#Q z<+xv0jeNpWC{mt7!^1r7Lf8^14+ztBhQlcSVLIV^yUvJTfp(=?n#qp@taAGeFDOo5 z(jF7Iz;Ux-akcyj`nHa+)>(EeN?s$V!tduj4=va1@SOe*X-)q zG@QpB<{snenPL7pUGEW?#zY&w?TLl?8yup3Dj}vOV@K%af}o(up)IgBc2DGXt zH-0WIN-MMKJx1p@ZU!&tJKOVF;*k=b1u%2CZRf)-g}j(rnU2D!X;c`JkUG=UdROj! z(4B<=lc_#gajf3ZD=_A?z@C7Jruss4V=ybh5B4K-|ZOlk4U*E1SnKqIdKXFbOxCWev=9$5 zAg3f)k6as`-rtjh{3H7;9IMawuDs?Zb%E(sG6?$vbSClvfQwiE1m@(u7_Hy~G@&N~ z`-JjdKEDg2{n{&dc2t?Bmk@-=0!&#lo)9>B*C~W%vetH66casDNH9uYq&t*BPdY%P zu+ont89Q`%7{r>PCj>Fnldof|6PG(o=N^9!s(Q5YnFbji(ts z_y5TH>aeP|^=rBt1*Aa{6zOgh5K&S}8U+ER*>ocyAks)T3eqK=(%ndR!=}5xxzuyN zd(ZEA_(SnL8_rsDzA@f0#v2MJtPi>)q2Q$S;LotAK>&h66AnHp{Tm8}8ogtKkSHpk zgL5Ed(TqIanNwC{@zJXaUe9#0uo7;nCY5(!1_s?%bM#a+S0)9 zyEO)}@sF27et}w`QChrt19g8Dn)ySDfTQQ&YUMQR(!;&)<^{Jb^8$K&#?}l?J(S*NasB+Ps1Qmv3q7kV<5b?wpJp);d4nW*W0vH zcs_H1l*70e6(Z7WI$CV&_rG#w9*kS*@6@^dupY`N)DAAjlNZ;{Zpk|b?30JgKHm7~ z)A%1eYtX;2smmv7wA`mJrfnHdHwmLx3Dg_C2N4kgY6Lr`GjN51@c&fW|4I4a54jL8 zm$^}Dq*o$EGJt3ptTW_E0Z7wF!NODtzn4JV*KK2lU&?u_kjV?kb`*WFZmT4MS#=s{ zrw2f`WSKyF_?O;_2R(c)(lw~}aY7;AYaFj}+W+1N=pC71^;bqSzD&38BNc(gZ^}+u zBzUKX0hgCo5@v(mq-#%CIfPwL*_&PYl1R&{Aw!?nL`i!Zlua^P@$0(|@C*A@ zpfEZXbO|bYR*qeefR!)ox1PCM^6GeQ5P7%JLX~)fFPI5zKZyuR{TG*E%m*9*-k+bJ zQYMN&v}d_E*zEZMR<1-$X=6Vi=Qa@s0|!+;ORXO!gfEp6N|3z((EJi~Vk%wqrv!wM z2^_qi_>X*rR^J=~iR0NfK;-jhLbTTM0K98mIi2eSO|agGVMDO@LvlT@g-^qaOoK{X zLQiR+Ahx-!S$LnY0G*_e(I|BnOx4x_VV%SbQ&oT&(SeVTT+}Rvd?x(ci}k};E)tse zFRt_MYW>P}t_2IUQh}6Z1L_2nkvJTG_J9)YP(t;gdWIRiajQiGd;Vpv!A~>{er{+* zVOVld^a+MDttWQfA=7Qmdp z0>aL` zGQEe9f_Cr81zq`3QBb~bfK@|}ZMMO2CqR@!v0@q(4ENMYA~ttF574c3m-CQMmfz86 z4@LmL^5IpSlm(Dv-V1#Fh)Oj_B^4Mt41#iJ#F7ChP168W0hK$eb0}}xK1qG?IQ0T$ zP%7sqlLEcs_QsngbaWc3DjE`i6*JsbP*9k_M-C{|hs6|way2!0YjWsg{dF3c$P3EW z4=DYOG#Jd_us1!&S^m?^{r?*UczvKZ?nXC$EY8UOw)0jv_Y|{QZVFhnx8CMGL6BOF zF~a~f&eGMVPgxrsQI^oMAF;PbK8V;z`1<2E-*G?XaS$KT;h*w>mLGf;x-?oF_#tqa zIe^V;+12%+Z z$*vE+4~wo@{Y#Ze@B_bmFIO}f2n+wA2|+=e1DeHsb`oc!^n_NGe!qn+hgU+pU}gUd zhov=`>cy9K!!-m1aRkWxTSFl6nAe^*Uf9XTq{Vth9_39DMGSvYG+119GzZ|6=gq-V z==O00+}KbfM*{H%eZA5^neJsp*xoveZQ<;)8N5M%d3hAAw+1dTF!r$)bX)oC&Z><& zm&Q(v5$IWKHQxwLFRJXf;_4Iyx^X|wBZ+xlLYH^HPW6B&)-A2w>a&2ef0pek+uAL)K*dyH(o5D0HIVLz!rp1W_xT938*AU@kJsL%NI@C!SA#>L@ZuqWCn4>hQNV z^Z#Gm212Ob@-O))I_WE(`o!0y!4okX5K2skfN}T}c$>qlo3!`Xv}_v3jsFcmHlWh# z3e&&hnIs&rZ&ec^Lvx}cV6`!FohX6-=W`qX@O80|(rlf&l)5WqBv|}FF78ADlziER z8rBdZdbN`}yBFpjc)OW_7j)FB?C?XqS153RfdV@(#fU3o-|0Tdj` zj&J zpQ;{JkNd?IpCH(X@~p+WmlkEAEu=sa4*23PMV!0%M!^iGzbYgr zHOst0siZ%)gTa(nlZBn0?Dugu%ZN#Q#JL3|PRpUJqct8ky0^o8i)>~_M^EB6o4;_q z?TobD-28x+R|g8Vh$)=tRcY>P(aJZ=&$n(-Kc?+QS#r9!T|fjo6WaRL72TvQZ9;0h zIp_=JC$PTk8(0G~h^<>uAaWy7O|qKkCl2U_k&ZeP0|}L|bxz7r9!j85%DhXP9Hwa% zH5deV3Sf?qXyF<~89GJNaFNCkkjDDE4-x}8WlIcw4e?)*5(bsKl`F*QMVt_q7n)ZG z(TM3zC*bB)c0}A)t~y*_1Dc^U&^-C5QLf@jrT})vFxYKWD;+=1Vhos!R-Xm~{*31- zA?5C`SVp;qZJyeCJ96dr8wjEXh|iAz)K{AR>9!!Rj%)+C z{aGDpyGwwN|Io5fDhY7V`wDbP@2fZVKo4vSocu~XD**-^`DwbKrPPfTTd9VC)?@MIly6G|ebj8N3m?^jAQJXw>E<2=(hAY3`O>qa%PZ5xrxWEK^_9`FI{Cm~ zw{uyiT7Q0_RLxS&o?n$zL3%*&PEQH-@()13)-%121 zyna|r3&sIb&iBRlZDMQcIcjp4RM!`?Hy<^rQ9nziD5gH?4AMy;U}#n~VW_4BjLp$k zR8qeM^9cVC_vvks{sc}|$nn8I81Z3|ffeXzA+-cIV4~M51LQGX*lE8X@F`4yeaAie zBcP4W^6kz*I_3na0aV??c7URzw!KOJYiMeMuUP-pM!srJ)Wf^x?R7pFOHxc8=ZEhm ziVXY&@6QT=O<|JBVjdYv2|qV$lamF5Z^UWoQL5~=o&;*Ja;CL4Dn`^pu)pplR*yjS zBJ|5OwH%5b|Mk@hHRyY*7$V#RUiq#E<-)TG5dR35lAnDzDS$Q&EJDc!rpQNg&xjv# zEZz>Kzyb3(HM{`LoqIf07~^iPu0JIHs{YuSz?I44;sB+jAM<-VT<{Yq43dbOLlo$@ z0cVWJsVL^RBZL8PgaF|0@QJ9vt9VptAa)w18Uy?hDR2|jmru4Pr9s)MI=K%yLVRJ) z@}FS9RtG%=)ALBCPSq=lj}T=nI1yI4I6Du5-sc@Fb|dIvRPS7(BN5Vt z@+;AwlM#r6en)*oGKl=k#_oXzC$iae8B3$ZI;bs`V{yPLXi1r~>XK5QeZtqCrYzFp zBoo6q%6^bx4fx;w1_o*i4y;}CcG+=ph3x_->R|_xy}XcOcCpXp@{Xm|}VKy4Ycy+7M{?>b)mPfftj3LXm;@CbtaEm!}6TMQroE#}~Z?wv?+ zz@+&Sqw#XK%w&LmSoit!bMQG?HWH_P8$fpJ>x_#<79kD>m*2syzLo6DN(5P3&>sU0 z8d*ux+U-A$I3RRZ+a+QA@8cJNTqDj7YAZ4>&d&!yYs@4U$9)ew1o_o2bMQJ~7b+#< zKGN@t=VSZAqNSDyaIZpLc|3n{oedz@Cze5DlZ#=Ur32-ALG$Lzr-2Z$3poDTrdGSw z^Sf+Q0^T{Tux3I7n5+$_eFE0W{o5igk6zYS>{V4)hT*d{gi4WNtlsO7DUpOWgW+z2 zxBb2-sKWHFI4!%^6IAS%J>)!V4PrN;hDPF^o;k!!nqM(S7A~-eez#8d18H;;aa(`W zLV_U%uyj1f;lCNcYakW!o#Ui>e59v+;`_SkW0q(@j0R=qlR&^V39nGeo2SWKEsCIS zw8m@~?yNC=bNfyQAi8`B1uDwGU?KLy-@}UpXsZ4zRvR4QrrV`e5DXT?(MPXO+O}rY z6PJY-pkE8d#Lqq5gHe|z#}YLI8<6ZZra;-Iwq+X{0^N8p+0EMd5cKNs0f*g&3V@a( zCPW=~X>YS=st6eD%+zXC)|jR_SnK&AS2z0H(&ppbw1g8 ztA1?____PG%bmB+&sLKqSVE9m85Oft9xxb^8cSgkX69;FhJ&RqUKFs_WYI$c(6F~a zAv+P%&vRN(TPepBLO{t59w8mKKf3>ZvWx}Pef@yG_MKL!^;5$$pA~>ujS7(`cl>753G$9V%X`NTwHQ|`#|p{b*ruM z1uf8K-$QB!c`j#FpIBFM{^td-kWQ0Gc-%k~7MeU7UNpB}VjT4y&yLK~6Xa&q98J}? z-%tHWr%VX%KE-ivLy|?oK)FxAqRG8~aDeSfc7Z1=?vB3GpHBt&OX@vapvB#*Hbz+i zY>XJA*m=nmOo-VIBBnkv2xyZCoN@o$-@n({>H+wE^{}l$VH9nl**NE?o;T=(JZAHe zlc~NHcF?5qHt-0CwW#9HpQ1=E(ObYdGc^ITNe)v<^76x<4`ct0x^m(F@7?7=bOCGv z$lc626BubI0Mh5k$=kbyo;sYcjuYWC3&!59;uM~*81S(=!ww<_tUQ%0qJWB5*ct?& z0@GQp!$Y?Z!lJ(WTlK~Lz8NFDO*KcIc@^$IOgdber&XE;d98ttt;4hljlt${6?o9L zF@yq=L3ZDr2a-QF`U0)5=gf#e5OtVBOkGuDq%Gjsn)s30R}=acxIMgD(zjbEdzZq@ z)OD04%EcmBU%Q=kY3G0|qC|D6jFnTRv@+rFekz)N}t zY-;dJnE@y88z8pl*nqH}$z|N@1L7Y)9`GaN5i9 zJpSwInv3=AgWC63?mhXm1z{1}G4??e_#lOb)uSC!qPPm}E!QhH@Ej@qg>2VSrY69S zJ*gZGvRtWR!wx2ZY}>B(Qf-CW=va;yf1B{QI_K$$;gB!~X$fFT8(+)B+%La3?-nDY z+xw#2X!whR#juf0VDrj_i2?f^WcopsBfyH4+*^20=jZ>*cyEVB-N~8{grDJTjR=UV zjb;V*XP*ap+%%@4v?VHN9o9ff{$Nsj893N{3U2@VK%;?=UofB9$K33%-YPiSTpwN^ zQnTnO%?<&Y05|m%gsWP&YExXw^xsTJMrc|{EKC3KzY_cJBg+Kk;P-<4ccf|iiwvZ* zwJSM6fgB8cnT*wUt3YsRS)z!-|IxdpeA7K=K8`? z0{G_rMiq^rTEWhU><`5z|1k(9o82(L+kf!-14^W4iyA(Sa0(FhtmBU4s|J3$#hSZw zBWOiiSzW2L^ek+cqX+W}z-EtGU*bK|j}FAT{ZNwOd8Fc~hrZteDVuI6#vp@MG+ZH$ zTPAVnI$|e`-gUOf06meH&N4nEk;OAUJay&oR$k8_7w=={`qV_yP00qNGQk=y6>Kqv4iEM)ezzr6u zVqsultP>@`Xd#J_ax4$!_pXmS*_oM)rIwRwM=;S@t_rKYwE2tAcz*!NO9=13G4Nvf zS1ulgI2`9dRna-eUk&+KFqDU0~))Ar&$)%+Pd)k!t>d}seYlJ~pi}+i&Ale;+zDM_+aA0?sk?=abaIxyXKI!X9 zjBc?0K?a1(!Ga;)_eK2b%}@VYS8TCEfI{7B70-#i0G<3s@t6|;t(^tNnbh+smw~AK zeqkYK-->EMp5spTZP7$y#2Ayqd@Ym)WK(o|z})AEX~9qd<-35kqFbW+x^X_-R>xm5~oN%*PUm`=|xncfPqi<5D#?7WaV>hsKM5xp_KkO4xwK zc(E^751%W^)+^%t`L~bFXMh}lla3DF%ls!z9jIx$iC+bsO{gL3AU%NYAjFS|F2Scl z%5*`~8i9(}&|gg@V06@SEM-T1*|*i=QTOez{9&iF&f8boiWJoF$b3vdaH&X{t@jZ7 zo|Z~YJZeWscXob`wkgQ$cCfpP@p3Hs1FC_jsHn3LLE$D-;cHZ+@W*Du{)G2JbT9et z1?IC5(tn6*q21?mi6XV8{j5L>O#O%bJ*q=Tr@-(y%U5jT3Hss*wFnT>7T)s$1~AH6 zy*1EG@;5r~as94iLrI+$>CK11Dph94jDcn4b6$7{`rrD245}YKJ3V3hSN%W<0f@m| z>T6k#0UuZY!A;>K4*dpCTB>s-2UQC!`B+XVs_hm$<~WSIRswJ#d_=-jfB#zB^}+KL zLqu=?G4Ru~EzthMIRdmckn+%3{&}*V%$Q>><9u}0Vvv$j!2%4FAx+fzn?AADkW6&F zzCiyPm(hG@4Jafgd!rv+-G{5NBGFhY)wg^|-o@(u)(Fb7T0&$qP?dPfSnabO&Ofr= zFMkj`>0v&#wv^to`{Ax)(G%2%>cAn*4MJR3m%*yudi;F;VDAP#6wvTVg1OSw3Yk4+ zgeU(8e84sRL@iQ9-Z15Kur|2d6)Zr8zF|i~Qsh@QH|X`zJ0FS*o?TGg^EQJV`Cr6y zu#*^2r%}jz_({EhV;j@Y7WjBcN1)|yEHMD071w_-@LU4sWKSi^+XPSXpFnd4oXqZp zXu4jWDv=h023=j~^KU~Y1Vm=&M_?&dK3Bh8_~JgbhnQG)$HFAe9ZtpS<1(kfF(VaE(Px5aUx4k06)NI|11i?{z6N@tXfhEa z9FfN4{$*!9PPX7OqR zpVEn$v@&oY32-o#m^`z3p3UKW%8e{7X!VC{-YFkd!pS|ZpZo_)MUexbXG-N9)Bj>H z2pB%lJaC@DZ&N$20Zft}095jaK^q>RJATl3)SOoffC1ZuQ%rJh$54&Yl+XhLjOXp! zZAkpq2Bhl8{uR=K_za&>AXxRUbHf+%M8F(-@=G#+I3gqjzvH6hojwdMa35@$S5I3k*WSi9jvRP$T?ZVRvzkKyfo3@S?Td}R?H~n>?+JjRE$FdLS!=*J z`lTimcH@OCD7JOp5VAE{nhuf7BD0iLtG)WrXA}U38v+D%OB$wtVMRlx#1tuq2M)=L z8$T*Wz*fH~CkQd zOjx)I{5tnnIwbbSTLkd1ul?|7@senJ(bCl_=H?idjo{BNKqU?+mwHExQvz$OBdK8tuT;ra$<$w~nD?AdSt zz;PD%&c;)59sApo75BYTeBkutlf`cX&rjxlB9EeYEB$7)e1Uoys>0s;`Zr;|4h!cs zKntu{VKTtiFL_TSjP#N28^&!=k}7N2KLnPQ+>ou^l3TTLf$nT%`AV>Gazv5AcHnB? zde3C}>-X&HukM^8C~Y!4k33%v!u7fXC@1k z?v8w1uS_WezS1%}PSo^7oKfevMzk zBxmMr?h}!jT(#Kh+5rPT;8-j*m(Q=>W<~ocB&=b(-h>Jk6}rAZOc67 zglyeKwvlY=!4D(Z3T7BD&nlSz8}ng(NW57o&r(>a4?5rg5vga;kh#T>Wh8W&ilf?Np-MZhV`YZ4YlvbW|8=?xx@YvB2w zBy)$bb7I0NALOawuQJ4%z4G%puguKJ>0e$)_@V&uOX_Pld+mpoiP1CIlmH3hr_+lN z>qXf>E!PWl5{y80#dEsL9|T3AhX&L_S}wCy->9xvlzbL7{TC~IR`A@zP|p^`lOh2j z7MAz!)&tGRiL!U;KIee3F{BW{HAhLpz-q*I;tbE9{%&n>r416uckIALtpmG-eW9dE z9BNLV-lDFYLy6+vZX?245~$)%I|1c3)4Ve98Tk$_{R8Fw$B6{aU^j$#bvWO3N+>s!b6#i zXREC?XbK;7#2=GDn)MlX0h{6FQ<4{fFHTi^FCzfMXIYU|K484$?Okh3Qr-PZPNv6c z{`;y{cL-rX<^yaVIz_H}r>j-Kx`gGmnOvs>%a4N}q-IAXFMGzlmtX-HgUl-5Qzov% zraOV5F=Tz5CI#=O1|&?aQ?3`*B)Ya>=$&y&Y28dvV|+12k_*MJ9^44^U<*Jbs$gkaFkpX=7+jO|-2d(g{w4L?&$eZe zW!8E|$$N$zkr=!CyPHMv@$t@jV=aL$1g*5Ru)z{({8G=yKH1?)sK5C>v5tTh++>5< zNgnB0Ljw$f;rlv3ktMr2`yF`J;ZA6cuK(&wm{*;7(Rc8q1l5c4LnR(6q}~Qjwoq#j z%%QNx@W02$a}mm0)+<;dJOqgN`1qQg!EcD}KK+~mtSw_lgdQ*>#RJXSAgE+|s?dD3 zy~P!u*jp3dmOcjik-&Cn2W@1Or@t+z{tnaK^H_zEj$Pq}I`Lei{_U@Pem?e%B(yH# zcX=UT>RVwJ=zaQv#Q24t%}RT!XnMeyX7hLkVSFN{>G1c*z+^ZDEKPVwSk9jsc(=MD zQ0s29o3{3>lp;&xSr{OWxaS-Cot8i1eh09R_i8>T5VfYeHHb#xrchC+f?(CQ<9P53 zDzoNiqsNW_E_sFm2cob{`x6vqXMkFnG!)cli?+{3vPVc027-zHgTR~R z5?Oo_U4LX3SrWfM0KN=;yLpT`c2KNU@L9R_)mN+6kl0bjAndwTE4NhU_z6w!^brpyl! zq%iaJEFlUVZ5z0pq5q8|xGwoieS8==GIeUFlQ6?Dgx)BMV(1lb_>156v~ULT3{I~h zNkB`WhE#dVrKzkt#+b}($U)P%2RQ9&9i4O<4rYmGoqOvV#wh!7iJoFjG+Z6-Zxf>w z{|vswCBc;Y+5?`5StF-Q~QOp-3;qp}&NxBO=0`j{2D&gcc+5%Og#Hrr_(9d$xxr50=1C z-qc?l3n71)_*%nqyPFD?$P50N+F#_dkBW!;CGB{$(9T6{wQ*8k~;BB?aUsz%UZY1Yv(C)M!+1(&ClYu+f|$S z2VJMghaNfwy43e9O?%f7FL#Ej%aMigAHP8$r0jIM8d2ACdPU`#h$5%&IOjfhbVosS z-OFY}MQ9Eo+0`kKLI1eSJcLYOu(ZhSFt@UDlyCQn2YD%&2ZL%i@tB6a?fgZxZE7|J zw@GOAptfGD#{Dqex48Bm!?s_=ws5-JFgd49OcAce(GJ=eMY&CXb)RrGtfo#Y%L}yV z7>PU7UtdiFLx#&YW%&J=aMAa}Oy%e4A=X}0Ri{eEpY zA!FB0{f&DG*V?UCFDn1f@!u%%O?zZSuo!3FV%B=3}xhh5-42!T4%HT ze5ZnPvmyz4BN15I#wCfn@vUH1O_e|OX)v{c@OXL_n6`;s%({7_Uo?LmInt$FPEW#n69Y3ADYQ^kM7JvzYwij(*Z_o7yH;7j4oIKJQQ*_21+|oT zv!09H4HDDlW#g?94SKG7Qmq$Gwt1TQpfI&4bz4Pt_v0TqN56G#>5W4}B`W;rI7D5~ z?hAVR4(}+em)M_JL3jYv?>!-#cfE_EAL(pFkBVM9;tL=r(&%q8#~=$?EDj)im??bi z9dD|5m(cSD(P3)dSr-=Maysv)qvbt=+z#(G@|U%w&p>5QLelhprh3u<0y=}2JDtKF) zN?6al!t)w_qEju2<(yHyNp(}RE7RaRg(qDs1iBvw$_53a#HXFQcdOhyhY6L!Dbv3F z+-wFXaQptNH~ObZ%sq>&`9Z?PuK_PFCVA zqiT9rO+xSmcN@lzw1S_fynX=%i()kpAvCH@hdKT?a?qka|BeLK*{lUCmzUQdMQhsx z2yNz;;5^YYWp{obdYfPxys&v8F~fF(9m&dN=`?D?5+sC|z)0g*Uk6kd`?frua72}^ zl9z0M;!y)Lt!J$Xo=+OW7{17%qQfFCUj;BGsiv@~;dtHFCwL%x67hJzgX}A1LNhVR zXDtldx>^A@WDmw{^h+ER+;$O$1x-0F{?v$Ee#;qQzP0@71x+55)aW=&kJKLh(wfPmoD(B#aF44jcPN8x5L84XKivWpd3VCzT@#m#trGx&pT6xA zKKSf1Rz?$qP}sKF=Z^kVp-dR7a1q7Biwarn>NNdA%rv1HRoiT}{<_NVtZU5alBUeV znjXx0!X08W@!Mh#gcU~tz*n1}?0UX_|AKKsJqfumnyudN!eW`(g3miZ^lFJ%x|RSL z9S1`}#7ER_;>Vb2IN5Q@)?#@5a5zzGj6}_o-znh}l1P=^LGm}tF&x|oc=dMjv4C9T zi;RV(2zi^JUj8NzqZ#Yf#Mt+0TVGQVFimvKKN8eMGm_U_gUyX!m9(VzG8=-U#9J$+ z@=;eG&H`T-56jhwu@_B_0+uzW3xU(Ww9JZ8AysGYk!xH=P+>3Sh{ zO6ww&lOzKjJR22o@C2$BdCdNO@IJ|cf04SMN6xy11-1wE>saui1;Ba*jEULrkDsFB zk}oCFTA$V^^Vvmod|kH2LU2b7tp(#-r(d04L_#Q?>7WeQo(|%eu#F*HN(lijN6u%z zI@xociK~bfDt}A)zQVD%O`2WSqv{F?KOc5=cZ7>DY3g-nJ%_}2z@?(#v|YO&|N7T0 z?G9{m&sp@N-zwM}zt^Jo&?oG~<>ZVE7c^pCA9wVSuqPpeMiB81qT$J7wUtYu~$lHI1r zycdMxn6La+p4Q#CUkJ694tykT9tN0rMO@;bBam%8k?z9K3cXEj|dH z(Yck{cTAz^TE~9g8Wr)039`V5t=7PUDWP z1v~ow{iONy-KLMrhzU)+NWwQ`Uu7)9uR_qa zXGzBeCb?lhsrlSpZNGT*;ymcVEkom#&(!nav!WnXpsg<=B;Ar(i8=rF6H5n&7t`ym zP%nI^_9Usg$a1!j|JbntF<_LU0#7p79i;=IG(>C$vDh5Mq8T1ow2u9rpzHCtF|{G$ z$A(zy%tJ8CtoUUs5J$N+yZ`_gvy1;7V7#xxR9%{y9(SaE!OIjj=TQ z4Q7mAIIACgp1_E#BaLWJxo91kxsA11_j%&W@TO?=F;Y3yk#%Dr1HH*Cdp>2nX>^eJG^iEZZrZ&_UiY<4qA4+@Ud;BnG z{+&o-U#84tWmcgbZC><6)g9kSTIVm+XgShS+GRN@3gH-oXWvV}T=OXthCI*)s` zv`Wp^AFAc9eVh=eBBCv^AZ36PlvpHMj=pVneDOw?fbT-B@wq0`pHjOe1B9A45tYx@(Z86d{P&@{|?!2Bl7hp+WU zZ7c^1ZZJ8NSBBSL?spBSO6^~A2-Ty|hX@wVx=)%@Zy4QAb}Fc=>aeeN*jz!hrQHXV zne(G9xi;Vd(2~$*t%Ckauim}Zuc&)M=k7jZ^x{cD7_di*p-#rrcqnyPHo?26G@$GcJN8+RslmMgab#+v z+}O$3X*TrwcLVh4l@}Ep7HpVrY5prvk7XQPdgiWqS%90F5pJE9=+)uSlOuglI2?Ok zDn;FEUR@=Qay{HpY71Vl`VNgz z+ygAWO=NuG{xETwmoskm?g?24Ms+re))Bp*Ly^X~O3}--h2mmgcfHf=XVEhowb7RD z@wl=RV^)1UsU-3z61CTWSIkwAr5X9}BU29IOVZ?pUOL-XB%m;}uimc8i3j`eDBd+F zKJ@Bhu)1Yk?j?kX^ZZ+W=QA%utfke?V1rT6ztq4;d-JfMj~8cs0F_$D_8{{S_vqlw z<$)5HL@pD%|KSzb694(`JGd~6yN#vx`=Syo03E4jt`WQTV1>?)a^XC7Q_y0Hw9LG^ zV+D~Y3psLlCc!e|=S}3MxgyU95EnTQQ+;%*;M5Q$K&#M-11 zVWiq~*637@tBG~8?jD|f4!jOdwB0b=6voP1{1IRM@o_)a8_YEbkDO_b{iOX7GMXE3 zM5j5I0PD4`DECVdyXwv(Lw7zu8jz-`%rR?4NJ~Kw8YS4N=N2$~5(2fsUT@S>0NHc9 zOv_?#GSQ?Ygq=eAewrV{MKQ7}DMj?C7)pCC88SmsD13D4m2h;Ac;9*{56Y z&i%|%QxuH1m+3w>@1_wQ=@*g_EDqNt5>5%3FE+<8xh z5C-;Yky9qPkKGu6_GN9!Zhv zD)tK9+*&Df8lg~K>{Nerme%~kmgwbf)3tyHQUkRXffJG~UPG^wHPBqt!i4eQpewp7 zS0QbtRZZsU4mbxhh)H(JxKYVq!!?B=yL0xyS8D@@ItFrAgYCw9JBf}90q=xXM|l~~ zsL{?m_B$A_~RXeLs7gnAHfR^lyeKslt+EvJj%lIAD&QS*0(+QLu{;jq5S z)$4;~GkDth4<*F>qx1k<&tZ6VpV`VSsXZoy2Kh!>Pl0pehwFFiPLReHZx3v$+~%)+ z-87afxyT~m__NVBOHH-%k-Fy{75B2(v|E3$l)`0jHS3m7yqy2O&_CY>e-U4Q3A*j| ziDYs)4(9%Wl>3zA^`{Hi=w^}c!BD(~hf_KM^hH%b1(T^MxBR>a>Fw52lMH0imkHxJ zDp?C)$JU1BkEi8(tH~bD>Ms#0L0jaK!cwK%_s#Pb(=^sxa{@LUbfIko5rnKLF+EUm z#TS^ndrB)7UA>FrW19F`=PLpkVZ42|^`4zH6*unXL-Il`aqdIf)|S;9(*fSkM6c@Ro z0uE!Z{VGY0uegVM@lkRuEAOUVZdYHNyjc4=7{rBRRz7WS0XBl#fie8S_4Gm`S}Xd#dfq6MWG z0{3O(gZ};mbJqGxo7rc;UH_$hNxpJs@Fv*bQQ(l|CAP;GgEh)L!Ww`zC)n9@|4(?} zzcrV!YKr!81dUW0r8V{|9~Rdvd-t+5w8*A}evH)9~#OVyvuxMZm@s|X}? zP{ap1lw`LHsr;wY|BFg z5|HDD>IV#3D}WTj%$_&|k75Ne%Yx!3!$X7$WRcQR$?nH!X6oFbSo$>n?|@j%0&GdF zo1SZM|MGZQ2|#|mP!*A1f9>qhM&)qo{+V240$IZ$9_9?;Z`C_>n;Ei6@Fb{`Fm-Y@ z&e9rh?Q~D#w97`%wIZ8>D*x@3d-&qIB}Q@{mpEkXv~|u&qolfysY0vp5`!))e~*@g zDMqq;A)Btn4M$0Ph11(2&W<|gGmh!#V@=mIZ#~6Ikbe9)xF$dv^Cofz={V&cQ~xi~ zyZBkU(=0p)#{=_CRJ*8LiG~l~Q`<%67-Ooris2Ua*-Ope=D)}-(M}|m-^FzUrxCo^I^~mG{uWrK*mKuW7KUh_Ht7#e3InTi{t8@(6x$&34OhXPxSoOt-8%yN}Nv zn*H=!C1qPV!tr2@U!c3$Is#m3Hon^&aTSI@i!@dl;x!KZ@4KeZu*L^L7TQ4JSm-XWb zy=UFMY^QZXefqhlDWVoVV@qzov-mvfER{zspzBG(Xhx9fdrW)~Lh`gMdNZ;l zr%80C^Q*TvW<1{6JZw>Y`ny5qK8E@O0pb|#dfezc~)>0+{K|B3FRnAj{zFFuE;Z+vG}3M^W%O^Dx} zqyvseq(VLC^>4vG&+;#@qiO6G(GaFs8uM7bwVk033%R2Gj)Z%PoWe*rX6g*pUqrg zyKH`Z%gD51geWu}lSANPz(*FSBN|HsM{G82-*oXm8S?K$0)Wvdh0%?P*>ty)kwB$b z>jE!oyF{}Vciza#d^vN<4*O~Nd>;JlsXgtmN$@Fs!B?|`+Ced`7rL-g<~3Zp1T(Ky zXub6hTE{t`yzX}~6k}PQ%SW4|Csxyd=X@2kZF>ua(`>q`)a7EUd>U$8=~^z!?j(#| zpKlgK@=1K?gOHviw_^M0baOe3O>(y0zR2z>a%@c$R@qTl!qX+lNrULSz)~* zdhHxlHEErmOKVZN*cxmnc6AuBIhLh77G$yOc2Wu9fqg!2yP~T%^Q(IckiQ~XtX_T{ zVuOT5pQ>d@^Ae~Y@eSfyKFL2&dK3m0Bf7)n1SC8mox+O#;eMEquUyDPD&;AP^%~-A&cA$ag zA%&cBjRa%NQz=Tj2@cL*JP5VhoAqumnXa~jIn2UBTtLM*{mL8dth?^8xNE<`o3z|= zrbN9qeozK5>90oQbE|ol(Plb^g-_ z<_4Y~OndkJ&pKU9Ww}T~Q7oG}o!-RGo(;$icnH`168J5>s?XAd<~)%vdDiZR2rt-= zy+(HkNK_}B*A)KjKZ{&3vr_Z55DyNDlw6_L>83X(@pl;SiAa{yoV&*+j)qS4?htgV z5q~)UArU&o=qbV!Arg|{!A0B;=(tlzFUnLRbx~uovUMiQf_?OjqMomoR}SWUdV2r& zLA8Mns?CIH*Z(=F^3Xx;y~R5tqW5k<;QH<0rs3bX--N8uUL#D@NSD&7I_N z?NqM+6G@Nid#GfzB)jlc^S3QrSXs+s4)Uq?B{cp*-fhc)2H(7NfhDAiFwdqSHrl2h zsY~4Bo-g=(HJNhpWimA3BX4v4C~f>lbHO z_nmr*yyU@BmR&wqFXD8sI!b)s0Fc+)d>>2^mk#wbJUeE=R184EU476RJlo9h$pS@fV^lGLOB@QY6sw}#rS9~z9BGwju6nk)d z0G~iYh>l^mPP@6pUq+AHyciL2`55gin<6f9k!X^tBklUSL+jImx~HKfXlzSN6?%PF zYR}*25?7B`B8*ys($L7+Y1pU+)4HPPnPP#i2>uWJvT^g^VwCVIyUlac8ka|pSqJRM z?Xa;6VYPh%c%vU;jC^PZ&Y#0!Fo(Yx)8 zE3AxlxHCIjjRPcOV@I-!Pakb}%s*8EZrxb$tj&GYmAqSW(V99=03tkx!trx#H|(44g!)d}cF9A){(3qbAc?UacqfNq2h+kz4a zhLLxHOQCcH@Vzs4!XY zbaVPqCkq?ow7HEhy|@E&=V&IMRs6^g@RgX%wC>y=y)$hP3cu+L1zV$OK{ zIGgjv|43g~fN(O5-G;^N)rlzS?ZUp>z&Go92q zL-ojYe)<=b$Q7*85(q9TK~wH{&z=@H4IRf8(zDJph`mKAT+IyrCF6Ao)k^iEn#>?( z?mcKYvzVo@*rhN$CTv)jMOh_1YkB?x$C=jVIzew-TjNB*JH1b6N4PwML6PI29pFne zwNdT2sW^G$DFcJV9jeFdNVrCGiw~v=^32K-KSkl-eXXxH_BSM%A zzYyjI>@J54r~O&#+l;0X z1R50f)eS%U4bcAcZKx;511HzG&0Uf0c_9)%1!C+qSeme{9bd&W(x7qn8rDt+q_`2Q z885TVziY26rO+Y>@!j(>&g#M)2RbgpAX@ZE+*vL#lR7~khj6oAqfqw zZqy>-{wL7q_Ee`8TQq&db$@RGR`s@d1zI29uMoghq_Pu4CVAS%bdM?tSywp;VH+Xe z17PS=LkePB(TLSBV!@3#Jllq2V6?u}_4Rz!|8DRutpMIT{%?dP{!sy~J#t%%!9u+v z@eMrDRfo6v1@G~y6jL0!s##BUmpt&ZwqRSsm8Yz;4oBW!xzFFg9dc=`ajGCjgnDtj zUA;47c>ChhVr-wX#XhoGVS&}#Hq7KJ$SA6whA%5YAMr}R<}()@cJ8*8>UVyH)!)m# z0esPAH>~`d1g>y3i;@xgt-s$j3A9F6fBi#0p1)vd)&!-WuIkYMtjzE3{ro^>pkGzF zSN8b?+hES^={^b&iLAO=b#t^;05YhvNV=!U7u3SGd);S@2u76nHF|@Hu|HMBt8@wn(ZLj$AqXWzgg>jx_yhYlnxHm}vV!@Dgj*E1A{YflGXYutsT=sOzqQ{7Is&+9x=tH{R=*a0-`y;0pg zJovqI7_RQr?~|EsBiEU$Pxp$%PJh+0NzuD~r|JMXetpHbnpYhhOv;<+2}{vS68>M| zdy5_}?MX@5jInA6i{7orm7F*KkFqZhhw^>jo-vpZ3MIQjC~LBgttiPB5y~=>rR>JO zjwqEZQI@PBlHv}QDXN7(3j3zIB-9YrRzK-JVgZY%HW-hwv5q&!8gGzYk8E1B&3$7Mz3ts~X<2Ar4 zZ-N=5QI2GoiG_ z!WFssYL1tOP)}N3=D}1WGU>5G2N|6JD@XO7k5t^V^@NJ7$8I}$ySC(KT5n~_q$QO! zq@&k-&G!*5;3{Ihf~oR zl*eO||3glvYRj5e^s3&Q;L}!2UwV8QxGr?U@(ukVcrLMK!t2u4T(9HWi(@Oyf zm5J&P>86P>UiIJd6K{B&-x1F9EHEny-*3qeOD&pSK5;-Ix|@V;kZ`#kql~7WIi{g5 zqt|*@2f(?la=K8I@#B|IK^lyttHv>71Dle2l~6uez^uZxLyNUD$X`ouC0Nw-7TohUI(QET&E1sb=n$nOZtBtVDKLSNvNxrtkFXZ>QPTTW;cm*Ec{S0 z4l+Pq*YvhImV+GDVbr;;yiv-}rDbSp(r6zi4HgYK2XSJVC zm-cn}FxxDGlT-F$+9N*U2|R<8E(O%VA-;4;#<-&JL$gZ--GnH&w7L+kr*TP`6K!K| z@IjBS*Jmsn?xsy%IF%OOh7$9-ap&7T)b7QRIrzH-sU$q6OW|PcYdv-6nsTp9(G*xY zo-9<^)6vlb-ZeY|*806D>7eTnC&}@RgTF#Q2)pAET%t&F2T6qyAAa7Z8rf!HY&j2; zUY@^#TtHklcf`>~_V_2|ECn7cnvb14dd{D+nk!mExJ3UpG?QltN#`{Ygxufjo?ESp z%F&9|MHX_`JITzIL`;W0P*y4J04onG!C0%crm(LCC>PZXYbN$u6rWWuta&p;Ov#_R z`HKcl*j?!tz;WKpA}gjr(?0@Z@XP{m!)!|4rUh8_QnoT zEnMm!!RVpK#s0~}-u6X_cy3*u+qy>B++-Dj$gI%}vH=69DuGpC~DyIk{w zRa_++Fsn7nm#?63%dfvWa9|boDX$Fg6W72tfZ`$ey;t9~LlzD4m3}j)9f55VmQ)QU zG3iPY-Bjr9XonspUi+kh>M}MP5@Rsz+VTbG_yaoc`i}h@?U#e86-j@y}lW0~~ zCi&Mq&=WO4FyTn?=dmy&xZ{H6+smy^PZAYC{)2{h7?^OBBVJRmha;nJJE~G> zJRt)g?>4t^u=oC2i-H&%c`Zu0Y z!R(}Ho7Are0)W5kIIn)VeE-)2u@Mi~890B9t+Jp+&3P~^Q0W*t((r<(IdYZ%71?eOxLp|q zv#j*ECBglYofUHvkkhv=QzxO-H|Ow9Cr!+W#j1Py(dT2fGV4v_QmiFLGa1S7$+;E- zE_q8np>`D{N_vk}d-9ikJw>Ijt)!|y#;$6ui?>#^JC-+Dcb}`2TEL9DAMS3?)*klX z)~S*n^XqGv?Mm@*AmtsH;Uq0xR4w2hd*2B6+b>;rK4^7ke@?^OjiKdW$Z)ydayEIQ zBGu5`{|5--X$Ra?Kmg0Y%cR_m#0Sq{g`wj&D^K(8XgzU&H-|26t)j@SsUiATw8aMA zAna`7>VMNXU~a!ZX>bO&%r|r{(4(X&xHI-!BPr-W!nr%mkM>yk4gRv8`vz&b2gJS5 zgDmNe0AD^9tv@27zvYmVKknW8OL6)4kPn)`Z9)^c%@xb=!ZS=~WL+~;>lr6bOV8*d zWTa(Or_$M0k`f=GE*P$#n#fuLzx8q?PnLuA-s}ljN}un|(Shv7!91hE4I41U8eO2`@ZB-XAv(>;1~Tlbexjq$@}?;@oEFHa`g+HL5>b_evn1HVsHJ z0|E;<<6%b0;%do6!%s%PVy-Zhos505JqV1LqxYQZPMx0fo{rBBtn3I)b8-cj@-)#h z0Oot^>C}@h=A`}9^6j70zFgF3`i1%bzO7*cejGy;+q7fi&S}Hw0mUcJH9B;wqxnWC z#XK+$rlTcuvoBsRZ1JA081?e+PzK|&`7yJ?H2p|fxylJ5N36>zek@;Cr9DNWx9~1*(!*3Vuz7pBRb%OHT5t1vjf0U6j&DkCRJanyS5Ytx=o~?PO!!-j_{}Y^@>jV z-z=8*xfV$Cs4pwrQ;&WNYxvLxOt!*UXbL07Ju7Elk}dyw8?yyR&&f6jM@@4GYXrtW zp$~piK#((mp>8@PRQ^iZ{}5#YzE|{j4O%yU2ref4+AQ$owd<pL-QSt|$Q=jQBH9zk@v?L-mu-9=W+g;4^UV^;-Pn>p%E^=jf}6d__*l9-nbAFf zqm95)LWrprolO-9m$27wwVC>EDJ;N%!OO`WeI4DKh)AHriQ#M-=VD>y{v~eb>iE~qv zFMc+8>H@V3W=bALl<9_#GqQ8ZtbSFK5s?+awr0RkXoZ5FSE}Uf%g18j+x$L}@S58x z0U!ys=HeW__B_ijM`EJ@?_$=F zS|^_~Qk!qO{&1Y7q&l%iYG8n;{kTP$sZJab_E~X^Av37>;*k3iPo(0)JGbVJl|OLU z{B6nQG$Pp3F?vDcZF}=Em~YnJQxVuL?8>j9%k>V?KgDT|U59^au}Gdt^;>VXE^w+m zpG5#<=zaCUwc%q0yF%z@sqat89e%S#)lIIPc~_{+Jtak2eU_J0rqV2sJ| z6`u}=s{ead^44+U#YjL;1xU+}CK?<8Jd$7xANLFz>FU$44CVb*1 z+5&dRQEc*Y=Mm`jD?xM@?-L^lszM$?v~UYgx6ldH35Am*jo!37w3%(6;MjMq%4@Kn z*MB*VVZw&VW)HpiR6En?$vaarnKZ<-v z`ceP4NPeLy@ygd5No%f*%->|mqt+}!Ic6>9VLnV0IL>lr|0eJ7#5UbD#cIVpcA2+= z-+GXtZV?|e$p|22yef4J%&%7>t`LF1e!cTIFEKMZDL5D<;zk}Nw2Z;7*wKD^P8o#~ z@xYCKLPkYWQc<&`aGT1%!Sv_*R)j}ioDZP?ZG?Gqkueo^ zWvG+!3QxpK2OjQc(#^sM{VnemWGeb>okNj|XcX_Y)xo3nHqn-`MG%YgtdX5y$#60L zRZs#814WC3vP$t32X{yUIrfBC!P5za5zxD>>P6~tJ5bySIPl#5T>Av{Xm_+vWnz<^ z%H|ikEWnVfc>2%7w0EI^eQ=uZYuUEUOcoU>e}H$(s5fO!zEdz}ojY>@*C~19vf!Dt z9mASCJDvF&n6s7n3%*}yi{8~g7*LXsk_#^pQywWIGv4FvgZakNVPYJq(BrDn_sx}i zuf|j41gR*0BFRL4#=kliTBI7F3=a z!k$T1%1!Bq)98H!0UX1OHfeY86}&`UB)7+ z>o)3tH3#;e-N#!-y7MPFU$uX+v{T;wgg3?(bwy>ukJ}vf5NC_FjD?$HRGw|icCTxQ z)-l;h_1Dd%_}-7g$m8hFbd}CCIy~C3?bb0FLtwe0$?=@s(pN~8Pm%FTce%Wh4_*;A zD3w_{$V%IZU<=F?$Fj3w7cY)(pO(CnX-ej?xLMN1b1!E(xu>i0^WW@7=CCyxbeMN0 zUdKx)U_tV$ID|#f6n;iRQnQE?Mg-J^j7gtM;)A0it|`Pfwt0Ew9RRs=a)D;huc*{a zjZviX*UQ`f*u_Cl-c!z>Ug_i3*i(%fi@vQj?Rlvkbe|lv;nkTCNEL{# zzp6rr1L+ZA9x*zInXObRFPOhma0IkG%N&dB>d!$5b(@c3??Ai)&QMOXVP!8 zmnJKjB_~br(w=56?p-OrFB?y`xoUYV2xeDS^QRIx3m1J&Rx~iqv_=OL!3RgbfkS~Y z4xAm_1Jw9{%;Z~>K-VNU3mw2J*QuhX$H&Nc5{#K z=zn z&z!vY_=^;`-<8MpCo0j1j(Q-_INV~fN%}=sp4g-OUFIA7j?z~DZAX`AHeOI%T{ zMn0Z{==9KN(6)Oc(_VVW1R)H`e(;l}si1V;4zoo`^gvuxP9z8F=MRYiWo4VOF49Uv z0}5y%6}-kkZl#jh$XxJ6|C~134A{F8Ax?CQaINdi_fI#^$m9Sj&0IW=B!JVtSJ|${ zfqNM8p^nArnQC7+5}T~3hN#%pL=ZNS(k>%xD&71^M8)FY<6!*}!)bm$@&KE_&J=}V z#A@a8z$fFrH=^caU6iX}6zh9J@h-~_8|`Ns(Zz0>c@*IYfO#L?v?MF#1>P&|;%BzO zl-IL`9rM!D4S6AZg?&XPrzB8jOz=znMEumj71-mL5$xE}Zdc&GPwcx^fNxnb+AU?t z+ZY)$!Ht5zgEszW&d1@EEvP(+LmH7vT%J+UXDA`pN%XJ_5Kc3UW(*uFuV;LL2r(dw zd@qvZD?*-1Ra-G&{P<@7NhRjyl<#Mdj`GOX0((y9(J@42&6208Ld6fd8Nj3RCjnZY z7d&*Gb1zQ=AI+^#)cVEKI{A`g<#rz~3R}c)%zI1O^|)!|2-{A3n!U1HUNn&aTF@Iw zi;TRP^y;pr=C|rfK5(}7Ne>wczWu@9~JM85u#%Xt9_Gy0SULk7CPi}`EPELp4+qsKbMLdJ41`K9d=nBI#i6Z zD@*3D@+Y>T`4D{>$^im#<2=t1z{kK+I=dl?=t{ds#WM@dClY8zCR72 zQJh1lj%KLQ2eKD(dL4n6d-9>sjmzl3HIAbGB9RS$qn-+-nhMB;rSt9&eN-;FcR*jyMLcIi+_7lD~rkRd&i@Ql~;;RKV4 zcV7=lOLMV8#*@v%FHKBpVxLY_1l^5w*8IE-;v+0o9sx-ms!5YI_HMC^`ZxL5ECD-p zs;V=Acp=ROqx%&==04$>!M3-##`ddZ@@%4&g9EFC-)V=HexLFS%9n<$h-_!iGepYo z+4dT;@2Dkm)@EqsocetqwG3;fd^OfZCUR?^?)9};`1nn)4T7++@=EtrejEfl3 zY|g9MT}}~J6~uOo+YCMS;1w5+F7x;f2`FMK#ifK@nF!5A>}Py~tN0lI%>93bFz#cL zYF#$zdTPBf^y;Alunzj6enL+@LLYGf5sgtn9^*)p)&-<$B@37&w4#ha!k8dsVrVsX z`Zdd?2RDpLpCf$jzgnkck`+!ZjAm)EAq5lz5B&;m1HweUvS9=VEkc>@xzuw$i~*c3 z6eLuKm>`heMIdA6{OPbPF7q?8{C6bC-d+K%2ECFsEUM56{Dmeh^j|BF5aprDDiI&v zTr~7SCr!OXpN2=XPa?Jta)J**$l?l+1k5P4WLQN4r9S;*j7q{m$J!n+RLQ_d6AU_m z8#L@VdZm45X@{Z9<4Sv7q$DEjF0eGc$=-vsb}_6nvAbr^>ebenG*`*;u;U3FAze{w zOXtt8EZPDt=i{s4E=D*3Zlad(pfCYO98q+Gxy@_8%W`t0jess!z(q+LM$Uv9Ccft3q5i zm`))BLctzDW%Q`DR*E1)4^=-tU_g)iuGAfLxR*0i;4+3syGEu!O6RP_5NWwabUcw~ z7ft{{v)g??`lQg}Sn-n+k5UjBh;7G`4yoErM=Z8-t&vvIqz^s@U-*q$CMfrV{&HOP z0@f2;L}<;ca1o$U>q&Exv5k=z1FH-0AAw?Vz|`|=?E7`{9o81YgNPYi*URUs!SgOx zlwj2$TVaBO$z|<4oSS8p{c->>;6R?@Xb<7az;UO(krGioD(egxV4##|qJwF;er7iJ zdv)`NQNmw5y13#-yTsucR0ntb7dlkC3SRjnY^YL&j!V%m#>G{>AF%g}A$BY+I_I|C zrJha6Y45PNeQ0i!dhZETyQW|R>U{&)7T7&IYf2f@sMOe+dQ+L>W$?4kLR=*X1R}7; zvz@=o0ESSRB<^c-SCb}pImvEJ$~0cXfbnZ0uUU6K%tQZHnlckwhr)#ZBNdwd=6~}b zxiIy3LsRefPMg0hzU-Ijov&CGwezJPzW(e1m5YV{L$B%TDR~Vk>rWQ3dA(x!UIGel z({S$J#cIyq29G^TmSRU3JAUSGju=Ab9^kf7Ky8Ql@IlCPv<&yY!>18fo`@p|9lLUSE^t9ZE9!bR8OnKV+B@Qp`$!`ydClKy3A zIUEn4r(vzkL8vhLN$G#k`&xmFI-b;JEpR}+`Z`4rZNE8dBU2u~W&!fQ zyx;!v`fR|CuB+BM=-k297?)Qa14btS`c)l0?dnFEhjx%V#h{1dM(PncRybnjNL(z$ z{Vc2p1*m;QL&90Cf3Sw)fyGgYC{=l*;^$UHUDvAu2)BA5DBG*=PW2}awm4L{40(a*OS1_K%V-g^EEb z@#x&^%r~9^fneg<;^79ycQZN}NZ#dFp?YxYr;rXnK!9-ajUd ze~w#J4RiyI`WL*iF@jH2f@pEl9k73I7Np<4T0f0-04R2{4d_>otC?MM6g)v+D6v;Z z9BM=D*(5T+>R>&(0@d99nH;!hs<-5Df=c9M_SuUQ9DAQ>6WtnVtgqsM@M$#JbL@9K zOVGikc6O>S9X?4#K~eChD&t@yNgPt&aUee#nGuxON5#H6D3w12ENS@#-|gvMB6i?N z@3ak7pC?P*1&rDToAvA z--7tm0BDwnI?SB`{DVPLAx$A2RkJkcae_!|0$F2_f@z^9%=Z`lNk!=!FLntfUF*Dx z>&Db#&7Twt_-+_o*pH2=z-^ltlMcC`pFPBgcnjyLLvacs;Y29kPP98gB$o4@!NC`nXEC?r98m8&UkEF8dW`dbFX&i zfI)#XUn^2(zYu?Btb%+JD65*y)KWhMcxrFHPXT~EDxIvu!9zt6^$es!Ekk0j7m@~= z)Vz1nVmoelbq?Cp6ts6w=GUxfo7~UGu zmNNxiVPc7ALfdJVih~=+ZQp8-fI>C4>kV{mV@tBh)feuqpSl6Wh5Nd1YEQC*c_ZC+ zEkL3^#}cNa-LCkX^aT~bd-|vrU~vs=&1~XcECWtSE%3ZPT5cxxW{_H#oPA+f7L1bW z!ek@DdSJEI?e`qY-gpY@coKTbZ+_VTlB(TliZs`x{g|MQo}$S+UcKu4-pEZ5+I!v5 z>IVk6{mW1{AJ_m%nSF)0Z_Aed-cAUgbvw70(xuz2V2S2I`?0xUO-|nNiMIn=FD_D- zza#tkR>tCS>ihJHt)fb$FK=0z`u0XJb(Ee%ngZ`-VR&G#n65XH8_p{n)MHA}<*v=X z)OWY_5ue;et=0way}CT_JBHgEk*zj4Hyf_~I7Yp1nH#v1%^P{RHD_*7#CcnH!m0Dk zZlKjjYT&`|!mZINw$Vp^E)eIlzIj zXfexAuD<@|DgC%TgU_MxqdX?({46*j?{4X;o`B$0HHbsdOPdFbf|=XUN2~vyNxI$! z53Z_eTyo*uE$)SMMF78~M;sM1897iMZZc;IE>+}%(S6o!11pUnXnNVG-G3renF?Z+ zsD_MK{K19GsKnX3@zY4#0>Bz zD=8FIHFLN$om?hMyu#Noc(eCmNzQk<P)i>RhvBordQ-EGI62)?CM)cDUuFd673o zc{zElgYZewfyZ6ZP7Zf6OsHT)PqUPn({-0544-~-xf%3HA07DuSv_mI!h}nCpzEV;sumZJ;Z9m|4ZVa=gZ5(E``B_ZR#3UAsm=+hxj|ET`I zJ{=J8OpvW3N$zM~6tl;RRRAzv2?lV(qv#Xba=JAh4gM@-%Iv9+=Z4fs@DX#+`+Ts#&)AM*iWE>R7k<}3QUV; z2YuR@>>s(z7{|zb8uzPpM(^3Z&tVm$JxT!-#&@w=5y7lZfwcX~C*G7gB;mg6!=JAS zLx`%LM5t0O)eV>}{|Fy=TP6M|To3Af{-0*px2 zc1$Yht8g-hLN~Gu%%5nKlVKhHa%Q?*fHkGitQ{SWA7QqR6HO{Cf=Pf~!B~9zda2nS ziKmyF=dJ)%yc*<)da12&-vmh-CctAf%IqQUS7>i+L`ZIc`h?Vw9pJV73Wj~kvogG# zXm=%`Z>&K7y1EUTjnE&5YY@B0?gSPRVZa>S(_Ke#5HJSj%e%~Gt1alT*$)3IJ7EhK zV0HCgxNfcrC#DRTE!ui=VR|fu3L4g~m)a6!rMn`47N}wL4W+!ONg>WZuIZI`!vPT! zs$a0YWxRh#atwMy@sa=hTLf>!DhNPu0mlh9N{U*i6CF$A1a9=_diU$nrQJ^mPd+?Yor)|&#e6%ThTkM^Tf=+;*5 zZIX57TUnCt!4J)AJn}?KK;8z)A7d5CdAK!kR;4y9* zVrh!dr$0%7pBf#^Se*PtoR=G_uD0pw&P4b{UVrH3&I|q2 z%NqKJg>!L$Hdv7}@hc)7)~t-&(0DY%dgX5r0eYF&nqWc}*M5*?_9r^!|H7?Nbr6sk z^Up|mqf8-EIYBl6D&+VENH##>65J-wVhl&xO!NR8C0qw*g9c97kkgqMdoFJlo;vz#e(Cx42n+`4}rUnF1y(_ zdKmCC3)Su`DM+8+z3;%=8MJ?44wHTuRVVw-g&vpg2DMOnPbjQHbUS`a zqEe5nf85^ZBdIIlAqyOiWe$yh8p#dJAXB=%CitREi3#Z z`u}cO{=fqmI2(&HfU(w31H;Gsx$D_q>^j6HUIp6(B-W%ap2`mh-g5*a&LR(BHFt+P z<3C~o7Dt-odcV4(Bzwx5J`qt@Fm)>^wu|S!X~?#1&-rVzI(xXpXcPF+O&t~KX+~b zp%s0FlC3(;J4YtR><-n4RUw-rSXHQxP=@2CWZR2NWVp^x6i_OW1c&|d!JY0y=XTdx z*V;AC4t=n5`}4@|#cFV&b}Il=k)Y&7tO8$x{tW;xjzbmW%j1t#db!FJ8k#lx#cliE zI}O6#7!XYH-s2Oi5m5m5$(VVljG@FSysXJQy5Q7b&oqh!8lzuVD?9gJjV+ex7gQu$ zpT_#X=Tu~)++W6o!w(O-T<~x7GC&e32TDS1~mun{pL4cCJsAdfRbYC#P)m2#)*l8 z!l35*Bg>ZR-zV|zX|mBPPODmy6t`3p4GlAq43KH?QFd^Yzwg+DQz0m5i~*~F3t_C&6yz< zT|(&dmxXHb4MkUqQ;wliFF%a-Z9inh?OGn6uo)>;A6D(=$~ge#f32-}OGg($pkVR5 z0ZUshnZsk9gb=>MqYCZ_Q{*?gGhAeqb;W#PA7mHX1?LXKT7{mUf3E!h44?mfi~nXY?2Vx(WqeS!rI!N}34|15qmbVG>2a3m zpU1#DlNC zL?rSTTe8bjyYc!uguT+yW15lQfYGMoklV@10)6eFXzzT`kB?Pg9`3bM`=*ijGI$ME z6)A~VFeb+y7zB4kqLyXMg*dBqekxJZ`%a&%XcMWaoI8$9vG*q{Nof>%Ju;=3vu>!;_Mg?H~R%lcF$uxnhFkjuAw|5iC z@VEQa-)Ac8LfWgGQ+G&w#_-N?8Bx6(LBbzk&l!{DJT^@ys(%MtwUs~cEOq!Z7Wn`H z(GP=fSfBm(oox&yG6)TxT)6&Q7Y#BO0(?3F?yAf!sFzy`3TKz?h0DR2wUT7Sm-asX zr{CL$v`cO1Fe$k(7!h;PvT1g^*|`S|f-gIqju%3^cpI~*M; z5@&euDQG>W zYCKN9&mS)sX1NNqFdJ4M&vb8qD1vH$9BV`y_Iu-=AKlJ3k*(-bB*|Fo8UbG5UfH8J zBmjT?id12!l5@9^wu`IDT%m)O;tXKr8&-ptuF6Apq-?U8-yi0Ta%RkY5 zx3DZP?+zW;a;n|U(wW!H`1s;x(94U?VC=htX{TlDdM;59m;4jP^X&@sO%vJ!QGfO9 z|5RC^kk$iVVg6A2NS^uBPF$5UfMx?qextSG4RQ@26@Df7vlNp*Uic%p z{=*YB0UljJJL-6ykT4A8Du&U{jTJV9)YG=#i=hm{D?iKtc*d$Y)z6$h&HoaDqs8#F zW)6-MimX#D;TWJlEAt)hTybp87G404J_b(EqzWvMP2zu%8o1YrTB5+#A8AQf0b3rw zu5N=6bzuW>$Zi=R_^qiHt3z<160M@QBHwrdqsp&)<>Hv1LIb4gMZbet$~9G<%9acV zBj9`dc*{|#tIJA(6CL?^2}S_=5m&!uMr;qjbX#3eT{$FHG*kg~|2LJ1fM3l@?Q~*5 z27*85cpbxmtmLGf*y2y|`ut_*IgAJ(YE>|u+&6#lkvM)1p(^@V>iLSV{MHdD+AoBa zzQd&R*%&F(d|~*CL2I^gF6*#ol*)&iiIR_t`B>2OmM_lpgjhZ2qyVs1ih>~ zk^`qp%;C`*P|)9a57^T2bJSsO9_-_=Bzl@C{3ixS51KW7*ZYw6-!H2kHy?O{*}O5- z>OuXdU>>%BIxFY~YvARVeLs};7Fwuy0^6!@DDJ!#Ail0y{fo~3p3`ccWwt@#4kv(P zeAIJ%6hzoy#@@*uuULA*Tn&!>a1FC(-Qg`P zj4l?Wc2{M(guh?$F&N1!*a;qpWc?O7wE^afCUYXDeq|31EUjQ$F4t0}{Hxhy?}rrh z&Gc$)Br$H+tz9?FxK2yIhuCLKAj7Lunp#D+YKjXl0aEtm%v(U7>fV8-tce+0>h2nNmdP5%`igKQXj@?SNn7 zxS$ZOb8S5B3Zg(k47EY|<{;pM*vCQ2E6Bb90^E**5#PIX&%3_G~* zD6d7#e$ynB4?8;Mz!yMH@Lb+BYxv?1Yv3} z<9-|A2rr}wp9knvLVeePqV&M-a2i?I*UUq&MBqpZ((z4@@OHjj0O+Wi4(?6>yksZv z53pYHxcZGw6zLG&AZk+CJ?gg6QB<#=r}x#tA-}`Iz678zJs z(k0gh^+f};Fy9qLfB7b^rERcm!@sJ#LDZggPeaZ#!eQf5aDYyc;KFmq(V7|nY<;jC z%yxTVUC)Lk139OZFy5nd{0Rk~Ux64O8j-DZSjmR+u>WK}xT6ZRD}*{|SR zEX;rM+Bt0UHp|iTyifb5*cgq$n=?Hg13u~vH0t@;TCR)|!PUV=*K{%9Z8t7&^F@O% zEsLW(RFZ631$pFX}5kq2}-n8o%hdjsZyZ+Kbs zxQF4y+a0^Bz2OnRfRCjH*$u}BDL&UxI_O9ImI*-^A*vBsz}MPYkP4i|5QjBfpvNZ- zR_>b9OV5_2`b{2MoVbV;gy$SQ7v@<9LW*&)3uK`8FckKHzg&DG_L%lUUYEjvIE~JH zhWW$5Nw`O>w8X6npkJszmbNrE`?=lg%*BG>8rl3_o~QKN`TaO*nU6>I!{coe-f1H> z%k{%`>)$VmaH6w+Ve*uO?Vp})rtha;<+P^yEXx|&>gsVAmdLd>d(X+6J9Q466c0n< zeB(&FOsS7vj~p`On$0;uk8!hm#jpy!agOwZfp`V)y& zsGY?SKqKDXmkQS3kRGr$Gh&@!#6AK_vc(wY@B;15UC_Witd_EK7uT;fAU830-it z+?m*!iogKVIngxf{`1I9bh|D5-*-%=@+I)Y_-|itd#`+!33rYD z8FKzt1Zw$4&3m&tAMXjIb2@0d9}XBoV%ond%0vV8x1c)Ph!AkOu(*3-_(2+vwkFJ^ zAFNv-bQQrA3kpHTh;uLsmo$V>rPDtIg3jt;o_w54mANI|tV~KMte#Tr{Ro&1%`xjF z$PWfDJG5VsH7+JUA=lZ46FCd}?qX5D8+&ggjxrb-ieYbuPpj~f;f+0gzC6tbG$SXT z&T8;5@OUOmz4(XdnaXAfGuXN(aUf^$A=z($Bzu4wVh4y# z;`&Am6` zG$k5T_9>2|-CCCWbn!jMa6T}+sCZ6N=Ii~(58_DDof8!H0Ylf*lH`;p&QenswhSP8 z!>TLP&Tj6w!yW1UZ`=|66dCx^DH6~$QwF_uf`wLqc&WE?8@N-V2Dd&n>HAi-Qx-E< zZG3d-Np#azctu9VG;cA`(0;fD08@2+^>~!>S>Zo{jG0h8W51i8;J=UXA5^IP5j5mF z27+&5$Q}xa;n3&V*RY%-HH6~Jj}9y5jc4s!wN7LuhReBUjYeJg>v9459Tx~lcEE2j zz&x%;(0IN9p|b)9$yHDIY0$Q?#T*HYpK;p8_n$voJObj(@2zby)LGvH1dFpd+cysP zOWOKUKdeTShre(yTeZcZM&8DJ9S47dVcE@7+lg;>f#|J{(Wkihc`OpgX?3Jmu>dTSUt~BHDU4W$Upo4>*;Wu(L`~sG_WH^>L3McJOg&cXl(8h}ad{lh!#%9g zJM$Tl(H^B@FkkSEqrjl}<}|L;_s#UI>J`^cwetEx_&(>ky{aZxMbgzKRNfr2FuPi$ zGgPBAWC45@;`YyP7!yZ@p7Ujr*_+Qe*wqS{ zlj#|A1eS!uik$gk)vmvgpX)XF82_lSpgWXFs*)mW7n_*a)h=9>9@d;NN=*RSPRqwN z0#X3zx#y(b{)Zn2Fg+gJ>NHu2kN(QX`n#|G^Vvs3IQ&n&pneBaR>`;??ni^0m(j~& z1R|tVHhHhYg5EMa!-_{gKDvF({q!#!DAWbj1KH2zuKpur-t93VE|oI}^bi%DaUYU+%lNy}39i zQoDL}8`Oy^5YN~a_fE(KV5}-&w-ml3H5Q~$Ywhv$soRPv#ZD zsW22LI~#9l0y?Js4KS)5=(@aG(EyZ1^WBY0rG47s*yKU>&qAhP)0BIp0?|n57fUzC z#K9MOB{$$ftwaH}^eAK>945~(FGL3gc5bTGpeWq_1ybghMB4oYymxGAy97FErwCauvd1CoA5N2)NR{%kQ* zG!iM0E@%|JyZ{olvTBxy!|}?$*yx#w+?oY%ce;2%A=49xt|-$%x%7E&-q6 za~pdldHYssUNZf%LEV7yc=kSEP}&ByUWUQ((0V1~5@qd%u?NsN)wtHIEJI?ritsCo zeq;nm+nj+qa(LxUBup4g4?Zp8M}x`uA!03WXC) z-ZCEkCQE^(0_z6ol$HS@-`4?_hB(kVR{o-F(eL_KewbfC{qJ$WVgMYR6@b#2t7Aj% zxP&BYFk;~T6xT9%yWQm(z#E%~l=B?4Vm|V}gqhW( z&x|fK0Ts-A7SN3@zyP#H_YF$EH=i}_$y)}vV7}4rG$qm_1*`z-=5YOXR(@-#BpFB` z$KVx=fU@$2wrV&Ikr&Dxfn-9Y`Dqwux8gl^yg&c#{TMnNr^9+t+&~sZVT`IXI3Fbc zYA_C!MF*Q6XBq=WXO8?y5KPilf4FCt$}9E*J?p;s^P2R%^pu!mNtzl<)9|nS)aje+{sX`WsZ7vFmhEUF+My$0p-^( z52Yv^_`aMz&}ECc`dP~BfzqVF(LM;E0n=7B!&3{p-o(R4kCx}a1jr3am#E1D)Luuy@NDW>$T7`I}54W?cV~M8P-VE-sqUMnJ;^VysbfgbY zVMP5mdR4t?Fm?ToM9GckmhB#CQDl6kK@c1912x06J^jg;;?kJl?xN}-*`Pb??EyQ?W8^7_RwF3etu>vZEl z*H{d79hG+xvkc0)l{yF(QIIh!MZ>%ZA5)K}vl2Yo=u+~HVy<`qCkRB#blaz2Im8b4 z08O=Inz+awru_qZe8uG^Vd-(gC~1R@rseLi$I|{z#*B0N(RbyCo6oQF1s8}e^?pV5 zMT%E!ols8Zl{AoQalx&N@mw)#sA{=B8xYWQjd|D7xA?&0QjhYwYEbUZ8=~TQnOUc* zt)DLm1wZFf7J)f+xp`QetUuY2yke{42hGX~xQCkEr8r_J8sPBalECzy99`Yhu@ic|33A9@IsJ2BtrYBcY&XxS04?=Bb>HgUy{5*!HTnXn4^;c@}Yt2HYFk+ z!5GlfKCF26?>*vb`UfqLg9p!Q`s0x!n9hRaRSXO-@k7p;USt%TDPzD-<_j`%?Lu?6 zfk89crUWFgZNS7LS`%r$_j+7##4B&Xz5q90WYuLvbL?fTKW2GRMs?#r%dga@4CxFR zRPTH;`TZ#6homslV~AT9t4a}lqMPLzb4NC-Sa_H}$XOOF{~FvB2ANvGZk_q_{=Z~2 zX3oGhmz~$Xa(bA7Iz#$Wy5ARJ5n-8;BUJhCwJ$at14(fl%q`f?|9H-1@eS9SjJ!;rLnwO9_|M9c~s;n0L> zFXEz!5!PP`aTh3i=6Ci7N5|TrEHB9D#zo)4b|ig<{hQ?laWG-rFY5B;X8m;#Wh>kA zl`k2k7KYm7uzb#PaY^Bez2F-VrrgDFER+5d{fY501|;BSUulVEDqp3zaqR5iQzPKW z;d#!l*vfBlP53pL@h9c7M?XxE(OE(%r7Ta){C>X|TF$HflY#XOidD85IK1WbFR%Bv z7h7N#dP#Rlw|KtkQ8?BD$}IPea9|08TG8XC&EI`>7e~6^sf^y%Q80))udnvc)KwcI zC2c(#{?Y}`K83ghYb6_2@Vc8>*Z+t4h;ymRJX&bL%xf^y4 zwnu5DM^4=1JPLX63;AZRyL%WlHyf54T(1EvW!4OT&<Z>&~xA7EEQgqy=U-iF;$bwYQcuh)g>ZUsMOVi_(&4TR5Uaw^@kU_)m>U!cmjhwJV znG)e99V~4VfV5Ep{AOSxb z-O@XFDWC4FrLWU8v+Exc@S+y=D$Bs2wf)(XR*&n{yy*pj4eu6q>g|;8+tENHn{n~~ zzqdCnS*xW(zh6}&QI7;<3pYzB=JtleM}V=eSLY#D)_a|rlLr4l%BU}YF6?Ac@NC@- za;qRPkv`Z3-R1+p6@m0%Z)^jh4~a*hIT~392(Hmep}`>wQHc08i)1A$L?}&7b8x{- zd6B#?7LZ(o?`Nh-i9utnkGGP7g-j~Nj8A$NSLJ!u?YnM+uHFaj4KajYIK7(=fNzp+ zUaslk)*4knDW#hOfv9sX>iqL*%QWUoFb z0xk-lC9<7x)ILY~xS99NDbImX&z6ty7jpyJpVAa8{OV|BTFR1O(JSbPh4Gm_dULVm zu1?enfpW|x)M;hGE?@mv;Wm^>h^8Xbpq*S9&DJrYIr3$Jc_{CJ$#Nywfu23eb=U4^ z=?6PtAeR=yan$?mu1BQH)} zn&~Fo@wx<`luP)izW?}0Z*MrLzg0B$cb&3AD4!Yr(~w7z{f0V2g*rW9VXT6|dwp$c z`;XwkYe}{?F~A84w~D-mU}$JI(qNW}C^UDPScfdRiDAgWpkYKm+lmzD&ry6uf#<2n zl`bH_FdZzmnqqcZXBF5R`I#qe%xSthUufZ&o$TSd1a%FhFk&^Em+zE4_ zbjTbyn6dwo5OU&2g+M09o2O1z)@{|qo3C|Q&R)DI0UG7rOM3b_yn5d%kFm6lEgl8b zH7I67o7@!UM!-K)y8-ee8!TY2TdHsJnqR}%g)lo-$Mgqt(mf@T--+*3LPx@eEyJhB zid2xO6eq0&3&+P*zSe3X17FFYTf4!1wCMU=KG@5lDP?I|>s8cr8gDSGPZxNw6;~?l z&=Pl<+RI^M@M-6hONN4G*LX!%y$388vHHbmO1$h7_MlN& z!@+yZdYTd^yA-HqwH3Epz=0W*bxqNct5-VOzr9)Q+WUV9d+&HG|L+excXxG5Wk&YR zZDeQ5yp4>62xUb@krf%)lnNQy*&`Ln-dj=`MMSqPWoBh>e&=euKkxVV_j^2k|GXb{ z^>(?g*LBA8obx>ASpIO=dOB;3c9R*Z^QkMgCA8JhdCW9T6r@CMy6bOIZ@Il9h~CVt zs?XJ47M*7=!sAPvLrQBs*6cRAm6`}kFp$6bOp)(s7H3M7)`x+crq0@U#wFwKlbhUS z)2K+uJed;Y)<`fjb0QMQI8mf>%a5f^o&}vt7JAEnm-t12ATnYetjxL|$Jd?}>cCQ=)v32))|$)wGLYX^-%i@x#GS}Cles#o%Cq~f+)7jTMBQtpO9XdOD|)~OMI8m=JF5vb z`OD@2xhk5>V;jeVcnjm+zKs= zzd~8D8TLcVjH{){)>z8UmL2XUZtjKElF7n+E8EX0rd*50GEbazcGMhT0 zS5I@Ct>kCB1M!JTN9)QGO;Y6gC3pdKh7anP4I^xku@_x3g9_ zaI2@vb>7+Jf}a;K4qZYA?AsEgE{fR=oJ44Wb-a-u6O%=&b}C3_1cjbZ@>Rh;ye-(@ zVdd*TagOay^rYT%gh*~tys`ZQ*b>od&o%8SP*vhlO_Vp$m+$Pl5$4d9)31#RyeAZF zrI%??Llh^kx%l!y02C~~Xa8f*i0#)W4@lUD@))ow-~UGf_Mv|XLp?JJGqZdC=@E1_ zHdVVsc4L$>;X)mnLHK$w&9mJ0^o}+WE2m$2dz= z2!qX6c_%I0lEL!zQ9Y9>DT9OCbMgn!TR~&`1wG$=D!o+s1oL&)XFlNAiN{^rQ?G6m zlgbr`e@F@#SQ~Ur*@nq1Vtp?3l=9m!-#S`LZtENetziF!mYbt1Ldudxu%+%r|7st7 zje9fFE<);yn2&BOY+Pu~P1h$kn?1rqAqDlgpKJ=%epS(yDw*8J*( z#~t#M&RescnO790GEZOc_#8GaiXr}`+uz`;Fp1O8{ZahCs{ZrgK2()l*U9}v0>qZ1 zD+>1yH`{X@HX`Jj)bJh`PEa-@sLs%Z+m;6oow@s0COkkUhKd&1IZ;&t#Z4rBPeA9d zEq*mW@HFFcRi{HI4Hbc}_JryHVoutt}uUxOO7M=)hDUhhfFpZs+Ol zB8o-l5D$kDSdbFqV%y)hNcVOJ@-_=GK{q=Tp!;71vanlcdTf+xG8luDQ+%QIhXF!G@?8Gch)Q3llu^ zTW|hRJPIu6&pxc3B32N68P*24!I(>%jBG;=)&#k%MS7Ac*G>X%n0^!B2gl~(VTr_#ehOuc)R*S?^V(JnQ+XD8Zp(i9HFS*Q23 z3w6xYX!ud}I0qlq7`+rCS?fA~dCGhJE(4h?Ny{bIJGkT`m5 zH(1#$r>yT9QJVCnX|mGyYrWt8HVeYjFVD!Q7+H3E3a)hmPtqt)c293LZ>LjTOb%5= zLVt){IknuNDWi&QyTI1SKO}KP0n6c+eqH+laJXl4ai6b$$ltrz_~A9}FIy)8Qp`vp zt?XKB{+d~QinN|HiA1ktFW24R$t!x zxBJ95QsB+B;O0E?F6_cJ+Q ze|%Oi-H!Y?JKfQN1S*Ulu(*Rdn>>{xWISB@M_*;YedU$sr16|AeuMq)BBm;o zHLFCQUo22^G(n!u>XD|9LRHJ`$6%^S+q3rRFT&`h^g$qOXbx8Y++Gr6%`Ky-E{%lw zTC_IQyooX%`Y?Zq16cY|NMdpInSGsi=Nly}xjR2oi>-%;Tu9)T^s<;rkl*P+Gq{aU zy_lVE2D15RMxeCDT|67*rvr@1E*gR69KnoWm3%qDk|bx|I{JUm#~vKm&J7hc44Z1I zafM@N(G9JHd5xv9d8i*pnZ@KR?YqrIpwP68ROGL5rN-0vAni+Zt-p1EW1=+eBV3N0?{rOA_B7VhX1UG%uP zByp>LpTWWkVX=F0Y;}n%0SpB>|(grD3SOdBs%SMw@^qT&Ri@-DkA?Je1Wf6+$4+cfsyVdWqvcd4Ko?cxW_GxvZ zT-c>~V`5<5^jO|;{Dalg^kG0$RHD57u9IJ>me!xh`FOjvqUFn>4b5cDx#O)0vkxyU zpm8*LL#!fkh0{=)cQ za4Z9XV_p7Pa^HwEi*rr!){Vl)J9gAUG|&P&4JLK@T>}1Ue^Qo*!<)%uDk z1N;-;JsHnj?acEmr|LwNHB^u~wFT=Q&pzLWA~wu`1H z+74Cse#vX^&C@`;n5d2@b;>#ah!PQTmgF+6^%T0gev4jYWXD?mM52d-%#d-WzplO1 zr5G)D(Oh2Rhxqg4pWd>~`E1>eyU+fjJu&?A3#~(Xa#KgpI`R0~c+pvY{)Zp-L^tCD z(Z2wIAXuySqkmk|KUmH0mqX!|Z&Ab&q3#_#8UYI$&K7T^m*fO~8P}?W&ese$!BR^` zN*3N#nxx$>HQGO(h>Z;KL8HDRk$HI(7>tAN~&Pr?>0Y%fB1y+@nKc@nZMYkh`mr^hc%nBa=0Z!6Pa zZG_3~McsE{*R-+53*nA!Sm?tNCc7A5-WoGod7lc&t{xF2VY&KZL-{(=?o@qEHlZ_f z4jMOO;Ah?i7|$#9n3j500LbHTInLBh4z;$CtB)y8-jKcMwHkNBJgT=_hsNtM=utUw zROOC~Rabu6$5X>IPKEEbnH7FvUq2uCID4&kalg;@4;)NeMNb9^t_+Ul7`zh*V(uLmRq9D z%JG1gnxlEja`rx@?VU;1D0~-@`K^_v9hc8ZZuIh~JqldQh=za1{ryBCC{rL7Yx`{* zQOXP_9H8KCvhMPZ?sX1-pged{en=*ZZ1{^ON6D4%%Je0&FxO*ops3yO%CJm|vY9>X zv3q4)(aEoKtB!^si1IKtFK4nr-+b~`er?brZ#HH(^vGiVBc=S6xLrK<8L^0{)O{*} z%f?V&=8W^2l?sfSqnt(jEruE#_-i4`<^BKh;20XXJR%esjI$(`wytFe4hkyVvF?5u z4FtgVd9|UZwA-BMtp?9}!cHsiZh4kJvYJ5-o~M}hMs+woHNZ`Sp*#J)b*6eOUnIMF zY-QVNh%OsqAxd3udZ57BIngO6UE=BPZkNcILDMI|6iP^$_tjRks?DHO@U4A7bZP%s(u_<<22l_ zBW^!5pnNMVG&H8vwx8n@ciCV2dLEuZniNY`i%!lo?mc$QO~!NLNw_>#3~tqXQrp&9GfJ6mfiB8Lu=`r#=~ z8Wd*O3{{_gtfxbRc$UZD%b5xs(t}merfGx~Y4>4UikNGVkWNHW+wkoBy&Y=7hFcZzAS9l_Z--tu$df4&}^mkGf zM06ywgSn_c*&cp-b>9{G^4fTvFT1M+9p~fb)J%>_d-H&N8&~}zv4ed-(T;4o_h6+? zhAMg771Z*py|Gwx%V-WHyhV${clesj?0)E<&WRk5H^c_;>?~5wSNazWMXk^F3R%pom)i8Qeh#tmm>a0bXB60f5AR6!sr`ba z^UTQ#$MIqk#UWY|)wUG56b6mY2a%&rMb5PNg5rDRQU7QE$&kzGlqzM4kLrQn!7EU^ zo<8dY<Xh8n3;Zpi52(@ZB)@-Fl?Wb)YhMxv%j3J~paJ)6U9s>U7^SCv#ulL@J;ap@Ik*{e*@&0|ESb12>;qcIt%PqoF4+;V3)GF`qX!3 zpY4%rNB3GU*Lu25bv%72lckqUZ8WlZCS2jyLH{EkJm_y6saLm-;YuRmTs|S<+P9=K z{{|8k9_t@0GDF#*^(#VDow|3AV4BO4*$8WH3<#$W!4sKbr0r_^*P&^5?9<2_zpTfG zzdrma`0!-O(v`>lb+RBP>~J|Xky`S&9okN`;X?p9ZYKW$DKkI9U$+i`0WlF!1bI$C)vs;k|^V{!K#BqNlO0c=oljFJ^xkowf>n#d zGWJ8&6}mZwdg)J}J_8n}H$hSU_bcNOd=lnwa6Onz^@%U8K)|H*T+Dg}y-q{UWAG`e zH$i~aG@fj-d}V};^t?nk&rfvc3oQYY%Wh|M;$GfpiwEp)PwI*5!Vn=5dgt|(o84>| z-USPNm=W?=`JorKQ4art$0j1yWNAi7?C(+kdU`B7i2Vz+b6>D33JQ=frI$Y`!o(tS ztDgL{;Ug`v0s=Db%lTv0aRG`-QVaC#2Ywkq&;>A}^=!gQ7`D6ka3gzmIuF)$Z7Ni= z1_(q&;AbYZG$jcChx^%Tim5;O%2bzgO>$e7##%X%JQJ9+@>~Z{Z)nk99BWZSVzi@P z%M0%d-9N@>*hf(#kA|j&kB3bltNd?5)?XSfKTc6NNXCu62|Eob2$ZQ z`R<_)9%Y#1uD&**Nh@WSIGwm3zMcf!_s~T3r|c#~_}p$M58(JO<)?~xZCErvX1QnU z-n%%qI2OU)Jb1M4-ahnI@}?BII_b3!!a~DJW^zuG=U-JNBEJgb18+Pc_db~Hw=?=< zV^uH1<>uCeFRV}SZHcEX;22`+p#yIem~KB4lp!zw42Y2wkm?2DUHSidZ2UJ2*?k#@ z?UP6SM!{0eF)u)#CjY}IQ#2-b<~ezN~Hu_H=wIq`aA0b0fn zY?L~*CXPmG$%Z=w2K>DA$G$S!!z8wum?U|*JCcYdz$yGoDy|}K!MgBp63c^!a|+l@ z#0!Ws!las*hOz-x$k{y$PC50u?(FtSy*xV;w~_la=CJj?v(!XN?&i-r)t95ZFVo!8XRE@79-pQxf>6>n^gQCa29<^=J)L`gRt%_ly;s|^T#AVc2% z-WBReBzXP(V81SF!sRzsVzla?C{LU+^Nr#+F+{2%d3C#$!#jG!mo4sS{58cUG`|&c zA8G^v)QLA@3y@HTELz5@T!#mZk_TKR7ZiRHBAbY}B_u;w+|RAe3bkD0ij3PYG$Z(o z!~$>jcFO&{Vtm+7CYV$FzylZSkp>+gu{Fox9tNK{t+P8&C4RtNJ`!?OdFZn-Ct0AO z?1^WEd7w>y?I7XX=tJa1A`oNh*f%Ni-xp~_-tUg%q~SWbrB&^+=Xl(WQGgUW1SMAW zb97d3LZD@6{#q%zLU-J<+-l*s^bFh&OXSqS-p4+3ND6M;nHQ@~r$ItW|iH(|~nfqt&ck z&^uLiuGZ4!=B>_`_I1%`u1}n$fF~b7JH3c(8{cb&q=*ZP9Imb*JG1M*sD0r*{{_EIv_qG;4TOjHN@{XRq!_h8uJJ)=&hnS8I6Ea&>v zd*-pOsn4apRrR$no+Ind-cE$9m;1)+(O!t@oTFbxxxiSt=A`GfkuNlxA=?QPGx!K& zpGo3A9&>o6Q40h?ja@-hf7tUjkm$bZl@Ak40w)BW|48t~4Uf4B6(RE`?AF?B@>90O$mkxh z8D`RMdqFWd-5KdjShe4AdGpl>_wjOtWUQ_)9_D+a!gMpm@)Pj zd0v9H0#L2^^HtR$P~b$Nv0nvknh2^eTgnDvLOj2V1-Mw|BWDYEDFtiawpO zuBuZwura$fOnH0~=TMUlTKrYO@3}_2YNgYZ`Ox-;pm*L*PM9NdT6YmYmETUbgJ{S9 zjFv(OT+aM>V&|w8YV++P#CkI)06^WSJGbhJ-+Z*&)&ipYEIxpbS({y?C| z^N;bpn#HGlC{s>V=~?z!q-WaqR_Zy%tG3V`H+dQtc527b-vJnU8;)Zcj{=KdS-m8m zrr|d<4`cK*>EE8)njd=^=E~_k^zg*tK`T1>1D=fi)S# z3aMV5E+RXXR*@1N1GD!ryMFaduOm|e(x&G~H>=#%e_o-Ly{g1|GyNpkCSJ7A^$#bZ zenlKJ_lXp7Y-*a|1e(b||L5Ca$*zFm!Pzv>+O<$Vk#I;K@Lc-12z=}&aPSn}Y2o7} zBLJRTS(l#8gLS6Vqq+Yl${kUK+cDgT7L>#Z~e_PD3MJEc)8}(BALg$_6>? z6D;TA8E8$zCQM1NWJYF$1yONSEpDKuv!Y1%6nUyBt5f!uoRUo4SC_OQD77@`$~GOo z{QZ3$oY|hm>u^`}WzUfVS!OPgT^Ut;ldQ3I#~6O*o~rmq`0`J}I91gZ)5e`0-KGmlxi% zS5|S?x_rgVz&zUsUXBP4m^q;oW>k9fI4&EP?<$@W3vIG*o#(xA(XL{S6)iuzt-Br* z?QHcoEpq2(*Uu%3I98XYRSkVi6m1;}Qa5vDZID71IH+0Kv9>~m(2!2V#!>*6FXbdH z$C~dk-q8I7J%AG)zVkh#bC5TpBk72&LG>l4D~F+)f)PMfE(Ok&3ccf9b*6rOpMaq~ zir5-*v`g}V|D5qYGzv_&Ouls1Q4}#n=Ebt)F7K?;ovX#b^dcS)6Sx=jle^KnvvRpr zAAJ39YqbaA-d^kh6|gbih%MP%H0FT#5WnU2%_!|4*n58P_kzuqRLcyf9h(q+emd+S zFs@NnRE}OfHrw9{d>G4T4$?GS0+{ZkOXNOTGTfq zwCh(sIk1l|ZZNPE?xeZ4c@4cf9anCuX6=GkO%Ti3dX_3F>otBZqUG7<)>4xqX|pEf ztv3ee)AtF*9T4(#q1DwoAi3Yqm;9u5y1$6kK+hL(`&~~1n2k)7oe+ea0h$tI%q;%s zChC8X6E6GjM?~+*nRjG5Xr?<~iKf2fB+TAcr0kuB!mjt4Y2A99j7KToFu|szhC|<9 zq>kq~_V<}$&w-D8*sLUU1cMgzlUGhRuC$5cHK-H`@K7UQ$(TTquC+Z?H#03zP!3~a zTzcLgmxtO)Jbl}In%Sqk`{(=!;E87iL*qTad}-6F-q9Zd7Vh*sEmVO8wpX zqXY2_Ojn4yqWG}yops%EL@d{@)Ilz z0Fuj_m338bfen@8Jl-Q5&v@vrD5E%uY6Q2WBYn{ug*1ix#){0A)KQc3a){vhpz@r4 zupyOEFx$=j`w#Xc88-NIA+b_`2na^F*!N(Xzo6lFAn{LlP6k;_SAs;;!AUnH0_>P zy1*pM87^rq5oQE1Uela6vajVr()a-S)ubpjWO7Gy>gQCy!l2f;YB?(%;ed{-w)a<${abzr6_i`jP;XN|U}m=0A?pEki|TC+K8z(E2VH&RmBxXx)RqU)uJ z(dOzD4(66bfhAHaBV4{M8V6P14YNga56GG88d%CQcR*cz(5hd*iIfIX5HyFw4u$jP z%WZuSUd#`y&Gli{&o#85*#PEX2ab~;g3OUKU-g$5&$MQq>@Bg5^_1|cOq6h7tv=Xf ziOAmxI7sQEf3)$hI-_F%KbX1c(4(ta^PxppEs`VTT%v^bvV>sP-{hWH|01NH#r_p1 zxWM_mu{%pkhQ1r>Th*w^wwjv2BuRDs*Rf3k`;>P!*X!7+X3Rti-NJwx46X|A&c4_R z(7Dmbujckdo1@SD!}`**eI?mS^h6pLespDx!7{rT2;2IX66`9~IG!9>&eC}n0n4OR znTv3VlphW5yML^6_^#}LUWIhAI`m7}^jSVev@xUK&g|}v1;Al3 zQ=s@+W4 zJMN{SiMg)2`f~2enWMn@gMlvQ2UcGzKDfh|pZ!>}H{d7CQqLD!_uGGH^lo5cqPP}Q zhsRvU$La2DP66q?>e)}gj~at8Hv-J*SycC-D;i6kgd-|5~g14VThMmSI`zga;k3xo_1(9YE)^nozk8Qw^BbSA=orJ3~ z#o|UD7sVnTDdVOfeVKmng(>yNBq_p|j$tJpn8!9xp~w6W==z(|z+;OsNoPD++gkBGuYkBMq8jvGp>ZjhF;N#X@jk%Vn@tJVTNMk@QWNv~1Dz zDGdl6xM$W>w>k9?P!xyx5icSH9TL6S)!7O;q3_kWN9($)%D|buuP}eRChopM5%TlL zXt$$esahmw)#8-AG0J0RH;T_l|MK(~jVJ>dVJ1#1k2Z$lNkTyX+Wi9dOYA(|I6>X{9s; z>O8UHABKrdz81940pd5On87p_uR3iBcWO)dw7I2J0!t+|6$Il+uRbL&8}&+Pxu z=CJo9VD1npQ?U4whWH>`Uun&yUpBR+I)s`M>0Rqvwo_!;m?Mc$xM1CE@VQ?8BDGxBUIy)6&&nYO1V{AeQ`y zvZJiyXu!7Z?&)tuJc>`4*IDy$Ly!v?0(v!iXc6B>0TwqElYOG^F z0CMZDd-&m7?EZ24dj}`5NO>?oBlW>X+xJ?J`RF&}dn>L7j|O=q+(tYf3j(~%UxbGK zB^~`fS^2YYS(WpF-FSK&u*1iy%YjW?UkJCX^8gg6R8~nT8RU|WXN0X_x(TSLu zvKs@q10TNK2fx@{#i39a*%u%14PZj?3#ckQp5rhqi*&l8biL>kYvR~yFvm;xIFC8< z!SJs@UvYh4$Lzdt25kFG=>Cq54t`B{;ngM6C2DTnrd?3DL=&a7SE3dlo@fVAaeXTY zK^W|B+2}TU)HZ8XEtZi?MwoIGA&G?ItbNVG16!c|yqWP%9s9*2p^oA$2|4UYk_B28;f&mK|40_qWh(R@;=L)WEdptYJs``&Gu zZ}eg|mUZZJy7&Ky6o?U%9;f<%$d8OYHM~cZ4*-{v3ciem%ovetYgtq`J$e-%ey-MC z*DP%q!iet}E*_)`1fxgWbgHRjTiUhoeYN4gBvq3ywB4R*oI zNa}l6T3VC7iN04+e!}V}M_&@%2)~^xU`sDh(S|ut!=p#jZj78AKRZDOq zfplBYVTEN*Q04fCQ8U>$*BHULt>=V}Vf;z`GO78@v&j2gX9j3x?_FghQ{@2h zZAMGRCB&_cJVY{dQ`;>(g%ER@+a--}vq0DxmVGP)oebJ}BK)lsU4^85vUF)c(2xBe zCN60@`ddwS!o0ghXm9QN4D?TR1KgDqQSGSe4ZL^%(`&}VFKRvMo#+Yec#<2=#h%tp&wU2 zz4)u(a2?L(wY1doe<_ZbGjKUg!g~2MF9xZXd#jZ+c#enxn@lMz3s!@rhxgR2eZ)UI3h)cLH1W8?i()>JB77#L!A&zgMt@rwYSAxX$)jgsrH!d`>Y6iQ3&>YVWsx z^N_TRqWtiPX4f*{(LPDhH$2xD9&o6b`E;m`mBm{wb4P^13_{H$44%ZSc?l%|} zBq4%|oc&VB3BiWTuFyPV0F(5P@DXH&6YzV<8jLEydaTYlce_FH>8u9jl-K)E#=^I% zE7=+u&e31~p}E1?5uiXPsy^l6Iv*fro$Vn@7ohrI>aF4V*V2-YLP;H*}w6vsT zU-A%St2%)}J`v-&^HF>eQo$8%9@V>*>PmZn)y+%$`2%y((~UbiM1O1~)Ob|2)R>Jf zZ}I5nPjHvjS)lYJrfLf!!3g8oI%vDqL1qJ-cA9)V*%quB#FuZq?0kGe zRG~)sqN~;#?ZHi^iTBJtQKHIXu8mGPT5oT!wWCGpcA$Hb6Gjz99e$qIr*(XyJxPjl zBKM5J#V`KKcEJ`fl%d$lY9?Jh3L>2S%SP7%|Td z`jeFZMI$Y6S63Bys4tv1qXYbB_}y3nr;w@6Yr4+7J7Sdh2|A(x)>} zzv53{d?tKG(iPf_Ju-|IN)z1+OrZx8`)H4&BLd*qDg2%pV%Al$TnAj3tF9Jh&zLkS z@wQuzy6n8r9!#2?;jTjVO6vz*G}tN|_Mti6oz&H+B+?#^d&pM?yeXi9in;0}=;vfW zF?)5{KM+0qUdi9J^N~O`_V~Hw+Zuf_%J5e&j+YzE9Pt~_gUZI8DD~|Ql=#LT(Azz( zP>vX`@hFSjsl?2Lewu6fI`S24TnrSN&x+uc!TRP<#rG){3)r#tSc|M6INOkN)*E&m zEPfDf2L`^uFRCNJofZdS>ObNeanEH5bw988d=R55n7ANINuvE>|! zaLd#3p+;!xB-xw-rofIAs7eLsiGL&%b>J5P{%u*))Ct86hIV z_v|cYR!!|m%n? zNklHjdY{g-XyD~COrXfds3=GI3|dUP1DUk)F4x$Yzkq5{GYry6nB2_#<0i>|AY$ls za7qr%de66I8X+g$Gr4)W6<4V8u3Gr)FhJa`wqOb})x%)F7cMxXT|Vl;kim;ot}>>7 zj1j3xGx--rzZ#fCCWgOHX)zex@y4%qcvg{oSXLfPD*xD*FD z=^Q`I3=`fFkhZE0>b`x&8E|j3_Vn;OzKw8z*03R3W6nB>P=x-Mw;~gR%Wtl8)2g1N z_|&9Qd3*ubhvpzRflLhmN(A5$j)yd47-4`w^XsR=kpy{X3Q;gKDowuL#Eig%v-f{j z-O)M<>6qm04_4%;WN;rI>5KECu6{Bv0wo4uG^EE8B=p3-HbU}X@1srVRgxK4+?9y% z0ilq3RZh68eM7{GZ4D`=0lvP`p2Q3zOJkv;2yGrD)J?0i{j&?OwGMkGh^pW{OoE6) zM&>Z#@{Nvc0}hM`tWWjZUvo2X4S)FxE0_gH2$A(%xP!7}`ZsnV!$`rAUlD(H1QZ!szLJc zBTNF-*4}`<6!yj8-OzPMzX5BFOje?}_0B>eoE1rdXg^i1$hG!9>XfGDdb1kh7a3)< zvJ>k>;8_HT?B6=SpuhG$B$83eRr1BJP7};ocn)MzWI&n$EhgXF1-U}l1 z-Fu+6+e-k(>R@umSRiNeWAMB!fCF=R7@#n41jxQo!1hvyk7~Rj>bM2W>6r-#E@p3= z5SnIS7)~+y7g%9nL~xd|94Ie{9)6*j^rI5evqhK_>Y zhj-BV-CXlOAS!P*p|;PDitQhMh{sRpB{-PDsj0gLP|FZNG{a-bEcEa5AbW{ur1SWs zI2xOZPRyL43p{`S7zLUYDa2*S?LXm@j}VA_I6x}nrGSq8jX&wV>6r`hKWb+{H?o>^1mImd5ealZXo*BE z|BF;G;_y*M#GVFoW?)8}!WCib`~Ln3=m}_U+h-HL0fY6`AX*=V2ETCFmF!PND*1bCa=tvXKEX1k~RsE);V9B!3 z=^tFF&e+v-PmfUg4O;7(1G!7*F}(cYh8I@9o#3A*#3BLs!?9wa%=4_LVDfu20MR4# zV&`B?O!8zdF!6PVyw*&N!tCU$!SX2))c#Zx{LUj~$d|$@b>KT5{^eltn*1;xCr6RP zw~}FfktBbJ1yK{u1Aa2zvIF<`p;;gLA_aes$UAl4kFqZ!XcQqoJ-gw%`lo#MM=Wn4 zj2%1SmOBceuyb?&Gbqm%-WpMeJ9R|?zlk6cbqGJsFeAip-!3)wKR7YtbJ`-rtJXct?TYh3_dc@JJiCASWFljT%zAreYRWJ#hf{0Dls33U3Xb zw*UL{fv-TE!gz0*J-IxVoBM3C*Q!Bpu~nqg^cOztwYjc>Na@w}YlZGBH>aSH1pgz0 z;t$C9*X{5MK%+_~W0`xnpNP{CG+xRAAf}6I#k{w?_x6u#z3YSDZk=Gg=8JFo^+A8= znrTJDaI83;pK}oEje>%4_3V#_f){Hza64dp{EKI1eki7gVfAz5Bs9Rc!y@NpPO0Gf zZ%YppTqI$>c%f;ZLQ*}_2w~e-91cV>RUjek7fR&Td#MUQUyQ zi4+h?WJfCBA=)j3PJXra5`yTPEh3uPzmAox5S{q8eA7?>LM9i6&7HSEQFwFg0*h3( z69Xz7td5N1csx)f|E)vK$YnQ)&xg_9*v_ZlA*Pi!&#xJVVF^gjN1|IzjYBJ68B&sc#9C3JtMqp3NDag$f zN4)2;(-`<&(EfJJ9hCB^&u`IL8oE=rz>Kk`{$Q%=IoM@JZ2mF+qinLA&t-m- zTDDjLlf&*p!|jY@%U&7xe!rk6+D62M(*K3o>$|15WAlu|0l5KDX#IH*69i=i;3?mu z8o2)BlK1&3!FShU!v`s13w|*bfjm~!y*y3O(ueg^J>0e%P_R)YDq@^~xX%Kx9#K~0 z**~8;4S3)(UDtzm;m~Ty&M#bh-xwP5z;aJsNEaSfitYmUhI}H-X~b)ZTR1@=po%6^*(dLL4y_b)@Fp$jkDEiK1Gu9pz9PAcm1k!VcMTno2AYWyrY-JKNl9OcN-hVht@vUt^zx}DuB z&r=p>fvJ!wfM&=_daG7@9tWNU#v|yO`IKL_H(1)}Qhit70}3wz1q;GX;oRr7Z{@8P zm_}qG!*3h=ukV3$(V<;T1H+k7YItNvO8Oowxa19?7eA&-0*tLl2STaFJi`ApDB{4b z=n(%wKT$|Fpwp(~BDfwhG^?JIuAhwJ=CU9DgpM-|Ytsu&k*3x0S}iR~rnb1l?ExWI z$KL$=__aEiZm}G7-%9alYW{8MoOIf=xCI+(n9z2Qm&JOZ}+8cgb!};^IVLCH=q5GGY&&^`_^6 zZAisruG1Tht~l|XpFN-`138ydBz{x(%Dz>WK1bCl52;)xJ>p?+T>*0#Gm;Jb^Rg!B z?R%1jtNGMrhlwRfB8%BjRrA{?L6DV@_JF2@+sa3X?7^XWs0VpEz%L4WbcFC9$3}!% zN1Em2?4s`C@xM$^bU;(4;iL&bc)a(#_bebDDjdlM2{2p1q>Koh#9u}=XNrZxB z8p6W$N^ypfguWU^u<8=w{uH(=6%SRp=3QKQpon(yvZLnGQHlZQOjop&*nR(myIie* zH4}4EM#aTcJdqXQc}R}udH}i76)n0CgzlHDRa^6c*MB^wBLgN-bBL||L!Ad3-Uvl_0_x1xLn2rHq#G|=eB~Zl#)}waBks`n$5nLBI z3}A%AE2A=gGuy6AZNH`7^?ecO_Gr6RkX04arc<#XP5 ztLHRvZlU%tvWiRp)swDQmj}fpY~LN}g?MFAM3YAlLl!4<@R%l|Lgk`cslSMRp*kW4RnV- zOP1LEVN_rv;N~iwHp2R#V6v#t$rHH^c|i1_SO;Kj4qtMCWnqg6WNY%;ww*C6aKu19e z(!~{}(3%iGIb2sa0A$NWAP}_x$6>!VyYnBtR6ufIP3Mj?0?do>^QG+QtKrfj49q`l zDS#e9Xn38gS-Xq?C&=W}gHvqt4qGm>Y(BM~ zi%Xj;Lbt!X8nsRQl8Z~NyH*xI{4DAE!h4D$jQ{VxM^-3{lYk*XKGyp)52lB}jjqK| zZTz24^rt~}^e#Sv@^9Iy^yz!Z=n|_YAYmuc2{lyX!OqRi{V%oCMa*E!xej2(1La@B z(LtiAo;yDl+Wge4nM8MX)`nM8n4qEJJpbkDlgQ+s(Y7R`AZkADSOL=*NG&!)*e=rQ z%mu$k2lNby9=4(18Yo+{?qTC%KGEriC6P1(-b8SV(-ad&6SWT(P?P`ejO{;2050GxMVH0goUJ-Btb3XTQu@LFuc|W!8|uz9UrWzVbug*wsYj zP-XocVBxKL#dG@Yz(Ck==x=QS!lXF>q}+5vQ!uNx8$c$l%$?pUhR*IT=Q;1x^eS9F zxbvaa2?*_6+OOW^~=DT3d4Kidq7H5)^Y)fcPKzA?I_VIQr_I=8xgk#@$`F&-`x4Nuf!=Cz!<% zZxa3OexbN2qwp6~|M6tMFls(>;H$LhKIDpc+}7#zH$O+-SXlX%dVk6Ej^xye`2dws ze`3m4PUM2;X@SKEH&;ShIs$wS?}Hnt`ugG|AGl{+KHihEH}CUnjEa+TZU|q#)YYxm z_YNTdU#Ca4obIFX+huZ`at~(8Fc;|7`gaMKS?&na@>{Qu2YMxj@=J0O`I%XD(msl#O&gR$-Gr((;NbUfQP9L%n#dPh+}xRNE!0iD7O?XZmXOp zR1UB^hosN(KYbt5wNiVyQ0k-Hnq%NB<=DMgyu(t}^Saxc2FhWVYwngkCGQiSu&+8c zyt5{%;Bd+YWUv9Ti#`6r#+Y>|MhzaiWWEA)%0V18LXr7*SoRYJ4^<6Mp2YaoQ$oR> zlnrK;!0wckfEIGUvoN9|%5kiv8TMxK>_2z}qg>rymC64!BQV?u-+#lQ1Y0H^=+X zw&^KDGAGeC{>=L1*GfD-3vX%8JbmRs(F5Hl9vwdMo$=d%0E^y3+gL$IMz&t|^|z~_ zr@d>}U}{v!+KLJ!Bwvl@z6Yv=C=9*GUfhOIo(no_zHr;Vgyq*Tq~U$g5}n_miIWdZ zZW@CwC$W?-X;m&(DPlG~*xPw$;_LK7reBk{4NmBvxxF^Z1&o!QEq1i$z7+}6KV9iZwOUF}{M^$Wjgq2!IO`Jw(ugI(>!Ak9yx?Igb zj-MH>Gp#i)WP5Oq$w(|rUFRYZ9ER|G=#x3`_DBr!>5St0poUP>&OA0JxGcAgif;j2l$g2 zBJkb1cGC6<<@sh~i`&sB7_wN%6l-@@`^O;7T%hMXKQyR3MWR&mPt@W9hxl4nahVh( zNeso!{XVpF5NM}n)8o!N3y@O?3W`9~7ABV49$2_|+3oEYD3KfY)_ANr+e*+-;PQd8 zx0AjHxd2X>tM2%+I4m@O4l~1d&S{$0ps=xta%&V4AIPKz!E~poAL2BSpC9kDsZVSDld;5_AIAK1Koz zhA-}!0+P{0uja?KP|;&h9zkg@uKWjDW6!}+J)CM3di9CzXA>?g0H-1H7q1;PFANNb z5x$`rCwT3_4o!Yg*5CHrzg;VIARk@mM9YCRR*8c;W+~$K$uoT=JZ&3l!S%Z^0>5a*sp4qx(gj9?&ku%-EIn#p**1U#|F8URhiF# zUS&kD<()=ty#x++PM~zo6jb1-2RDAe{2?KSZ%Q5-K9*5GAobS-DMs_jPibOd!$A(YRT4;VnsNve zA0&GpIkbNc3|h`HER7PrEKh=7*o3H>)uZ(%foc+3a_* zQ=j4c=1wbfM{A1V?mhwFFiCdCXJ+JhI-nHYDGaNlSIq{;;o^Px{lecOL z>};AFg28rb5DY#86y^9W{*=tMO{de==xL5|QMXKv;{bUC=Hlnuk|fn)cnypdDnlzQ zbfco1R=m!DJDXWk=Z>CcHT2R--@Ezc<#B<#2ID(nE#C#YvnXFYp1bTPORD$WHDc-i z;q1HPv0mf9xw{oonMtx&MnqAzY(gnpMrLL2J#HhEEeRn}DSL!$l0BmAy~S;16Q0kv z(>c#`&hPd7@jS1W*U`ywpZmVP*Y&wR<2{mPJ3orDg7g*3V%?Rjv-N0w=UQ?p4$A ze-)AvH8&x?>iaQLqobeJTwKSDUScKP2is732%x963U%J~X0w0o8adXh!10Imj7I3r zzL+~Sux9yaqm$u;uOWZC@tKJjRUXFp;OxOd8mr!h$J(R}C&7;+AinWD2R%diSPpfB z{3Fl_5N7N?SFF6wJu(Rxecu%TYJ>)=D^vyTEb84NoeROBh<0%ABTd+o4Eu5b9rR5J z$~4bS0%!#i=b-^pwz>IIv9pXxWh%~~IJb3}#<>qmWGEd}v3Ns(UbjLJNJ2Im=%Foy&bSk|{yX~B=qGqZVrSz!NT+-|A z^mQTi%&!@}2#pD3BsCyeC6miO((f=o`zq|RMjt?H8 zOALHz-tKxQv2W~s7=Cu{=gp5c2*0(yDatO~sc3h33K@zlfKvV0;11rWGO>5<@v$}u zeUf|Uef*-zs^RW7R0BW@Q!d+O1OK*lhxU(2se_4nZ)n};86Ngld_pQf+Wpj%Hk*QJ z<#In9a@s_<`@a@^zU zMS>9(AnEWzXpo$|KmQHFBMwf1?eojC^d!cF?G*_tEx)dv1$iuFEO=lTdk2UqyyV znWH*aw?qX1EOb(aeIBq^qY?WPilp0bwjl}CX8!?}6Oo`g@le3SAkZbObeXpKHj_e* zR(_oLzYQ~q@6X=DxC);c*9K;m zR{Tq!&#-vyZTwtv8EZzIR~*xFLr^(SKZ0k+LbSPo!0Y`hX79e4nz_8>W#udKcr^4j zOavTL_-sZ+;vYI|_FT1D9zK^lF}&U=rU4nRG>;{!`+?eztl7;ERa@Wsg7@}RBl;3A z&TyPE7kEm)$v=1do6D(SGJ`vxbiWv7&wcDOh~Tj(82c887D^6?4!3*d);_`he*UGf zi}ibr%Cg1cmi96&HPhvE6|hUlUtfcO*czECN4h8SRnRGVx#fGO2&G*7 z+7HR&UN&9dPNQ z#45V^bep12xL3ahk{&2;c;8|d!|3_Ed(@W$+GBQ;^&!PRDm3kF7V-xpkM6&2T&xT9 zKG+Kpo7?o@9bu4YOnVxb6iBd4GMXf`q3RRi6*34j8kGoRALL4t3j3qG_)o0+M_-d( z244-d`_2RP5UB4S9Exa;kGyoRGk?9JyW_&As~r8Or7?jF(tU&-_zaf#9ztaEd zitIOzVjjRUHhi2G0g%1K$M=i@gbW;*4Cz#o@5XEtmk@Y26KUC4tLbZs54Ne2vXTnx zk46UJ+Z*>-)zbip46jLzz{e6qO?lrTDed`n>4BTo9EM<}+D6<${zGxbHBcn;jk?~Ugj$i&m2^fv+fSX*w82YxG$oc zJ>Nd=%1ZXB^>IB@KMQGS-kGZ>TQ-D2#ICy^kLakgg6Hd;?hP`A1IPeguW(K(=AIWE zJvd0@uIQmTu4a^L7_bBEvCp!z2ETs1+uFYvdw7Ocq6WQNmvg`B?2*Ee&bMoL!t8Ejt2P$#vaOqPtx zx0aT;2>@FVGr(H?F5L>W9vM3D*YaDT;l*0_DHjSCFBaK93X=cScx%v5h)6l(!T=P} zW!@ovn2QY+XVNu2+6b0lIk?8f?dJzde0=Wk%T=;VOB_{Me6Vy5`wGN6 z&Hbj|w8~ng8cK{_W2$`-VUch#w#S{1F=8)72A0Ru<}QUfW}iIC zZwe}6tiWE3xb@lk7pbFozbo9*$8M#qsxT=dM01vOlGmZodP@_vT-0^|C%hZzsJCladuU zS{|~INnuNHR0SHKX?9We9Fy`hPYTej>jjoC3jj~-pAM*Rvyfpko(;q*m^vwg7( z1#9GA@o{h|NI9r$Rq2wRk@_*A%B51|@R`qG=0F4B2?eK)oz2%shURYNDt1ZfxqlT0A_vZ1jgFInc*^+B8V;(7FUbKUL?o9Tzu_99B zZCIJC`J3ISiF=&sv2ed3yQ`G{PKhiwoKyMARM-6UM+5ifSedz-Giw>(WZWl0bNwj} zBwws>b33)?&#J5H1^$$jWps(=dkLnNo&~P?6hkMdNrn`R+b2)x^=Ec+Qsg^D`ET8< z-nNUDZR?YtIkPbABf*x$YM_Pz|PE?1}>1RXRvfVCsG??Q0_@1&k-uMDr zF{yP+6>m#ex!!5JX*(?}AZ6fL*Zh1!QI)b8ukV)L-8fDCC}=$l2*n1*sLj%edsmpW zyif)9J7ZV5bMuf{pT8d-Vf}7}MzQ$8Mhp?ZNQD9jK;LG(JcIXgc5!0=YWx%9n?rptAyOU+5NUi? z6o=Y_fa+&Y>cVSil{$~_krJReMt!P9j>e93;^+C9o_}F!l|em~rgfj%AxtMf8eUF#2kiNu;X`9pFj{#+xSXJ- zrg48jp{(#jOLKnAHPpTuldQ!)2T?g2#angkCUawnK-aj_b`N+m4kW1#w+c1_(&^=5 zTFc1Evs{s#)jM?uKb;z{jk>0ZSiX+tq;r|rDGR0L&M@?xsKeXsm8Nlt=Js2B@Y-+D z2uBZ}zYo6y?5qpt&a>Z3dtx!|xP)i`BSLTjlBIV?zfEDa3#VbM%>7VjJ>z;dNh`mO zw`Vi-AtML}0U47*^m z`}Scb?C;lm`sSrvbOORkkj*|K7@-wBL!u{u#+sQhHbCnG4tHOZ0VLWc{q@*agC+h0DdS zo@J$mzdp)Ps|eblo!Oq06_y%;T6FQ)pby(agUxLzp9se}|JqrPEGV3Y=E7sxa^x;0 zc1eF2EB5lZr+u%Fq+jpi^;OX0D^D4}_@Ang{xs|gJKqUD^qpv5+1Rfc6z6ee#D5W) zfa+1yh_#w{sO7>I?tS{9SKNQ?J_!T}cIJ8fD>|4;q?6tzVG?}3{^3EoedsmzG!h^C zUt>oDSw7Ufv!Mf{qY5l-Mh0^tTcT{Y@ z8t1y#9mSLYsXI2O~=$S?3~`tb6srChi{?j)WR$vWlrNT*A{h2)_dhXqpmP}N=UkBoiu&l z%1I$AZ9(S``jurBj+Z328fE3TpIyLs!2|vYepm`&=lPkRhD2v8Gzx>$+U`bSrwGMI z`)b9*!?)|QT)B5kaxz_Sl95z)Peth7@Ob=CGe(xN@4;mJ71ILyQo=TVWxA6_Ttm z+#`cdhj{v*^4y(03qiZdfJ|@E+n59JQ!nWBnv=z4N-gJM zwcR;Q$6Lou+$Hv-BJ)-5kGF}|1xik-0dEa_4tm7coRmgcy-xRK8i?0oAvb1ZltMx1 zBEzY-`cLQ*L&lCI=LQ&n;F%ELbm@pcs_p8Jv|Lr?$wgzh{AZL4`QN?GnLXAE_IK^H z>wiR4h{NIKKHF(f0e%&fx%^bE)p#33yT?y6rDYIJGFRsIf%v4a3mTbiM63Yc^{^cZ zNxGq2yo4~S^<$+ z#FA9eU8~zRVN&A(n?a`$!3V~E;|LC$Z}q6qg>{ur^XSNDy|rrJ;zkG;i46XwrY$Q< z)7JAX=L=-CB~6qEpr8Mm%Zijen~x_07fzT^;74Z|@&)FQr^vueR@r_}e_q38DL43*FK`L5aXRwJc> z(t1*F%!20uV`WGCvn=Tz-aHIjs2PUt&dagdHO+L?m|wTa!0LhcN#F7n^Ug9;br%|2 zOM0rWi3wh`lMcC~m3ngYfzYhGHCSY|I2d$_Q_Tn3ihkY$`^l#Jiynypu5YrZD3J@j z@Z-ajRBSFo?HK?=UZKJ{F-g|HHWGf9tI_Q`9qxNNC6YA)?Dgleg-@mKoS-x@2(k=s zBy2Iki*kfPcvTXDbKh0>l*qUx_D>=vBf01QL?nk=;|(W_J6pZXGqHxs*t8k9LU zW1lX@QS?V8jUK19m~-TlU}~QXui@`}A*Q#rGRE^lDKvzPXMXJbv@7~hTuRn;<3z+1 zM0%;W58X_R<+ct=HTG)JvoKRgY}>fGDYD@L$o02kHq{sNJkmZJR}sIeSw6|~oB@}V z{$F2X1h?VprMMX_(S$$#xy(5M7u}^_XlFT6#&2lFlt{$)-0)#5KoeRnw9nW5`BjV- ziv&hgL+*K>42qff6dJH7?=I3lIh2xZJkq%NoXakz`$T5HzeI{iMBr-!OMkRch(uIf z;F~D33>gW-pq&Q!LG;Vc1A2^tcb{7tp@I*ANP96Vo*PacNIb3~ec266VOF)+sM<*ht8>a+>Cmw229moCHpa zm{mf$+I`Ayn|pluOVk1P<=gFKKvv>XkkSZrg!o_crcdq+=Fj+h>6U9;djCdz{$`^} zlG1To8>`~GH!49s-dej-Rg4=$!?#mUKt|LVcQuNbFLIwV&sgO4EhDt^5YMnsBc~YV zAit3oc=yzaJ-GA_)7qyR!<(gm(b2HP-ynYRfhxKkrg8B%~+1u#G3GC{FR3^ zDHg?E!tY-JL9$!6@OxTLHpmDZdhWJK<0}QrU!>2Idg5XhL-(e@$36CYc1^gD{>q?w znV>R(^ubhw1r!!@Iar1%85CZAk2#igZ*`Yd@2yf)P(hQ36{ zHs$z*l8f?RA@E1b`yyXPu{d~JS~S}Flmw9yMZ|fGQ_;EAr?px2&9oz)%bnP#I*Bf@ zc-($kXpNTFz3tq<+V+@PVs$h(?tX<0fn#sa(K+M|5G$f;N&Dy8k}vIqre=B3Wo<>}39 z?HQ-tc4y4mvCE6rl?2#}pS%;WuhgZqGVuiRhm+_B%#&1^F~?VZ0!k#R8Jajc^iDy8vFOqjp=PZMw@fA{}3=fdwdY zoHH|kksIR`YPNPO^Jm3fLpg!X`zw{^hXNJKk$EWL20AjK)oTIyU&fwap|3P1#pUas zvRU<`waN1FC&0to3cz1;l|*06W0MYURQSF8e?0``pEcs}Ob@2MbTbo^qIPOLpw<62 z8&RLV-Wa`wnXkFhDZJKQ;{XQ}!bmbrn5@9BL6X!v*zogz#2rk!OX`C2Ld#j9fbu9-Fl zyU^)N3K}UUBkdm{4EoVA0+Bf}!HxEp_s+zL7%P#~oFa{7pqYi-0=vhuao zq+q2dGmgA!j<(Q&tfq_P~C}6BY&fbacam*#e~3#6>yCQZw)Y zQQl;d4ZYmwZ%VH@f8sIGpTHh01KY#cPR&gjo?w?sP0LJaICV34)DUZ^(Jj8%p^p1* zO}LI6;u?IlJK984_oH@yZd#9^Gu13 z&l((&DPZVvUf_|5EuvfZbM|A8^4y12;l9De8(K&GNI?wE&iW2?2 z_mZf*L;KZm>{^F!FbI#R)b7v=do>VIDZSk*2KofdYwpP~fm#jKp?JKw!P{FY%L|Dk zFR~+evND8iztR`BZc(8;J|91o4sn*1X%`CR_BDV{w}PLVas7d{(7+@N*o!$zbhDU! z=XUpWPN3r3g)^m(zIBSX!Uvnv=yf|mhU=bD5pKCG5c$PxsMsnRpfI1(od$`pxlYSm z$cVZfMo8h<0T%!8V~6^exsxZ@^QIgsCpgNd)}A)*I>z>rkoSu=uX$&{(>+7+uvqHf zAvjVE{{9vRQ~P455GuEFE8!WZW`}2!*3H5)v=GP%TNmhif$K9$JNf@|g8Q@D;n7m` zXJ>?$(dRKu2|=i)zluh{b`Q0xs~%>p4A~@FzD<1?pDoRan~0(597*mx>Zfe(oS;%i z2YiqFK3UNcymLQN;wlOLp}QJY8Zlrcdj(bARA< z!SZ5);2`pBFP_1zD)wf>A#kh8`409%I(L3^G+u&LKGrv`e8$+h#pfa=;b z?6w~28~ziaKqVHMUKwME1rBS@{1ajM$0o;8O2WQb9PF=Dn}(Sk$~A0(bZz42v}fvY zrAN_J-%1g)pG$Hf)L?(mQ}$8f@Qa-ik#4nj<;LBHj4(NF(X)ZbbFaKxpQUoJ)cJ6_Za!KI!rR##D#!Gc(hX+9?Oyc~9B2SwF> zbxIy!bjru0d-rM?{<+elkSqOyx;`01gDhg-rk$Q>76Sd#0Z%w|&Q8Re((K1Qpo!uV zWp%}HNuX2}=@SX%o9e1`XEJ&$dYeGEBW**mf9W<^&AL$C?yyy1q3*-3xW1s+vF?@4%^hVwXWKdbijA^qz8zN2*dVf12Yd)BU zLy~E64*;BA3&#rr;@Iyt1byJlRdafxd?Yis ztYza{GcFRj5l&}DSDN1byIYB7CJO+Wdz$SKZPpwV0AsPlwNXvXL9eb2g@{=9yPI>5 z%m=QGA#9U2uf)_5r`5sZ1(_w_A{vu=0VT$&WoYD2F~|@kYlC z-hQpossH1dmGuR^#M1des?LUUrm>(JlP>?h zrC_1(DC}LyPZ3#g-h#}Zbi(UD`9AVY$op+V={i|4uYRkZZNPxwd&HS_?~u9AJ@wAx zwI}GFS@%8fs}G8A8m;{&uW zTeP?5GG2o@S|XY}TcGmpYg>v~glw9zd_zmm*vSJc@pCf{(@hLN)Qp1CmtJ1L&);`xcNF`X@bxrrHJobM$*Kvei+vHEqeC1?qWVVlWlnF(4; zYmVKTuSW&1Rnw}C9=b6H^*bAur4KPTwNmT#T6Dt3|& zYwB)QT6|7qqd;GPKkTwn**&f!+i}f_@epo5GHn_Yo7azVSoucY@RiT4i|W(WCB=)X zp@hY9Iq#L`?F#FFyirZV>DR`<%vSO`WNyBmTfpdjj+s~)Ey^F1T=As<)BE+smO?Vq1MQJQ~>wn@~q^}LKR1G0- zV%n;Z@=5oaHV>Czv0h?qU1O@(is!1!ce=C~p7B|h+o0p7$fF!0V zg&8qd<2r}_ZK?ZWr>-bb5sS-t?MS=1-6o!X%`i$P=BjEh zWji{n#yYpQkHYO@o~}^^3ohv@vjk=&27hJ$rZ1|zx?U^!PeJ)lMN>fry@{kV5z!J_ z1QOgqCgBiZAHNj$5qove465uEAo%sRL#%SYsl`C^c#*e;?4OS~fqcYarkX9JLK^o$ z)6)(rEgGxeb}>^AdRgkV?_A=wrDVP}Vrx43(QD!oL8PxRh7_vDq`{Bv9XU1?41m>l z=Be>uCYDBZ^qsgmwR?VI>Eo9%>l=o0XC3foi3c>Zb!KcR#A4soER(UGRb)hq;5mB? z5QJdK`!BJNM&l5;lEPb82-rS=hRBG45!bDMr+5;mQDi4~1j#ATXQ4U3-d$(31a$Ne z<(lIa;>NNOQNs2k48Zt{fzj%T3h#d&-vlh0=*x`(0p#)hitsgm?i4H++3!Wr=^#4e zVz6LH1zUNXxO8f3=#5pPz}uhqKL@oYS9GX-OS9-Y*;kHq>2lu z&32nc-`*;l2?IJY8eD^}5O8W2nEXy_!4?}qoX$E_fLwD~M?N()d<5o9z#11C{!Ebk z#sCyO;WIS4@CeB-qYgkh`-?W)LK6^%wJW=N?+-P{UwJp(o~U`6M#yB)b+bpaJ(WS~ zaX;g%0C4!<-h(?05i&0@Zaj_jV+EZL>+ovN_@nD?(2HS4w!S!YKN-1&`F)2(`m({U zPnLEaGl0`^z|!#ZUwsQ`A&e*C;5n~&x;{hKDbj;C37`G6{I?*07PrEx(yksBJ?c?J zxGRNNJD^vf^u;-u|LS;QY~g9qs_a=NLcnAv^SvHyYo=|}Rq=_0h^!@nit<{b=`}G` z_8f=O2>1_n(DakN;OC~2EW|Q)wCaCB#P#w{SOFgCD#q7qBu_@z<%iL=b_){y7~-lb zR?)YwJ)zsH=cGk&VP6sYt$_G_HsmCE;_xWX4j5f-2>u&nXPo1Z9_pO}6NNsn@_e5o zZv;-;ym|;Q=vF}HnU<;$=6Ain%T_4(==;SRl>+AlEH9P}+doBAYXGiir4Wu&zsbR|JJFlMwHShjhr7npK6}{1 z|Bxqtb5#VkEj5!b!49I>h`;nrz0NdJBAvKqk)$9}*nmxB#)h zp&8bNdAbC{gxKr2uZ8quK%S2^GLaZT<-&3$T2+XFH^g^KIx)|F&*LC%DqA~thOdmt zPR5g}|A3@FDhm}q!H~_AxCc{B<;td_@I0^Tsc_+RAmN+Q1?<9w9gkgu^6=Vzhr%SPNS*_vBfsdk}b;zLFt|cVtns4MFkyxW{9>SOGrvgi1j2Mz6c9-O)+xr zKVcif4E$hO#5DrV7+w=R!NlCD=Y}>o4>UdA@q<=i0x2b2Raj*?TTm(W%=}Ni59v3o zASYAvBD{R?S(HGl(4GYgV8ARIC4;p+&j_!I)0Rx|=t+>!@ zXd6^JP8NJ*ivI%SQ6<4&b-NiS)+Vq6KN}yPGNZ1pS>iS|Ip$Y=%BB&EzNUM2U~*~y zm`&sDQ7e%Pb8fWjFTc)WBT9@j8r^83dga~7GcpOC`dD`~=J+zB(mj$QPAks6r4|>h(Jw88f9`)Lv)yyWea@Ml~-j zayvU$6@OqVUMssNlRNNu{alCLllhK5%T8nGZr|+2c#^RpUi>u`k`aeHN$Xlh`%lCI z%fyqtPLT`Sc)o13ZSTn6z(%aUnA37!%v{&V90(Da$JR9%&4`fzqxI=Gar%6E4|}uXu_x=|YT$38p_XX@2qNT7 ztgMT7nePej|J{(jrs#{>4PqtT46wc@FVHe# z%#2Gm4<{mfFq;(vs-S&Zyi8>~P`cfoFi*q!ohm@^BC0f|LZurmz$UQL2Y zFY4$qw#2KNh9xWecit-S%q(uF*3gFEFiu2H!^J$g9UPe(+wi@XKL6=|c8sbFlrCzg zD1{Tr2~0pV(yZg)D@#Q4(ojJRld^J_I2*XGJC*=$woo5?Y9jgh_wi{hX64-`~d!G?@D%wi`OZlEUU z$1`iJzc>xa5n`XCZ6Jn=ASoep-TonxJM!>8N2-#4eIZR>kGaoj%)}(wNsrKB`B-b(4SjNyTya;P6DA)i7jA8AUvoIPQ!t%#&*k z&QIx5c_mk3U2qGp{|&S|;0Vwx#eF`Esc}NX;kXsYomBLoOA;8AW{0wn+OdH=8~+k0 z61QTx3f{ajfEq0pLO^q%#cLt-PPyaHK`7wvPdgD} zk5l>tVk9JCl2~e8F8}|Y3o`mifJj%mNpmy+UP*44y)l#qTuVC05wRX4uevCu`%iz| z84{p0FZ6&9EZ1!k-@I^JGj${j95}(y@o5Yj3ZzKx&MGPEBgNbhsI`f;P{cM4V{Y%# z6=M~>_jc!MG&%<5>D!Q{liO~M zW({~mO<+XLjL=RHP9PRhOeOiwkT8^o@_>=O@>yq-dvY&Z7C_N&^J(TR zVk$P^GSfmK=!|_ae)JBeT8RabgS@t>~s?vC^4+c^g{2nUBKs0{Vj17N$=Xz8mr(Q4glb&O;){$%*un7g5+~zk$q9I zG*OYip1s&}J59iBih85omYo`XNxHv}pGnx7ikX-b4YMA=;{xI5ma`sck-EJB8m~RD zF{s2mOD%lXCcnIaKD{h?iSSxU0Vr}J7DoetZ3{5exjN7VZkm=`#zyj>AAr~WEDVf! z65R71O@T8CsGPi+C2skiqo@1QPgv|`z{LodtrbHn=zc-5aO-RF6Q?gkLYuNRf2xr^ z?-)4~7gE3(jFm&J+G?AxN*YXv>;tz{eu5KikOQ?8Af;_Oi#rf-SYer zCurpJQX={Vtde=t$F% zcrVd}!tDLJ6jV9SGp)yHQaSn*!t(Y+{wr)}E?kul&-mI(kI`Z<#M_e4y9HRf(9qLs z2f#D7)9`=}A~o(IY@M8`ro8Y2VEHNAM&qZuJQD?7?t8%DRhPR*-L&I9WK|Mh(6nlO zY@jb5Gtxb7l|i5dFiyLXtq4f}YlkF=OUr?bo-2-2Cm;pV0_o5()k}-`WDr8`qv)jC z?-)j;)=t0qla>D6sgg#+&q;WRuxJ8THrxHBl2%}_DW@VGDF8rKq(nBQK{q0XQs1@K zlc4vW%-O$VlQfbFcZvC7#bHJ02_)3yWczagAfRq>C9kopbh&KYf|__B=|bL+S?V@d z>pt8Q1Z1V!S0teFRwsNk`=B1JH*qRH?e{K1UCNU?NeQpVcL)0}Mk4DZ7GRh$>}8Y3 zSl<^}^{GJdIbGpteIi)GJ?X(!&W^-i)NG|+=b^qQ@K5Qk}weJ zBgJa(1EmofA;(Cd6rtbH{F7LwGQ!?bOCLB5ADx-Z){I6@93b>;Lq~g@(d8F@RtZw= zCeB=$PBQgpCdQzxD>pTMq^B^^zwLaqzT2)1O<7#HHHtNxCUQU(J|`bcYTDTl=O@5^=;3*t@p?)oZodf0z+- z4!AWm+7U52_exz=BLkCept_{#d-0q@zaY#^?Lrfz1-`KTb$l4H8iAo;y0`yv?-aUl zvbwH0!9w$sVt3Dr83lO4hMg$2ywWk+Ko~>yL2N$@BvP-LsgLA2^0l0QQ$uTd>?~#k zzGM9Bxc}2~l;(#Ip(^T$^}NIQ^{q0s8)%Kf&B$q{y=fEiN+R^}b#qxERzstjm`1J+w^o?adfsmixxupQ!avvDzm=!2Pn-;xd zQ1WBnr-FM*7!S?ouIV*P7CjJ zA+wxjx4;!JP+OeFAi)P%_M8xa;-H8mEGY6PVO<}|jUTehW_OYq5f`u|4sC^>PFeB- zc<@fT?ftur$Fm(kz3i=}q%ocu(xGx%9<9`cN`6M;XsKl#6V1W`@!8M4FI#u6_r!&2 zq^kuw09OdYXu0mn@|kv6HarA>HfyIZ-87IfT}_zybe0f1G0~y%pB{)&96Y;z8eM(u zP!d-}a1B1~><66^>!mG>Jw4AROep~Y?e@jQ5NW)=6!Q{_>?!=K6M{>M&yH;SFYOJm z?d#-guJ~U!Sp?_bzl|1#$29g31!0N+ad;Fp81g}Kw8 z_!D^S{ zDX}<&gXy;TZ{&d@2AccB+u0?d^H!ZDC1aqa{rT~!D=x1zf3Q;er82y+@O{Rw4vTz#_xYV0MJ~trmaE@XT(cQ4az4`)a@@^+?zw3~Wu;LdVaMx9 zPmxlZV2m)d$_dlEpaTm;ARKTTx%Stzn1J~7PTec{0MToth$mvC^~v9$xCs~exx zJcC7+6Xv^biEsQ;xRQMNPveJVNuVmI|S-&w(Z?bS`Uomzyx~T8d zF>Y}bG=b0TOo;8fGD%CChmNdV7o?bv{PFRrqSfCCB_P+}Q30pay5JpUAJG^-r@AhN zW}iNYY3dqZh*Y#JmtL8gYy=e(h&;tMaO6{RXni%A(l54*7pyvr5LbF^XR?vZ=T;E) zis24ZtE)T3fWzYS)!@OVmxtZ0Jc%KX=#=7(T2d>a3-={>9&zY+p9{k!R;C>!3epUg zT-#q=ytYJH5N-VCd4$-WeA)Z&VHRL`7$jj^E%#;-QqmVcZ8n!U;k|qzAQPfaR zT#+nkcdXW#v8~Ac0n$E#-w3g#{?p>skB&RoE&++x1pK&1`&KmQvuspVFT@WzjMxR; zhd~|g!|V`$V#KzsIfj)A_W+ujX8iZEjQo|b#Dj?zj7;(cNSq~ItvX57n}qUMfvg;m z^sE*l1uI~`cZXyCNjWs!D2sX|*lvvXBE6n|rn@41%jUgY*kCb^awZG!EPz01QFnbc zID@zK^@{m_%g20WxRfm;cyhTP5SLc$SUSq>D}YV{pRONF-#X@kwEfDBxFirBx`L;bKms9M9T*l6HR*O87;L%jBn@ zZT(EQp-<-a>ET_6`!;9J8A_M*mn-U%{FUA@lLeb%M(k6rx#SFNkoQNKmdC3+I)Kcg zZB2znxCMN#E;c2MDr!2(XuZE+x_Iq@;nn~+$mxSPTA>i9w7H3zoJ0WK)~JVBd39Bl zRE;$Nb3a9F0o(FXv3bXf`_o%^a1{dHU(ae{958P)UJ)zjft~-LAbw@|)3=uq26x4G zYUBsR9~sej-ooHZr}*6M8t#+Wa`Qa$Tg+80d^_|I`hMdEx!zt({=z3EXvbdhm6~pY zoKVxvzESFdwS}C?!vl_GDP|jJ)X?9oI%RUZ(L11IvB6GQU&F95JY=1BF5mb2EtZ97XF_ZCYYt+t_w0PxuZw&sph?X7U@6b&?>s^P0%|R5 z+aA20q;id!cFB^Y;X4VdhXGq>WAG6hsPhLpg*uGF!v5%JK3o%=?7fX|e%QRx*A8*q zqo>!;{kf5ci81M1dA}jQ_sCjt*AVokSpqMTVt41MGJWnzBtO8kO#Rs0^z2L){RvQ@ z@-QhdhYrO#B}hZnVA1!%w4kytTeDN$+SiP|6DCty?1RODD4xuWFr~3%OsCZ&Fy&Fm z@W+nr95R8%<*d(31nW*-vRThEvOj2!zghJd{JK&SWU3%C+iAF!sLcD90=&ezmfZK( z?$3Dp7t+4X=*Z@%y!n{jbyKJ#?RqguqdC-#qIk}p@75)ggsw9?L6XfcKm zXSY!V&hzim4bLErllN+0CdsOaYX=%IZ%>Iup9(lv&}O~;C#OLp5YShXrZey=4D{pi z6V%%b`BFLt1ttVDn-<2A9Hxp2F+xuGvdjeu5@wAt;h|wsJ;Gb8y#z$G*DtHa$zw!S z`&~(YKM&sscn3$lqUksX_Zah}f`i&t zsCM+f6J1tvAA0H6g?kxaN{L?}%XZLdJ3)|%V!bLVV9kttX$_DNPGWRfC ztC0&!`8(*X@RnbxL9}3Hb?rERNOYz#!zk3iM&rEw7x1>uT~O2i>tp;~Aw|OM5>}ei zTR;VMNRk2KKjSC5B;vM3pdai*v7>dK?wm1IGpM^XN$WlO36{%iYf5LC{>-JCkV>JT z!M_sF*{fkV%$uOjif>Jltlca6#(C^f*wP)i{5(9}oTB9D^M4BE1fV>o!`M82cfj+d zVX7)+$fKttlzDZj?cTETlVYPyz$2QhKfh!r#uZt+4?)D}N(x9FYxjN#2;-UpS1Dqp zJWq9*M<#aLy&8{_JcZP+R82V~g2w{l6hFB{p+OhXBIJXYiK_OOg|*WgYwte>jH44^mAppXZ}#TsEouakoeajuHt6cLsQD$N#lommqO2OJY8nKjJsi&rY z{Whr-ycZkPK9Z?^T>Ne)W%*{KQ~c;2>mxsE8(eL#;r@I?r%W^dw^c8%@pKTK_vQTx zg8oh@GOm7S76s5#blK(tA|c%-2n_G%?~>g)R4@m!``CpCDC!W3eufWHZHQFPZ$F`y zmji_(=&c{$-YP)H4KWjsBuJ3BSe5r{vdsGB6nmbyE*LLa_TL7~tl< zRHsHfHeS^n4m@C^wT|)agwMPm`ULMk5m>&44L-0}UA-hTG1c#iGlqnwGX6D{81M$D zi^A^x|GoV_4xGddeU~u z`C*%g_1+|ji+EcI9|}yxO<<&L5Ap`v;lniuqFw=Ujsfd7*1Ksu>I|P}n!mmCVsGJN zCbmUcZCp7fVtqi+Nx||%l(N^U7U$fmg{`VW!0!G0>VJl? z;DgJLZW-ra2PN!rB5w9>z}*<(3$GS-%y6Syg|>MsBD5S)6@-Y6w10AbndQ6GhGP?< z4iZ-jnWd*Ol^rK}UW{l_o!y)iT?5e9o7k#83G|1~w|sQoEEg2s>^>UJCIEuu{8k+SAqt07x0+XM9!H`!UQU-_R_mdaFX?izN2j#Y)Y z^-JcP&y`+MO`Ze-j~D-tFlyz?s`zXqgB0_v<>3O5TXysL=j7+`l zj3)jrp8x3x%U^{bgvd6MDbS0cS0~H|iypQI!5cnvT89mBbe!7wtW^K^tUyOM>&2gD zxgXME&uc2shc2fCv8~zV$LXizeWR|U;A;P%9w}XHJ;DJLFQt2?1TTA_h={3NO|+?s z0vR2a+k@MmL8tSX@Jhwq;`cRtK8KzuD(Sx=5QL<-ojr`658oEH*#49MvcfwYg$qgr z)wKsRD$BqSPP#c7e$!B{AX$}3Ey}$x3X=p-9V2n)ETX8z)bznR4O-tn-TQLPVte*Zo$nuEZQaY%Ih_*U zKfk>-glLpA6>ej@qyRQgpj>ySns?EFtFhQf2J$H!nOvA6TfC( z!+8DLUpdK_l$K)PxfZCaE-$hX?)}albh=Bon4Ti_!pxhu%3=NbfX#Ivia9=t9r)HX zlT%lQA;2@L7qxD1Qj+bUum{mqj5EyRD7t;Kv z!J94#%YTRJp}u(=@dL<%$NlxBVP4r5R90J7D!Z4;hmIU6cR9&?>hCoN<4b6V2^e8{ z2E~r z(RZa}7eVHdor{LC1Y&((|G%^40%sX09#dovpAsndewu&FNkt9e!N6IKZI*VIWS~3 zET00Meap->cVy78sO2ik5boc^ouYBXGw@y^n5z%Ox)jW2jsdrWf2OZ(y~!~mU8YH7 zg^ED0#N5q8Jjl%Pgu(ZdAxR99n4}0lYQQ-i$P)bY{P!}#jF=5(m`NvAq9p3ce8>H` z&IKdFpnwzb@0~*!JD_}}bO4qRSG9%wde3pP{>gmk5!3@ZBP$wcEj*~?fe?1vy}-`7 zI^zD5V-6AN-*xL@Xxw(D*a39z;?(fL%coj`wfUVNem(Io9w#-K_)?TM*$Q|QHG>>i z{MT{-7{z&##=!(L^!gn1sO{&!GHAvks&Eg_3^12i4b2WloNC(IRVQqL`5;ILs~oPU z95p3zth^cqCjUsw>*aFCx?|{PFgFTWIwSoo=H0FG*clq2r{O9!)IK34kTGv3ZTS!M-Y&syQl~n(EtPUnU#iiuv*MEr+#K{5@&Mj+AS{ENMoFVv}FP?Zn&}8lmqWtAQGICsh)!&ui}S zUBg>RLLYy0T9u5z#o7Hg+lc+=#{2@UVCjM@yp4|O3{n&U#so|bgsi^ z)IJOrI3q;2Uf^P~o<%OLvA6;RuU+h2&FNj5M&c%{H;BM}f~*Gbi1PIcT!XP*Cr;zj z8VgkE6x3V)e2$h>#L@2OeaMzGzCdQDIwAJ^jbjXvb4K)`uP;PBJVD?g<0nVFRL6(k z61LwW&0Hv}v@Y9)-9SYl6L#isMaRt$l!s#&rN_;GgKUU*A+eM@JeMEDY)gZIqp1i4 zI#Xy7cC$3yw3W(w@a=6FiBQ7)iEhx~D9;fe{u`?trAQARr;m@~r0%4v(7o5{=lN?$ zA0=8Yl1)`=g}_?Rfj)-tH=wDtig5#(6ZEU5KIAk$`Mfn&macVCYncwHzKOd$o$ z5r8f<+rVbN37+%2`S93$dNjjrxpL0%Jy(QI>=vHv+;UC%ooCVifSr`=*qE%T0?vx?M`8xrTfuN{fxvhD zPv-p3JY@%FN##BjS_x zbhnbyCyv}>s!m? zag938?ET+Q{sJjs<)F=)76{91B%z9MSkh4$PXdgls>& zqe=B!ZilApOg!B)=t)REtqNe12wvt?`abkUn;37_q$Gfdblghy$SE zP|$T45DR4KbR3@-SrryA63@?(0KDEE5l4w}3|fci{-bLe_|EjtNo>)}O7tVB>iI*6L0w zNuPd!1tS9W3wL7lcbV;O%-IE1Qd5vLUUsM=#qL74PqqWFW22nBNWqL8TLSzf*FjJ( z|DZcwwgCB{aXR0JpV~A8gr%#^$ zH>iP;@M|U|m@~Mo7)%EvZT9jeG0`leN-lOV5na66U8JE|Od)HcIQr_{R}d*!W)oD|9l1pJvFU`*UWH!6yPt52bL|&D^=n4H-zS2B=bK5 z?Zcb9sZ2$00BV4-=sW-C+4x&aa3Gxet4D%{*sz=$ZzF}_axYL#o2xG@eS6)ut zyyR=zAoZLE+5R#t%2`0*RM-sJ<{^d3AYAHjP>Hw#b2kLM7p5E~P>RMQU=|pbsg#1M zb_0w_3CP4zNfN#rh`v2kODSM;vjPyHm^&o1jJ<;;Q}G_kkv~EH`Y^~3u5Y8(O@=E7(5I%MqE^M|gB71{|B;92%Aj7~u6 z-wB2Yx}Q5ri9q(@`vI1=%^T^y%s(S!jsN;lgVf{0(?3oV$)CeQS)xJbI(ks-{^-L{ zrbk0PKxRA|S&np*70aJ#KG+7W|E(-!k;;NlLHiPt;iSpoG)X}V32htq2j1_HuodK= zWCY>Dp-j;fu^C`NoO@4!?&J$(*%o+KOdWyEwq%>}i0XE%8)U>HWxGb}U6cAaU^^ZuCYD(nstkelW9Iu1qJp3+2;ADCoifQUj99S~_X1rfNn9C;;ju8N34mGS)`F8#1RRESa=Zm z%ry)-$5==bkzY>N$loZ2|96<=rXxA#Uq5ajO^)Vqa)~P_d-7KjE?J1wG{VORQoMip z^#dEd**eCv71tFJYyzCj=Yq#>9f-N3_2)hABO8#BbCk_Zi`e|G<;{Tp~}hq zBKj7pfZIruJ2dQa*L;J=>PxMQ)r7N$Bx67v0IBK*m5^YquIRT5RR$pyM4)d>ZI~+z zD{%RPcg{auqXQh(8tLu*|2b4i&*AB^pgE6!i9U-4-$EWhp@spc>?-JJ(<(m7M_g_Y z^lw^)KEO2r*)S?ptM_M!%ODFM_y{3O`Y#C=dgT0)YCe2g_r7K^;0SdeX+wLEKexm- zzIrdLdW}7Ce*Vz*#!CofG4AvHv2z(+i}kz&81@skicIivLoaa6Bc$I8$a!J+1Y~e1 zvQq^KMxnJJl!s6t+4f%|*`MnL^gp-{T^707ETb1+!oTKZV8u0_>jL<(v4TZE82X5tYAqVFNR<;Czr$F^=`465a8EkyeNCUa_r$LXsJ5phFO4$1YihX zR&L`DSE`8oeLw<`1DZSTN{3t!mLPbeTKh5G4}dPjbXURtgOw3@SGVU&>`^nc7qLU? z-)xc*`a~M@ColN=zRwPz@ee?YU1!z$B_x8Qgw_VjH%_gE83xKD&%3>5AN&85FBS-m zB3d!nQ*8xpTQXgn}tbbeG9Mf3rl@5d-8(f~C<8f;Sj74bkL0m7Z1p#J}eAt{N& z>+Es4fYRW=g6UoZt;<(Nm~w}WOQaSw4T)L*39XCSTI!8F%XQ1r zEsaXA0|#UeT#>)1nZaZMILH*E-7SZiPFC~f*Vg8M*mj4RIHLWn94ErdVe?b}9vI^Q z>Vw+>+}6F-PFQZjfcmQRt_SKJC@Y~ec%h1vYkc}hwk9fcNPqojOr7ajrFq5uQmze| zVP%DCd`7W`Yasd~@*MWH=imC*-N&*ZFOt#RH{$!Br>`A}F56q&6)QDV-F0722K2Ik^Qbq|vC74tkf)5L9n+ zB2yylcAv*m<3jNWpUq&QAh$UBy&cqnm&4Z4(GRm%%512-r(93y=G~4kXu`-4V2=3} zlOh1s+7m<)|K>xHyn&NkKV7qu_g@De0BQf+Kf>c!@Oo*06{7`L`n%3{c*YKCictp-K#TXgrA7Mp0o1 zBXFeb>Scu*wup(g1mi96{&oH+oYx_K=r*kIwv$x`i&}@4ZM{=bf?3&FRpwWeWwjMY zGHMXma-+vm75Bbnq@B`Q$QI+z1dYUztLq{4&ugU3gPMEk+oqGk4}y#qD}UuqUR3Oq z+t-oph9W?OTQz!4#UTW$4|hlol9G`j?VsuA<|w1|I$;xshsA+I9EZgAU~FzQcXaJy z;6=n2!R;9(&TPT&!b$9oCq?yg$=T#|baS^=CGd<}jvfTHPC^S8WVMF4B+4}%2tUnd z>F2V>?{oK2{ZWE6%`^2Q+8EDRlaxo`cGa^J2cu8XVJ}JWR_@HXcu03`<{da;y}0n@ zqDlSO7hL`Fi^lL4*WN4*_SUVBg^vDN$!NY7Co^Bfuq}fI?yg@L1;HyIFqPZv)oz-u zj0JrGc5w&`=2?KWSf2UIh_bvV{GlQ?dSD;s{ebgKj==A~^CwIFuJ|A7Kv=H){_Wzw zp37hTA($l*V5&ox(PaKSD1-nf?YZ?zb$z-+4qU&;9s zI(qRMTF<1|<`Uko;N#9D48>F2+RE-uOSYz_N{#Uy8Y41{`X%NH_T9R9oVujz*b{WJ zwN-!tyQI78yun!K($TyO9ZB`#%^b{{=NvE4zOKi1=C>G0W4>Mhesh2%zVFV;CqUFj z<(vC-(#8PzxM}%k-5vB&Oz|AdOAE>X>}aKV7WOIWwSN5i51R+f|i=@F!EwSm{Sq$vn@2pSXjY+aGz6yGHW$HrbG_ z=fd=0c7j^wMFH=vw0GkLs_2T>*_C@}wy|$wTl-EQ^$ikioVqC#f0-g|bzXx_dBBBq z1=@`f@9LRD+Vq=XvS9imD}?+#q-3s6YY=$&Cx0=dL5C6FO@FY$>%Z?cRu*YJERr&U z(U+7NK!p#uR6<=`Ro0KM(>+B7v!P)O%CKAGJEf9$;?H0G5hGhbIKL_U%t?zPyY1d* z)rN0Gm-`jz&Qr&Q-guyTR=18^d|x4$`O8mqOWuiF>6ul;KO#E@`Xj5qXRjJq4<4Tf zX|D)`PK8Agu$14uTxWUikk)6L#Hi$S@Hgidv9)FqiPf0J9!ZAjf&vS0sHicGF;X69 zqP}u+#;ea{=Egs%nH3@VcHdK^%BW29b0GDdB_o$X^*Wg1aZz{Bb9e8Q-U}D%%2*63 zQ4Bg`W-4NOn0~8Og3WOYgnj}qJTSamc8L5`Lb{P5U?N7FSeA58nzhTA9#kwfx%|yz z@q9+x>>YCQ({#AGi`uJ?xm{zj5vE?+wlD#)Hl0w}MyD2le zdL&~9(n^SY;Hh@~stpM2{`7Dm!~+Xs^K;O*hKNWs@%MN*m=#skFel-%Ai(Wzayv5a zv@a2Vl-(C*$g@021Q_MAkI9_Idw`!c=+I{Mto*b(&uSx>sCB2^$icJnpmr#_1|~NUh0n!Ra4rb?E#0DU4vqe_I{|RLXRcOs|w`B)mg(vhLHT zS7!9q9<0;4+4j6gYOR!p?3riVLtHE@eALg8a%o=tOIY|jM+lKshsB;$nheTy_je}v z=VvTZ@U_e9(i8b&YapRMCoDvqF9l4Ml-wWk`x}=>+CXt?905ay$H|5wzve9Pm-k5g z0c@XObv0hHR2E%dD0bP#d$yz_kE9U-FNgQu<~t|c@yRNM+h}|@SA}qU^Q@aD*_W51 z+4^@4O!33nWJ}+(s{|goZrBbfAMT#A>b~!s26<`a{n7g*Uo+>Q%}>4(x(_u@sTzSr zLv5w7plGeQfj$Jg=dVOH#N_1XQZ8-5mi@S$PF>K6HJgUf4+>}H?>ci&otPdmUYTm+ z=TB=m*+Gn5ra5QwaJ;7(H=OrC;(f5ABPif1aAdE3JZ4Wv+^yQM(*}~rC$wo`MY z+f*igcF``L@4>rMGL!ZXi4FyvfLgS_Z1$iwTj~_+HR;dPAIA*0UU}-~<1gjazdry~ zaTwqn%j6Z?y!L*4A{JO1av8$B;Zy#2AuF6NWQfkWC0b0QH9>}deckR8b;G&fD@Sd(OZ9jhb0X{X$=n>AiqSdI( z>57r%faYRPkJ>pgZ`kC7X-(W$`tCB@#191e9!=@Zaq{9x`&}rDSG`l*Gd~H&_u|eg zJAAZSV~aDHq;{nbQLl4S=94pYl-X*?_aEHluljk3ODAhyPs7NRLf8`8w!WDIs%detjW5yrw4 zAE=UM`0*6ezeig?cCe#h;9h^#@#cB=;k4s-#FfmJg`;;*CB+CT9H-|i9(GUX_TdU* zJR~lVIJVpA_naoKu0^EQvB4EVzPYFN`E4<_Tr?`idNJqQd{THm&sRK{8y!Xa-2g;q z&47Q9bw^`Oip%}|;KSy&0E2(iM|HswYhE_L0^v(z>uc_B}30fg9AMdsI)vR(`z1x@)#_TY4 zhsQ7Y^f&wk&a^0}<MFmvDxOz8OxpG41Pmq+Mke-&h#yJmU^1LRw0l^UV|{W zVex_PMs-TC3}M`J%^b;h8B%vTYXcrx<1_V&MZC9fULFOoUpTZOF&Q;Ql5~!)-bUrB zwB_8x`T%&4*n7w2%V$u)!7$eSd*$*#tin;Q!e$lPbaG^W z$#jxS&eMGzS_z)dT^4@iVdp+uEh{=sAvVL`c>=V=uFjb=-;6>pX;_bU1sr(wb+uBJ zO#FS@2az8JaoWb=%Wi&-g(oG0ze^Fs=MTC}n>>|C$+iy}J^#%HM&JVLKE&v@gzV{A zr*8;`neJfGzXe_XBiQ#xP~=yI1>+1t3K&BqGPPio82r)5hLPXkOOo-p$gI#*1m zM@neS5+=cBu+LIB>PS2K_h`x!VsD=MC6Cs4>F9?6F)R-_O{yE|D2b)yQLylPe+M*` zfO;R41qq|~Nba2vJ)x_hn09kf4M=_QhiNt;1O-xdF~>=va>hgWF6Ck?t*WBXtAcs0-FpkLz8`qM zGexb?EpK zDMk_3#TY=?L<#xeI3$;9sW_hDXe;n+Yh~?5I=Y``nU*UtpK%HX+qt)o8A<#&yPB63 ziS1Fd9^IsEvvUoqU6nM-Uf$Y_syb}beZ~A0$Kw^4&#jUlRlfPDR=6$CHS4ne@&f&Z z`>!*0)^&mc-_i?3mS(`hK^$0TkkB4`?a{4xUEik_ir52=h!&GEGk`-|Q4XF=t% zjjI9Tm zVmg`&79pW=cGbHW$J5&kwQ8E7IKlL?Rp262f_npsEx*9Xzp>%!$%7}DqMuI=Ozm4F z&LY)Fc^^BA9FsS+d9&tB~H>{Dt$x8_LJ3`g!s&4eS+81;%BZZ z^;bETRd$|k$k$^Pw)BGAIDdE@Ub3=AkQ0vrlpFSfh|EO-UuPwW=e-}q5l9k$7bZwI zGU=pa>;Klr;JNguM}lp}QQX%+l3o%6dI|FACQK5G0<=g$c-MG z1sMjvf7OgtWd5vJTHFe@YFO^DernR%5iVtkLu8l}6a@^{hP{nuRSFIig$yXUyy&bj z?FSw2&90?sriF09?}L%X~jA#^Sf@2qXYbK zVuP+03u}=n`mN>#OWv-%)3D5~ljRh5IE~(6e^swlkO3=q_(^Incw8~JxJBN}+~(F! zWsqC1Y$fKJaQl2sC8mj7P0~KDKuT|fc?c$*Y!V{lx7$iWK`S=RB3SGGu!fsQ-BXHq zt(fhGOV0uh?nzSK|GqzS=U{N|GS^x037J9v2-Xi@HEchRA64e?=RbBI_Vvn?PRf)u zV>P&%J8Nta1OQ&U%O>8p>SytGL$cH{#`>d_5VKTX9COI!O4t-AysGsy=1(*V++<)51lgm95J`CHR} z!8|&UwfnK|S)Sd9G_yu)v{+Z5*j|-n{_*;r^N`<}SD@cg?j3vS^!+2KoSVvH)~47a zRB}5X$&j%T;7cqVjfKelHLhj}JCI(!R5hsVB%Y!>Wb-vbH2V~8YS*aJ10mzLEW#1Gk@J+OL@#PcbuoA9n;X^l zqqRFfzk9}+6y0Qxe#`_z-~a2B!XZ8We;v~Q4E}F1o%7ss`VcmXNMOO*SHQIL;RWA7 zyM}0S&6nMG|NKo^7EarE|61D~p4DAP(ICBPlERN(!ex_aqSCYnap3R$QfPuhT z=g@J9L7f0&dZZ0-n489D$yHF)V51$nd6v3;S@MP2gtM}`4>~B|9#llj1C^pqtc1!A zmque@YSwfwj_0L5d&^ki+k9#ETOZHlRg5_jqXk--S1SZFY8J%;j*zc2B-R&?aONqg z7Zv=_aQ65LKcKV^H_!fA!2KS&DAuirp{x}k@OTb>9RAr00jdKr)u zVThIm_9ppzi`ShL0tQT0cQjgL&%ErTcwd$C7l-@<-UB_%8Pza@O0T3YYwcmV> z2zLeV9XoSNZTE&gZJ{TxPuTCNcrHD5l^3jjvjAE{h=FFaH1{ht&@7YQ-f_%t+x$|r z4>IUmJp2-EM-=^C3xyAPCZm1b4MdZd*|y@(X&$^cRJC95RR^{Y$F)Og$@xJ&`rAXI zc@l;XbZy**T1BmE3GKYUh4BkW{pGQhkE^Is)l;6jo%}H3`I;GA#>pwl5`Y06PEAYQ z+4a8mIqqs(qiVdTbJh=7TT{-w3m==`KC65+k4e&Kc&Mg5?{zpWz_|1{td(c3? zmCzx*2PDiFT|{cHnkQfMacgIAWIA6aeOF^UmsjlCcF4ZU@91pnt2f*pCIrt_#SemK znP!Iil-R9SH`SaX&fm@H-=5%CGIW8d?2y}!fA_b)O1b|?$I1GVq#9ROb>9deGXBhT z%p&zW(qjQ@eNgB~0G;mVf9R{6=p;jvC$WRZ#^#w7J-Hg}KR z>XN8ylOyy&#%Xqi4iN6xEhnHyxkqHR`xW}IK=0C;>j>~L5i(& zX|2?-g=lYZT+kFylzk{CbDuBIU+$^eJKULs8fl$uE1>M$Zd=N@bN#I{H8oVLvM>C) zC9NivnT3hLJ;$`sTUM}`Nl-j5-(3MzaL{>l!}-G2-3=-td>Um1q-$+y56>DOb~Iqm zYroXI{S!O{)=Sz@&bD5# zx!5o9NSOg5`RJO(Q&nvikq#B27eFFX_B^|SNpYSSTjkC=&Un&UBvG|rOIdQneAMx< z|HRG9hYk{XMOGC(0HH>s^n&yoYxL)2LDqGe>lwyjs&&ELGv~-tDt`-v1AoyodBpP)%Ds9H2xiS}$stg*en3_AP&F5VFwOJ*DzQAzt-nN|zi@it42cxiL^=|@IeegBfz$a+m08%jrVnlHp* z9SdoNr?|_sTl*KuC*@ScaC0Z=MU;^{KGw3-3`{A>6z;8o)Y~jg^L`Tmep3^pvwjk! zvUK?Mvon6jX}J(U)g!GFCG=P+21?AO(Nm~gmYlgbR@-wfhS$I8rz`@nu|I*cEvkFP zu4;4gMb$eO#S&EcUR8<T{Tyi9Y}T@rS#=QHaEX!lcvg61$#J}yv+TOkv%UFViOT7XuO}4R zaRkovo@=4g--USz=P3^vGyKcQF{B-_vvPN*KKt7z9gG>lr;#Cq0A9{NApX)a0ENGw428(ZpUS0EkN39DFD$Lao5=l0( ztsE6ESioxrS)j^EH!kt^2cC0v4?4Rav88tOVPbsDuzQp8)UF?mZO+W9_s;T@0^w?M zu6G$6(Scc2>ofdC3rE>n(+=(B;WvRSTYX5*){tSW-f7Tc=%ZV!`60vXCMzUtJh7uhfyOI^Cdb`m4Cw(w z;?x-!!A6v7Y2q&H4in7T=0>i#5ADQyjy)GeC3Tg*nr<7te)R%sTX@p;K4|IW+Q{@J zU%Ba$`U8KtT2xikyQ`hxhDwZZ{z55%tUHdZGUjS(-T98k7q>hD_=?V0R!w<=6KtXg zqfPVocz_r5m8VS`3Kh^AUPzW%T|3F={+w=dJ9m5g2!Nt3(g^?KD|-N20@mzw#TttB z|2{hhu&0ri-;m1{GlbBgkZwl>)kUoB5iOL{%*EBB7JK0a_>h`uj6Cu*iPzozCjKjm z9XP1S01$nZ&_#3kQ5Pz{7qIa0eZSgzS&i7}tI~N5jJ6!}Njgc3%hHB~BpIncK&Js85|-^#7&@>A=5RL~jlzq` z(^C6@Jb6Cq!o4tSVJ7E{Q-&7z7YcDrn+no0T`bFV!j0u+t|rBs#b0<#JJ1hQ@G9vR zWM15>;xQmRnxR+7l{p*bUa_E%CV=+e>-r|J zI$&_@cgi9Olm$soI0cQwY!qT&!-m1mIy=01{fmKXqKd4_-NS*js9dE3SSd>L$I@oL zvf706WgON3GLt@tM+9M~j9nS)9`Wv8+H- zCkvFaCAkQ?=&e#sYW>L4N{{^NR!^%Ab8hYrs^=VbF6IB^do!^<)0LS~?b=A@hU1a$ zDwmr^ovREF82*rS>G4tZx~~D>t@15oG9RyQZG@IDR&?Q54G;XF&;My&R;plmY|$yS zo%N*w6QtI>F&u%ustNA1)2(Q}!+jtP>?B!gA-lnx!%gs%`8CSJmVAXI-7KM0gA^Pf zT2Jo&@$!}h8wl_QUzu+g{V$hF20G;Bv5XFLLNp_ImExMlr09zkW1{qJ6$P%GZ(#K- zN9MSo>GOQ|Tm>TSV6$ERZrQHbDzRL$(8M*I$O%3U_rKvxcL1D^m;yiyV5ibQ-%9 zCbdHc^)101e%ix}7XhjT(h0JS@3{>U6?pcWy}Uxq+>$PtptlLt-~>^=|8RBcb?-K* zjfNJ{A=8O{l^D@$#*XGOG+EU~i$VhBO;dUZI1n+)p5I&$?KfL&T7E`de~=l(PaR{$R0Q}p9^U+bzq{`~-$`pdEqU~r+= z#EqP@!pcGTySsm82|QUVr+khT`sJmSZWD> z?_C@l;N(J|@s%kdqqZ%BdZ1!}?wi%ih)Ln7p%@lpz`43rC=w_9^!gDjV>%f~E#UnD zW!I&i_`uDrh9y72I4_xhXjrRV$WjUe#M$C6(v}XDO*_tWJ>ef8ANw+WJBKq$u^v}y!5~Asa zoHof-We@#%IDU^^wN2mOBa8BC*!lK^z6t1FOkf25=i&;;GMqt`̒NubK#)G|Z0 zY7I3+xXovB?}}x%9QM6HG~KOzDx(m@6lAg`!H#vKrf{!VM`M`3;>V>M2U!t?<0qlR z>vq(8697kyNC$XXH{mQ-X*$pTyR>rK-K|qvAi2f?y-D~7Iltt-1Zp*sK0zM`R~(_D z3GVV4n-)XjHDR%Poh+blYwSvOO>x0V!x0se*lyOPWG&uZd-I-Xr(mNyBmq&}`_(gwp*iazI4KnN~8c0Ngp z35~ZPf9v`8e3C^#+)Zlb8T%VBejnSsr|`0N=u{brKN*%yC0Yr#)MgutLo4la#FuBd zurZb6Vb{h9!rk9a^qVI{04GN1{$)IT5abgI1jybcoVVpL3Z=yk-S|cdF+l`kLZir7 zfQ%6uUz;%IO3#S$m~Sz;fGWpVfzT_$h1D&z)OvgTFjh+AoFnN!A1?$x2TGMo?tebs z3wSy5`_y+5e@X0IR=KMC+Vm}&Uir1=lJy*yQ{b-k(y>)c5df~t50MBF!nb%2h@|SJLH7C}x)bH$HhRS;x6e}azx2*kc;D-$tRT6?zqwr- zkTW=nMn2E-Rk+4R=0RRZn4tm*JidMBF?@=Q=Gy(O(81*QI;A@fVxaAVK(Y5FOo~(` z7KBQn6r2F=L?o~cuD}5QK|P|lHd173F$CJm1Kv-F=_3IR(+G`|O&oAlufb0EkkK13 zWXCW1BgmI*DZ@UGj>}a2<*Z(J<=rZ!{}XIv86@GNep^lw`6r3~3eVUA2oUcs`;U?M z3!>oMt_F-%y03=;phrMMvAxJfKMisfqE!t9pqFX5$=gidlm@?bYZZO#B^IF%wPe4! zJ^us_D#4fWvm-jDItq+4dXA;wck*Nel6BiR#-+!K? zTZm!zAxf?PDiIG7`pUxRL@Q}xWh+Qu0Y$EI{Ri+K5Wa8JfTtbm0dNV&p=xRFo~vJ} zLj?>bE;^1K)f}N6gR=@3@%EFN;27`F7Vo$xHFOdhm(&;zV3nER8((={s{EEz)}&3S z+khhuLQNMD-_v&!7j~W8AoiwQPrS6U3VwkA<9VQCHC#~|>)flmf@nPhlQtO&5~Qf= z8&frr8H&wLB#DmBN-Af|PO$$>jQ|?;mwj|mD8M=a;U?hwPmI^rb@{D*H*ZnTb?S2_vTreZG8a5%QeI}r?t^Uc7aF^Y&@^0I;IqwZYyJnrStK0|AowAniq&u`x= zTo?ufjfPV`B;u;6G}^AK!(+p%OwP)K%SCgQMi=Utdg$$pcLtBYli3@)Q0!{YX-U?- zRJCnuqX3oyYyeIQ2j0NbYbx{-m<$BVL+1?&t@Cfl`{ugSm{Xc7XrT9A%xAWoRXwexeJL^Fkt(MW5W}vzLGaFcEE) z6`EbWrx)+~J)pV5sVsXUqGhPaqg$bJaHdM>4Etg71fK=mq`Jgpr9tX?#Z(Sb8Z(d* znnuXDx`9doH}+urE$P~J|^Go}X-3n5a&6#IyJ89;t*XM@fIe(4$_Aq^rv z;>W-9=U8o(v1Kjw=TE(mpfPgp(GrQ;&2N(q2Q7)8wN5r_RP(@Q?Zho2I#sa1iL}bD z)P`FA?qfHKsSVl-qvUL)FmVsD* z&Mlu!A`ZX%U6^SxW$c0Dh zUx%*)f-~KT_XR{)9=7|fVp9!Zv}D7NuoY!$o0|D3M2oM|xd;?qv%6Jc(UGKFS`pq^ z`KWA=%JxV_m>b97w<5LQ# z$prX-A;kPw*tv=XUW80Usah{M{|R!b2s6MtK0jF z$T5>ek@&x;p?hDl7{d7f@qj?}xps8kJi)UFIODaL6*z+{-=Fa}0gpE>scR3ljZmqe z*bPU72au6hbWzsj@UKv&I3Sha(^Yz4Z6oBWArPywL-zR#c&UxAW8qzpyt$CaVrYq? zPeW*YV;rK^e7w4X!-xflFrdnHY;nGrrSsKyU620LWRUqmR{W^Udgxyx2PDz^!pnim z=%I(Q$wWeS!vN zSDfJus+^6UPWr(G>kvq<%wuR76rz(>;kI&79}RbEXJb5%WcN7Jy_Ocv`>8b}wn1H4+;tU$6=}@jLm+lAx$5`h0|9 zCT&48fb<6A2~@UP0w1Uslu0g+hihYwYF7gL{TDkX_x9uCXCyD&EY!e@inx5~s0;eF zo}m?wb%>=UQR0VBvKBU0aT6K)r3=bcpFIejFcXGumjI81h)fu1BceB;e;40*!y9YJ zJ>D9pk3cTLh!Ed{t6rFiJ4wZJ(}aq#>`-*`FziTUa`r$6u6zie3k{~WSAw|ndpmCy zG{&YmV3tP2I^+af;!s5QI7a6Q^xJV?~|9UrH zq^#Cd?|vqXN<2ERCbRw`_0;;HT{U8neu?fdj7C#aPqV>=IETF)eQfg4*Wm3%1>dx3x@k7z=|+ z!#@*+H)JgB=I2=$Yqs{7&QyAA+%?k)TNxmn&lO$3qVYFp&4hq#tNOCFs^M!y2L;5) z+vVO4<>~(lkp5Z_33J5-*Z#~3|HMsy9e89CP_ftt1ok-2*t^jA5z#!%FiXKi;2OqygfB+K zPPKzhje+DS-0|xiH`wC|Yv0#oRgX2tp8u4Tp`UFO$-oO8z}kbI%2iJNw;8+F*Wz3##ndgA%~<~tj81`IGkBmRc6L!T7HGqelekDqra*Dl)TS~#9{39XOo4P+0aeH>0QZ=4Hc5!AG z*>mQO+#CBGRMdgYWo;CN4GP|SvxFtf}jTYja51JJp@Gr zlDZTV^9v~Bu3}QQ=2%#6#xA6IGz!V^7Lx;*Sjl`gc zy|w%EeR@{d9mU{*%L<~y_kCi`TDOt8IrKm$%MMh+$Svu637@JS)TPsHM|okM>-dta zGV<#9RGdfFyZ~<7bkkvT9k)NssKPz7EYU(M4$ZMywxH#jg-6!#&>a9WS7DsS9<8>$ zFEq2m|e|^HFYr)O0Ss* z9QtC{Dlk}A#WXQwKB|eQWDKf5 zbg=km=aue!+LYRB6S%yio6+ASje+x_Nhr2X(5 zLh3h+mR<{?7&#+ic}mEzi)fzfQ^LJYGlW=J_w&i~(JNS41!VH(OE;wuUL%`-vtFN) zU-xzbA1q_aw@(w`2mWdSyjZx8#u&UWp|q>KG>q{!ax@P%s&sFBtWboftlX{-NtBy__(>Hpvn{<5E49;iC%9k8-p+8T zh^?yF4%hm0dwBQA{?AW~BX;sOm0y&qVI~Dd?J!r;?MnRRISaRYAJJl82NJi} z#F0nzI9CiqW$Kb$aNb45-cdhKp2*|r(OKfC&Ff+=VAYjHJu*h`*XE-V{WJc^AdFBH z{C1bG))?P(8L3|1#^{aYk2yC(QSiI`!Tv-#Yx-4r{{NacVt+D*grFp5=9X7c$?qC3 zKRksWkG4}dhJ*)7Bs_fNX@9Z?x4L}e>xRrvS*VXDhFJGd9`Ax^C%`#MO+&#&Af@tGv2E4jo$FuzIEFGb8&pr~(xTPppclMw>;0R7`m6mPYB zZ9%10Z6IFAK54uyVPtV1+ALEj|Fpw;$8C94O}r7~pA}dwfYr0eK_l`uXKYxcw1Nl# zLbH>htL*Gn>Y@mIm^A_vhkZ|vofy07i2a=@8I1mfd&GPpeKg*yJ^7WD{+35Xd{>zY zO^gtNkr=50yl7Ja&Pc-S+Y7ct88cr8UM`1h0nQ0Al}n{>Bk8wR)wnTpxoP~>JZ^y( zvcoFyoKLQJRzovky{)gj;s$M8LA06)&cnsoJ$e35){4G5K}*%XVoiC71~rZuP!k7#^&z2ngRCn0<)y515qRG>&V5n zuK<=na={BLjU|vOmwa+_T2a(^!xw&x`aB`_Vehunqlx4x_ea^t>^$hvM2xW*#bxI! zrMIYc-cTgATSP61;_z9=tb~1S5pykCg!s+DAu*9_h*cnFxS-C97yj&_E{&UN&N2U5 zN)J@=Te22QLKTB7i%~qWC4h5`~Ck^bM58GxS1)Lh%aS z_0v2ii-m_F&(^{1RL^+kipPx!3>QGZk~GpH&fooxX}%`o5=d5=oygDCMOdfx1`|;$ zJr$~00%+Gl{Zf9ej_d^7`BO!5gWnJuT+uQpgd`WPP%C?;ju6zuy}0mVu**9?Wv$= z6Izu)9A}_Xl-Ul*h?lhPD|IaC$x-eeyHgvA_hpyMr@w@KD_NW}ovDcLw7PLsWtcct+?+ZYviOM?y%@z?QTbYC=nJ9dYA&1Yd2l zgTBQDuOUHUOZo!6x6pmH@TNiWVvYK0b8NoIYBf&)mhoNb-GHX-#G+Jkhp3s4!1WDX z%PDzik-(xaL{pi!YBH=>WTx}?Rd4r??7x<~*jMV6(+w^obk;!wqfKd+i^IXVn%W+4 z=vzKJ2d}-rOnmyZ7oS@m!=XqYBJ}CB=~j@w3q=a*_Q@_UwjH~?3;!dX-#tV@8&(Ue zo$`~wAIUjxAkye&)8`>_sW!&qp615+mc|1akvUgEBq;77ph#h-=B*+ym5tR4Y_%*g zm6|x)>@@1OI*+X%%pY_m$_m!8_M&u!!KfT-+;JBcUi&jAQ=2NQKytjEF>xg#s+?N= zA)!$mCSuf^hyMJtU#5}`DUjMG6*>9sV}xJ@R|lt24o0uLycS^n6ZLhx^Ew1`(~=z! znnb7JAew~$QqF-P$t)w^qpr}wcB1~vgdzv3Q|8y&pi87n!{<(S%J#@}tiz!f&=P5C zS<+;~7Wh_f;m&au4li69BQ4oHM|)V)V$;f=kQ`@5cs^j_M5QNcj~h4>(aGKu9GP8? zpMv*Jc20Ii05g+3chVH+_@y>H7udQu&4zfwu`U^d<9KBBQNyE98ePbKyhAl!gL)VZ zQ~G(t%55+IZbC*5)I;(X8GWTTEY}`i9=zD}vet)ny2wYYkYx1>ma`Rw6KB)Uyns5O zG%)9+Tt~B_af_28Oeoz^UT8`=Z>nAeG{SwzL`HB^H3g=?EKv#Z`C-r>d#hC9uV1r< zB*#pQ|6W#7!eCG0F((qsR|mVSRMnrZkhhkn}BJ`ZS+#-lyXcYzAOl0WFa>{hg zvd%t?c65nnN{1Oqd#nK=Re{WS@XCMtI6h@`lBd_FulQaay*?JwN<7tgFOry1I*X&7 zf@`aj9#t`4p2q>BsI{5TF>*8p%{pRPyqYuV+;!001EADhx+Uf;9i^s6v{t8jePDtyiX-LuSn2vh+lFe(KUpW2 zK8@0Cz;>T#QpicwuUUaw372A&V*7g6@|(j@7dl`88_Jb&1VPGfZAL%Gq6s&9TUA>J_t1xGZLiZBcouqE7r(@mI z5ANz_vNx(E?n6Cfi7K^BY(#fgWz%J_ti0qA-8@{P@!(I?98Ys^DIyFvB0xf_mB>zdbMFr7RC}pPcC7)+O8WEyO5H79E}MG^x@lyK z6i+V%dxg`DE%)`=yG?wvDyw(4#d87rJ_)KDiKh`iBTG>ZI}PN0*Fl>zAkjAhQPefZ z7ffA(ZL<^w}*q6z@wRJ)o(HEhgTi8i= z@d-ovBF~JI07K+Dgg6L&SNmPanHe>f`{(HD9_|B}${GYbMsYT&DQVqkNxkNoku1|0 zsLsj&w?`jDv>B`e2<_@w%?_C4u} zpJP70MtqCxt4*ztN$y4a5hD-D7M6i3JFG5Cc#SZ}+2Xl?qS=W;d+pK=$6g;`&D6&o zvBTO|S#oN_v^vO5Vy$m4xxS2TTi? zdAj5gcPZGI88e;VMSd7ApW1~=VFDkfd1i0~5rO~|2myCB_f2qUZg`2#b~PAvKFek5 z7C9sXKob?lh~@ppdUe;!&ZVz=OxDM?qx$SSY1xaRNTrA;;y4ur>^162UXGUyeEI9o zA8*9IkR8#oZO66AjM~MOb>4rvRx5Irjc4DPuv{qf`=pPLm=!~?$#P@Vi7mkFN-6PJ zLb!eCgdnK)MN?pA&XrqpT|%-J1#$p*VX5&hN@YsZWap}g^%|W#uLa>G6Eb53hC|@m z)KJARB2fJ$)@;}=H|zoe*QyBNNf1(+{8;x@x`z#GrkzHeuVd^M^fkN2bzs?9hhWxy z-~|m-lrjg0dsz4mDTq5b>_R-A;t2KnwbL37SQ59c7-P95h)TblsWPKQNXoP|;+#21 zgy#HCtn`vtRgAYqB1jzoT}Y%w8xLm~10wHSVYn7vp`J&EAsA53i)iRFka%&dWbn1w zFjsI7`k(+xH;pP${zAp%tU`c`QXru|^gSJR5VUfTaq6jH>4)$oH(|6x;iyQLUT%b@ zch9r_`odts_HF-WyK?YfO?cxkq{mPXNx=-0m}evrpsq83Or+Eht|FN) zKS#u`wN#`g1E;th{|%q+0a#=NjY-!G2)>vij^5g1a)DGAT9E5DNf@HFejq zEDCCVNaO8xOnUP!ZMW{+)senYF(x~W6@6Mgu-#dvoo&ocqdALqaka%?aDp=vSo3x4 zn~O9rFz7+sk(O)d(luv?f}m1tissG915wIO`)oSGBG7o*6diUhtB()PR0HN2__y86 zZ?AUUiewHcjJcml(FMVq^C$^YuTYB^-cL=I=?|B$ zFQDkP1Div)4mI|c+4hhudj%Z_w}xJ}TqW)si!fp)&fCA2mrO6t9!MGOk`Ej`C#p*0 z5LYDQiWQ%8jVQ2B8L-&1XWzk$iV?Opm_(tX+|1ZgXm%%@K$0G7RuB`A_d>gD9;V^u zb(Tm`PDhS3$#LfW!0Q$^mttCl(K)z>vznGgKMP-^#h8dueCH)9elncvuC$q`W>_$@ z!h;EaLPp`#4~~Q%R|vzJ5s<$+tblGEPW?Ru^!W*r>ebzcJQZ9&-b!RRm3ox-=T$1+SSM;#UDMUBWNW7pxaD&gKJO{X%!zrlFfHe`1X`{=1&Bvg4q~0 zQ=~x8JF}6eiLe$FJP0M%g?2$C+n%CVtc47=M(D>=7jwu=iY40B-)mEZIE=OY9QCct zzX?L|4f^X)Rr~EGLUXR?9!z*03iV&ka0ioB=?=hVC z;^!ZA2Hjpwu^OM z%@S89X(H*hL8+Pi1tj=g_+(`nqt2LiS$mk}YLTN=SVjW3cocr-T<)8N5Vi}4VT^SN z;1;va;F`Y2@+j;+@TzY>L+FN*jG^yQ*<+j7QxSMVchL<@0bM123`hFDQFq0_BKJjI z2N{S+5S-YB=#De#qqTcDLO&M)XF}cL+H#3jciKT(;f{ErQ77L^Gf#tl@KS3_%mC(T zIiRJ?=eSAt8Dmw0(z$xMI7n~ii0qRW%&lBlb|7@4P5N>sJ$VIPmUTO$FrF)USW`{(1k*9opK^SqOsA4%#8V(9uR?XjKBe98_*vhtZ&-fhogOW#;= zq)RclxdHW405oMe0ZaVh+|}k_L?_cZgphom-*cn5_;yrGuFV3K=enWr^>5}DtI4^( zYfti-=B#Xv_fhP&C`_SE9K0|y1*OXV!X~FEv!RmX7I~Ki?T6@iZhdbV3C`Pvg@H$bTJ;0$Ldl!Qa>6juxK-|3UC>S+~r8{_EW87-jB|Mph5KpIGO?^3W` z95OQnm6dQliS}3IExqG6zj_PwUzfB$%@foz8v3&2o*WN@6f#_v`}TJ_oM5B&YZuG2 z6WtNaVkmMrH<^?@7bOey2Xem(`O=ClUT zhX87xTP+vns}Ld%1eZ%RrdQ_{v!wk`q^1*W1?{5r02@^5Yc;;R^JJ-$J~i7o zS;(M(Wy$5$_VWJHH}dBiNFL9N8Lp=1!K{c272^=2D{^U|6-O_a$A;==^LZ`nKDv}!1jD%3 zwp=ro$TTe`m744#<8-oMV5?-EAHgn8e4EPYa@WOCDAsAeTXbEqLI(UGCEI(7*>NW*_Azhv@v0Z%G~@ zrs4z&IJw$vXp`2eFnLXLFkSSg9*@Q9p30-M$EMDh-FWZW z(?OMXc73Tnm>KS*pECKg%Vkn%sb2aA+pFXjq|#Lh&WUce3Jni^`Y6uaE$y9gW8yVd zMyJlSGF`kv7xeyy)k$&(AP#Q-vIxAUxszq*xtb{zcpU-?D*M>N3VVd&aYY$=`~d~B z(^{&pa($T}30;#rkG~*p{3R~~0X{X~8+_*_g<(n`Eno3qX70L;IDeb9&S3zlM$p`O zvu8&dN0g2ql}Fja5MJV5+vyO>)y6*1+a8@Cl*=5O%9Bef zQpOn5&*e1|IuvhrUIKUiQyw@wh#d*bLT5MwVYYaCJw8uR2Ur|Fg?la;8fPyughrud zhDpM-Uz*i@&YF^fQH6AfqC&X{o*=*c}Y;aHA#1|HG7E^LoS$3M;654!pA zcYc}^^F2t*bwzl2h(Ws>hoG+2?{!;#YiRd(slx2ubGJt)5hAOxg`8-&>9d0FCj(z_ zG{%sZEM%9;Na5x(Ysbtx>^c&{2ng5=&Dx6N`v}g2zDi44f)uC{j7}p@4<#Ne$cyrq zDcG<6xcFC$aoT)68=J_`c*hgy>P99Q%14}+(c`*;TOru5C@^owpfuzfZ$GZ0D42h< zNA7H^z#;ofsjF+s9uNqO-6tGZO86FT47lW&D>&#?V%$eUGskq$xxL2pYiL>Z2cu8+ z00-_88*YPr({14=+KBS%5rF+(xTskvRXSX$2$_;g{gexP6+x-3~RqaQ5XIc zUA7X~uIBDQUqqwS3>D859$JxJNdw=={FsF4%c33^oF}DwRyBt^X=&d^2XE^J@c@t| zM$`Hu+rqQ;2Y*CVLQ3?YzS{qYN-v$3E?K?=#>~_)&LBO+6MV|JJp=lXRx}3EP3}&@ zTBNr=TI_K-09=i~|M+lOUn;ZuNf!_-y+p-xa0JS06;uwKR0t?MqCll#^-DN#1s$i; zno*yoFG6kh*<8WNsz3BjQ*5*hp$4m7!%s*@>&8lxEdf=SQRHwMv)QO{&@np?K65_-n?t8l!lhq7 z9&ne%w&GZ73YVj$`!Z{a%&f*b^HDT*Z_u~hiodv=VK|8|$oJB$FlU9f>;?5rDz3aQ z&^gT!$`>rhng1E)N39m5?mx@?S_0p)c>Yz9pn0f}4wgOT_HDffg8H=!!#5k&RO8E^ zT_JW+zdE>qG$#*(-MCSE_ASJIH+)6u3CEVZ)TlD|ouMXcnSWvKockUr{N|-5O`$eO z^ljMo7w!rFR5FF6$jhF$g(!iZnhmk}(nl}K?P6WS_jar2EEee{;uSpccT6da$^)YR z@Dg+j!cq+A0Da@yiB9W}Ps`YL=^F~Gsv8!6a9h;Wbj4s$!XFQMPbCi$Z{vF6f@Oic!nn1JwtbMG+8)fI5?Ram<#(UJXWh{EKQXs)Otu%>6y~a-Z>{;$z|?0 z>vsP&Ep?^_@Rf_Gf?JEm;szQ-x{r#;nBOZ=>AabW{gi9j-Nl(P1o_ctW}{}42QsC9 zvfAw16)c;Uw7y6FV{sR?=pD@->9*WoOh|z zNADZzN~7&MaIFv~w?7&|qrn{k`%uZE6fyQL&av@JJ+AtSlk6Pqu#_(chZLgwjri+{ z@$bWZk<#|&fkpK}15<>n&9|r9jvo?o>_QEZKb1oX1q_cKn%obW9RQSADSdKpBth$+ zxWN9!32G9MkY6&oP5GAhy>`DL2Ld*P-r;4~R&lJY4dqK;YNvMX^8|VX<_f5FFuAcw z{&mnAW*b(SGh@W9tvLGuca6ct16Ydy7kMo7mwHu}%ZH^w+(Po_lIHSe{ScCodDozi zz+hrzDdrA2`qmt^+lmV13%vH$Vrjhd{lVrLh0IZAu?Y z^uIfkhRgu3f>4%y+&krD%0n1=cP^v!*>rT}{0DUZf>~t=r|fs_T1#1`_e!HC;s%p< z+OXQ4qgS5ad)Bqv0BYtl&44w2LhF88sd%Db)Ce-63wA9v(vy&)%Vy`w_zHbIV`fTA z_-uk22oBAr-_3{M-SKb2$}OLwB{s?&q|(n})wLe-!|X>~mR>3Xcr=d7KZ%|+cDD9$S}xlSQtQ^|Pr0 zKfUfMhIDQ5Yq!Xy zkfH^8d3E&3^Hcp^oJG=rl8fDzwXOitOHxk_T_x3ZxZQ1cz?a(831AGDwXq~WFWQZr zo;*(G{*wA;qJ%(Wie|&JIv-j`x|)W$2P>#Od!?bEFh;vpXg7Lf>do%>7w-H~MJf~t zKp>~aP8jF0`(khF@PHh_^exHT(#eS!^z%bGuiJ}DEyR}%avzd3zlhKV$8QO|_0)%x zBc?rWfQ-8vQ_vn+&a&>KHtQP7+ny=#LL>MX1KSzGVDtj+g;eY*Flt3YEu# zOf)?53Ec3r=m;UOqBCgOXyK7XWCFW*9SH~BBI6zBio;9vRW)@zBC5mqYB@)m34Q`` z-Fv1)htXrE6W(zSVrHEM*_|dTbv5|~p1M@_kV`7G=!S5nSzrHqUEmX>=C?-yp_fT6`Sx%Z=ab?>$7c10iFLC>;#I%6vlPy^$)0 zO`Nq=XVHnhTPdQu0N|5J~(lZ4ES-3J2o<&!`|7tXl)UEun)X%}#8>bp8_ z=ma(gr(HkwpvH+UR;26g6Ut}vdyFZ6@_ulJl&p1#zt3VANqoa$hTKnrmfUlFB(1EV;oXf|0(vP zzG?kWBrl?(@5EFy@CGx%c89!_ll(P|UBJ9AbnYr+W{y^pc+wk@U;33@`8yW8`qIf{ z=g`*r&mIbT-5JKuElb5$JdX%Jtg=7{}lr=c-A zij#7qjd%EV#Xy6;?hN5r-A%bFf26~W`b8EV>K@T;jBHrobL?El7r9q&0>0B&gf$}B zstsUV4pMGhSowZ&M_z$$jqr02h9-5+F8qQ5^;ylBhpS?3C<59BbmY9h6qU;1Rb<65z zSsO_i?PRM8jO#VP4Wzz>nZ?}VvPUljGAq4;f~8B?fxJ{K^+M0{3rvbCUvOcc=dvmT zl`a`Je4c-mEL4rRYh5^R_Q~s#p-aQ#T4mIig|n5hZR#jpD*n@v12v&u%z0*YZj;BCTw{haiAj=zSZ^t+ z$}b+Fq1ld2%NE=ZbG>NsNx*^ou36 zB;uUi^b?+!bl!Jy6E{$LuJiQ;L%NAKt%+15ShzWA8v?>uNet6_2i~8|hqrh3Pp|Y3 zJ;EI+szWqZC#$~*R>QZGk){-?d&83NC(}NFsxBRy#$N)cx`MIH<6G^2HqlDP*#e6@F=W;k|nb!+5vF~95 z7eYBO5dZKH)2ZDGH?amsG*s_kGho{3VO*!x?*@;jN}y||1uW-|?ItYM}%%-E`wU)`6 zDljr(GMT5WLv{rQSI-J9TD&nN|FQS=%kit^mGebF4$~%E)#?2?#j=3cN1HlvEKWk? z0C4YTAK>*GL5i!m`)+k>??};uX4b#_-%6!6rY70wD&V4HYYEJ>0|(W-vF=Hvr3<*p z2WkhMO1mW23e3{7*`<7`DQ2A3C?n4ePX`~@bcbw3J#Zqff4Urr+dqbmAs*{2un&T! z%QoHB2#>XwjkXFgBF=qJ70et4^QCnA?tMPdo@E-?9@KL!a19V%Owdr2gA4iu(!(a zl4%BMN`A$u&3w}PoQMX&!eTdxqh|4=rCqc8W*$91QUQ&8dzz;wY?B5;UD4#3XMh|g zJ0ovB^?Rq)x|Biy?JY)!Vz&b8;T!b>^GAjr>^+oaI>weIVs7N5U__F8n|ovg-9ZHf4U|q?4$ZAoAAn4|Uaj%0D&&{Nn_~Lh0(mf)J=S&SCX~ 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̟KXvXKq@80&)dTCk4hA^KO7WsRz5FouHF6V3} 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