💲

Compoundの内部実装について調べてみた Part2

に公開

とあるossのDeFiレンディングプロトコルにコントリビュートさせていただける機会があったので、レンディングプロトコルの先駆けであるCompoundの内部実装を調べてみることにしました。

ほぼメモかつ、途中のところが多々あるので、読み物としては非常に読みにくいかもですが、備忘録として容赦してもらえると、、🙏

誤り等あればぜひご指摘もお願いします!

Part1

https://zenn.dev/shodaimomiyama/articles/cfbcde469a02d9

Part2はCompoundのmain contractについてです。


main contracts

Comet.sol

  • Compoundの基本機能の本体

    基本設定・管理機能(内部運用・管理機能)

    1. initializeStorage(): コントラクトのストレージを初期化

      • 実際の実装

        function initializeStorage() override external {
            if (lastAccrualTime != 0) revert AlreadyInitialized();
            lastAccrualTime = getNowInternal();
            baseSupplyIndex = BASE_INDEX_SCALE;
            baseBorrowIndex = BASE_INDEX_SCALE;
        }
        
        • プロトコルの初期状態を設定
        • 最後の金利計算時間を現在時刻に設定
        • 供給と借入の基本インデックスを初期化
        • 実際の使われるフロー
          • デプロイ直後

            プロトコルがデプロイされた後、ガバナーまたは管理者が一度だけこの関数を呼び出して、内部状態のリセット・初期化を行います。

          • アップグレード前のセットアップ

            アップグレード可能なコントラクトの場合、ストレージ初期化処理としても使用されることがあります。

    2. pause(): プロトコルの各種アクション(供給、転送、引き出し、吸収、購入)を一時停止

      • 実際の実装

        function pause(
            bool supplyPaused,
            bool transferPaused,
            bool withdrawPaused,
            bool absorbPaused,
            bool buyPaused
        ) override external {
            if (msg.sender != governor && msg.sender != pauseGuardian) revert Unauthorized();
            pauseFlags = uint8(0) |
                (toUInt8(supplyPaused) << PAUSE_SUPPLY_OFFSET) |
                (toUInt8(transferPaused) << PAUSE_TRANSFER_OFFSET) |
                (toUInt8(withdrawPaused) << PAUSE_WITHDRAW_OFFSET) |
                (toUInt8(absorbPaused) << PAUSE_ABSORB_OFFSET) |
                (toUInt8(buyPaused) << PAUSE_BUY_OFFSET);
        }
        
        • ガバナーまたは一時停止ガーディアンのみが実行可能

        • 各機能(供給、転送、引き出し、吸収、購入)を個別に一時停止可能

        • ビットフラグを使用して効率的に状態を管理

          • ビットフラグを具体的にこう使用している

            個別のブール変数と比較すると…

            例えば、供給(supply)、転送(transfer)、引き出し(withdraw)、吸収(absorb)、購入(buy)の各機能の状態を個別のブール値で管理すると、各フラグごとにストレージを消費します。しかし、Solidityでは複数のブール値がまとめられずに、場合によっては各変数が32バイト(1ワード)を占めるため、ガスコストが増大する可能性があります。

            ビットフラグの利点

            1. ストレージ効率の向上:

            1つのuint8は8ビットを持っており、その中のそれぞれのビットを個々の機能のオン・オフ(True/False)状態として管理できます。例えば、最下位ビットを供給状態、次のビットを転送状態、というように割り当てれば、5つの状態を1バイトに収めることができます。

            1. ガスコストの削減:

            ストレージ操作はブロックチェーン上で高価なため、複数のブール値を一つの変数にまとめることで、ストレージ消費とその更新コストが大幅に減少します。

            1. 高速な操作:

            ビット演算(シフト演算、ビット和など)は非常に効率的です。特定のビットを操作することで、各機能が一時停止状態かどうかを迅速にチェック、更新できます。

            コードの動作について

            上記のコードでは、以下のような処理が行われています:

            1. 認証チェック:

            msg.senderがgovernorまたはpauseGuardianであるかを確認し、そうでなければ実行を中断します。

            1. 各ブール値をビットに変換&シフト:

            それぞれのブール値(supplyPaused、transferPaused、など)を、toUInt8を用いて数値(0または1)に変換します。

            この数値を定義済みのオフセット(例えばPAUSE_SUPPLY_OFFSETなど)分だけ左にシフトすることで、各ビットがどの位置に対応するか決定します。

            1. ビット和演算による統合:

            各シフト後の値をビット和(|演算子)で合成し、1つのuint8変数pauseFlagsにまとめます。これにより、例えば0b10110のような形で、どの機能が一時停止中かを一目で判断できる状態が作られます。

        • 実際の使われるフロー

          • 緊急停止機能(Circuit Breaker)

            この関数は、システムに異常が発生した場合やマーケットの急激な変動など、リスク管理の必要が生じたときに、プロトコル内の特定の機能(供給、転送、引き出し、吸収、購入)を一時的に停止するためのものです。

            こうした緊急停止は、スマートコントラクトに対する攻撃やその他の予期せぬ事態に迅速に対応するためのセーフガードです。

    3. withdrawReserves(): ガバナーがベーストークンの準備金を引き出す

      • 実際の実装

        function withdrawReserves(address to, uint amount) override external {
            if (msg.sender != governor) revert Unauthorized();
            int reserves = getReserves();
            if (reserves < 0 || amount > unsigned256(reserves)) revert InsufficientReserves();
            doTransferOut(baseToken, to, amount);
        }
        
        • ガバナーのみが実行可能
        • プロトコルの準備金から指定額を引き出す
        • 準備金が不足している場合はエラー
        • 実際に使われるフロー
          • 準備金の管理

            プロトコル内で生成される手数料やその他の収益として蓄積される「準備金」を、管理者が引き出すための関数です。

            これにより、必要に応じて内部の資金を外部に移動させたり、リスク管理や流動性調整に充てたりすることが可能になります。

    4. approveThis(): ガバナーがアセットの承認を設定

      • 実際の実装

        function approveThis(address manager, address asset, uint amount) override external {
            if (msg.sender != governor) revert Unauthorized();
            IERC20NonStandard(asset).approve(manager, amount);
        }
        
        • ガバナーのみが実行可能

        • 指定されたマネージャーにアセットの使用許可を付与

        • USDTなどの特殊なトークンの場合は注意が必要

        • ガバナーが実行する運用フロー

          1. ガバナンスプロセスによる意思決定:

          プロトコルの運用に関する提案(例えば、資産運用戦略の変更や流動性管理の見直し)が議論され、提案が承認されると、その実施フェーズにおいてガバナー(または管理者のMulti-SigウォレットやDAOコントロールアカウント)が具体的な操作を行います。

          1. 許可設定の実行:

          提案内容に基づいて、ガバナーが対象となる資産とマネージャーのペア、そしてその許可金額を決定した後、このapproveThis関数が呼ばれます。ここで、プロトコルが保有するERC20トークンについて、マネージャーに対して最大でどれだけの額を操作できるか(引き出しや送金が可能か)をシステム内部で設定します。

          1. 運用・リバランス操作の実現:

          許可設定が完了すると、マネージャーは設定された額の範囲内で資金の移動や運用が可能になります。たとえば、内部流動性の調整、市場状況に応じた急な対応、または外部戦略プロトコルとの連携など、運用上のさまざまな目的で資産が動かされることになります。

    アセット情報関連

    1. getPackedAssetInternal():アセット設定情報を効率的にストレージに保存するために、データをパックする

      • 実際の実装

        /**
             * @dev Checks and gets the packed asset info for storage
             */
            function getPackedAssetInternal(AssetConfig[] memory assetConfigs, uint i) internal view returns (uint256, uint256) {
                AssetConfig memory assetConfig;
                if (i < assetConfigs.length) {
                    assembly {
                        assetConfig := mload(add(add(assetConfigs, 0x20), mul(i, 0x20)))
                    }
                } else {
                    return (0, 0);
                }
                address asset = assetConfig.asset;
                address priceFeed = assetConfig.priceFeed;
                uint8 decimals_ = assetConfig.decimals;
        
                // Short-circuit if asset is nil
                if (asset == address(0)) {
                    return (0, 0);
                }
        
                // Sanity check price feed and asset decimals
                if (IPriceFeed(priceFeed).decimals() != PRICE_FEED_DECIMALS) revert BadDecimals();
                if (IERC20NonStandard(asset).decimals() != decimals_) revert BadDecimals();
        
                // Ensure collateral factors are within range
                if (assetConfig.borrowCollateralFactor >= assetConfig.liquidateCollateralFactor) revert BorrowCFTooLarge();
                if (assetConfig.liquidateCollateralFactor > MAX_COLLATERAL_FACTOR) revert LiquidateCFTooLarge();
        
                unchecked {
                    // Keep 4 decimals for each factor
                    uint64 descale = FACTOR_SCALE / 1e4;
                    uint16 borrowCollateralFactor = uint16(assetConfig.borrowCollateralFactor / descale);
                    uint16 liquidateCollateralFactor = uint16(assetConfig.liquidateCollateralFactor / descale);
                    uint16 liquidationFactor = uint16(assetConfig.liquidationFactor / descale);
        
                    // Be nice and check descaled values are still within range
                    if (borrowCollateralFactor >= liquidateCollateralFactor) revert BorrowCFTooLarge();
        
                    // Keep whole units of asset for supply cap
                    uint64 supplyCap = uint64(assetConfig.supplyCap / (10 ** decimals_));
        
                    uint256 word_a = (uint160(asset) << 0 |
                                      uint256(borrowCollateralFactor) << 160 |
                                      uint256(liquidateCollateralFactor) << 176 |
                                      uint256(liquidationFactor) << 192);
                    uint256 word_b = (uint160(priceFeed) << 0 |
                                      uint256(decimals_) << 160 |
                                      uint256(supplyCap) << 168);
        
                    return (word_a, word_b);
                }
            }
        
      • 目的

        ガス代やストレージコストの最適化。複数のパラメータを個別に保存するのではなく、ここでは 2 つの単語にまとめることで、ストレージ使用量を削減し、また後でまとめて一括で処理できるようにしてる。

        資産の設定情報(AssetConfig 構造体)を受け取り、その内容をガス効率の高い形で 2 つの 256 ビット整数(word_a と word_b)に圧縮・パッキングして返すための内部処理

      • 実装の詳細

        1. メモリからの取得とアセンブリ利用

          • AssetConfig の抽出

            関数は第1引数として AssetConfig の配列(assetConfigs)と、設定すべき資産のインデックス i を受け取る。

            • インデックス i が assetConfigs 配列の長さ以内かどうかをチェックし、範囲内の場合はアセンブリを用いて直接メモリ上から該当する AssetConfig をロードする。
            AssetConfig memory assetConfig;
                    if (i < assetConfigs.length) {
                        assembly {
                            assetConfig := mload(add(add(assetConfigs, 0x20), mul(i, 0x20)))
                        }
                    } else {
                        return (0, 0);
                    }
            
            • なんでassemblyを利用するのか→要はガス削減

              Solidityのメモリ配列とAssemblyによる要素アクセスの解説

              Solidity初心者向けに、以下のインラインアセンブリコードが何をしているか、背景知識も含めて解説します:

              assembly {
                  assetConfig := mload(add(add(assetConfigs, 0x20), mul(i, 0x20)))
              }
              

              このコードは、メモリ上の動的配列assetConfigsから、インデックスi番目の要素(構造体AssetConfig)を読み出しています。通常のSolidityコード(例えばassetConfigs[i])ではなくAssemblyを使ってアクセスする理由や、そのメリットについても説明します。

              メモリ上の配列と構造体の配置

              Solidityで動的配列をmemory(メモリ)上に保持する場合、メモリ上のレイアウトは以下のようになります :

              • 最初の32バイト:配列の長さ(要素数)が格納される
              • その後:配列の各要素が順番に並ぶ(各要素のサイズは32バイトの倍数)

              例えば、uint256[] memory arrというメモリ配列があれば、先頭の32バイトにarr.lengthが入り、その直後にarr[0], arr[1], … の値がそれぞれ32バイトで格納されます。

              構造体を要素とする配列の場合も基本は同じですが、構造体1要素あたりが複数の32バイト領域を占む点が異なります。Solidityでは、構造体の各メンバ変数もメモリ上では32バイト境界に配置されます(小さい型でも1つのスロットを使います) 。つまりメモリ上の構造体は、その各フィールドごとに最低32バイトを使うので、全体として32バイトの倍数サイズになります。

              AssetConfig構造体の場合、以下のフィールドがあります:

              • address asset(20バイト)
              • address priceFeed(20バイト)
              • uint8 decimals(1バイト)
              • uint64 borrowCollateralFactor
              • uint64 liquidateCollateralFactor
              • uint64 liquidationFactor
              • uint128 supplyCap

              このように合計7つのフィールドを持っています。メモリ上では各フィールドがそれぞれ32バイト幅で配置されるため、AssetConfig1要素あたり 7×32バイト = 224バイト(0xE0 バイト) を占有します(Solidityではuint8やuint64といった小さい型も、それぞれ独立した32バイト領域を占有します )。

              したがって、メモリ上のAssetConfig[] assetConfigs配列は次のように配置されています(要素数をNと仮定):

              • 配列先頭(アドレスP)に長さassetConfigs.length = N(32バイト)
              • 要素0の構造体データがその次から始まる(アドレスP+0x20が要素0の先頭)
                • assetConfigs[0].asset がアドレスP+0x20に格納(32バイト)
                • assetConfigs[0].priceFeed がアドレスP+0x40に格納(32バイト)
                • assetConfigs[0].decimals がアドレスP+0x60に格納(32バイト)
                • …以下略(各フィールドごとに+0x20ずつアドレスが進む)…
                • assetConfigs[0].supplyCap がアドレスP+0xD0に格納(32バイト)
                • ※ここまでで要素0は7つのスロット(0x20~0xD0)を使用し、合計0xE0バイト
              • 要素1の構造体データはその次(アドレスP+0x100が要素1の先頭)
                • assetConfigs[1].asset がアドレスP+0x100に格納
                • 以下同様にフィールドごとに32バイトずつ配置 …
              • 以下、要素iは先頭アドレスがP + 0x20 + 0xE0 * iとなり、各フィールドが順次32バイト配置される

              このように、メモリ上では配列長の直後に各要素(構造体)のデータが連続して並んでいることを押さえてください。

              mload

              add

              mul

              命令の意味

              Assemblyブロック内では、EVMの低レベル命令を直接使用できます。上記コードに登場する命令の意味は次のとおりです :

              • mload(p):メモリのアドレスpから32バイトのデータを読み込む命令です 。返り値は指定アドレスから32バイト読んだ後の値(uint256相当)になります。
              • add(x, y):整数値xとyを加算します 。ポインタ(アドレス)同士の演算にも使えます。
              • mul(x, y):整数値xとyを乗算します 。アドレスのオフセット計算などに利用します。

              Assemblyではこれらを組み合わせてポインタ演算(アドレス計算)を行い、任意のメモリ位置の値を取得できます。

              Assemblyコードの内容:配列要素の読み取り

              では、本題のコードを分解して見てみましょう:

              assembly {
                  assetConfig := mload(add(add(assetConfigs, 0x20), mul(i, 0x20)))
              }
              
              • assetConfigsは、関数引数で渡されたAssetConfig[]メモリ配列です。Assembly内でassetConfigsと記述すると、その値は配列の先頭アドレス(長さが格納されている位置)を指します。 実際、assetConfigsには配列長Nが入っているアドレス(前述のP)が格納されています。
              • add(assetConfigs, 0x20):配列先頭アドレスPに32バイト加算しています。これによって、配列要素が開始する先頭アドレス(つまりassetConfigs[0]が始まるアドレス)を計算しています。0x20はちょうど配列長の格納分を飛ばすオフセットです。
              • mul(i, 0x20):インデックスiに32バイトを乗じています。この結果は**「要素サイズの一部としてのオフセット」**を意味します。一見すると「i番目の要素」を指すように思えますが、ここでは0x20(32バイト)を掛けているため、配列内の32バイト単位のブロック数でオフセットを計算していることになります。
              • add( add(assetConfigs, 0x20), mul(i, 0x20) ):これは上記2つを組み合わせています。まずassetConfigs + 0x20で要素領域の先頭アドレスを得て、そこにi * 32バイトを加算します。結果として、**「配列の先頭から数えてi個分の32バイトブロックを進んだ位置」*のアドレスを得ます。ここで重要なのは、AssetConfig構造体1つあたりは本来7つの32バイトブロック(224バイト)ですが、コード上はi * 32バイトと計算している点です。これは後述するAssembly特有のテクニックによるもので、実質的に「i番目の構造体の先頭アドレス」*を計算する目的だと考えてください。
              • mload( … ):前段で計算したアドレスから32バイトを読み込みます。つまり、配列中のi番目の構造体の先頭32バイト分のデータを取得します。この先頭32バイトには、AssetConfig構造体の最初のフィールドであるasset(アドレス値)が格納されています。
              • assetConfig := ...:読み込んだ32バイトのデータを、Assemblyブロック外で定義されているローカル変数assetConfigに代入します。ここでassetConfigはSolidity上でAssetConfig memory assetConfig;と宣言されていた変数です。Assembly内で直接代入することで、Solidityの構造体変数に値を設定しています。

              一連の処理によって、結果的にassetConfigというローカル構造体変数が、assetConfigs[i]の内容を参照またはコピーした状態になります。今回のコードではAssemblyを使っていますが、これを高級なSolidity文に置き換えると概ね以下と同等の処理です:

              AssetConfig memory assetConfig = assetConfigs[i];
              

              ただし厳密には、Assembly版では構造体全フィールドを個別にコピーする代わりに先頭アドレスのポインタ操作で取得しているため、実装上若干挙動が異なります。例えば、AssemblyのテクニックによりassetConfigが直接メモリ上の配列要素を指す参照のような形になり、余分なコピーを省いている可能性があります。そのためコード上ではmul(i, 0x20)と最小限の計算になっているわけです。

              通常の

              assetConfigs[i]

              アクセスとの差異

              Solidityでは通常、メモリ配列に対してassetConfigs[i]と書けばi番目の要素(構造体)にアクセスできます。しかし、内部では以下のような処理が行われます:

              • iが範囲外でないか自動で境界チェックが入る(オーバーラン防止のためのrequire相当)
              • 構造体全体を新たなメモリ領域にコピーして、ローカル変数assetConfigに値をセットする(各フィールドを読み出して代入する)

              一方、Assemblyを用いた場合、上記を手動で最適化できます。今回のコードでは、既にSolidity側でif (i < assetConfigs.length) {...}というチェックを行っているため境界チェックを二重に行う必要がありません。また、Assembly内で直接メモリアドレスを計算して読み込むことで、**構造体のコピー処理を簡略化(あるいは参照として扱う)**しています。これは高級言語の安全機能をバイパスしているため注意が必要ですが、その分効率的です。

              まとめると、通常のassetConfigs[i]ではSolidityが面倒を見る処理を、Assemblyでは開発者が手動で行い、不要なオーバーヘッドを避けているのです。

              Assemblyを使う理由とメリット

              このように低レベルのインラインアセンブリを使ってメモリ配列にアクセスする主な理由は、ガス消費の削減と柔軟なメモリ操作にあります。

              • ガスコストの削減:高レベルの構造体コピーでは複数回のメモリアクセスが発生しますが、Assemblyではポインタ演算と1回のmloadで済ませています。そのため不要なメモリアクセスを減らし、ガス消費を抑えています (Assemblyブロックは最適化や低コスト化のために用いられることがあります)。特に構造体が大きい場合、この差は顕著です。今回のAssetConfigは複数のフィールドを持ちますが、Assemblyでは実質1つのスロット操作で取得している点が効率的です。
              • 高レベル言語の制約回避:Solidityの通常の文法では直接扱えないようなポインタ演算やメモリ参照を行えるのもメリットです。例えば「構造体配列の先頭アドレスから任意のオフセットにある値を取得する」といった処理は、高級な記述では困難ですがAssemblyなら容易に書けます。今回のケースでは、Assemblyを使うことで配列要素への参照を直接操作し、コピーせずにデータを読むという挙動を実現しています。これはSolidityの型システムでは通常許されない操作ですが、Assemblyなら可能です。
              • 型変換の柔軟性:Assemblyでは32バイトのデータを任意の型として解釈できます。構造体ごとuint256に詰めたり、ビット演算でフィールドを組み合わせることも容易です。実際この後のコードでは、読み取ったassetConfigから各フィールドを取り出しつつ、2つのuint256(word_a, word_b)にパックしています。Assemblyによって低レベルなビットシフトやマスク操作でこれらを効率よく実現できます。

              以上の理由から、Compound Financeのような高度に最適化されたプロジェクトでは、ガス節約や高度なデータ操作のためにAssemblyが用いられています。

              注意とまとめ

              インラインAssemblyを使うと、このようにメモリ上のデータ構造を自在に操作できますが、その反面Solidityの安全機構(型チェックやメモリ境界チェックなど)を回避してしまうためバグを生じさせやすくなります 。初心者の方はまず高級な方法で確実に動く実装を書き、それから必要に応じてAssemblyによる最適化を検討すると良いでしょう。

              今回のコードでは、AssetConfig[]メモリ配列からi番目のAssetConfigを取得する処理を丁寧に手動実装しました。背景にあるメモリ配置の仕組み(配列長と要素データのレイアウト、構造体のメモリ上の並び)を理解することで、Assemblyのアドレス計算も読み解けるようになります。

              最後に要点を整理します:

              • メモリ配列のレイアウト:最初の32バイトに長さ、その後に要素が順に並ぶ(各要素は32バイト単位) 。構造体要素の場合も同様だがフィールドごとに32バイト使う 。
              • Assemblyによるアドレス計算:add(add(assetConfigs, 0x20), mul(i, 0x20))で「配列先頭+32バイト+i×32バイト」のアドレスを求め、mloadでそのアドレスの32バイトを読み込む。
              • 高級操作との違い:Assemblyでは境界チェックや自動コピーが無いため、自前でチェックしつつ効率良くデータ取得が可能。
              • メリット:ガス削減と柔軟なメモリ操作。特に大きな構造体配列を扱う場合に有効。

              以上の点を踏まえれば、冒頭のAssemblyコードが何をしているのか理解できるでしょう。通常は高級な記述で十分ですが、Solidityに慣れてきたらAssemblyを用いた最適化手法も少しずつ学んでみてください。

        2. 基本的な入力チェックと早期リターン

          • アセットが存在するか

            取得した AssetConfig 内の asset(対象資産のアドレス)がゼロアドレスの場合、意味のある設定が存在しないと判断し、即座に (0, 0) を返す。

            // Short-circuit if asset is nil
                    if (asset == address(0)) {
                        return (0, 0);
                    }
            
          • 外部コントラクトとの整合性チェック

            • 価格フィードのアドレス (priceFeed) に対して、実際の価格フィードコントラクトを呼び出して(IPriceFeed)、その decimals(小数点桁数)が定数 PRICE_FEED_DECIMALS と一致するかを確認。

              if (IPriceFeed(priceFeed).decimals() != PRICE_FEED_DECIMALS) revert BadDecimals();
              
            • 同様に、対象資産のトークンコントラクト(IERC20NonStandard)の decimals が、AssetConfig 内の設定値と合致しているかをチェック。

              if (IERC20NonStandard(asset).decimals() != decimals_) revert BadDecimals();
              

            これにより、設定ミスや不整合により将来的な計算誤差が発生するのを防ぐ。

        3. 担保係数や供給上限に関するバリデーション

          • 担保係数の整合性チェック
            • borrowCollateralFactor(借入担保係数)と liquidateCollateralFactor(清算担保係数)の関係について、借入担保係数が清算担保係数以上であってはならない(不適切な設定なので revert)とチェック。

              if (assetConfig.borrowCollateralFactor >= assetConfig.liquidateCollateralFactor) revert BorrowCFTooLarge();
              
            • また、清算担保係数がシステムで許容される最大値(MAX_COLLATERAL_FACTOR)を超えていないかを検証。

              if (assetConfig.liquidateCollateralFactor > MAX_COLLATERAL_FACTOR) revert LiquidateCFTooLarge();
              
        4. スケーリングとパッキング処理

          • 値のデスケーリング(descaling)

            担保資産の各種ファクター(係数)を保存するためのストレージ最適化とその後のビットパッキングのための処理

            1. 基本概念
              • FACTOR_SCALE = 1e18 - ファクター(係数)の最大スケール(標準的なEthereumの18桁小数表現)
              • descale = FACTOR_SCALE / 1e4 = 1e14 - ファクターを4桁の精度に縮小するための係数
            2. 何をしているのか
              1. データ圧縮
                • borrowCollateralFactor(借入担保係数)、liquidateCollateralFactor(清算担保係数)、liquidationFactor(清算係数)は元々18桁の小数(1e18スケール)
                • これらを16ビット整数(uint16)に圧縮して保存するために、4桁の精度(1e4スケール)に縮小
            uint64 descale = FACTOR_SCALE / 1e4;
            uint16 borrowCollateralFactor = uint16(assetConfig.borrowCollateralFactor / descale);
            uint16 liquidateCollateralFactor = uint16(assetConfig.liquidateCollateralFactor / descale);
            uint16 liquidationFactor = uint16(assetConfig.liquidationFactor / descale);
            
          • 供給上限の正規化:

            • assetConfig.supplyCap は、対象資産の小数点情報(decimals)に基づいて、実際の「整数単位」(whole units)に変換されます。これにより、供給上限が小数の桁を持たない値で管理されます。

              uint64 supplyCap = uint64(assetConfig.supplyCap / (10 ** decimals_));
              
        5. 2 つの 256 ビット単語へのパッキング

          • ビットシフト演算子

            << - 左辺の値を右辺のビット数だけ左にシフトする

            • uint160(asset) << 0 → アドレス値をシフトなしで配置(0-159ビット目)
            • uint256(borrowCollateralFactor) << 160 → 借入担保係数を160ビット目から配置
            • uint256(liquidateCollateralFactor) << 176 → 清算担保係数を176ビット目から配置
            • uint256(liquidationFactor) << 192 → 清算係数を192ビット目から配置
          • word_a の組み立て:

            • 下位 160 ビット: 資産のアドレス(asset)

            • 次の 16 ビット(ビット 160~175): borrowCollateralFactor

            • 次の 16 ビット(ビット 176~191): liquidateCollateralFactor

            • 次の 16 ビット(ビット 192~207): liquidationFactor

              word_a (256ビット):
              +----------------+----------------+----------------+----------------+
              |  asset address |    borrowCF    |   liquidateCF  | liquidationF   |
              |    (160ビット)  |    (16ビット)   |    (16ビット)   |   (16ビット)    |
              +----------------+----------------+----------------+----------------+
              0               159              175              191              207
              
          • word_b の組み立て:

            • 下位 160 ビット: 価格フィードのアドレス(priceFeed)

            • 次の 8 ビット(ビット 160~167): 対象資産の decimals 情報

            • 残りのビット(ビット 168 以降): 供給上限(supplyCap)

              word_b (256ビット):
              +----------------+--------+------------------+------------------------+
              | priceFeed addr | decimals|    supplyCap    |        未使用          |
              |   (160ビット)   | (8ビット)|    (64ビット)    |                      |
              +----------------+--------+------------------+------------------------+
              0               159      167                231                     255
              
    2. getAssetInfo(): 指定されたインデックスのアセット情報を取得

      • 実際の実装

        /**
             * @notice Get the i-th asset info, according to the order they were passed in originally
             * @param i The index of the asset info to get
             * @return The asset info object
             */
            function getAssetInfo(uint8 i) override public view returns (AssetInfo memory) {
                if (i >= numAssets) revert BadAsset();
        
                uint256 word_a;
                uint256 word_b;
        
                if (i == 0) {
                    word_a = asset00_a;
                    word_b = asset00_b;
                } else if (i == 1) {
                    word_a = asset01_a;
                    word_b = asset01_b;
                } else if (i == 2) {
                    word_a = asset02_a;
                    word_b = asset02_b;
                } else if (i == 3) {
                    word_a = asset03_a;
                    word_b = asset03_b;
                } else if (i == 4) {
                    word_a = asset04_a;
                    word_b = asset04_b;
                } else if (i == 5) {
                    word_a = asset05_a;
                    word_b = asset05_b;
                } else if (i == 6) {
                    word_a = asset06_a;
                    word_b = asset06_b;
                } else if (i == 7) {
                    word_a = asset07_a;
                    word_b = asset07_b;
                } else if (i == 8) {
                    word_a = asset08_a;
                    word_b = asset08_b;
                } else if (i == 9) {
                    word_a = asset09_a;
                    word_b = asset09_b;
                } else if (i == 10) {
                    word_a = asset10_a;
                    word_b = asset10_b;
                } else if (i == 11) {
                    word_a = asset11_a;
                    word_b = asset11_b;
                } else {
                    revert Absurd();
                }
        
                address asset = address(uint160(word_a & type(uint160).max));
                uint64 rescale = FACTOR_SCALE / 1e4;
                uint64 borrowCollateralFactor = uint64(((word_a >> 160) & type(uint16).max) * rescale);
                uint64 liquidateCollateralFactor = uint64(((word_a >> 176) & type(uint16).max) * rescale);
                uint64 liquidationFactor = uint64(((word_a >> 192) & type(uint16).max) * rescale);
        
                address priceFeed = address(uint160(word_b & type(uint160).max));
                uint8 decimals_ = uint8(((word_b >> 160) & type(uint8).max));
                uint64 scale = uint64(10 ** decimals_);
                uint128 supplyCap = uint128(((word_b >> 168) & type(uint64).max) * scale);
        
                return AssetInfo({
                    offset: i,
                    asset: asset,
                    priceFeed: priceFeed,
                    scale: scale,
                    borrowCollateralFactor: borrowCollateralFactor,
                    liquidateCollateralFactor: liquidateCollateralFactor,
                    liquidationFactor: liquidationFactor,
                    supplyCap: supplyCap
                 });
            }
        
    3. getAssetInfoByAddress(): アドレスからアセット情報を取得

    4. getPrice(): 価格フィードから現在の価格を取得

    5. getCollateralReserves(): プロトコルの担保準備金残高を取得

    6. getReserves(): プロトコルのベーストークン準備金を取得

    金利・インデックス関連

    1. getSupplyRate(): 供給金利を計算
    2. getBorrowRate(): 借入金利を計算
    3. getUtilization(): ベースアセットの利用率を計算
    4. accrueInternal(): 金利と報酬を計算
    5. accrueAccount(): アカウントの金利と報酬を計算

    供給・借入関連

    1. supply(): アセットをプロトコルに供給
    2. supplyTo(): 指定アドレスにアセットを供給
    3. supplyFrom(): 指定アドレスからアセットを供給
    4. withdraw(): アセットをプロトコルから引き出す
    5. withdrawTo(): 指定アドレスにアセットを引き出す
    6. withdrawFrom(): 指定アドレスからアセットを引き出す

    転送関連

    1. transfer(): ベーストークンを転送
    2. transferFrom(): 指定アドレスからベーストークンを転送
    3. transferAsset(): アセットを転送
    4. transferAssetFrom(): 指定アドレスからアセットを転送

    清算・吸収関連

    1. absorb(): 複数のアンダーウォーターアカウントをプロトコルのバランスシートに吸収
    2. buyCollateral(): ベーストークンを使用して担保を購入
    3. quoteCollateral(): 担保アセットの見積もりを取得

    残高確認関連

    1. totalSupply(): 流通中の総トークン数を取得
    2. totalBorrow(): 総借入額を取得
    3. balanceOf(): アカウントの正のベース残高を取得
    4. borrowBalanceOf(): アカウントの負のベース残高を取得
    5. isBorrowCollateralized(): アカウントが借入に十分な担保を持っているか確認
    6. isLiquidatable(): アカウントが清算可能かどうかを確認

Discussion