Back to Blog

Radiant V2 セキュリティテストレポート

Code Auditing
March 23, 2023
38 min read

レポートマニフェスト

アイテム 説明
クライアント Radiant Capital
ターゲット Radiant V2

バージョン履歴

バージョン 日付 説明
1.0 2023年3月15日 初版
2.0 2023年3月21日 第二版

1. はじめに

1.1 セキュリティテストについて

Radiant Capital様よりご依頼いただき、Radiant V2のスマートコントラクトに対して、潜在的なリスクを特定するために(レッドチームとして)セキュリティテストを実施いたしました。責任あるチームとして、Radiant Capital様はセキュリティを重視されています。そのため、複数のセキュリティ企業による監査を実施済みであるにも関わらず^1、スマートコントラクトのセキュリティ強化にさらなる努力を注ぐことを決定されました。

セキュリティテストは、その目標と要件において、セキュリティ監査とは異なります。具体的には、セキュリティテストは、攻撃者を模倣してプログラム/プロトコルを破壊することにより、追加的/異常な脆弱なポイントを発見することを目的としていますが、セキュリティ監査は、可能な攻撃サーフェスを列挙することによって、比較的包括的なセキュリティチェックを提供することを目的としています。そのため、セキュリティテストでは、限られた時間とリソースのために、セキュリティ監査で特定される可能性のある複雑なロジックバグをすべてカバーできない可能性があります。

1.2 対象コントラクトについて

情報 説明
タイプ スマートコントラクト
言語 Solidity
アプローチ 静的解析、動的解析、半自動および手動検証

対象リポジトリは Radiant_v2.1.1 です。セキュリティテスト中のコミットSHA値は以下の通りです。本レポートは、最初のバージョン(すなわちバージョン1)およびレポートの問題を修正するための新しいコード(以下のバージョン)のみを担当します。

なお、本レポートは、このリポジトリのradiant_v2.1.1/contractsフォルダ内のスマートコントラクトのみを対象としています。これには以下が含まれます。

  • bounties
  • deployments
  • flashloan
  • leverage
  • lock
  • oracles
  • staking
  • zap
  • eligibility
  • misc
  • oft
  • protocol
  • stargate

バージョン8の更新後、本セキュリティテストで対象となるファイルは以下の通りです。

  • lending/AaveOracle.sol
  • lending/AaveProtocolDataProvider.sol
  • lending/ATokensAndRatesHelper.sol
  • lending/StableAndVariableTokensHelper.sol
  • lending/UiPoolDataProviderV2V3.sol
  • lending/UiPoolDataProvider.sol
  • lending/WETHGateway.sol
  • lending/WalletBalanceProvider.sol
  • lending/configuration
  • lending/flashloan
  • lending/lendingpool
  • lending/tokenization
  • radiant/accessories
  • radiant/eligibility
  • radiant/oracles
  • radiant/staking
  • radiant/token
  • radiant/zap

1.3 セキュリティモデル

リスクを評価するために、OWASPリスクレーティング方法論^2および共通脆弱性識別子^3など、業界および学術界で広く採用されている標準または提案に従います。リスクの全体的な重大度は、可能性影響によって決定されます。具体的には、可能性は、脆弱性が攻撃者によって発見され悪用される可能性を推定するために使用され、影響は、正常な悪用が成功した場合の結果を測定するために使用されます。

本レポートでは、可能性と影響の両方をそれぞれの2つの評価に分類し、それらの組み合わせを表1.1に示します。

Table 1.1: Vulnerability Severity Classification
Table 1.1: Vulnerability Severity Classification

したがって、本レポートで測定された重大度は、の3つのカテゴリに分類されます。完全を期すために、リスクを適切に判断できない状況をカバーするために未定も使用されます。

さらに、発見された項目のステータスは、以下の4つのカテゴリのいずれかに分類されます。

  • 未定 まだ応答がありません。

  • 承認済み 項目はクライアントによって受信されましたが、まだ確認されていません。

  • 確認済み 項目はクライアントによって認識されましたが、まだ修正されていません。

  • 修正済み 項目はクライアントによって確認され、修正されました。

2. 自動セキュリティテスト

2.1 自動静的セキュリティテスト

Slitherに基づく社内静的解析ツールを使用して、脆弱性の存在をチェックしました。結果を手動で確認したところ、問題は見つかりませんでした。詳細なテスト結果は、付録の表4.1に記載されています。

2.2 自動動的セキュリティテスト

ファジング技術を使用して、対象コントラクトの堅牢性、信頼性、および精度をテストしました。具体的には、ファジングプロセスの初期シードは、関数セマンティクスとコントラクトテストスクリプトに基づいて決定されます。オンチェーン環境をシミュレートするために、コントラクト LendingPool および MultiFeeDistribution とやり取りしたアドレスのセットも維持しています。

ファザーは、トランザクションシーケンス生成中に、関数セマンティクスも考慮します。たとえば、コントラクト MultiFeeDistribution の関数 stake およびコントラクト LendingPool の関数 deposit は、シーケンスで最初に呼び出される可能性が高いです。関数パラメータとシーケンスへの変更は、コントラクトコードカバレッジによってガイドされます。特定のパラメータまたはシーケンスがより高いコードカバレッジに達した場合、次のファジングラウンドで変更される優先順位が高くなります。マジックナンバーによって制約されるパスを探索するために、実行時にストレージから読み取られた値(すなわち、SLOAD命令)を収集し、変更プロセス中にそれらを関数パラメータとして使用します。

合計で100,000件のテストケースを生成し、31個のオラクルを使用して、障害が発生したかどうかを検出します。各テストケースには、指定された順序で30個のトランザクションが含まれています。最終的に、1つのクリティカルな問題(すなわち、セクション3.2.6)を発見しましたが、これは手動セキュリティテストプロセスでも発見されました。詳細なテスト結果は、付録の表4.2、4.3、および4.4に記載されています。

3. 手動セキュリティテスト

手動による作業を通じて、全体的な設計と異なるモジュール間の相互作用を理解し、以前の研究と経験から導き出された潜在的な攻撃サーフェスに関するノウハウに基づいてセキュリティテストを実施しました。

合計で17件の潜在的な問題が見つかりました。さらに、3件の推奨事項と1件の注記が以下にあります。

  • 高リスク:2件
  • 中リスク:8件
  • 低リスク:7件
  • 推奨事項:3件
  • 注記:1件
