報告清單
| 項目 | 描述 |
|---|---|
| 客戶 | 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 優化
-
代碼質量和風格
以上檢查點為主要檢查點。我們可能會根據項目的功能在審計過程中增加更多檢查點。



