NearOinDao 安全审计报告

这是我们在2021年12月为NearOinDao进行的 the security audit 报告。

NearOinDao 安全审计报告

报告清单

项目 描述
客户 Oinfinance
目标 NearOinDao

版本历史

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

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 奖励代币的总奖励可以通过多签管理器修改 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)代表相同的合约状态,即用户的清算线。它们在合约的不同函数中使用(Listing 2.1 和 Listing 2.2)。但是,self.liquidation_line 可以通过 set_liquidation_line 函数修改,而 self.cost 无法更改。在这种情况下,如果修改了 self.liquidation_line,self.cost 将保留原始值。这会影响 assert_user_ratio 函数的逻辑(Listing 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.");
        }
    }

Listing 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");
        ...

Listing 2.2: internal_can_mint_amount:lib.rs

影响 用户在合约不同函数中的清算线不一致,这会影响整个合约的逻辑。

建议 I 在计算用户质押比例并将其与系统的清算线进行比较时,我们可以统一使用这两个属性。

2.1.2 潜在问题 2:清算奖励分配不当

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

描述 此问题在 Commit-4 或之前引入。清算发送者的账户和合约所有者的账户可能未注册(Listing 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);
    }

}

Listing 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
        );
    }

Listing 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");
            }
        }
    }

Listing 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");
            }
        }
    }

Listing 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"
            );
        }
        ...
    }

Listing 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
        );
    }

Listing 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());
        ...
    }

Listing 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);
        . . .
    }

Listing 2.10: deposit_token:lib.rs

影响 攻击者可以调用此函数来增加其账户的余额。

建议 I 此函数应更改为私有函数,因为它在收到存款代币后被内部调用。

2.2.5 潜在问题 9:Oracle 缺少时间检查

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

描述 此问题在 Commit-1 或之前引入。oracle.rs 中的 assert_is_poked 函数仅检查代币价格是否为零。这没有意义,因为代币价格在不断变化。

pub(crate) fn assert_is_poked(&self) {
        assert!(self.token_price != 0, "Oracle price isn't poked.");
    }

Listing 2.11: assert_is_poked:oracle.rs

影响 此问题会影响价格预言机。如果代币价格长时间未被 찍(poked),断言仍然可以通过,并且相关交易可能会以过时的价格执行。

建议 I 合约应为 찍 过的价格设置一个有效的时间段。

2.2.6 潜在问题 10:不合适的 Oracle POKE 时间间隔

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

描述 此问题在 Commit-3 或之前引入。types.rs 中定义的常量 POKE_INTERVAL_TIME 现在表示 1000 天。这个时间间隔似乎太长了。需要一个合理的值。

pub const POKE_INTERVAL_TIME: u64 = 86_400_000_000_000_000;

Listing 2.12: types.rs

影响 찍 过的价格的时间间隔不合适。

建议 I 重置찍 过的价格的时间间隔为一个合理的值。

2.2.7 潜在问题 11:缺少 Oin_Price 的断言

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

描述 此问题在 Commit-1 或之前引入。此函数不检查 oin_token 价格是否已被 찍(poked),因为用户的稳定费是根据 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)
    }

Listing 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
        }
    }

Listing 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)
        }
    }

Listing 2.15: staker_debt_of:views.rs

影响 用户可能获得额外的奖励。

建议 I 从挖矿奖励的计算中移除新分配抵押品的划分。我们可以让挖矿奖励仅与用户存入的代币数量相关。

2.2.9 潜在问题 13:用户可能支付较少的稳定费

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

描述 此问题在 Commit-1 或之前引入。假设一个用户在第 0 天铸造了 1000 USDO,当时的稳定费率为 0.01 Oin/硬币/天。如果用户在第 100 天返还 1000 USDO,并且在这 100 天内稳定费率没有改变,那么他需要支付的稳定费是 0.01 Oin/硬币/天 * 1000 硬币 * 100 天 = 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);
    }

Listing 2.16: set_stable_fee_rate:dparam.rs

pub fn update_stable_index(&mut self) {
    }

Listing 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
    }

Listing 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);
        ...
    }

Listing 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;

Listing 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();
        
        ...
    }

Listing 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),
            );
        ...
        
    }

Listing 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()
        }
    }

Listing 2.23: internal_sys_ratio:lib.rs

影响 系统可能因比例不正确而关闭。

建议 I 将 if 条件 total_coin = 0 更改为 token_usd = 0。

2.2.15 潜在问题 19:奖励代币数量可能大于上限

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

