返回部落格

NearOinDao 安全審計報告

Code Auditing
2021年12月10日
閱讀約 31 分鐘

報告清單

項目 描述
客戶 Oinfinance
目標 NearOinDao

版本歷史

版本 日期 描述
1.0 2021 年 12 月 4 日 初次發布

1. 簡介

1.1 關於目標合約

目標合約包含一個穩定幣模塊。在此基礎上,它還實現了其他模塊,包括質押(Staking)和挖礦(Farming)。這些模塊為穩定幣(即 USDO)的穩定性創造了正向反饋循環。

信息 描述
類型 智能合約
語言 Rust
方法 半自動化與人工驗證

審計的代碼庫包含 NearOinDao ^1

審計過程是迭代的。具體來說,我們將進一步審計修復問題後的提交(commits)。如果有新的問題,我們將繼續此過程。因此,本報告中引用了多個提交 SHA 值。審計前後的提交 SHA 值如下所示:

審計期間

審計後

項目 提交 SHA
NearOinDao 3bd117606c753d3c2f66b6dcddd1ae18ea47a20a

1.2 安全模型

為了評估風險,我們遵循業界和學術界廣泛採用的標準或建議,包括 OWASP 風險評級方法論 ^2 和常見弱點枚舉(CWE) ^3。因此,本報告中衡量的嚴重程度分為四類:高、中、低 和 未確定。

2. 審計結果

總體而言,我們在智能合約中發現了 22 個潛在問題。我們亦提出了 12 條建議,如下所示:

  • 高風險:19

  • 中風險:2

  • 低風險:1

  • 建議:12

詳細內容請參見以下各節。

ID 嚴重程度 描述 類別 狀態
1 高 修改 self.liquidation_line 時出現邏輯錯誤 軟件安全 已確認並修復
2 高 函數 liquidation 可能無法運行 軟件安全 已確認並修復
3 高 設置合約開啟時間戳時邏輯錯誤 軟件安全 已確認並修復
4 高 若跨合約交易失敗,合約狀態未回滾 軟件安全 已確認並修復
5 高 任何人都可以增加獎勵餘額 DeFi 安全 已確認並修復
6 高 任何人都可以增加穩定池獎勵餘額 DeFi 安全 已確認並修復
7 高 任何人都可以燃燒其他用戶的代幣 DeFi 安全 已確認並修復
8 高 任何人都可以增加其賬戶餘額 DeFi 安全 已確認並修復
9 高 預言機未檢查時間間隔 DeFi 安全 已確認並修復
10 高 預言機時間間隔過長 DeFi 安全 已確認並修復
11 高 Oin 價格沒有預言機 DeFi 安全 已確認並修復
12 高 用戶可以獲得額外獎勵 DeFi 安全 已確認並修復
13 高 用戶可以支付更少的穩定費 DeFi 安全 已確認並修復
14 中 多簽請求可以在相對較低的確認比例下被確認 DeFi 安全 已確認並修復
15 中 每年區塊數不準確 DeFi 安全 已確認並修復
16 高 可鑄造代幣數量計算錯誤 DeFi 安全 已確認並修復
17 高 支付穩定費可能導致用戶存入的代幣丟失 DeFi 安全 已確認並修復
18 高 質押比率不正確 DeFi 安全 已確認並修復
19 低 獎勵代幣可能超過限制 DeFi 安全 已確認並修復
20 高 不同權限用戶使用相同的白名單 DeFi 安全 已確認並修復
21 高 未檢查穩定費地址 DeFi 安全 已確認並修復
22 高 獎勵代幣的 total_reward 可被多簽管理員修改 DeFi 安全 已確認並修復
23 - 冗餘斷言 建議 已確認並修復
24 - 對清算線的重複考量 建議 已確認並修復
25 - 冗餘的白名單檢查 建議 已確認並修復
26 - 未使用的函數 建議 已確認並修復
27 - 冗餘代碼 建議 已確認並修復
28 - 函數名稱與實現衝突 建議 已確認並修復
29 - 冗餘代碼 建議 已確認並修復
30 - 計算精度可提升 建議 已確認並修復
31 - 系統可能未記錄之前戳記過的價格 建議 已確認並修復
32 - 清算時抵押代幣分配不連續 建議 已確認並修復
33 - 計算精度優化非必要 建議 已確認並修復
34 - 中心化設計的風險 建議 已確認(未修復)

2.1 軟件安全

2.1.1 潛在問題 1:同一用途存在兩個不同屬性

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。兩個屬性(即 self.cost 和 self.liquidation_line)代表相同的合約狀態,即用戶的清算線。它們在合約的不同函數中使用(列表 2.1 和 2.2)。然而,self.liquidation_line 可以通過 set_liquidation_line 函數修改,而 self.cost 不能更改。在這種情況下,如果 self.liquidation_line 被修改,self.cost 會保持原始值。這可能會影響 assert_user_ratio 函數的邏輯(列表 2.1)。

pub(crate) fn assert_user_ratio(&self) {
        let user_ratio = self.internal_user_ratio(env::predecessor_account_id());
        if user_ratio != 0 {
            assert!(user_ratio >= self.cost, "User ratio less than standard.");
        }
    }

列表 2.1: assert_user_ratio:lib.rs