ID 重大度 説明 カテゴリ ステータス
1 関数ポインタのリセットのための予約済みインターフェースがない ソフトウェアセキュリティ 修正済み
2 オラクルの不適切な計算 DeFiセキュリティ 修正済み
3 BaseBounty を介した潜在的な資金流出 DeFiセキュリティ 修正済み
4 潜在的な無効な放出スケジュール DeFiセキュリティ 修正済み
5 スキップ可能な放出スケジュール DeFiセキュリティ 確認済み
6 マイグレーション中の交換レートの変更 DeFiセキュリティ 修正済み
7 _transfer() の不適切な実装 (I) DeFiセキュリティ 修正済み
8 UniV2TwapOracle での期間チェックの欠如 DeFiセキュリティ 修正済み
9 返金不可のダストトークン DeFiセキュリティ 修正済み
10 _transfer() の不適切な実装 (II) DeFiセキュリティ 修正済み
11 操作可能な複合報酬 DeFiセキュリティ 修正済み
12 setLeverager() でのアクセス制御の欠如 DeFiセキュリティ 修正済み
13 addLiquidityWETHOnly() でのスリッページチェックがない DeFiセキュリティ 確認済み
14 loopETH() での borrowRatio チェックの欠如 DeFiセキュリティ 修正済み
15 setPoolIDs() での assets と poolIDs の長さチェックの欠如 DeFiセキュリティ 修正済み
16 addBountyContract() での mint 権限剥奪の欠如 DeFiセキュリティ 確認済み
17 Minters は一度だけ割り当て可能 DeFiセキュリティ 確認済み
18 - ガス最適化 (Mfd の zapVestingToLp()) 推奨事項 修正済み
19 - BountyManager で空でない Bounty Reserve 推奨事項 修正済み
20 - requiredUsdValue() での一貫性のない命名 推奨事項 確認済み
21 - 廃止された MFDPlus の注記 注記 確認済み

詳細は以下のセクションで提供されます。

3.1 ソフトウェアセキュリティ

3.1.1 潜在的な問題1:関数ポインタのリセットのための予約済みインターフェースがない

アイテム 説明
重大度
ステータス バージョン7で修正済み
導入元 バージョン1

説明 3つの関数、getLpMfdBounty()、getChefBounty()、getAutoCompoundBounty()は、コントラクト BountyManager を介して関数ポインタで呼び出されます。一方、OwnableUpgradable からの継承は、このコントラクトがプロキシの実装になることを示しています。これは、実装コントラクトが将来アップグレードされる可能性があり、関数ポインタに関連する問題が発生することを示唆しています。

function initialize(
        address _rdnt,
        address _weth,
        address _lpMfd,
        address _mfd,
        address _chef,
        address _priceProvider,
        address _eligibilityDataProvider,
        uint256 _hunterShare,
        uint256 _baseBountyUsdTarget,
        uint256 _maxBaseBounty,
        uint256 _bountyBooster
    ) external initializer {
        require(_rdnt != address(0));
        require(_weth != address(0));
        require(_lpMfd != address(0));
        require(_mfd != address(0));
        require(_chef != address(0));
        require(_priceProvider != address(0));
        require(_eligibilityDataProvider != address(0));
        require(_hunterShare <= 10000);
        require(_baseBountyUsdTarget != 0);
        require(_maxBaseBounty != 0);
 
        rdnt = _rdnt;
        weth = _weth;
        lpMfd = _lpMfd;
        mfd = _mfd;
        chef = _chef;
        priceProvider = _priceProvider;
        eligibilityDataProvider = _eligibilityDataProvider;
 
        HUNTER_SHARE = _hunterShare;
        baseBountyUsdTarget = _baseBountyUsdTarget;
        bountyBooster = _bountyBooster;
        maxBaseBounty = _maxBaseBounty;
 
        bounties[1] = getLpMfdBounty;
        bounties[2] = getChefBounty;
        bounties[3] = getAutoCompoundBounty;
        bountyCount = 3;
 
        slippageLimit = 10;
        minDLPBalance = uint256(5).mul(10 ** 18);
 
 
        __Ownable_init();
        __Pausable_init();
    } 

リスト 3.1: BountyManager.sol

影響 上記3つの関数のオフセットが変更された場合、関数ポインタは期待どおりに機能せず、コントラクトのロジック全体が変更される可能性があります。

提案 コントラクトは、関数ポインタをリセットするためのインターフェースを提供する必要があります。

3.2 DeFiセキュリティ

3.2.1 潜在的な問題2:オラクルの不適切な計算

アイテム 説明
重大度
ステータス バージョン11で修正済み
導入元 バージョン1およびバージョン4

説明 コントラクト ComboOracle の関数 consult() は、複数のソースから平均価格を計算するために使用されます。バージョン1の実装では、算術平均を使用して最終価格を計算しており、これは単一のソースオラクルに影響を与えることで操作される可能性があります。

function consult() public view override returns (uint256 price) {
        require(sources.length != 0);

        uint256 sum;
        for (uint256 i = 0; i < sources.length; i++) {
            uint256 price = sources[i].consult();
            require(price != 0, "source consult failure");
            sum = sum.add(price);
        }
        price = sum.div(sources.length);
    }

リスト 3.2: ComboOracle.sol

バージョン4の実装では、平均価格が最低価格×1.025より大きい場合、最低価格が返されます。ただし、ソースオラクルのいずれかからの戻り値が異常に低い場合、それでも戻り値を操作できます。

/**
    * @notice Calculated price
    * @return price Average price of several sources.
    */
   function consult() public view override returns (uint256 price) {
       require(sources.length != 0);

       uint256 sum;
       uint256 lowestPrice;
       for (uint256 i = 0; i < sources.length; i++) {
           uint256 price = sources[i].consult();
           require(price != 0, "source consult failure");
           if (lowestPrice == 0) {
               lowestPrice = price;
           } else {
               lowestPrice = lowestPrice > price ? price : lowestPrice;
           }
           sum = sum.add(price);
       }
       price = sum.div(sources.length);
       price = price > ((lowestPrice * 1025) / 1000) ? lowestPrice : price;
   }

リスト 3.3: ComboOracle.sol

影響 ComboOracleからの戻り価格は操作可能であり、攻撃者が利益を得ることができます。

提案 平均値ではなく中央値を使用することをお勧めします。ソースオラクルが2つだけで、大きな差がある場合、平均価格が最低価格よりもかなり大きい場合は、トランザクションをロールバックするのがより合理的です。

