报告清单
| 项目 | 描述 |
|---|---|
| 客户 | Oinfinance |
| 目标 | NearOinDao |
版本历史
| 版本 | 日期 | 描述 |
|---|---|---|
| 1.0 | 2021年12月04日 | 首次发布 |
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
影响 由于抛出账户未注册的异常,liquidation 函数无法成功执行。
建议 I 在 liquidation 函数开始时断言清算发送者账户和合约所有者账户的存在。
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 的任何时间都可能发生清算。例如,用户在第 0 天存入了 100 个代币。在第 999 天,触发了另一个用户的清算,导致 account_allot.token 可能增加到 1000 个。
当该用户在第 1000 天申领奖励时,由第 999 天的清算所产生的 1000 个代币本应仅计算一天的挖矿收益。然而,合约实际上计算了从第 0 天到第 1000 天整个抵押奖励的挖矿收益。
// 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 或更早版本中引入。假设一个用户在第 0 天铸造了 1000 个 USDO,此时的 stable_fee_rate 为 0.01 oin/coin/day。如果用户在第 100 天归还 1000 个 USDO,且稳定费率在过去 100 天内没有变化,他需要支付的稳定费为 0.01 Oin/coin/day * 1000 Coin * 100 Day = 1000 Oin。然而,如果所有者在第 99 天设置了 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 像计算奖励代币一样实现稳定费的系统指数,并确保每当合约用户调用 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 中得到考虑,因此无需稍后检查 user_ratio 是否达到了清算线。
#[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(占 allot_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 或更早版本中引入。由于 self.gas_compensation_ratio 相当大,列表 2.42 第 832 行添加 1 并不能增加计算精度。
#[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 优化
-
代码质量与风格
上述是主要的检查点。在审计过程中,我们可能会根据项目的具体功能使用更多的检查点。