// TODO liquidation
    #[payable]
    pub fn liquidation(&mut self, account: AccountId) {
        assert!(self.is_liquidation_paused(), "{}", SYSTEM_PAUSE);
        let ratio = self.internal_user_ratio(account.clone());
        assert!(ratio > 0, "No current pledge");
        assert!(ratio <= self.liquidation_line, "Not at the clearing line");
        ...

列表 2.2: internal_can_mint_amount:lib.rs

影響 用戶的清算線在合約的不同函數中不一致,這影響了整個合約的邏輯。

建議 I 我們可以在計算用戶質押比率並將其與系統清算線進行比較時,統一這兩個屬性的使用。

2.1.2 潛在問題 2:清算獎勵分配無效

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-4 或之前引入。清算發送者的賬戶和合約所有者的賬戶可能未註冊(列表 2.3 的第 193 和 206 行)。在這種情況下,當發送者試圖執行清算操作時,由於拋出了賬戶未註冊的異常,交易無法成功執行。

pub(crate) fn personal_liquidation_token(&mut self, send_id: AccountId, account_id: AccountId, liquidation_gas: Balance, surplus_token: Balance, liquidation_fee: Balance) {
        //self.owner_id
        let coin_id = ST_NEAR.to_string();
        let mut sys_reward_coin = self.internal_get_reward_coin(coin_id.clone());
        
        let account_reward_key_o = self.get_staker_reward_key(send_id.clone(), coin_id.clone());
        let user_reward_coin_o = self.internal_get_account_reward(send_id.clone(), coin_id.clone());
        
        self.account_reward.insert(
            &account_reward_key_o,
            &UserReward {
                index:  user_reward_coin_o.index,
                reward: user_reward_coin_o.reward.checked_add(liquidation_gas).expect(ERR_ADD),
            },
        );
        
        let account_reward_key_t = self.get_staker_reward_key(account_id.clone(), coin_id.clone());
        let user_reward_coin_t = self.internal_get_account_reward(account_id.clone(), coin_id.clone());

        if surplus_token > 0 {
            self.account_reward.insert(
                &account_reward_key_t,
                &UserReward {
                    index:  user_reward_coin_t.index,
                    reward: user_reward_coin_t.reward.checked_add(surplus_token).expect(ERR_ADD),
                },
            );
        }

        let account_reward_key_s = self.get_staker_reward_key(self.owner_id.clone(), coin_id.clone());
        let user_reward_coin_s = self.internal_get_account_reward(self.owner_id.clone(), coin_id.clone());

        self.account_reward.insert(
            &account_reward_key_s,
            &UserReward {
                index:  user_reward_coin_s.index,
                reward: user_reward_coin_s.reward.checked_add(liquidation_fee).expect(ERR_ADD),
            },
        );
       
        sys_reward_coin.total_reward = sys_reward_coin
            .total_reward
            .checked_add(liquidation_gas).expect(ERR_ADD)
            .checked_add(liquidation_fee).expect(ERR_ADD)
            .checked_add(surplus_token).expect(ERR_ADD);

        self.reward_coins.insert(&coin_id, &sys_reward_coin);
    }

}

列表 2.3: personal_liquidation_token:reward.rs

影響 由於拋出賬戶未註冊的異常,清算函數無法成功執行。

建議 I 在清算函數開頭斷言清算發送者賬戶和合約所有者賬戶的存在。

2.1.3 潛在問題 3:開啟系統時 block_timestamp 被保存到 closed_time

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。調用 internal_open 函數時,env::block_time_stamp() 不應保存到 self.closed_time。

#[private]
    pub fn internal_open(&mut self) {
        self.closed_time = env::block_timestamp();
        self.open_stake();
        self.open_redeem();
        self.open_claim_reward();
        self.open_liquidation();
        self.open_stable();
        log!(
            "{} open sys in {}",
            env::predecessor_account_id(),
            self.closed_time
        );
    }

列表 2.4: internal_open:esm.rs

影響 合約的開啟時間和關閉時間完全錯誤。依賴時間信息的進一步更新可能會出現邏輯錯誤。

建議 I 我們建議創建一個名為 self.opening_time 的新合約狀態,並在調用開啟合約時將 env::block_timestamp() 賦值給該值。

2.1.4 潛在問題 4:跨合約函數調用失敗時,合約狀態未回滾

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。在跨合約函數調用過程中,storage_deposit 和 ft_transfer 的處理可能會失敗。我們無法保證轉賬一定能正確執行。如果調用失敗,回調函數不會回滾合約狀態。

#[private]
    pub fn storage_deposit_callback(&mut self) {
        match env::promise_result(0) {
            PromiseResult::NotReady => unreachable!(),
            PromiseResult::Successful(_) => {
                log!("Transfer success");
            }
            PromiseResult::Failed => {
                log!("Transfer failed");
            }
        }
    }

列表 2.5: storage_deposit_callback:ft.rs

#[private]
    pub fn liquidation_transfer_callback(&mut self) {
        match env::promise_result(0) {
            PromiseResult::NotReady => unreachable!(),
            PromiseResult::Successful(_) => {
                log!("Transfer success");
            }
            PromiseResult::Failed => {
                log!("Transfer failed");
            }
        }
    }

列表 2.6: liquidation_transfer_callback:ft.rs

影響 當交易失敗時,由於回調函數未回滾合約狀態,用戶可能會損失其資產。

建議 I 我們需要在跨合約調用的回調函數中(當轉帳失敗時)回滾合約狀態。

2.2 DeFi 安全

2.2.1 潛在問題 5:inject_reward 缺乏訪問控制

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函數 inject_reward 是公開的。任何人都可以調用此函數來增加合約中獎勵的餘額。

pub fn inject_reward(&mut self, amount: U128, reward_coin: AccountId) {
        // self.assert_owner();

        if reward_coin == String::from("NEAR") {
            assert!(
                amount.0 == env::attached_deposit(),
                "Amount not equal transfer_amount"
            );
        }
        ...
    }

列表 2.7: inject_reward:pool.rs

影響 任何人都可以隨意增加合約的獎勵餘額。

建議 I 該函數應更改為私有函數,因為它是在接收到轉入的獎勵後在內部調用的。

2.2.2 潛在問題 6:inject_sp_reward 缺乏訪問控制

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函數 inject_sp_reward 是公開的。任何人都可以調用此函數來增加合約中穩定池獎勵的餘額。

pub fn inject_sp_reward(&mut self, _amount: U128, sender_id: ValidAccountId) {
        self.reward_sp = self.reward_sp + u128::from(_amount);

        log!(
            "{} add sp_reward  {} cur amount{}",
            sender_id,
            u128::from(_amount),
            self.reward_sp
        );
    }

列表 2.8: inject_sp_reward:stablepool.rs

影響 任何人都可以隨意增加合約穩定池獎勵的餘額。