フィードバック ソースオラクルは2つだけになります。大きな差がある場合は、関連するコントラクトを一時停止するためにOZ Defender Sentinelを使用します。

注記 コントラクト ComboOracle は削除され、使用されなくなりました。

3.2.2 潜在的な問題3:BaseBounty を介した潜在的な資金流出

アイテム 説明
重大度
ステータス バージョン4で修正済み
導入元 バージョン1

説明 ユーザーは、固定期間トークン(RDNT)をロックして報酬を得ることができます。ロックが期限切れになると、他のユーザーは executeBounty() 関数を呼び出して、このユーザーのトークンを再ロックして BaseBounty を獲得できます(AutoRelock が有効になっている場合)。再ロックプロセス中、期限切れのロックはクリアされ、内部関数 _cleanWithdrawableLocks() でプールに再ステークされます。ただし、maxLockWithdrawPerTxn という変数があり、クリアできるロックの最大数を制限しています。この場合、executeBounty() 関数が実行された後でも、クリアされていない期限切れのロックが存在する可能性があります。これにより、MFDPlus コントラクトの claimBounty() 関数の106行目のチェックがバイパスされ、issueBaseBounty が true に設定されて返される可能性があります。

**
    * @notice Withdraw all lockings tokens where the unlock time has passed
    */
   function _cleanWithdrawableLocks(
       address user,
       uint256 totalLock,
       uint256 totalLockWithMultiplier
   ) internal returns (uint256 lockAmount, uint256 lockAmountWithMultiplier) {
       LockedBalance[] storage locks = userLocks[user];

       if (locks.length != 0) {
           uint256 length = locks.length <= maxLockWithdrawPerTxn ? locks.length : maxLockWithdrawPerTxn;
           for (uint256 i = 0; i < length; ) {
               if (locks[i].unlockTime <= block.timestamp) {
                   lockAmount = lockAmount.add(locks[i].amount);
                   lockAmountWithMultiplier = lockAmountWithMultiplier.add(
                       locks[i].amount.mul(locks[i].multiplier)
                   );
                   locks[i] = locks[locks.length - 1];
                   locks.pop();
                   length = length - 1;
               } else {
                   i = i + 1;
               }
           }
           if (locks.length == 0) {
               lockAmount = totalLock;
               lockAmountWithMultiplier = totalLockWithMultiplier;
               delete userLocks[user];

               userlist.removeFromList(user);
           }
       }
   }

リスト 3.4: MultiFeeDistribution.sol

具体的には、攻撃者は同じ有効期限を持つ1 weiのトークンを複数回ステークできます。これは maxLockWithdrawPerTxn よりもはるかに大きいです。その後、攻撃者はアクションを getLpMfdBounty に設定し、executeBounty() を繰り返し呼び出すことができます。クリアされるロックの量は maxLockWithdrawPerTxn によって制限されるため、 BountyManager コントラクトの BaseBounty は攻撃者によって枯渇される可能性があります。

影響 攻撃者は1回のトランザクションで BountyManager コントラクトのすべての資金を枯渇させることができ、設計されたバウンティメカニズムが混乱する可能性があります。

提案 関数 _cleanWithdrawableLocks() がすべての期限切れロックをクリアできるようにし、関数 _stake() で最低ステーク額を設定してください。

3.2.3 潜在的な問題4:潜在的な無効な放出スケジュール

アイテム 説明
重大度
ステータス バージョン10で修正済み
導入元 バージョン1

説明 コントラクト ChefIncentivesController では、関数 setEmissionSchedule() は、オーナーによって異なる報酬率のスケジュールを設定するために呼び出されます。この場合、各スケジュールの開始時刻 (_startTimeOffsets[i] + startTime) は、現在のタイムスタンプより大きいことが検証される必要があります。ただし、最初の要素 _startTimeOffsets[0] のみがチェックされ、これは十分ではありません。さらに、_startTimeOffsets[i] は emissionSchedule に追加される際に uint256 から uint128 に変換されますが、元の入力が大きすぎる場合は切り捨てられる可能性があります。

function setEmissionSchedule(
        uint256[] calldata _startTimeOffsets,
        uint256[] calldata _rewardsPerSecond
    ) external onlyOwner {
        uint256 length = _startTimeOffsets.length;
        require(length > 0 && length == _rewardsPerSecond.length, "empty or mismatch params");
        if (startTime > 0) {
            require(_startTimeOffsets[0] > block.timestamp.sub(startTime), "invalid start time");
        }
 
        for (uint256 i = 0; i < length; i++) {
            emissionSchedule.push(
                EmissionPoint({
                    startTimeOffset: uint128(_startTimeOffsets[i]),
                    rewardsPerSecond: uint128(_rewardsPerSecond[i])
                })
            );
        }
        emit EmissionScheduleAppended(_startTimeOffsets, _rewardsPerSecond);
    } 

リスト 3.5: ChefIncentivesController.sol

影響 _startTimeOffsets が昇順でない場合、約束された一部の報酬はユーザーに配布されません。_startTimeOffsets[i] が uint128 の範囲を超える場合、無効な放出スケジュールが追加されます。

提案 _startTimeOffsets が昇順であり、すべての要素が uint128 の範囲内にあることを確認してください。

3.2.4 潜在的な問題5:スキップ可能な放出スケジュール

アイテム 説明
重大度
ステータス 確認済み
導入元 バージョン1

説明 コントラクト ChefIncentivesController では、関数 setScheduleRewardsPerSecond() が emissionSchedule を反復処理して、開始されたばかりの最大のインデックスを持つターゲットスケジュールを特定し、それに応じて報酬率を更新します。ただし、この場合、一部の放出スケジュールがスキップされる可能性があります。

function setScheduledRewardsPerSecond() internal {
		if (!persistRewardsPerSecond) {
			uint256 length = emissionSchedule.length;
			uint256 i = emissionScheduleIndex;
			uint128 offset = uint128(block.timestamp.sub(startTime));
			for (; i < length && offset >= emissionSchedule[i].startTimeOffset; i++) {}
			if (i > emissionScheduleIndex) {
				emissionScheduleIndex = i;
				_massUpdatePools();
				rewardsPerSecond = uint256(emissionSchedule[i - 1].rewardsPerSecond);
			}
		}
	}

リスト 3.6: ChefIncentivesController.sol

