レポートマニフェスト
| アイテム | 説明 |
|---|---|
| クライアント | 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に示します。

したがって、本レポートで測定された重大度は、高、中、低の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 追加の推奨事項
-
ガス最適化
-
コード品質とスタイル
注記:上記のチェックポイントが主なものです。プロジェクトの機能に応じて、監査プロセス中にさらに多くのチェックポイントを使用する場合があります。