建議 I 該函數應更改為私有函數,因為它是在接收到轉入的穩定池獎勵後在內部調用的。

2.2.3 潛在問題 7:burn_coin 缺乏訪問控制

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函數 burn_coin 是公開的。任何人都可以調用此函數來燃燒任何人的代幣。

pub fn burn_coin(&mut self, amount: U128, fee: Balance, sender_id: ValidAccountId) -> Balance{
        assert!(self.is_redeem_paused(), "{}", SYSTEM_PAUSE);
        let sender_id = AccountId::from(sender_id);
        self.assert_is_poked();
        self.accured_token(sender_id.clone());
        ...
    }

列表 2.9: burn_coin:lib.rs

影響 任何人都可以使用此功能燃燒任何人的代幣,導致用戶資產損失。

建議 I 該函數應更改為私有函數,因為它是在接收到用於燃燒代幣的穩定期費後在內部調用的。

2.2.4 潛在問題 8:deposit_token 缺乏訪問控制

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函數 deposit_token 是公開的。任何人都可以調用此函數來增加其賬戶餘額。

pub fn deposit_token(&mut self, amount: u128, _sender_id: ValidAccountId) {
        self.assert_is_poked();
        assert!(self.is_stake_paused(), "{}", SYSTEM_PAUSE);
        let _amount = u128::from(amount);
        let sender_id = AccountId::from(_sender_id);
        . . .
    }

列表 2.10: deposit_token:lib.rs

影響 攻擊者可以調用此函數來增加其賬戶餘額。

建議 I 該函數應更改為私有函數,因為它是在接收到存入的代幣後在內部調用的。

2.2.5 潛在問題 9:預言機缺乏時間檢查

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。oracle.rs 中的 assert_is_poked 函數僅檢查代幣價格值是否為零。這沒有意義,因為代幣價格一直在變化。

pub(crate) fn assert_is_poked(&self) {
        assert!(self.token_price != 0, "Oracle price isn't poked.");
    }

列表 2.11: assert_is_poked:oracle.rs

影響 此問題影響價格預言機。如果代幣價格在很長時間內沒有被「戳記」(poked),斷言仍然可以通過,相關交易可能會以過時的價格執行。

建議 I 合約應為戳記價格設置一個有效的時間段。

2.2.6 潛在問題 10:預言機「戳記」間隔時間不合理

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。在 types.rs 中定義的常量 POKE_INTERVAL_TIME 現在意味著 1000 天。這個時間間隔似乎太長了。需要一個合理的值。

pub const POKE_INTERVAL_TIME: u64 = 86_400_000_000_000_000;

列表 2.12: types.rs

影響 戳記價格的時間間隔不合理。

建議 I 用合理的值重置戳記價格的間隔時間。

2.2.7 潛在問題 11:缺少 Oin_Price 的斷言

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。該函數沒有檢查 oin_token 價格值是否已戳記,因為用戶的穩定費是由 self.oin_price 計算的。

pub fn internal_user_stable(&self, account: AccountId) -> u128 {
        let user_stable = self.account_stable.get(&account).expect("error");
        let allot = self.get_account_allot(account.clone()); 
        let coin = self
            .account_coin
            .get(&account)
            .expect("error")
            .checked_add(allot.0)
            .expect(ERR_ADD);
        let current_block_number = env::block_timestamp().checked_div(INIT_BLOCK_TIME).expect(ERR_DIV);
        user_stable
            .saved_stable
            .checked_add(
                self.stable_fee_rate//16
                    .checked_div(BLOCK_PER_YEAR)
                    .expect(ERR_DIV)
                    .checked_mul(current_block_number as u128 - user_stable.block)
                    .expect(ERR_MUL)
                    .checked_mul(coin)//8
                    .expect(ERR_MUL)
                    .checked_div(self.oin_price)//8
                    .expect(ERR_DIV)
                    .checked_div(ONE_COIN)//8
                    .expect(ERR_DIV),
            )
            .expect(ERR_ADD)
    }

列表 2.13: internal_user_stable:lib.rs

影響 過時的 OIN 價格可能導致價格操縱,而無需檢查預言機戳記價格的時效性。

建議 I 在計算用戶穩定費之前添加 self.assert_is_poked(); 斷言。

2.2.8 潛在問題 12:用戶可能通過質押代幣獲得更多挖礦獎勵

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。申領的獎勵計算不準確。調用 internal_get_saved_reward 函數來計算用戶從 t0 到 t1 的特定挖礦獎勵,公式如下:

請注意,account_allot.token 是通過其他用戶清算添加的抵押品獎勵。然而,清算可能在 t0 到 t1 之間的任何時間發生。例如,用戶在 day0 存入了 100 個代幣。在 day999,觸發了其他用戶的清算,使得 account_allot.token 可能增加到 1000。

當用戶在 day1000 申領獎勵時,day999 清算產生的 1000 個代幣應該僅被計入一天的挖礦獎勵。然而,合約實際上計算了從 day0 到 day1000 的抵押獎勵價值。

// TODO[OK] Calculation of reward
    pub(crate) fn internal_get_saved_reward(
        &self,
        staker: AccountId,
        reward_coin: AccountId,
    ) -> u128 {
        let reward_coin_ins = self.internal_get_reward_coin(reward_coin.clone());
        let (stake_token_num, _) = self.staker_debt_of(staker.clone());

        if let Some(user_reward) = self
            .account_reward
            .get(&self.get_staker_reward_key(staker.clone(), reward_coin.clone()))
        {
            user_reward
                .reward
                .checked_add(
                    U256::from(
                        reward_coin_ins
                            .index
                            .checked_sub(user_reward.index)
                            .expect(ERR_SUB),
                    )
                    .checked_mul(U256::from(stake_token_num))
                    .expect(ERR_MUL)
                    .checked_div(U256::from(reward_coin_ins.double_scale))
                    .expect(ERR_DIV)
                    .as_u128(),
                )
                .expect(ERR_ADD)
        } else {
            0
        }
    }

