Back to Blog

NearOinDao安全审计报告

Code Auditing
December 10, 2021
32 min read

报告清单

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

  • 代码质量与风格

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

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit