返回博客

NearOinDao安全审计报告

Code Auditing
2021年12月10日
阅读约 32 分钟

报告清单

项目 描述
客户 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 优化

  • 代码质量与风格

上述是主要的检查点。在审计过程中,我们可能会根据项目的具体功能使用更多的检查点。

订阅最新动态
损失约4.18亿美元:Bitget、NEAR Intents遭利用攻击 | BlockSec Weekly
Security Insights

损失约4.18亿美元:Bitget、NEAR Intents遭利用攻击 | BlockSec Weekly

本双周安全报告记录了2026年9月21日至10月4日期间发生的八起事件,涉及以太坊、BNB Chain、Solana、比特币、波场、XRP Ledger、Zcash、Avalanche、NEAR等网络,损失约4.184亿美元。详细分析涵盖Bitget因第三方安全产品漏洞损失3.875亿美元,以及NEAR Intents因退款验证缺陷和状态回滚缺失损失387万美元。

Bitget 3.875亿美元链下漏洞事件:不止于密钥与合约
Security Insights

Bitget 3.875亿美元链下漏洞事件:不止于密钥与合约

2026年9月24日,攻击者利用第三方安全产品漏洞,获取内部凭证并伪造提现指令,导致Bitget部分运营钱包资金转出约3.875亿美元,涉及以太坊、其他EVM网络、XRP Ledger、Zcash和TRON,私钥及冷钱包未受影响。本文梳理事件路径与资金流向,分析资产快速转换及生态恢复应对,提出机构纵深防御体系,并说明授权区块链渗透测试如何验证跨层安全假设。

损失约1130万美元:Multicall Router、Nostra | BlockSec Weekly
Security Insights

损失约1130万美元:Multicall Router、Nostra | BlockSec Weekly

本报告覆盖2026/09/14至09/20,分析以太坊与Starknet上两起共约1130万美元损失的安全事件。较大一起中,多重调用路由器将自身地址当作调度目标,致嵌套调用以路由器已授权身份而非外部调用者身份进入Safe钱包的Gateway模块,约2900个aEthrsETH被转入攻击者创建的Uniswap v4池。Starknet上,Nostra预言机仅需一个聚合来源,三个价格源中仅两个参与聚合时,被操纵的浅池报价与正常报价平均,使NSTR估值约49.52美元,支持约350万美元超额抵押借贷。

Web3 最佳安全审计方

在上线之前验证设计、代码与业务逻辑,对标业内最高安全标准。

BlockSec 审计