列表 2.14: internal_get_saved_reward:views.rs

pub fn staker_debt_of(&self, staker: AccountId) -> (u128, u128) {
        if let Some(token) = self.account_token.get(&staker) {
            let coin = self.account_coin.get(&staker).expect(ERR_NOT_REGISTER);
            let allot = self.get_account_allot(staker.clone());
            (token + allot.1, coin + allot.0)
        } else {
            (0, 0)
        }
    }

列表 2.15: staker_debt_of:views.rs

影響 用戶可能獲得額外獎勵。

建議 I 計算挖礦獎勵時刪除新分配抵押品的劃分。我們可以使挖礦獎勵僅與用戶存入的代幣數量相關。

2.2.9 潛在問題 13:用戶支付的穩定費可能更少

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。假設用戶在 day0 鑄造了 1000 個 USDO,當時的 stable_fee_rate 為 0.01 oin/coin/day。如果用戶在 day100 退還了 1000 個 USDO,且穩定費率在過去 100 天內沒有變化,他需要支付的穩定費為 0.01 Oin/coin/day * 1000 Coin * 100 Day = 1000 Oin。然而,如果所有者在 day99 將 stable_fee_rate 設置為 0.005 oin/coin/day。這時,用戶只需要支付 0.005 Oin/Coin/Day * 1000 Coin * 100 Day = 500 Oin。事實上,準確的費用應該是:(0.01 Oin/Coin/Day * 1000 Coin * 99 Day) + (0.005 Oin/Coin/Day * 1000 Coin * 1 Day) = 990 Oin + 5 Oin = 995 Oin。

在這種情況下,495 Oin 不需要由用戶支付。

// TODO [OK]
    pub fn set_stable_fee_rate(&mut self, fee_rate: U128) {
        self.assert_param_white();
        self.update_stable_index();
        assert!(fee_rate.0 <= INIT_MAX_STABLE_FEE_RATE, "Exceeding the maximum setting");
        self.stable_fee_rate = fee_rate.into();
        log!("Set stable fee rate {}", fee_rate.0);
    }

列表 2.16: set_stable_fee_rate:dparam.rs

pub fn update_stable_index(&mut self) {
    }

列表 2.17: update_stable_index:stablefee.rs

影響 合約用戶支付的穩定費可能變少。

建議 I 像計算合約中的 reward_coin 一樣,實現穩定費的系統索引。並確保每當合約用戶調用 set_stable_fee_rate、liquidation 和 update_stable_fee 時,都會更新穩定費的系統索引。

2.2.10 潛在問題 14:不合理的多簽請求確認率

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。多簽名請求確認率是根據請求創建時的多簽管理員數量計算的。但多簽名管理員的數量隨後可能會發生變化。在這種情況下,如果管理員數量增加,請求可以在較低的確認比例下被確認。

pub(crate) fn is_num_enough(&self, request_id: RequestId) -> bool {
        let request = self.requests.get(&request_id).unwrap();
        let confirmations = self.confirmations.get(&request_id).unwrap();

        let num_confirmrations = request.num_confirm_ratio * (request.mul_white_num);
        log!(
            "confim num is {} num needed is {} ",
            confirmations.len() as u32 * 100,
            num_confirmrations
        );

        (confirmations.len() as u64) * 100 >= num_confirmrations
    }

列表 2.18: is_num_enough:multisign.rs

pub fn add_request_only(&mut self, request: MultiSigRequest) -> RequestId {
        self.assert_mul_white();
        ...

        let request_added = MultiSigRequestWithSigner {
            signer_pk: env::signer_account_pk(),
            added_timestamp: env::block_timestamp(),
            confirmed_timestamp: 0,
            request: request,
            is_executed: false,
            cool_down: self.request_cooldown,
            mul_white_num: self.mul_white_num(),
            num_confirm_ratio: self.num_confirm_ratio,
        };

        self.requests.insert(&self.request_nonce, &request_added);
        ...
    }

列表 2.19: add_request_only:multisign.rs

影響 多簽請求可能以較低的確認率確認,因為合約只考慮了請求創建時的管理員數量。

建議 I 考慮使用當前合約狀態下的多簽用戶數量來計算多簽請求確認率。

2.2.11 潛在問題 15:每年區塊數不準確

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。鑑於 NEAR 協議主網上每秒生成一個區塊,每年的區塊數應該是 31536000(365 天),而不是 31104000(360 天)。

pub const BLOCK_PER_YEAR: u128 = 31104000;

列表 2.20: types.rs

影響 不準確的 BLOCK_PER_YEAR 常量會導致使用該常量的計算結果與現實不符。

建議 I 將 BLOCK_PER_YEAR 修改為 31536000。

2.2.12 潛在問題 16:可鑄造 USDO 的最大值計算不對

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。allot_token.0 代表分配的債務。在計算 USDO 的可用鑄造金額時,不應計算分配的債務。否則,債務極高的用戶可以鑄造大量的 USDO。

pub(crate) fn internal_can_mint_amount(&self, account: AccountId) -> u128 {
        self.assert_is_poked();
        let token = self.account_token.get(&account).expect(ERR_NOT_REGISTER);
        let guarantee = self.guarantee.get(&account).expect(ERR_NOT_REGISTER);
        let allot_token = self.get_account_allot(account.clone());

        let max_usdo = (U256::from(token)
            .checked_add(U256::from(allot_token.1))
            .expect(ERR_ADD))
        .checked_mul(U256::from(self.token_price))
        .expect(ERR_MUL)
        .checked_div(U256::from(self.liquidation_line))
        .expect(ERR_DIV)
        .checked_div(U256::from(INIT_STABLE_INDEX))
        .expect(ERR_DIV)
        .checked_add(U256::from(allot_token.0))
        .expect(ERR_ADD)
        .checked_sub(U256::from(guarantee))
        .unwrap_or(U256::from(0))
        .as_u128();
        
        ...
    }

列表 2.21: internal_can_mint_amount:lib.rs

影響 用戶在調用 mint_coin 函數時可以鑄造額外的 USDO。

