報告清單
| 項目 | 描述 |
|---|---|
| 客戶 | 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。風險的總體嚴重程度由可能性和影響決定。具體而言,可能性用於估計攻擊者發現並利用特定漏洞的可能性,而影響則用於衡量成功利用後的後果。
在本報告中,可能性和影響均分為兩個等級,即高和低,其實際組合如表 1.1 所示。

因此,本報告中測量的嚴重程度分為三類:高、中、低。為了完整性,未定也用於涵蓋無法明確判定風險的情況。
此外,所發現項目的狀態將屬於以下四類之一:
-
未定 尚未回應。
-
已獲悉 客戶已收到該項目,但尚未確認。
-
已確認 客戶已認可該項目,但尚未修復。
-
已修復 客戶已確認並修復了該項目。
2. 自動化安全性測試
2.1 自動化靜態安全性測試
我們使用基於 Slither 的內部靜態分析工具來檢查漏洞的存在。經人工檢查結果後,未發現任何問題。詳細測試結果可在附錄中的表 4.1 中找到。
2.2 自動化動態安全性測試
我們利用模糊測試 (Fuzzing) 技術來測試目標合約的穩健性、可靠性和精確度。具體而言,模糊測試過程中的初始種子是根據函數語義和合約測試腳本確定的。為了模擬鏈上環境,我們還維護了一組與合約 LendingPool 和 MultiFeeDistribution 互動過的地址。
我們的模糊測試器在生成交易序列時也會考慮函數語義。例如,合約 MultiFeeDistribution 中的 stake 函數和合約 LendingPool 中的 deposit 函數更有可能在序列中優先被呼叫。對函數參數和序列的變異是基於合約程式碼覆蓋率來引導的。如果某個參數或序列達到了更高的程式碼覆蓋率,它在下一輪模糊測試中被變異的優先級就會更高。為了探索受幻數約束的路徑,我們在執行時收集從儲存中讀取的值(即 SLOAD 指令),並在變異過程中利用它們來生成函數參數。
我們總共生成了 100,000 個測試案例,並利用了 31 個預言機來檢測是否發生了故障。每個測試案例包含 30 筆指定順序的交易。最終,我們發現了一個關鍵問題(即第 3.2.6 節),該問題也在我們的手動安全性測試過程中被發現。詳細測試結果可在附錄中的表 4.2、4.3 和 4.4 中找到。
3. 手動安全性測試
我們投入人力來理解不同模組之間的整體設計和互動,並根據我們對既往研究和經驗所累積的潛在攻擊面的知識,進行安全性測試。
我們總共發現了十七個潛在問題。此外,我們還有三個建議和一個備註,如下:
-
高風險: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 | 中 | 不可退還的零頭代幣 (Dust Tokens) | DeFi 安全 | 已修復 |
| 10 | 中 | _transfer() 實作不當 (II) | DeFi 安全 | 已修復 |
| 11 | 中 | 可操縱的複利獎勵 | DeFi 安全 | 已修復 |
| 12 | 中 | setLeverager() 缺乏存取控制 | DeFi 安全 | 已修復 |
| 13 | 中 | addLiquidityWETHOnly() 中沒有滑點檢查 | DeFi 安全 | 已確認 |
| 14 | 低 | loopETH() 中缺乏對 borrowRatio 的檢查 | DeFi 安全 | 已修復 |
| 15 | 低 | setPoolIDs() 中缺乏對資產與與池 ID 長度的檢查 | DeFi 安全 | 已修復 |
| 16 | 低 | addBountyContract() 缺乏鑄造許可權撤銷 | DeFi 安全 | 已確認 |
| 17 | 低 | 鑄造者只能被分配一次 | DeFi 安全 | 已確認 |
| 18 | - | Gas 優化 (Mfd 中的 zapVestingToLp()) | 建議 | 已修復 |
| 19 | - | BountyManager 中的非空賞金儲備 | 建議 | 已修復 |
| 20 | - | requiredUsdValue() 中的命名不一致 | 建議 | 已確認 |
| 21 | - | 已棄用的 MFDPlus 備註 | 備註 | 已確認 |
詳細資訊如下文各節所示。
3.1 軟體安全
3.1.1 潛在問題 1:沒有用於重置函數指標的預留介面
| 項目 | 描述 |
|---|---|
| 嚴重程度 | 中 |
| 狀態 | 版本 7 已修復 |
| 引入版本 | 版本 1 |
描述 三個函數 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.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 返回的價格可以被操縱,從而允許攻擊者從中獲利。
建議 我們建議使用中位數而不是平均值。如果只有兩個來源預言機且出現較大差異,則在平均價格遠大於最低價格時撤銷交易是更合理的方案。
反饋 將只會有兩個來源預言機。如果出現較大差異,我們將使用 OZ Defender Sentinel 來暫停相關合約。
備註 合約 ComboOracle 已移除,不再使用。
3.2.2 潛在問題 3:透過 BaseBounty 耗盡資金的潛在風險
| 項目 | 描述 |
|---|---|
| 嚴重程度 | 高 |
| 狀態 | 版本 4 已修復 |
| 引入版本 | 版本 1 |
描述 使用者可以鎖定代幣(即 RDNT)一段固定時間以獲得獎勵。當鎖定期結束後,如果該使用者啟用了 AutoRelock,其他使用者可以呼叫 executeBounty() 函數為此使用者重新鎖定代幣以獲得 BaseBounty。在重新鎖定過程中,過期的鎖定將在內部函數 _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 可能會被攻擊者耗盡。
影響 攻擊者可以在一次交易中耗盡 BountyManager 合約中的所有資金,導致設計的賞金機制中斷。
建議 確保函數 _cleanWithdrawableLocks() 可以清除所有過期的鎖定,並在 _stake() 函數中設定最低質押金額。
3.2.3 潛在問題 4:潛在的無效發放計劃
| 項目 | 描述 |
|---|---|
| 嚴重程度 | 低 |
| 狀態 | 版本 10 已修復 |
| 引入版本 | 版本 1 |
描述 在合約 ChefIncentivesController 中,setEmissionSchedule() 函數由所有者呼叫,用於設定不同的獎勵費率計劃。在這種情況下,每個計劃的開始時間 (_startTimeOffsets[i] + startTime) 應經驗證大於當前時間戳記。然而,它僅檢查 _startTimeOffsets 中的第一個元素,這是不夠的。此外,當 _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 的實作允許使用者用指定的 exchangeRate 將 tokenV1 交換為 tokenV2。然而,在遷移過程中,這個 exchangeRate 仍然可以由所有者透過 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 的 ERC20 標準 _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 中,屬性 _period 在 initialize() 和 setPeriod() 函數中未經驗證。
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 代幣。它將呼叫 addLiquidityWETHOnly() 函數來為 LP 代幣增加流動性。在此過程中,可能會存在應退回給使用者的零頭代幣 (Dust Tokens)。然而,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() 將呼叫 handleActionAfter() 函數來更新合約 ChefIncentivesController 中使用者的狀態。然而,如果傳送者等於接收者,參數 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
影響 攻擊者可以搶先交易 (Front-run) 來操縱價格並獲利。
建議 在 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
影響 如果 leverager 一開始未設定,攻擊者可以將 leverager 設定為任何地址,從而獲得對 depositWithAutoDLP() 函數邏輯的控制權。
建議 在 initialize() 函數中設定 leverager,或者為 setLeverager() 函數增加存取控制。
3.2.12 潛在問題 13:addLiquidityWETHOnly() 中沒有滑點檢查
| 項目 | 描述 |
|---|---|
| 嚴重程度 | 中 |
| 狀態 | 已確認 |
| 引入版本 | 版本 1 |
描述 使用者可以使用借入的 WETH 代幣(或自己的 ETH 代幣)或 MFD 合約中的歸屬 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() 中缺乏對資產與與池 ID 長度的檢查
| 項目 | 描述 |
|---|---|
| 嚴重程度 | 低 |
| 狀態 | 版本 10 已修復 |
| 引入版本 | 版本 1 |
描述 setPoolIDs() 函數允許所有者為不同資產設定不同的 poolIDs。然而,並未檢查這兩個陣列的長度是否相等。
// 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
影響 資產將不會被分配到正確的 poolIDs。
建議 確保資產和 poolIDs 的長度相等。
3.2.15 潛在問題 16:addBountyContract() 缺乏鑄造許可權撤銷
| 項目 | 描述 |
|---|---|
| 嚴重程度 | 低 |
| 狀態 | 已確認 |
| 引入版本 | 版本 1 |
描述 addBountyContract() 函數用於設定新的 BountyManager。然而,原始的賞金合約仍然持有鑄造特權,這與原始設計背道而馳。
function addBountyContract(address _bounty) external onlyOwner {
BountyManager = _bounty;
minters[_bounty] = true;
}
列表 3.19:Leverager.sol
影響 已過時的 BountyManager 仍然擁有鑄造特權。
建議 撤銷原始 BountyManager 合約的鑄造特權。
反饋 addBountyContract 函數只會被呼叫一次以初始化 BountyManager。
3.2.16 潛在問題 17:鑄造者只能被分配一次
| 項目 | 描述 |
|---|---|
| 嚴重程度 | 低 |
| 狀態 | 已確認 |
| 引入版本 | 版本 1 |
描述 minters 用於記錄那些擁有存取 mint() 和 addReward() 函數權限的人。然而,當其中一個鑄造者(例如 ChefIncentivesController 合約)被更新時,過時的鑄造者無法被移除。
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
影響 當它們升級時,過時的鑄造者無法被移除。
建議 實作一個特權函數來修改鑄造者。
反饋 因為 BountyManager、ChefIncentivesController 和 MultiFeeDistribution 都將是可升級的,所以鑄造者始終保持相同的代理位址。
3.3 額外建議
3.3.1 潛在問題 18:Gas 優化 (Mfd 中的 zapVestingToLp())
| 項目 | 描述 |
|---|---|
| 狀態 | 版本 10 已修復 |
| 引入版本 | 版本 1 |
描述 zapVestingToLp() 函數只能由 LockZap 合約呼叫,以將使用者的鎖定收益轉出。它從索引 0 開始遍歷使用者的 earnings 陣列,並檢查 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 中的非空賞金儲備
| 項目 | 描述 |
|---|---|
| 狀態 | 版本 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 | 檢測器 | 描述 | 影響 | Found | FP | 結果 |
|---|---|---|---|---|---|---|
| 1 | arbitrary-send-erc20 | 使用任意 from 呼叫 transferFrom | 高 | 1 | 1 | 通過 |
| 2 | array-by-reference | 按值修改儲存陣列 | 高 | 0 | 0 | 通過 |
| 3 | incorrect-shift | 移位指令中的參數順序不正確 | 高 | 0 | 0 | 通過 |
| 4 | multiple-constructors | 多個建構子方案 | 高 | 0 | 0 | 通過 |
| 5 | name-reused | 重複使用合約名稱 | 高 | 0 | 0 | 通過 |
| 6 | protected-vars | 未經存取控制直接修改變數 | 高 | 0 | 0 | 通過 |
| 7 | rtlo | 使用從右到左覆寫控制字元 | 高 | 0 | 0 | 通過 |
| 8 | shadowing-state | 狀態變數遮蔽 | 高 | 1 | 1 | 通過 |
| 9 | suicidal | 允許任何人銷毀合約的函數 | 高 | 0 | 0 | 通過 |
| 10 | uninitialized-state | 未初始化的狀態變數 | 高 | 3 | 3 | 通過 |
| 11 | uninitialized-storage | 未初始化的儲存變數 | 高 | 0 | 0 | 通過 |
| 12 | unprotected-upgrade | 未受保護的可升級合約 | 高 | 1 | 1 | 通過 |
| 13 | arbitrary-send-erc20-permit | transferFrom 在使用 permit 時使用任意 from | 高 | 0 | 0 | 通過 |
| 14 | arbitrary-send-eth | 向任意目的地發送以太幣的函數 | 高 | 0 | 0 | 通過 |
| 15 | controlled-array-length | 受污染的陣列長度分配 | 高 | 0 | 0 | 通過 |
| 16 | controlled-delegatecall | 受控的 delegatecall 目的地 | 高 | 0 | 0 | 通過 |
| 17 | delegatecall-loop | 在迴圈中使用 delegatecall 的應付函數 | 高 | 0 | 0 | 通過 |
| 18 | msg-value-loop | 在迴圈中使用 msg.value | 高 | 0 | 0 | 通過 |
| 19 | reentrancy-eth | 重入漏洞(以太幣盜竊) | 高 | 5 | 5 | 通過 |
| 20 | storage-array | 有符號儲存整數陣列編譯器錯誤 | 高 | 0 | 0 | 通過 |
| 21 | unchecked-transfer | 未經檢查的代幣轉帳 | 高 | 12 | 12 | 通過 |
| 22 | weak-prng | 弱偽隨機數生成器 (PRNG) | 高 | 0 | 0 | 通過 |
| 23 | domain-separator-collision | 檢測函數簽名與 EIP-2612 DOMAIN_SEPARATOR() 碰撞的 ERC20 代幣 | 中 | 0 | 0 | 通過 |
| 24 | enum-conversion | 檢測危險的枚舉轉換 | 中 | 0 | 0 | 通過 |
| 25 | erc20-interface | 不正確的 ERC20 介面 | 中 | 0 | 0 | 通過 |
| 26 | erc721-interface | 不正確的 ERC721 介面 | 中 | 0 | 0 | 通過 |
| 27 | incorrect-equality | 危險的嚴格相等判斷 | 中 | 23 | 23 | 通過 |
| 28 | locked-ether | 鎖定以太幣的合約 | 中 | 1 | 1 | 通過 |
| 29 | mapping-deletion | 對包含結構的映射進行刪除 | 中 | 0 | 0 | 通過 |
| 30 | shadowing-abstract | 來自抽象合約的狀態變數遮蔽 | 中 | 0 | 0 | 通過 |
| 31 | tautology | 重言式或矛盾 | 中 | 0 | 0 | 通過 |
| 32 | write-after-write | 未使用的寫入 | 中 | 3 | 3 | 通過 |
| 33 | boolean-cst | 布林常數誤用 | 中 | 0 | 0 | 通過 |
| 34 | constant-function-asm | 使用組合語言程式碼的常數函數 | 中 | 0 | 0 | 通過 |
| 35 | constant-function-state | 改變狀態的常數函數 | 中 | 0 | 0 | 通過 |
| 36 | divide-before-multiply | 不精確的算術運算順序 | 中 | 20 | 20 | 通過 |
| 37 | reentrancy-no-eth | 重入漏洞(無以太幣盜竊) | 中 | 12 | 12 | 通過 |
| 38 | reused-constructor | 重複使用的基礎建構子 | 中 | 0 | 0 | 通過 |
| 39 | tx-origin | 危險地使用 tx.origin | 中 | 1 | 1 | 通過 |
| 40 | unchecked-lowlevel | 未經檢查的低階呼叫 | 中 | 0 | 0 | 通過 |
| 41 | unchecked-send | 未經檢查的 send | 中 | 0 | 0 | 通過 |
| 42 | uninitialized-local | 未初始化的區域變數 | 中 | 33 | 33 | 通過 |
| 43 | unused-return | 未使用的返回值 | 中 | 19 | 19 | 通過 |
4.2 自動化動態安全性測試結果
表 4.2:與借貸相關邏輯的測試屬性
| ID | 屬性 | 結果 |
|---|---|---|
| 1 | 呼叫 deposit 永遠不會導致 onBehalfOf 的 RToken 數量減少 | 通過 |
| 2 | 呼叫 withdraw 永遠不會導致 msg.sender 的 RToken 數量增加 | 通過 |
| 3 | 在穩定利率模式下呼叫 borrow 永遠不會導致 onBehalfOf 的 StableDebtToken 減少。 | 通過 |
| 4 | 在變動利率模式下呼叫 borrow 永遠不會導致 onBehalfOf 的 VariableDebtToken 減少。 | 通過 |
| 5 | 使用不等於 msg.sender 的 onBehalfOf 呼叫 borrow 永遠不會導致 msg.sender 的借貸額度增加。 | 通過 |
| 6 | 在穩定利率模式下呼叫 repay 永遠不會導致 onBehalfOf 的 StableDebtToken 增加。 | 通過 |
| 7 | 在變動利率模式下呼叫 repay 永遠不會導致 onBehalfOf 的 VariableDebtToken 增加。 | 通過 |
| 8 | liquidityIndex 永遠不會減少。 | 通過 |
| 9 | liquidityIndex 在同一區塊內將保持不變。 | 通過 |
| 10 | variableBorrowIndex 永遠不會減少。 | 通過 |
| 11 | variableBorrowIndex 在同一區塊內將保持不變。 | 通過 |
| 12 | 減少抵押品數量永遠不會導致健康係數小於 1。 | 通過 |
| 13 | 增加借貸數量永遠不會導致健康係數小於 1。 | 通過 |
表 4.3:與質押相關邏輯的測試屬性
| ID | 屬性 | 結果 |
|---|---|---|
| 1 | 使用者的總餘額永遠等於鎖定餘額、解鎖餘額和已賺取餘額之和。 | 通過 |
| 2 | 使用者的鎖定餘額永遠等於 userLocks 金額之和 | 通過 |
| 3 | 使用者的 lockedWithMultiplier 餘額永遠等於 userLocks 金額與使用者鎖定倍數的乘積之和 | 通過 |
| 4 | lockedSupply 永遠等於使用者的鎖定餘額之和 | 通過 |
| 5 | lockedSupplyWithMultiplier 永遠等於使用者的 lockedWithMultiplier 餘額之和 | 通過 |
| 6 | rewardPerTokenStored 永遠不會減少。 | 通過 |
| 7 | rewardPerTokenStored 在同一區塊內將保持不變。 | 通過 |
| 8 | totalSupply 永遠等於使用者的金額之和 | 通過 |
| 9 | accRewardPerShare 永遠不會減少。 | 通過 |
| 10 | accRewardPerShare 在同一區塊內將保持不變。 | 通過 |
表 4.4:其他功能的測試屬性
| ID | 屬性 | 結果 |
|---|---|---|
| 1 | LockedZap 合約中的 WETH 和 RDNT 餘額永遠為零。 | 通過 |
| 2 | LiquidityZap 合約中的 WETH 和 RDNT 餘額永遠為零。 | 通過 |
| 3 | BalancerPoolHelper 合約中的 WETH 和 RDNT 餘額永遠為零。 | 通過 |
| 4 | UniswapPoolHelper 合約中的 WETH 和 RDNT 餘額永遠為零。 | 通過 |
| 5 | 呼叫 loop 永遠會使使用者有資格獲得獎勵 | 通過 |
| 6 | 呼叫 loopETH 永遠會使使用者有資格獲得獎勵 | 通過 |
| 7 | 使用 _execute 為 false 呼叫 executeBounty 永遠不會導致儲存變更。 | 通過 |
| 8 | 使用 sender 等於 receiver 呼叫 transfer 永遠不會導致餘額變更。 | 版本 1 失敗。版本 7 通過 |
5 注意事項與評論
5.1 免責聲明
本報告不構成投資建議或個人推薦。它不考慮且不應被解釋為考慮或與代幣、代幣銷售或任何其他產品、服務或其他資產的潛在經濟效益有關。任何實體不應以任何方式依賴本報告,包括用於做出買賣任何代幣、產品、服務或其他資產的任何決定。
本報告並非對任何特定項目或團隊的代言,且本報告不保證任何特定項目的安全性。此安全性測試不保證能發現智慧合約的所有安全問題,即評估結果不保證不存在任何進一步的安全問題。由於安全性測試不能被視為全面,我們始終建議進行獨立審計和公開漏洞賞金計劃,以確保智慧合約的安全性。
本安全性測試的範圍僅限於第 1.2 節中提到的程式碼。除非明確指定,否則語言本身的安全性(例如 Solidity 語言)、底層編譯工具鏈和計算基礎設施均不在範圍內。
5.2 審計程序
我們遵循以下程序執行審計。
-
漏洞檢測:我們首先使用自動程式碼分析器掃描智慧合約,然後手動驗證(拒絕或確認)它們報告的問題。
-
語義分析:我們研究智慧合約的業務邏輯,並使用自動模糊測試工具(由我們的研究團隊開發)對可能存在的漏洞進行深入調查。我們還與獨立審計員手動分析可能的攻擊路徑,以交叉檢查結果。
-
建議:我們從良好程式設計實踐的角度為開發人員提供一些有用的建議,包括 Gas 優化、程式碼風格等。
我們在下方展示了主要具體的檢查點。
5.2.1 軟體安全
-
重入性 (Reentrancy)
-
拒絕服務 (DoS)
-
存取控制
-
資料處理與資料流程
-
異常處理
-
不受信任的外部呼叫與控制流程
-
初始化一致性
-
事件操作
-
易錯的隨機性
-
代理系統使用不當
5.2.2 DeFi 安全
-
語義一致性
-
功能一致性
-
權限管理
-
業務邏輯
-
代幣操作
-
緊急機制
-
預言機安全
-
白名單與黑名單
-
經濟影響
-
批次轉帳
5.2.3 NFT 安全
-
重複項目
-
代幣接收者的驗證
-
鏈下元資料安全性
5.2.4 額外建議
-
Gas 優化
-
程式碼品質與風格
注意:以上為主要檢查點。我們可能會根據專案的功能在審計過程中增加更多的檢查點。