影響 関数 setScheduledRewardsPerSecond() が長期間呼び出されない場合、約束された一部の報酬がユーザーに配布されない可能性があります。

提案 関数 setScheduledRewardsPerSecond() は、関数 claim() および _handleActionAfterForToken() の内部で呼び出されるため、放出スケジュールがスキップされる唯一の方法は、放出エポック中にプロトコルに誰もインタラクションしない場合です。

3.2.5 潜在的な問題6:マイグレーション中の交換レートの変更

アイテム 説明
重大度
ステータス バージョン5で修正済み
導入元 バージョン1

説明 コントラクト Migration は、指定された交換レートでトークンV1からトークンV2への交換をユーザーが行えるように実装されています。しかし、マイグレーションプロセス中、この交換レートはオーナーが setExchangeRate() 関数を介して調整できます。

/**
    * @notice Migrate from V1 to V2
    * @param amount of V1 token
    */
   function exchange(uint256 amount) external whenNotPaused {
       uint256 v1Decimals = tokenV1.decimals();
       uint256 v2Decimals = tokenV2.decimals();

       uint256 outAmount = amount.mul(1e4).div(exchangeRate).mul(10**v2Decimals).div(10**v1Decimals);
       tokenV1.safeTransferFrom(_msgSender(), address(this), amount);
       tokenV2.safeTransfer(_msgSender(), outAmount);

       emit Migrate(_msgSender(), amount, outAmount);
   }

リスト 3.7: Migration.sol

影響 マイグレーションプロセス中に交換レートが変更された場合、他のユーザーにとっては不公平になります。

提案 マイグレーションが開始されたら、交換レートは固定されるべきです。

3.2.6 潜在的な問題7:_transfer() の不適切な実装 (I)

アイテム 説明
重大度
ステータス バージョン7で修正済み
導入元 バージョン1

説明 コントラクト IncentivizedERC20 では、関数 _transfer() は、送信者と受信者が同じアカウントである場合(いわゆる自己転送)の状況を考慮していません。具体的には、送信者が受信者と等しい場合、受信者の残高を更新する際に送信者の残高が上書きされます。この場合、ハッカーは自分自身のアカウントに繰り返し転送することで、無限に自分の残高を増やすことができます。

function _transfer(
        address sender,
        address recipient,
        uint256 amount
      ) internal virtual {
        require(sender != address(0), 'ERC20: transfer from the zero address');
        require(recipient != address(0), 'ERC20: transfer to the zero address');
    
        _beforeTokenTransfer(sender, recipient, amount);
    
        uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
        uint256 recipientBalance = _balances[recipient].add(amount);
    
        if (address(_getIncentivesController()) != address(0)) {
          // uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionBefore(sender);
          if (sender != recipient) {
            _getIncentivesController().handleActionBefore(recipient);
          }
        }
    
        _balances[sender] = senderBalance;
        _balances[recipient] = recipientBalance;
    
        if (address(_getIncentivesController()) != address(0)) {
          uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
          if (sender != recipient) {
            _getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
          }
        }
      }

リスト 3.8: IncentivizedERC20.sol

影響 トークンは無限に発行される可能性があります。

提案 関数 _transfer() を適切に実装してください。例えば、OpenZeppelin の標準的な _transfer() 実装です。

_balances[sender] = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
_balances[recipient] = _balances[recipient].add(amount);

リスト 3.9: ERC20.sol in OpenZeppelin

3.2.7 潜在的な問題8:UniV2TwapOracle での期間チェックの欠如

アイテム 説明
重大度
ステータス バージョン9で修正済み
導入元 バージョン1

説明 コントラクト UniV2TwapOracle では、関数 initialize() および setPeriod() で属性 _period が検証されません。

function initialize(
        address _pair,
        address _rdnt,
        address _ethChainlinkFeed,
        uint _period,
        uint _consultLeniency,
        bool _allowStaleConsults
    ) external initializer {
        __Ownable_init();

        pair = IUniswapV2Pair(_pair);
        token0 = pair.token0();
        token1 = pair.token1();
        price0CumulativeLast = pair.price0CumulativeLast(); // Fetch the current accumulated price value (1 / 0)
        price1CumulativeLast = pair.price1CumulativeLast(); // Fetch the current accumulated price value (0 / 1)
        uint112 reserve0;
        uint112 reserve1;
        (reserve0, reserve1, blockTimestampLast) = pair.getReserves();
        require(reserve0 != 0 && reserve1 != 0, 'UniswapPairOracle: NO_RESERVES'); // Ensure that there's liquidity in the pair

        PERIOD = _period;
        CONSULT_LENIENCY = _consultLeniency;
        ALLOW_STALE_CONSULTS = _allowStaleConsults;

        baseInitialize(_rdnt, _ethChainlinkFeed);
    }

    function setPeriod(uint _period) external onlyOwner {
        PERIOD = _period;
    }

リスト 3.10: UniV2TwapOracle.sol

影響 この場合、_period が小さすぎると、オラクルは予期しない値を返す可能性があります。

提案 関数 initialize および setPeriod で _period の最小制限を設定してください。

3.2.8 潜在的な問題9:返金不可のダストトークン

アイテム 説明
重大度
ステータス バージョン5で修正済み
導入元 バージョン1

説明 コントラクト UniswapPoolHelper では、関数 zapWETH() は、ユーザーが WETH トークンを LP トークンに交換するのを支援するように設計されています。LP トークン(WETH-RDNT)のためにプールに流動性を追加するために、関数 addLiquidityWETHOnly() を呼び出します。このプロセスで、ユーザーに返却されるべきダストトークンが存在する可能性があります。しかし、UniswapPoolHelper はこれらのダストトークンを処理する機能が実装されていません。

function zapWETH(uint256 amount)
    public
    returns (uint256 liquidity)
{
    IWETH WETH = IWETH(wethAddr);
    WETH.transferFrom(msg.sender, address(liquidityZap), amount);
    liquidity = liquidityZap.addLiquidityWETHOnly(amount, address(this));
    IERC20 lp = IERC20(lpTokenAddr);
    
    liquidity = lp.balanceOf(address(this));
    lp.safeTransfer(msg.sender, liquidity);
}

リスト 3.11: UniswapPoolHelper.sol

影響 ダストトークンはコントラクトに残存し、 zapTokens(0,0) 関数を介して他の人に抽出される可能性があります。

提案 流動性追加後にダストトークンを返す関数を実装してください。

3.2.9 潜在的な問題10:_transfer() の不適切な実装 (II)