建議 I 代表分配債務的 allot_token.0 不應被計入可用鑄造的 USDO。

2.2.13 潛在問題 17:用戶穩定費處理不當

項目 描述
狀態 已確認並修復(相關邏輯現已移除)

描述 此問題在 Commit-1 或之前引入。當用戶調用 burn_coin 函數時,穩定費是用「OIN」代幣支付的,而不是「ST_NEAR」。然而,合約會減少用戶質押代幣的餘額,這是不準確的。

pub(crate) fn burn_coin(&mut self, amount: U128, fee: Balance, sender_id: ValidAccountId) -> Balance{
        ...
            assert!(usdo >= amount.into(), "Insufficient amount");
            let token = self.account_token.get(&sender_id.clone()).expect(ERR_NOT_REGISTER);
            self.internal_burn(sender_id.clone(), amount.into());
   
            self.total_token = self.total_token.checked_sub(unpaid_fee.into()).expect(ERR_SUB);
            self.account_token.insert(
                &sender_id.clone(),
                &token.checked_sub(unpaid_fee.into()).expect(ERR_SUB),
            );
        ...
        
    }

列表 2.22: burn_coin:lib.rs

影響 由於對用戶穩定費的處理不當,用戶的質押代幣可能會減少。

建議 I 使用正確的代幣支付穩定費。

2.2.14 潛在問題 18:系統比率不正確

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。如果 total_coin = 0,該比率應為 +∞。將其設置為 0 是不正確的。

pub(crate) fn internal_sys_ratio(&self) -> u128 {
        self.assert_is_poked();
        let token_usd = U256::from(self.total_token)
            .checked_mul(U256::from(self.token_price))
            .expect(ERR_MUL); /* 32 */
        let total_coin = self.total_coin + self.total_guarantee;
        if total_coin == 0 {
            0
        } else {
            token_usd
                .checked_div(U256::from(STAKE_RATIO_BASE))
                .expect(ERR_DIV)
                .checked_div(U256::from(total_coin))
                .expect(ERR_DIV)
                .as_u128()
        }
    }

列表 2.23: internal_sys_ratio:lib.rs

影響 系統很可能因不正確的比率而關閉。

建議 I 將 if 條件 total_coin = 0 改為 token_usd = 0。

2.2.15 潛在問題 19:獎勵代幣數量可能大於上限

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。當現在有 20 個獎勵代幣時,列表 2.24 第 131 行的斷言可以通過。在這種情況下,可以再添加一個獎勵代幣,獎勵代幣的總數量可能大於 REWARD_UPPER_BOUND。

pub(crate) fn internal_add_reward_coin(&mut self, coin: RewardCoin) {
        assert!(
            self.reward_coins.len() <= REWARD_UPPER_BOUND,
            "The currency slot has been used up, please modify other currency information as appropriate",
        );

        match self.reward_coins.get(&coin.token) {
            Some(_) => {
                env::panic(b"The current currency has been added, please add a new currency.");
            }
            None => {}
        }
        self.reward_coins.insert(&coin.token, &coin);

        log!(
            "{} add the RewardCoin=> {:?}",
            env::predecessor_account_id(),
            coin
        )
    }

列表 2.24: internal_add_reward_coin:pool.rs

影響 可添加獎勵代幣的數量與系統設計衝突。

建議 I 將斷言更改為 self.reward_coins.len() < REWARD_UPPER_BOUND。

2.2.16 潛在問題 20:不同權限的用戶使用相同的白名單

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函数 assert_param_white、assert_white、assert_esm_white 和 assert_oracle_white 用於不同的權限。然而,它們共享相同的白名單。

pub(crate) fn assert_esm_white(&self) {
        self.assert_white()
    }

列表 2.25: assert_esm_white:esm.rs

pub(crate) fn assert_param_white(&self) {
        self.assert_white();
    }

列表 2.26: assert_param_white:dparam.rs

pub(crate) fn assert_oracle_white(&self) {
        self.assert_white();
    }

列表 2.27: assert_oracle_white:oracle.rs

影響 不同權限的用戶共享相同的白名單。

建議 I 為具有不同權限的用戶實現不同的白名單。

2.2.17 潛在問題 21:burn_coin 未檢查代幣類型

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函數 burn_coin 沒有檢查代幣類型。在這種情況下,攻擊者可以轉移任意代幣並指定數量來支付穩定費。

