Back to Blog

NearOinDao 安全审计报告

Code Auditing
December 10, 2021
31 min read

报告清单

项目 描述
客户 Oinfinance
目标 NearOinDao

版本历史

版本 日期 描述
1.0 2021年12月4日 首次发布

1. 引言

1.1 关于目标合约

目标合约包含一个稳定币模块。围绕它,还实现了包括质押和挖矿在内的其他模块。这些模块为稳定币USDO的稳定创造了一个正向反馈循环。

信息 描述
类型 智能合约
语言 Rust
方法 半自动和手动验证

已审计的仓库包括 NearOinDao^1

审计过程是迭代的。具体来说,我们将进一步审计修复了根本性问题的提交。如果有新问题,我们将继续这个过程。因此,本报告中提到了多个提交SHA值。审计前后提交的SHA值如下所示。

审计前及审计期间

审计后

项目 提交SHA
NearOinDao 3bd117606c753d3c2f66b6dcddd1ae18ea47a20a

1.2 安全模型

为了评估风险,我们遵循行业和学术界广泛采纳的标准或建议,包括 OWASP 风险评级方法^2 和通用弱点枚举^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:同一个用途的两个不同属性

项目 描述
状态 已确认并修复

描述 此问题在提交-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:清算奖励分配无效

项目 描述
状态 已确认并修复

描述 此问题在提交-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

项目 描述
状态 已确认并修复

描述 此问题在提交-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:跨函数调用失败时合约状态未回滚

项目 描述
状态 已确认并修复

描述 此问题在提交-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 缺少访问控制

项目 描述
状态 已确认并修复

描述 此问题在提交-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 缺少访问控制

项目 描述
状态 已确认并修复

描述 此问题在提交-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 缺少访问控制

项目 描述
状态 已确认并修复

描述 此问题在提交-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 缺少访问控制

项目 描述
状态 已确认并修复

描述 此问题在提交-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:预言机缺少时间检查

项目 描述
状态 已确认并修复

描述 此问题在提交-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

影响 此问题会影响价格预言机。如果代币价格很长一段时间未被输入,断言仍然可以通过,并且相关交易将以过时的价格执行。

建议 I 合约应为输入的定价设置一个有效的时间段。

2.2.6 潜在问题 10:不适当的预言机输入时间间隔

项目 描述
状态 已确认并修复

描述 此问题在提交-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 的断言

项目 描述
状态 已确认并修复

描述 此问题在提交-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:用户通过质押代币可能获得更多挖矿奖励

项目 描述
状态 已确认并修复

描述 此问题在提交-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:用户可能支付更少的稳定费

项目 描述
状态 已确认并修复

描述 此问题在提交-1 或之前引入。假设用户在第 0 天铸造了 1000 个 USDO,当时的稳定费率为 0.01 oin/coin/天。如果用户在第 100 天赎回这 1000 个 USDO,并且在过去 100 天内稳定费率没有变化,那么他需要支付的稳定费为 0.01 Oin/coin/天 * 1000 Coin * 100 Day = 1000 Oin。但是,如果所有者在第 99 天将 stable_fee_rate 设置为 = 0.005 oin/coin/天。此时,用户只需支付 0.005 Oin/Coin/Day * 1000 Coin * 100 Day = 500 Oin。实际上,准确的费用应为:(0.01 Oin/Coin/Day * 1000 Coin * 99 Day) + (0.005 Oin/Coin/Day * 1000 Coin * 1 Day) = 990 Oin + 5 Oin = 995 Oin。

在这种情况下,用户无需支付 495 Oin。

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

列表 2.16:set_stable_fee_rate:dparam.rs

pub fn update_stable_index(&mut self) {
    }

列表 2.17:update_stable_index:stablefee.rs

影响 合约用户可能被收取更少的稳定费。

建议 I 像本合约中计算 reward_coin 一样,实现稳定费用的系统索引。并确保在合约用户调用 set_stable_fee_rate、liquidation 和 update_stable_fee 时更新稳定费用的系统索引。

2.2.10 潜在问题 14:不合理的多重签名请求确认率

项目 描述
状态 已确认并修复

描述 此问题在提交-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:每年区块数量不正确

项目 描述
状态 已确认并修复

描述 此问题在提交-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 的计算不正确

项目 描述
状态 已确认并修复

描述 此问题在提交-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:用户稳定费处理不当

项目 描述
状态 已确认并修复(相关逻辑现已移除)

描述 此问题在提交-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:系统比例不正确

项目 描述
状态 已确认并修复

描述 此问题在提交-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:奖励代币的数量可能大于上限

项目 描述
状态 已确认并修复

描述 此问题在提交-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:不同权限的用户使用同一个白名单

项目 描述
状态 已确认并修复

描述 此问题在提交-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 不检查代币类型

项目 描述
状态 已确认并修复

描述 此问题在提交-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 可被多重签名经理修改

项目 描述
状态 已确认并修复

描述 此问题在提交-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 重复断言

项目 描述
状态 已确认并修复