アイテム 説明
重大度
ステータス バージョン9で修正済み
導入元 バージョン7

説明 コントラクト IncentivizedERC20 では、関数 _transfer() は、コントラクト ChefIncentivesController のユーザーの状態を適切に更新するために handle_ActionAfter() 関数を呼び出します。しかし、送信者が受信者と等しい場合、senderBalance は更新されず、これは正しくありません。

function _transfer(
        address sender,
        address recipient,
        uint256 amount
      ) internal virtual {
        require(sender != address(0), 'ERC20: transfer from the zero address');
        require(recipient != address(0), 'ERC20: transfer to the zero address');
    
        _beforeTokenTransfer(sender, recipient, amount);
    
        uint256 senderBalance = _balances[sender].sub(amount, 'ERC20: transfer amount exceeds balance');
    
        if (address(_getIncentivesController()) != address(0)) {
          // uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionBefore(sender);
          if (sender != recipient) {
            _getIncentivesController().handleActionBefore(recipient);
          }
        }
    
        _balances[sender] = senderBalance;
        uint256 recipientBalance = _balances[recipient].add(amount);
        _balances[recipient] = recipientBalance;
    
        if (address(_getIncentivesController()) != address(0)) {
          uint256 currentTotalSupply = _totalSupply;
          _getIncentivesController().handleActionAfter(sender, senderBalance, currentTotalSupply);
          if (sender != recipient) {
            _getIncentivesController().handleActionAfter(recipient, recipientBalance, currentTotalSupply);
          }
        }
      }

リスト 3.12: IncentivizedERC20.sol

影響 ユーザーが自分自身に転送すると、コントラクト ChefIncentivesController の状態は正しく更新されず、報酬にさらなる問題が生じます。

提案 関数 handleActionAfter() で senderBalance を修正してください。

3.2.10 潜在的な問題11:操作可能な複合報酬

アイテム 説明
重大度
ステータス バージョン10で修正済み
導入元 バージョン5

説明 MFDPlus コントラクトでは、関数 _convertPendingRewardsToWeth() は、ユーザーの報酬を Uniswap ルーターを介して WETH にスワップして再ロックします。しかし、スワップ後のスリッページチェックがありません。

IERC20(underlying).safeApprove(uniRouter, removedAmount);
    uint256[] memory amounts = IUniswapV2Router02(uniRouter)
    .swapExactTokensForTokens(
         removedAmount,
         0, // slippage handled after this function
         mfdHelper.getRewardToBaseRoute(underlying),
         address(this),
         block.timestamp + 10
     );

リスト 3.13: MFDPlus.sol

影響 攻撃者はトランザクションをフロントランして価格を操作し、利益を得ることができます。

提案 claimCompound() 関数にスリッページチェックを追加してください。

3.2.11 潜在的な問題12:setLeverager() でのアクセス制御の欠如

アイテム 説明
重大度
ステータス バージョン9で修正済み
導入元 バージョン1

説明 コントラクト LendingPool の関数 setLeverager() にはアクセス制御がありません。

uint256[] memory amounts = IUniswapV2Router02(uniRouter)
    .swapExactTokensForTokens(
         removedAmount,
         0, // slippage handled after this function
         mfdHelper.getRewardToBaseRoute(underlying),
         address(this),
         block.timestamp + 10
     );

リスト 3.14: LendingPool.sol

影響 レバレッジャーが最初に設定されていない場合、攻撃者はレバレッジャーを任意の Сlass に設定でき、これにより depositWithAutoDLP() 関数のロジックを制御できるようになります。

提案 関数 initialize() でレバレッジャーを設定するか、関数 setLeverager() にアクセス制御を追加してください。

3.2.12 潜在的な問題13:addLiquidityWETHOnly() でのスリッページチェックがない

アイテム 説明
重大度
ステータス 確認済み
導入元 バージョン1

説明 ユーザーは、借り入れた WETH トークン(または自分の ETH トークン)または MFD コントラクトの vesting RDNT トークンを使用して、LP トークン(WETH-RDNT)を取得できます。

しかし、プールに流動性を追加する際、必要なトークンの計算はプール内のリザーブ量に基づいており、これは操作される可能性があります。この場合、ユーザーが WETH トークンしか持っていない場合、関数 addLiquidityWETHOnly() が呼び出され、スリッページをチェックせずに不均衡なプールで WETH トークンの半分が RDNT トークンにスワップされます。

function addLiquidityWETHOnly(uint256 _amount, address payable to)
    public
    returns (uint256 liquidity)
{
    require(to != address(0), "LiquidityZAP: Invalid address");
    uint256 buyAmount = _amount.div(2);
    require(buyAmount > 0, "LiquidityZAP: Insufficient ETH amount");

    (uint256 reserveWeth, uint256 reserveTokens) = getPairReserves();
    uint256 outTokens = UniswapV2Library.getAmountOut(
        buyAmount,
        reserveWeth,
        reserveTokens
    );

    _WETH.transfer(_tokenWETHPair, buyAmount);

    (address token0, address token1) = UniswapV2Library.sortTokens(
        address(_WETH),
        _token
    );
    IUniswapV2Pair(_tokenWETHPair).swap(
        _token == token0 ? outTokens : 0,
        _token == token1 ? outTokens : 0,
        address(this),
        ""
    );

    return _addLiquidity(outTokens, buyAmount, to);
}

リスト 3.15: LiquidityZap.sol

function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
       require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
       require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
       uint amountInWithFee = amountIn.mul(997);
       uint numerator = amountInWithFee.mul(reserveOut);
       uint denominator = reserveIn.mul(1000).add(amountInWithFee);
       amountOut = numerator / denominator;
   }  

リスト 3.16: UniswapV2Library.sol

影響 攻撃者はトランザクションをフロントランして価格を操作し、利益を得ることができます。

提案 addLiquidityWETHOnly() 関数でスリッページをチェックするか、UniswapPoolHelper からのみ呼び出されるようにしてください。

3.2.13 潜在的な問題14:loopETH() での borrowRatio チェックの欠如

アイテム 説明
重大度
ステータス バージョン10で修正済み
導入元 バージョン1

説明 関数 loopETH() はレバレッジ借入に使用され、借入比率を指定するパラメータ borrowRatio を受け取ります。しかし、borrowRatio はループ開始前にチェックされません。