pub fn burn_coin(&mut self, amount: U128, fee: Balance, sender_id: ValidAccountId) -> Balance{
        assert!(self.is_redeem_paused(), "{}", SYSTEM_PAUSE);
        let sender_id = AccountId::from(sender_id);

列表 2.28: assert_esm_white:esm.rs

影響 用戶無需支付 Oin 代幣。相反,他們可以通過轉移任意代幣並支付所需數量來支付穩定費。

建議 I 檢查接收到的代幣地址。

2.2.18 潛在問題 22:獎勵代幣的 total_reward 可由多簽管理員修改

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。inject_reward 函數裝飾了 #[private]。因此,多簽管理員可以通過多簽請求調用此函數,並在不注入獎勵的情況下任意增加總獎勵金額。

#[payable]
    #[private]
    pub fn inject_reward(&mut self, amount: U128, reward_coin: AccountId) {
        // self.assert_owner();

        if reward_coin == String::from("NEAR") {
            assert!(
                amount.0 == env::attached_deposit(),
                "Amount not equal transfer_amount"
            );
        }

        if let Some(reward_coin_ins) = self.get_reward_coin(reward_coin.clone()) {
            let mut reward_coin_ins = reward_coin_ins;
            reward_coin_ins.total_reward = reward_coin_ins
                .total_reward
                .checked_add(amount.into())
                .expect(ERR_SUB);
            self.reward_coins.insert(&reward_coin, &reward_coin_ins);

            if reward_coin == String::from("NEAR") {
            
            } else {
                log!("Transfer is not required for post-processing");
            }
        } else {
            env::panic(b"No the reward coin.");
        }
    }

列表 2.29: ainject_reward:pool.rs

建議 I 刪除 #[private] 裝飾器,並將 inject_reward 函數的可見性更改為私有。

2.3 附加建議

2.3.1 冗餘斷言

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-2 或之前引入。inject_reward 函數應該僅由 ft_on_transfer 在內部調用。獎勵代幣的地址在 ft_on_transfer 中檢查。在這種情況下,我們不需要在 inject_reward 函數開頭檢查獎勵代幣的名稱。

#[payable]
    #[private]
    pub  fn inject_reward(&mut self, amount: U128, reward_coin: AccountId) {
        // self.assert_owner();

        if reward_coin == String::from("NEAR") {
            assert!(
                amount.0 == env::attached_deposit(),
                "Amount not equal transfer_amount"
            );
        }

    ...
    }

列表 2.30: inject_reward:pool.rs


    pub fn ft_on_transfer(
        &mut self,
        sender_id: ValidAccountId,
        amount: U128,
        msg: String, /* token */
    ) -> PromiseOrValue<U128> {
    ...
            FtOnTransferArgs::InjectReward => {
                assert_eq!(sender_id.to_string(), self.owner_id, "ERR_NOT_ALLOWED");

                assert!(
                    self.reward_coins.get(&token_account_id).is_some(),
                    "Invalid reward coin"
                );

                self.inject_reward(amount, token_account_id);
                amount_return = 0;
            }
    ...
    }

列表 2.31: ft_on_transfer:lib.rs

建議 I 刪除 inject_reward 中關於獎勵代幣名稱的檢查。

2.3.2 用戶清算比率的重複斷言

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。清算線已在 internal_avaliable_token 函數中考慮,因此無需檢查用戶比率是否隨後達到清算線。

#[payable]
    pub fn withdraw_token(&mut self, amount: U128) {
        assert!(self.is_stake_paused(), "{}", SYSTEM_PAUSE);
        let mut amount = amount.0;

        let token = self.internal_avaliable_token(env::predecessor_account_id());
        let debt = self.get_dept(env::predecessor_account_id());

        log!("token :{} amount: {}", token, amount);
        assert!(token >= amount, "Insufficient avaliable token.");
        if debt.0 - debt.2 == 0 {
            if token - amount < self._min_amount_token() {
                amount = token;
            }
        } else {
            self.assert_user_ratio();
            if token - amount < self._min_amount_token() {
                env::panic(b"Please return all coins first");
            }
        }

列表 2.32: withdraw_token:lib.rs

建議 I 刪除列表 2.32 第 559 行的冗餘斷言。

2.3.3 冗餘白名單檢查

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函數 set_reward_speed 調用 assert_param_white 函數來檢查權限。同時,由 set_reward_speed 調用的 internal_set_reward_speed 再次調用了 assert_white。assert_white 擁有與 assert_param_white 相同的白名單。

pub fn set_reward_speed(&mut self, reward_coin: AccountId, speed: U128) {
        self.assert_param_white();
        self.internal_set_reward_speed(reward_coin, speed);
    }

列表 2.33: set_reward_speed:dparam.rs

pub(crate) fn internal_set_reward_speed(&mut self, reward_coin: AccountId, speed: U128) {
        self.assert_white();
        self.update_index();
        . . .
    }

列表 2.34: internal_set_reward_speed:pool.rs

建議 I 刪除 internal_set_reward_speed 函數內部的 assert_white。

2.3.4 未使用函數

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。函數 on_inject_reward 未被任何其他函數使用。因此,可以將其刪除。

#[private]
    pub fn on_inject_reward(&mut self, reward_coin: AccountId, amount: U128) {
        match env::promise_result(0) {
            PromiseResult::NotReady => unreachable!(),
            PromiseResult::Successful(_) => {}
            PromiseResult::Failed => {
                let mut reward_coin_ins = self.internal_get_reward_coin(reward_coin.clone());
                reward_coin_ins.total_reward = reward_coin_ins
                    .total_reward
                    .checked_sub(amount.into())
                    .expect(ERR_ADD);
                self.reward_coins.insert(&reward_coin, &reward_coin_ins);
            }
        };
    }

列表 2.35: on_inject_reward:pool.rs

建議 I 刪除函數 on_inject_reward。

2.3.5 冗餘代碼

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。account_allot.get() 用於獲取分配的獎勵和債務。在函數 set_account_allot 內部,不需要調用此函數。

pub(crate) fn set_account_allot(&mut self,account_id: AccountId){
        //Update [personally assigned debt, personally assigned pledge] to system value
        let (allot_debt, allot_token) = self.get_account_allot(account_id.clone());
        let token = self.account_token.get(&account_id).expect(ERR_NOT_REGISTER);
        let coin = self.account_coin.get(&account_id).expect(ERR_NOT_REGISTER);

        self.account_allot.get(&account_id);

        self.account_allot.insert(
            &account_id, 
            &AccountAllot{
                account_allot_debt: self.sys_allot_debt,
                account_allot_token: self.sys_allot_token,
            }
        );
        self.account_coin.insert(&account_id, &coin.checked_add(allot_debt).expect(ERR_ADD));
        self.account_token.insert(&account_id, &token.checked_add(allot_token).expect(ERR_ADD));       
    }

列表 2.36: set_account_allot:allot.rs

建議 I 刪除第 42 行對 account_allot.get() 的調用。

2.3.6 函數名稱與實現相反

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。is_stake_paused、is_redeem_paused、is_claim_reward_paused、is_liquidation_paused 和 is_stable_paused 函數被定義為表示函數是否被暫停。然而,當特定的屬性為 live 時,它們返回 True。

// TODO [OK]
    pub(crate) fn is_stake_paused(&self) -> bool {
        self.stake_live == 1
    }

    // TODO [OK]
    pub(crate) fn is_redeem_paused(&self) -> bool {
        self.redeem_live == 1
    }

    // TODO [OK]
    pub(crate) fn is_claim_reward_paused(&self) -> bool {
        self.claim_live == 1
    }

    // TODO [OK]
    pub(crate) fn is_liquidation_paused(&self) -> bool {
        self.liquidation_live == 1
    }

    // TODO [OK]
    pub(crate) fn is_stable_paused(&self) -> bool {
        self.stable_live == 1
    }

列表 2.37: is_{stake|redeem|claim_reward|liquidation|stable}_paused:esm.rs

建議 I 將 is_{stake|redeem|claim_reward|liquidation|stable}paused 的函數名稱更改為 is{stake|redeem|claim_reward|liquidation|stable}_live。

2.3.7 冗餘代碼

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。函數 update_stable_fee 用於更新所需的穩定費。穩定費與質押的代幣無關。因此,更改用戶的代幣餘額不需要更新穩定費。

pub(crate) fn deposit_token(&mut self, _amount: u128, _sender_id: ValidAccountId) {
        self.assert_is_poked();
        assert!(self.is_stake_paused(), "{}", SYSTEM_PAUSE);
        let sender_id = AccountId::from(_sender_id);
        assert!(_amount > 0, "Deposit token amount must greater than zero.");

        if let Some(0) = self.guarantee.get(&sender_id) {
            assert!(
                _amount >= self._min_amount_token(),
                "Deposit token amount must greater the minimum deposit token."
            );
        }
        self.update_personal_token(sender_id.clone());
        self.update_stable_fee(sender_id.clone());
        self.set_account_allot(sender_id.clone());
        . . .
    }

列表 2.38: deposit_token:lib.rs

建議 I 刪除第 344 行調用 update_stable_fee 的代碼。

2.3.8 計算精度可增強

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-3 或之前引入。internal_user_stable 函數旨在計算穩定費。通過在除法之前執行乘法,可以增強計算精度。

pub(crate) fn update_stable_fee(&mut self, account: AccountId) {
        if let Some(mut user_stable) = self.account_stable.get(&account) {
            let allot = self.get_account_allot(account.clone());
            let debt = allot.0;
            let current_block_number = self.to_nano( env::block_timestamp()) as u128;

            let coin = self.account_coin.get(&account).expect(ERR_NOT_REGISTER).checked_add(debt).expect(ERR_ADD);
            let delta_block = current_block_number.checked_sub(user_stable.block).expect(ERR_SUB);
            if delta_block > 0 && coin > 0 {
                let fee = self.stable_fee_rate//16
                        .checked_mul(delta_block).expect(ERR_MUL)
                        .checked_mul(coin).expect(ERR_MUL)//8
                        .checked_div(BLOCK_PER_YEAR).expect(ERR_DIV)
                        .checked_div(self.oin_price).expect(ERR_DIV)//8
                        .checked_div(ONE_COIN).expect(ERR_DIV);//8
                        
                self.saved_stable = self.saved_stable
                        .checked_add(fee).expect(ERR_ADD);

                user_stable.saved_stable = user_stable.saved_stable
                        .checked_add(fee).expect(ERR_ADD); 
            }
            
            user_stable.block = current_block_number;
            self.account_stable.insert(&account, &user_stable);    
            log!("Current stabilization fee: {:?}",self.account_stable.get(&account));
        } else {
            env::panic(b"Not register")
        }
    }

列表 2.39: update_stable_fee:stablefee.rs

建議 I 對第 25 到 30 行的計算,在除法之前執行乘法。

2.3.9 系統可能未記錄之前戳記過的價格

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-1 或之前引入。函數實現不正確。由於在大多數情況下合約中存入的總代幣數量大於 0,系統可能無法記錄戳記過的價格。

pub fn poke(&mut self, token_price: U128) {
    ...
       if self.total_token > 0 {
           if self.internal_sys_ratio() <= INIT_MIN_RATIO_LINE {
                self.internal_shutdown();
           }
       }else {
            log!(
                "{} poke price {} successfully.",
                env::predecessor_account_id(),
                token_price.0
            );
        }
    }

列表 2.40: poke:oracle.rs

建議 I 記錄戳記代幣價格的行為不應受到合約中存入代幣數量的影響。

2.3.10 清算時抵押代幣分配不連續

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-4 或之前引入。當用戶的質押比率大於或等於 108.5% 時,用戶必須支付 liquidation_fee,即佔 allotted_debt 的 2%。然而,如果用戶的質押比率小於 108.5%,則無需支付清算費。這導致質押比率較高的用戶在清算後分配給池的質押代幣較少。

#[payable]
    pub fn liquidation(&mut self, account: AccountId) {
        ...
        if ratio >= INIT_NO_LIQUIDATION_FEE_RATE {
            liquidation_fee = _allot_debt
                            .checked_mul(self.liquidation_fee_ratio).expect(ERR_MUL)
                            .checked_mul(STAKE_RATIO_BASE).expect(ERR_MUL)//16
                            .checked_div(self.token_price).expect(ERR_DIV);
        }else{
            allot_ratio = ratio
                .checked_sub(self.gas_compensation_ratio).expect(ERR_SUB)
                .checked_add(1).expect(ERR_ADD);
        }
        ...

列表 2.41: liquidation:lib.rs

建議 I 對於質押比率在 108.5% 到 110.5% 之間的用戶,建議將清算費設置為 (質押比率 - 108.5%)。

2.3.11 計算精度優化非必要

項目 描述
狀態 已確認並修復

描述 此問題在 Commit-4 或之前引入。在列表 2.42 第 832 行加 1 無法提高計算精度,因為 self.gas_compensation_ratio 相當大。

#[payable]
    pub fn liquidation(&mut self, account: AccountId) {
        ...
        if ratio >= INIT_NO_LIQUIDATION_FEE_RATE {
            liquidation_fee = _allot_debt
                            .checked_mul(self.liquidation_fee_ratio).expect(ERR_MUL)
                            .checked_mul(STAKE_RATIO_BASE).expect(ERR_MUL)//16
                            .checked_div(self.token_price).expect(ERR_DIV);
        }else{
            allot_ratio = ratio
                .checked_sub(self.gas_compensation_ratio).expect(ERR_SUB)
                .checked_add(1).expect(ERR_ADD);
        }
        ...

列表 2.42: liquidation:lib.rs

建議 I 刪除列表 2.42 第 831 行中添加的「1」。

2.3.12 中心化設計的風險

狀態 已確認(未修復)

描述 該項目具有高度中心化的設計。合約所有者擁有極高的權限,可以添加/刪除多簽管理員,並且可以提取清算費和獎勵等。 這種機制是絕對中心化的,對所有代幣擁有完全的控制權。我們強烈建議項目方採取安全機制,保護合約所有者的私鑰以管理合約。

3. 注意事項與備註

3.1 免責聲明

本審計報告不構成投資建議或個人推薦。它沒有考慮,也不應被解釋為考慮或與代幣、代幣銷售或任何其他產品、服務或其他資產的潛在經濟效益有關。任何實體不應以任何方式依賴本報告,包括用於購買或出售任何代幣、產品、服務或其他資產的決策目的。

本審計報告並非對任何特定項目或團隊的背書,報告也不保證任何特定項目的安全性。本審計不保證能夠發現智能合約中的所有安全問題,即評估結果不保證不存在進一步發現的安全問題。由於一次審計不能視為全面,我們始終建議進行獨立審計和公開漏洞賞金計劃,以確保智能合約的安全性。

本審計範圍僅限於第 1.1 節中提到的代碼。除非明確說明,否則語言本身(如 Rust 語言)、底層編譯工具鏈和計算基礎設施的安全性不在本審計範圍內。

3.2 審計程序

我們按照以下程序執行審計:

  • 漏洞檢測:我們先使用自動化代碼分析器掃描智能合約,然後人工驗證(拒絕或確認)它們報告的問題。

  • 語義分析:我們研究智能合約的業務邏輯,並使用自動化模糊測試工具(由我們的研究團隊開發)對可能的漏洞進行進一步調查。我們還與獨立審計員人工分析了可能的攻擊場景,以交叉檢查結果。

  • 建議:我們從良好編程實踐的角度為開發者提供有用建議,包括 Gas 優化、代碼風格等。

我們在下面展示了主要具體的檢查點。

3.2.1 軟件安全

  • 重入(Reentrancy)

  • 拒絕服務(DoS)

  • 訪問控制

  • 數據處理和數據流

  • 異常處理

  • 不可信的外部調用和控制流

  • 初始化一致性

  • 事件操作

  • 易出錯的隨機種子

  • 代理系統的錯誤使用

3.2.2 DeFi 安全

  • 語義一致性

  • 功能一致性

  • 訪問控制

  • 業務邏輯

  • 代幣操作

  • 應急機制

  • 預言機安全

  • 白名單和黑名單

  • 經濟影響

  • 批量轉賬

3.2.3 NFT 安全

  • 項目重複

  • 代幣接收者驗證

  • 鏈下元數據安全

3.2.4 附加建議

  • Gas 優化

  • 代碼質量和風格

以上檢查點為主要檢查點。我們可能會根據項目的功能在審計過程中增加更多檢查點。

訂閱最新動態
Bitget 3.875億美元鏈下違規事件:超越金鑰與合約的範疇
Security Insights

Bitget 3.875億美元鏈下違規事件:超越金鑰與合約的範疇

2026年9月24日,攻擊者利用第三方安全產品漏洞,獲取內部憑證並偽造提款指令,導致約3.875億美元從Bitget部分營運錢包(涵蓋Ethereum、其他EVM網絡、XRP Ledger、Zcash及TRON)轉出;私鑰及冷錢包未受影響。本文深入梳理事件路徑與資金流向,分析快速轉換為原生資產及生態恢復應對,提出機構縱深防禦框架,並說明授權區塊鏈滲透測試如何驗證跨層假設。

約1130萬美元損失:Multicall Router、Nostra|BlockSec Weekly
Security Insights

約1130萬美元損失:Multicall Router、Nostra|BlockSec Weekly

本報告涵蓋2026/09/14至2026/09/20期間,探討以太坊與Starknet上兩起合計損失約1130萬美元的安全事件。較大一起中,多重呼叫路由器將自身位址接受為分派目標,導致巢狀呼叫以路由器自身已授權身分而非外部呼叫者身分進入Safe錢包的Gateway模組,約2900枚`aEthrsETH`被轉出至攻擊者建立的Uniswap v4池。在Starknet上,Nostra預言機整合僅要求最少一個聚合來源,當三個設定價格來源中僅兩個達成聚合時,操縱的薄池報價與正常報價平均後將`NSTR`估值約49.52美元,支撐約350萬美元以高估抵押品借貸。

损失約3.2億美元:Liquid Network與Symbiosis遭攻擊事件 | BlockSec
Security Insights

损失約3.2億美元:Liquid Network與Symbiosis遭攻擊事件 | BlockSec

本報告涵蓋2026/09/07至2026/09/13,探討兩起造成約3.2億美元損失的安全事件,包括前份報告未提及、發生於2026/09/06的Liquid Network漏洞事件。較大者為該Liquid Network漏洞:Elements中的rangeproof驗證快取藉由雜湊四個欄位(其中兩個長度可變)串接而成金鑰,且未標示欄位邊界,導致某輸出的驗證結果被錯誤套用至另一個未經檢驗證明的輸出,使攻擊者得以憑空創造4,000枚無擔保L-BTC,並將幾乎全數兌回比特幣。在橫跨BNB Smart Chain、Ethereum與Rootstock的Symbiosis跨鏈橋比特幣路徑中,讀取比特幣存款的鏈下程式碼從存款人可自行控制的欄位取得其身份,並在未檢查手續費是否為負值的情況下自存款中扣除,使330聰的存款鑄造出`46,116,860,184.27388234 syBTC`;其須賣出所經之資金池僅有11.26 syBTC,致使流動性提供者與用戶損失約9.97 BTC(約77萬美元)。

Web3 最佳安全審計方

在上線之前驗證設計、程式碼與業務邏輯,對標業內最高安全標準。

BlockSec 審計
NearOinDao 安全審計報告