描述 此问题在提交-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 用户清算比例的重复断言

项目 描述
状态 已确认并修复

描述 此问题在提交-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 重复的白名单检查

项目 描述
状态 已确认并修复

描述 此问题在提交-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 未使用的函数

项目 描述
状态 已确认并修复

描述 此问题在提交-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 重复的代码

项目 描述
状态 已确认并修复

描述 此问题在提交-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 函数名与实现相反

项目 描述
状态 已确认并修复

描述 此问题在提交-3 或之前引入。is_stake_paused、is_redeem_paused、is_claim_reward_paused、is_liquidation_paused、is_stable_paused 函数定义为表示函数是否暂停。然而,当特定属性处于活动状态时,它返回 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 重复的代码

项目 描述
状态 已确认并修复

描述 此问题在提交-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 计算精度可以增强

项目 描述
状态 已确认并修复

描述 此问题在提交-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 系统可能不会记录之前输入的定价

项目 描述
状态 已确认并修复

描述 此问题在提交-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 清算中抵押代币的分布不连续

项目 描述
状态 已确认并修复

描述 此问题在提交-4 或之前引入。当用户的质押比例大于或等于 108.5% 时,用户必须支付清算费,该费用占 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 计算精度优化是不必要的

项目 描述
状态 已确认并修复

描述 此问题在提交-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 免责声明

本审计报告不构成投资建议或个人推荐。它不考虑,也不应被解释为考虑或对代币、代币销售或任何其他产品、服务或资产的潜在经济性有任何影响。任何实体均不应以任何方式依赖本报告,包括用于做出买卖任何代币、产品、服务或资产的任何决定。

本审计报告并非对任何特定项目或团队的认可,报告不保证任何特定项目的安全性。本审计不保证发现所有智能合约的安全问题,即评估结果不保证不存在任何进一步的安全问题发现。由于一次审计不能被认为是全面的,我们始终建议进行独立的审计和公开的 bug 赏金计划,以确保智能合约的安全性。

本审计的范围仅限于第 1.1 节中提到的代码。除非另有明确说明,否则语言本身(例如 Rust 语言)、底层编译工具链和计算基础设施的安全性不在审计范围内。

3.2 审计流程

我们按照以下流程进行审计。

  • 漏洞检测 我们首先使用自动代码分析器扫描智能合约,然后手动验证(拒绝或确认)它们报告的问题。

  • 语义分析 我们研究智能合约的业务逻辑,并使用自动模糊测试工具(由我们的研究团队开发)进一步调查可能的漏洞。我们还由独立审计员手动分析可能的攻击场景,以交叉验证结果。

  • 建议 我们从良好编程实践的角度为开发人员提供一些有用的建议,包括气体优化、代码风格等。

我们在以下内容中展示主要具体的检查点。

3.2.1 软件安全

  • 重入

  • 拒绝服务

  • 访问控制

  • 数据处理和数据流

  • 异常处理

  • 不受信任的外部调用和控制流

  • 初始化一致性

  • 事件操作

  • 易出错的随机性

  • 代理系统的使用不当

3.2.2 DeFi 安全

  • 语义一致性

  • 功能一致性

  • 访问控制

  • 业务逻辑

  • 代币操作

  • 紧急机制

  • 预言机安全

  • 白名单和黑名单

  • 经济影响

  • 批量转账

3.2.3 NFT 安全

  • 重复项

  • 代币接收者验证

  • 链下元数据安全

3.2.4 附加建议

  • 气体优化

  • 代码质量和风格

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

Sign up for the latest updates
Newsletter - April 2026
Security Insights

Newsletter - April 2026

In April 2026, the DeFi ecosystem experienced three major security incidents. KelpDAO lost ~$290M due to an insecure 1-of-1 DVN bridge configuration exploited via RPC infrastructure compromise, Drift Protocol suffered ~$285M from a multisig governance takeover leveraging Solana's durable nonce mechanism, and Rhea Finance incurred ~$18.4M following a business logic flaw in its margin-trading module that allowed circular swap path manipulatio

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly
Security Insights

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly

This BlockSec weekly security report covers eight attack incidents detected between April 20 and April 26, 2026, across Ethereum, Avalanche, Sui, Base, HyperLiquid, and MegaETH, with total estimated losses of approximately $7.04M. The highlighted incident is the $1.3M GiddyDefi exploit, where the attacker did not break any cryptography or use a flash loan but simply replayed an existing on-chain EIP-712 signature with the unsigned `aggregator` and `fromToken` fields swapped out for a malicious contract, demonstrating how partial signature coverage turns any historical signature into a generic permit. Other incidents include a $3.5M Volo Vault operator key compromise on Sui, a $1.5M Purrlend privileged-role takeover, a $413K SingularityFinance oracle misconfiguration, a $142.7K Scallop cross-pool index injection, a $72.35K Kipseli Router decimal mismatch, a $50.7K REVLoans (Juicebox) accounting pollution, and a $64K Custom Rebalancer arbitrary-call exploit.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Best Security Auditor for Web3

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

BlockSec Audit