function loopETH(
        uint256 interestRateMode,
        uint256 borrowRatio,
        uint256 loopCount
    ) external payable {
        uint16 referralCode = 0;
        uint256 amount = msg.value;
        if (IERC20(address(weth)).allowance(address(this), address(lendingPool)) == 0) {
            IERC20(address(weth)).safeApprove(address(lendingPool), type(uint256).max);
        }
        if (IERC20(address(weth)).allowance(address(this), address(treasury)) == 0) {
            IERC20(address(weth)).safeApprove(treasury, type(uint256).max);
        }

        uint256 fee = amount.mul(feePercent).div(RATIO_DIVISOR);
        _safeTransferETH(treasury, fee);
        
        amount = amount.sub(fee);

        weth.deposit{value: amount}();
        lendingPool.deposit(address(weth), amount, msg.sender, referralCode);

        for (uint256 i = 0; i < loopCount; i += 1) {
            amount = amount.mul(borrowRatio).div(RATIO_DIVISOR);
            lendingPool.borrow(address(weth), amount, interestRateMode, referralCode, msg.sender);
            weth.withdraw(amount);

            fee = amount.mul(feePercent).div(RATIO_DIVISOR);
            _safeTransferETH(treasury, fee);

            weth.deposit{value: amount.sub(fee)}();
            lendingPool.deposit(address(weth), amount.sub(fee), msg.sender, referralCode);
        }

        zapWETHWithBorrow(wethToZap(msg.sender), msg.sender);
    }

リスト 3.17: Leverager.sol

影響 borrowRatio は RATIO_DIVISOR よりも大きくなる可能性があり、元の設計と一致しません。

提案 borrowRatio が RATIO_DIVISOR 以下であることを確認してください。

3.2.14 潜在的な問題15:setPoolIDs() での assets と poolIDs の長さチェックの欠如

アイテム 説明
重大度
ステータス バージョン10で修正済み
導入元 バージョン1

説明 関数 setPoolIDs() は、オーナーが異なるアセットに異なる poolID を設定できるようにします。ただし、これらの2つの配列の長さが等しいかどうかのチェックは行われません。

// Set pool ids of assets
    function setPoolIDs(address[] memory assets, uint256[] memory poolIDs) external onlyOwner {
        for (uint256 i = 0; i < assets.length; i += 1) {
            poolIdPerChain[assets[i]] = poolIDs[i];
        }
        emit PoolIDsUpdated(assets, poolIDs);
    } 

リスト 3.18: StarBorrow.sol

影響 アセットは正しい poolID に割り当てられません。

提案 assets と poolIDs の長さが等しいことを確認してください。

3.2.15 潜在的な問題16:addBountyContract() での mint 権限剥奪の欠如

アイテム 説明
重大度
ステータス 確認済み
導入元 バージョン1

説明 関数 addBountyContract() は、新しい BountyManager を設定するために使用されます。しかし、元の bounty コントラクトは mint 権限を保持しており、これは元の設計に反しています。

function addBountyContract(address _bounty) external onlyOwner {
       BountyManager = _bounty;
       minters[_bounty] = true;
   }

リスト 3.19: Leverager.sol

影響 非推奨の BountyManager は mint 権限を保持しています。

提案 元の BountyManager コントラクトの mint 権限を剥奪してください。

フィードバック 関数 addBountyContract は、BountyManager を初期化するために一度だけ呼び出されます。

3.2.16 潜在的な問題17:Minters は一度だけ割り当て可能

アイテム 説明
重大度
ステータス 確認済み
導入元 バージョン1

説明 minters は、関数 mint() および addReward() にアクセスする権限を持つものを記録するために使用されます。しかし、minters の1つ(例えば、コントラクト ChefIncentivesController)が更新されると、古い minters は削除できません。

function setMinters(address[] memory _minters) external onlyOwner {
        require(!mintersAreSet);
        for (uint256 i; i < _minters.length; i++) {
            minters[_minters[i]] = true;
        }
        mintersAreSet = true;
    }

リスト 3.20: MultiFeeDistribution.sol

影響 アップグレード時に、古い minters は削除できません。

提案 minters を変更するための特権関数を実装してください。

フィードバック BountyManager、ChefIncentivesController、および MultiFeeDistribution はアップグレード可能であるため、minters は常に同じプロキシアドレスを保持します。

3.3 追加の推奨事項

3.3.1 潜在的な問題18:ガス最適化 (Mfd の zapVestingToLp())

アイテム 説明
ステータス バージョン10で修正済み
導入元 バージョン1

説明 関数 zapVestingToLp() は、コントラクト LockZap のみがユーザーのロックされた収益を転送するために呼び出すことができます。ユーザーの収益配列をインデックス0から開始して反復処理し、unlockTime が現在のタイムスタンプより大きいかどうかを確認します。その場合、この収益は配列から削除され、転送されます。ただし、配列内の unlockTime はインデックスとともに増加するため、配列の末尾から先頭に向かって反復処理を開始する方が効率的です。unlockTime が現在のタイムスタンプより小さい場合、ループを終了できます。

function zapVestingToLp(address _user)
        external
        override
        returns (uint256 zapped)
    {
        require(msg.sender == lockZap);

        LockedBalance[] storage earnings = userEarnings[_user];
        uint256 length = earnings.length;

        for (uint256 i = 0; i < length; ) {
            // only vesting, so only look at currently locked items
            if (earnings[i].unlockTime > block.timestamp) {
                zapped = zapped.add(earnings[i].amount);
                // remove + shift array size
                earnings[i] = earnings[earnings.length - 1];
                earnings.pop();
                length = length.sub(1);
            } else {
                i = i.add(1);
            }
        }

        rdntToken.safeTransfer(lockZap, zapped);

        Balances storage bal = balances[_user];
        bal.earned = bal.earned.sub(zapped);
        bal.total = bal.total.sub(zapped);

        return zapped;
    }

リスト 3.21: MultiFeeDistribution.sol

提案 earnings の末尾から先頭に向かって反復処理を開始してください。unlockTime が現在のタイムスタンプより小さい場合、ループを終了できます。

3.3.2 潜在的な問題19:BountyManager で空でない Bounty Reserve

アイテム 説明
ステータス バージョン10で修正済み
導入元 バージョン1

説明 関数 _sendBounty() で、コントラクト BountyManager に転送するための十分な RDNT トークンがない場合、イベント BountyReseveEmpty() が発行され、コントラクトは一時停止されます。しかし、まだ RDNT トークンが残っている可能性があり、これは発行されたイベントとは一致しません。