描述 此问题在 Commit-1 或之前引入。当目前有 20 个奖励代币时,Listing 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
        )
    }

Listing 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()
    }

Listing 2.25: assert_esm_white:esm.rs

pub(crate) fn assert_param_white(&self) {
        self.assert_white();
    }

Listing 2.26: assert_param_white:dparam.rs

pub(crate) fn assert_oracle_white(&self) {
        self.assert_white();
    }

Listing 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);

Listing 2.28: assert_esm_white:esm.rs

影响 用户无需支付 Oin 代币。相反,他们可以通过转移任意代币来支付所需数量的稳定费。

建议 I 检查接收代币的地址。

2.2.18 潜在问题 22:奖励代币的总奖励可能被多签管理器修改

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

描述 此问题在 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.");
        }
    }

Listing 2.29: ainject_reward:pool.rs

建议 I 移除 #[private] 装饰器,并将 inject_reward 函数的可见性更改为 private。

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"
            );
        }

    ...
    }

Listing 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;
            }
    ...
    }

Listing 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");
            }
        }

Listing 2.32: withdraw_token:lib.rs

建议 I 移除 Listing 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);
    }

Listing 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();
        . . .
    }

Listing 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);
            }
        };
    }

Listing 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));       
    }

Listing 2.36: set_account_allot:allot.rs

建议 I 移除第 42 行的 invocation 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
    }

Listing 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());
        . . .
    }

Listing 2.38: deposit_token:lib.rs

建议 I 移除第 344 行的 invocation 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")
        }
    }

Listing 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
            );
        }
    }

Listing 2.40: poke:oracle.rs

建议 I 记录 찍 过的代币价格的行为不应受合约中存入代币数量的影响。

2.3.10 清算中抵押代币分配不连续

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

描述 此问题在 Commit-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);
        }
        ...

Listing 2.41: liquidation:lib.rs

建议 I 对于质押比例在 108.5% 到 110.5% 之间的用户,建议清算费为 (质押比例 - 108.5%)。

2.3.11 计算精度优化不必要

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

描述 此问题在 Commit-4 或之前引入。在 listing 2.42 的第 832 行加 1 不会提高计算精度,因为 self.gas_compensation_ratio 相当大。

#[payable]
    pub fn liquidation(&mut self, account: AccountId) {
        ...
        if ratio >= INIT_NO_LIQUIDATION_FEE_RATE {
            liquidation_fee = _allot_debt
                            .checked_mul(self.liquidation_fee_ratio).expect(ERR_MUL)
                            .checked_mul(STAKE_RATIO_BASE).expect(ERR_MUL)//16
                            .checked_div(self.token_price).expect(ERR_DIV);
        }else{
            allot_ratio = ratio
                .checked_sub(self.gas_compensation_ratio).expect(ERR_SUB)
                .checked_add(1).expect(ERR_ADD);
        }
        ...

Listing 2.42: liquidation:lib.rs

建议 I 移除 listing 2.42 的第 831 行中的加 "1"。

2.3.12 中心化设计的风险

状态 已确认

描述 项目具有高度中心化的设计。合约所有者拥有非常高的权限,可以添加/删除多签管理器,并且可以提取清算费和奖励等。 这种机制是完全中心化的,对所有代币拥有完全的控制权。我们强烈建议项目所有者实施安全机制来保护合约所有者的私钥。

3. 通知和说明

3.1 免责声明

本审计报告不构成投资建议或个人推荐。它不考虑,也不应被解释为考虑或对代币、代币销售或任何其他产品、服务或其他资产的潜在经济效益产生任何影响。任何实体均不应依赖本报告,包括为购买或出售任何代币、产品、服务或其他资产的决策。

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

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

3.2 审计流程

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

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

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

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

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

3.2.1 软件安全

  • 重入攻击

  • 拒绝服务(DoS)

  • 访问控制

  • 数据处理和数据流

  • 异常处理

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

  • 初始化一致性

  • 事件操作

  • 易出错的随机性

  • 代理系统的使用不当

3.2.2 DeFi 安全

  • 语义一致性

  • 功能一致性

  • 访问控制

  • 业务逻辑

  • 代币操作

  • 紧急机制

  • 预言机安全

  • 白名单和黑名单

  • 经济影响

  • 批量转账

3.2.3 NFT 安全

  • 重复项

  • 代币接收者验证

  • 链下元数据安全

3.2.4 额外建议

  • Gas 优化

  • 代码质量和风格

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

Sign up for the latest updates