function _sendBounty(address _to, uint256 _amount)
		internal
		returns (uint256)
	{
		if (_amount == 0) {
			return 0;
		}

		uint256 bountyReserve = IERC20(rdnt).balanceOf(address(this));
		if(_amount > bountyReserve) {
			emit BountyReserveEmpty(bountyReserve);
			_pause();
		} else {
			IERC20(rdnt).safeTransfer(address(mfd), _amount);
			IMFDPlus(mfd).mint(_to, _amount, true);
			return _amount;
		}
	}

リスト 3.22: BountyManager.sol

提案 不十分であっても、残りの RDNT トークンを転送してください。

3.3.3 潜在的な問題20:requiredUsdValue() での一貫性のない命名

アイテム 説明
ステータス 確認済み
導入元 バージョン1

説明 関数 requiredUsdValue() は、RTokens を保有することで報酬を得る資格を得たいユーザーに必要なロックされた値を確認するために使用されます。計算は、関数 getUserAccountData() から返されるユーザーの担保価値に基づいています。ただし、返される値は totalCollateralETH と名付けられており、関数 requiredUsdValue() (すなわち totalCollateralUSD) のものと一貫性がありません。

提案 関数名の命名規則を正しいトークン名で標準化してください。例えば、requiredUsdValue() を requiredEthValue() にリネームします。

フィードバック AAVE コントラクトを可能な限り類似させたいので、名前は更新しませんでした。

3.4 注記

3.4.1 潜在的な問題21:廃止された MFDPlus

アイテム 説明
ステータス 確認済み
導入元 バージョン10

説明 コントラクト MFDPlus は、もはや使用されていません。コンパウンディングのロジックは AutoCompounder コントラクトに移動され、その他のロジックは MiddleFeeDistribution コントラクトに移動されました。

4. 付録

4.1 自動静的セキュリティテストの結果

表 4.1: 自動静的セキュリティテストの結果。Found はツールによって報告された問題の数を示します。FP は手動検証後の偽陽性の数です。

ID Detector Description Impact Found FP Result
1 arbitrary-send-erc20 Calling transferFrom with arbitrary from High 1 1 Passed
2 array-by-reference Modifying storage array by value High 0 0 Passed
3 incorrect-shift Incorrect order of parameters in a shift instruction High 0 0 Passed
4 multiple-constructors Multiple constructor schemes High 0 0 Passed
5 name-reused Reusing contract’s name High 0 0 Passed
6 protected-vars Modifying variables directly without access control High 0 0 Passed
7 rtlo Using Right-To-Left-Override control character High 0 0 Passed
8 shadowing-state State variables shadowing High 1 1 Passed
9 suicidal Functions allowing anyone to destruct the contract High 0 0 Passed
10 uninitialized-state Uninitialized state variables High 3 3 Passed
11 uninitialized-storage Uninitialized storage variables High 0 0 Passed
12 unprotected-upgrade Unprotected upgradeable contract High 1 1 Passed
13 arbitrary-send-erc20-permit transferFrom uses arbitrary from with permit High 0 0 Passed
14 arbitrary-send-eth Functions that send Ether to arbitrary destinations High 0 0 Passed
15 controlled-array-length Tainted array length assignment High 0 0 Passed
16 controlled-delegatecall Controlled delegatecall destination High 0 0 Passed
17 delegatecall-loop Payable functions using delegatecall inside a loop High 0 0 Passed
18 msg-value-loop Using msg.value inside a loop High 0 0 Passed
19 reentrancy-eth Reentrancy vulnerabilities (theft of ethers) High 5 5 Passed
20 storage-array Signed storage integer array compiler bug High 0 0 Passed
21 unchecked-transfer Unchecked tokens transfer High 12 12 Passed
22 weak-prng Weak PRNG High 0 0 Passed
23 domain-separator-collision Detects ERC20 tokens that have a function whose signature collides with EIP-2612’s DOMAIN_SEPARATOR() Medium 0 0 Passed
24 enum-conversion Detects dangerous enum conversion Medium 0 0 Passed
25 erc20-interface Incorrect ERC20 interfaces Medium 0 0 Passed
26 erc721-interface Incorrect ERC721 interfaces Medium 0 0 Passed
27 incorrect-equality Dangerous strict equalities Medium 23 23 Passed
28 locked-ether Contracts that lock ether Medium 1 1 Passed
29 mapping-deletion Deletion on mapping containing a structure Medium 0 0 Passed
30 shadowing-abstract State variables shadowing from abstract contracts Medium 0 0 Passed
31 tautology Tautology or contradiction Medium 0 0 Passed
32 write-after-write Unused write Medium 3 3 Passed
33 boolean-cst Misuse of Boolean constant Medium 0 0 Passed
34 constant-function-asm Constant functions using assembly code Medium 0 0 Passed
35 constant-function-state Constant functions changing the state Medium 0 0 Passed
36 divide-before-multiply Imprecise arithmetic operations order Medium 20 20 Passed
37 reentrancy-no-eth Reentrancy vulnerabilities (no theft of ethers) Medium 12 12 Passed
38 reused-constructor Reused base constructor Medium 0 0 Passed
39 tx-origin Dangerous usage of tx.origin Medium 1 1 Passed
40 unchecked-lowlevel Unchecked low-level calls Medium 0 0 Passed
41 unchecked-send Unchecked send Medium 0 0 Passed
42 uninitialized-local Uninitialized local variables Medium 33 33 Passed
43 unused-return Unused return values Medium 19 19 Passed

4.2 自動動的セキュリティテストの結果

表 4.2: Lending関連ロジックのテストプロパティ

ID Property Result
1 Calling deposit never leads to a decrease of onBehalfOf’s RToken amount Passed
2 Calling withdraw never leads to an increase of msg.sender’s RToken amount Passed
3 Calling borrow with stable interest rate mode never leads to a decrease of onBehalfOf’s StableDebtToken. Passed
4 Calling borrow with variable interest rate mode never leads to a decrease of onBehalfOf’s VariableDebtToken. Passed
5 Calling borrow with onBehalfOf that does not equal to msg.sender never leads to an increase of msg.sender’s borrow allowance. Passed
6 Calling repay with stable interest rate mode never leads to an increase of onBehalfOf’s StableDebtToken. Passed
7 Calling repay with variable interest rate mode never leads to an increase of onBehalfOf’s VariableDebtToken. Passed
8 liquidityIndex will never decrease. Passed
9 liquidityIndex will remain constant within the same block. Passed
10 variableBorrowIndex will never decrease. Passed
11 variableBorrowIndex will remain constant within the same block. Passed
12 Decreasing collateral amounts will never lead to health factor less than 1. Passed
13 Increasing borrowing amounts will never lead to health factor less than 1. Passed

表 4.3: Staking関連ロジックのテストプロパティ

ID Property Result
1 User’s total balance always equals the sum of locked balance, unlocked balance and earned balance. Passed
2 User’s locked balance always equals the sum of userLocks amount Passed
3 User’s lockedWithMultiplier balance always equals the sum of userLocks amount times userLocks multiplier Passed
4 lockedSupply always equals the sum of users’ locked balance Passed
5 lockedSupplyWithMultiplier always equals the sum of users’ lockedWithMultiplier balance Passed
6 rewardPerTokenStored never decreases. Passed
7 rewardPerTokenStored will remain constant within the same block. Passed
8 totalSupply always equals the sum of users’ amount Passed
9 accRewardPerShare never decreases. Passed
10 accRewardPerShare will remain constant within the same block. Passed

表 4.4: その他の機能のテストプロパティ

ID Property Result
1 WETH and RDNT balance of the contract LockedZap will always be zero. Passed
2 WETH and RDNT balance of the contract LiquidityZap will always be zero. Passed
3 WETH and RDNT balance of the contract BalancerPoolHelper will always be zero. Passed
4 WETH and RDNT balance of the contract UniswapPoolHelper will always be zero. Passed
5 Calling loop will always lead to user eligible for rewards Passed
6 Calling loopETH will always lead to user eligible for rewards Passed
7 Calling executeBounty with _execute equals false will never lead to storage change. Passed
8 Calling transfer with sender equals to receiver never leads to balance change. Failed in Version 1. Passed in Version 7

5. 通知および注記

5.1 免責事項

本レポートは、投資アドバイスまたは個人的な推奨事項を構成するものではありません。トークン、トークンセール、またはその他の製品、サービス、またはその他の資産の潜在的な経済性を考慮しておらず、考慮されるべきでもありません。いかなるエンティティも、このレポートを、トークン、製品、サービス、またはその他の資産の購入または販売の決定を行うための目的を含む、いかなる方法でも依拠すべきではありません。

本レポートは、特定のプロジェクトまたはチームの推奨ではなく、本レポートは特定のプロジェクトのセキュリティを保証するものではありません。このセキュリティテストは、スマートコントラクトのすべてのセキュリティ問題を検出することについていかなる保証も与えるものではありません。すなわち、評価結果は、さらなるセキュリティ問題の発見がないことを保証するものではありません。セキュリティテストは包括的とは見なされないため、スマートコントラクトのセキュリティを確保するために、独立した監査および公開バグバウンティプログラムを実施することを常に推奨します。

このセキュリティテストの範囲は、セクション1.2に記載されているコードに限定されます。明示的に指定されない限り、言語自体のセキュリティ(例:solidity言語)、基盤となるコンパイルツールチェーン、およびコンピューティングインフラストラクチャは範囲外です。

5.2 監査手順

以下の手順に従って監査を実施します。

  • 脆弱性検出まず、自動コードアナライザーでスマートコントラクトをスキャンし、その後、それらによって報告された問題を、手動で検証(却下または確認)します。

  • セマンティック分析スマートコントラクトのビジネスロジックを研究し、自動ファジングツール(当社の研究チームによって開発された)を使用して、潜在的な脆弱性についてさらに調査します。また、独立した監査担当者と協力して、可能な攻撃シナリオを手動で分析し、結果をクロスチェックします。

  • 推奨事項ガス最適化、コードスタイルなど、優れたプログラミングプラクティスの観点から、開発者に役立つアドバイスを提供します。

以下に、主な具体的なチェックポイントを示します。

5.2.1 ソフトウェアセキュリティ

  • 再入可能性

  • DoS

  • アクセス制御

  • データ処理とデータフロー

  • 例外処理

  • 信頼できない外部呼び出しと制御フロー

  • 初期化の一貫性

  • イベント操作

  • エラーが発生しやすいランダム性

  • プロキシシステムの不適切な使用

5.2.2 DeFiセキュリティ

  • セマンティックの一貫性

  • 機能の一貫性

  • 権限管理

  • ビジネスロジック

  • トークン操作

  • 緊急メカニズム

  • オラクルセキュリティ

  • ホワイトリストとブラックリスト

  • 経済的影響

  • バッチ転送

5.2.3 NFTセキュリティ

  • 重複アイテム

  • トークン受信者の検証

  • オフチェーンメタデータセキュリティ

5.2.4 追加の推奨事項

  • ガス最適化

  • コード品質とスタイル

注記:上記のチェックポイントが主なものです。プロジェクトの機能に応じて、監査プロセス中にさらに多くのチェックポイントを使用する場合があります。

Sign up for the latest updates
Newsletter - April 2026
Security Insights

Newsletter - April 2026

In April 2026, the DeFi ecosystem experienced three major security incidents. KelpDAO lost ~$290M due to an insecure 1-of-1 DVN bridge configuration exploited via RPC infrastructure compromise, Drift Protocol suffered ~$285M from a multisig governance takeover leveraging Solana's durable nonce mechanism, and Rhea Finance incurred ~$18.4M following a business logic flaw in its margin-trading module that allowed circular swap path manipulatio

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly
Security Insights

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly

This BlockSec weekly security report covers eight attack incidents detected between April 20 and April 26, 2026, across Ethereum, Avalanche, Sui, Base, HyperLiquid, and MegaETH, with total estimated losses of approximately $7.04M. The highlighted incident is the $1.3M GiddyDefi exploit, where the attacker did not break any cryptography or use a flash loan but simply replayed an existing on-chain EIP-712 signature with the unsigned `aggregator` and `fromToken` fields swapped out for a malicious contract, demonstrating how partial signature coverage turns any historical signature into a generic permit. Other incidents include a $3.5M Volo Vault operator key compromise on Sui, a $1.5M Purrlend privileged-role takeover, a $413K SingularityFinance oracle misconfiguration, a $142.7K Scallop cross-pool index injection, a $72.35K Kipseli Router decimal mismatch, a $50.7K REVLoans (Juicebox) accounting pollution, and a $64K Custom Rebalancer arbitrary-call exploit.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit