Back to Blog

NearOinDao 보안 감사 보고서

Code Auditing
December 10, 2021
31 min read

보고서 목차

항목 설명
클라이언트 Oinfinance
대상 NearOinDao

버전 이력

버전 날짜 설명
1.0 2021년 12월 04일 최초 릴리스

1. 서론

1.1 대상 컨트랙트 개요

대상 컨트랙트에는 스테이블 코인 모듈이 포함되어 있습니다. 또한 스테이킹(Staking) 및 파밍(Farming)을 포함한 다른 모듈도 구현하고 있습니다. 이러한 모듈들은 스테이블 코인, 즉 USDO의 안정화를 위한 긍정적인 피드백 루프를 형성합니다.

정보 설명
유형 스마트 컨트랙트
언어 Rust
접근 방식 반자동 및 수동 검증

감사 대상 저장소에는 NearOinDao ^1가 포함됩니다.

감사 프로세스는 반복적으로 진행됩니다. 구체적으로, 발견된 이슈를 수정한 커밋에 대해 추가 감사를 수행합니다. 새로운 이슈가 발견될 경우 이 프로세스를 계속합니다. 따라서 본 보고서에는 여러 커밋 SHA 값이 참조됩니다. 감사 전후의 커밋 SHA 값은 다음과 같습니다.

감사 전 및 감사 중

감사 후

프로젝트 커밋 SHA
NearOinDao 3bd117606c753d3c2f66b6dcddd1ae18ea47a20a

1.2 보안 모델

위험도를 평가하기 위해, OWASP 위험 등급 방법론 ^2 및 공통 취약점 목록(CWE) ^3을 포함하여 산업계와 학계에서 널리 채택된 표준 또는 권고안을 따릅니다. 이에 따라 본 보고서에서 측정된 심각도는 High(높음), Medium(중간), Low(낮음), **Undetermined(미확정)**의 네 가지 범주로 분류됩니다.

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 - 불필요한 어설션(assertion) 권고 사항 확인 및 수정 완료
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

영향 계정이 등록되지 않았다는 예외가 발생하여 청산 함수가 성공적으로 실행될 수 없습니다.

제안 I 청산 함수 시작 부분에서 청산 발신자의 계정과 컨트랙트 소유자의 계정이 존재하는지 확인하십시오.

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 함수는 공개(public) 상태입니다. 누구나 이 함수를 호출하여 컨트랙트의 보상 잔액을 추가할 수 있습니다.

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 이 함수는 전송된 보상을 수신한 후 내부적으로 호출되므로 비공개(private)로 변경해야 합니다.

2.2.2 잠재적 이슈 6: inject_sp_reward에 접근 제어 없음

항목 설명
상태 확인 및 수정 완료

설명 이 이슈는 Commit-1 이전 또는 해당 시점에 도입되었습니다. inject_sp_reward 함수는 공개(public) 상태입니다. 누구나 이 함수를 호출하여 컨트랙트의 스테이블 풀 보상 잔액을 추가할 수 있습니다.

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 이 함수는 전송된 스테이블 풀 보상을 수신한 후 내부적으로 호출되므로 비공개(private)로 변경해야 합니다.

2.2.3 잠재적 이슈 7: burn_coin에 접근 제어 없음

항목 설명
상태 확인 및 수정 완료

설명 이 이슈는 Commit-1 이전 또는 해당 시점에 도입되었습니다. burn_coin 함수는 공개(public) 상태입니다. 누구나 이 함수를 호출하여 다른 사람의 코인을 소각할 수 있습니다.

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 이 함수는 코인 소각을 위한 스테이블 수수료를 전송받은 후 내부적으로 호출되므로 비공개(private)로 변경해야 합니다.

2.2.4 잠재적 이슈 8: deposit_token에 접근 제어 없음

항목 설명
상태 확인 및 수정 완료

설명 이 이슈는 Commit-1 이전 또는 해당 시점에 도입되었습니다. deposit_token 함수는 공개(public) 상태입니다. 누구나 이 함수를 호출하여 자신의 계정 잔액을 추가할 수 있습니다.

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 이 함수는 예치된 토큰을 수신한 후 내부적으로 호출되므로 비공개(private)로 변경해야 합니다.

2.2.5 잠재적 이슈 9: 오라클에 시간 확인 없음

항목 설명
상태 확인 및 수정 완료

설명 이 이슈는 Commit-1 이전 또는 해당 시점에 도입되었습니다. oracle.rs의 assert_is_poked 함수는 토큰 가격이 0인지만 확인합니다. 토큰 가격은 계속 변동하므로 이는 의미가 없습니다.

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: 부적절한 오라클 업데이트 간격 시간

항목 설명
상태 확인 및 수정 완료

설명 이 이슈는 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 이전 또는 해당 시점에 도입되었습니다. 이 함수는 사용자의 스테이블 수수료가 self.oin_price로 계산되므로 oin_token 가격이 업데이트되었는지 확인하지 않습니다.

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 사이 어느 시점에나 발생할 수 있습니다. 예를 들어, 사용자가 day0에 100 토큰을 예치했다고 가정합니다. day999에 다른 사용자에 대한 청산이 발생하여 account_allot.token이 1000으로 증가할 수 있습니다.

사용자가 day1000에 보상을 청구할 때, day999에 청산으로 발생한 1000 토큰은 하루 동안만 채굴로 계산되어야 합니다. 그러나 컨트랙트는 실제로 day0부터 day1000까지 담보 보상에 대한 채굴 보상을 계산합니다.

// 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 이전 또는 해당 시점에 도입되었습니다. 사용자가 day 0에 1000 USDO를 발행하고, 당시 stable_fee_rate가 0.01oin/coin/day라고 가정합니다. 사용자가 day100에 1000 USDO를 반환하고 지난 100일 동안 스테이블 수수료율이 변경되지 않았다면, 납부해야 할 스테이블 수수료는 0.01 Oin/coin/day * 1000 Coin * 100 Day = 1000 Oin입니다. 그러나 소유자가 day99에 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 이 컨트랙트의 reward_coin 계산처럼 스테이블 수수료의 시스템 인덱스를 구현하십시오. 그리고 컨트랙트 사용자가 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 프로토콜 메인넷에서 블록이 매 초 생성된다고 가정하면, 연간 생성되는 블록 수는 31104000(360일)이 아닌 31536000(365일)이어야 합니다.

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 함수를 호출할 때, 스테이블 수수료는 'ST_NEAR'가 아닌 'OIN' 토큰으로 납부됩니다. 그러나 컨트랙트는 사용자의 스테이킹 토큰 잔액을 감소시키며, 이는 정확하지 않습니다.

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 함수의 가시성을 비공개(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"
            );
        }

    ...
    }

목록 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 함수는 해당 기능이 일시 중지되었는지 여부를 나타내기 위해 정의되었습니다. 그러나 특정 속성이 활성화된 경우 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% 이상이면, 사용자는 allot_debt의 2%에 해당하는 liquidation_fee를 납부해야 합니다. 그러나 사용자의 스테이킹 비율이 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 이전 또는 해당 시점에 도입되었습니다. 목록 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);
        }
        ...

목록 2.42: liquidation:lib.rs

제안 I 목록 2.42의 831번 줄에서 추가된 "1"을 제거하십시오.

2.3.12 중앙화된 설계의 위험성

상태 인지함

설명 이 프로젝트는 고도로 중앙화된 설계를 가지고 있습니다. 컨트랙트 소유자는 다중 서명 관리자를 추가/삭제하고 청산 수수료 및 보상을 인출하는 등의 매우 높은 권한을 보유하고 있습니다. 이러한 메커니즘은 완전히 중앙화되어 있으며 모든 토큰에 대한 완전한 제어 권한을 가집니다. 프로젝트 소유자가 컨트랙트를 관리하기 위해 컨트랙트 소유자의 개인 키를 보호하는 보안 메커니즘을 강화할 것을 강력히 권장합니다.

3. 주의 사항 및 비고

3.1 면책 조항

본 감사 보고서는 투자 조언이나 개인 추천을 구성하지 않습니다. 본 보고서는 토큰, 토큰 판매 또는 기타 제품, 서비스 또는 자산의 잠재적 경제성을 고려하지 않으며, 이를 고려하거나 관련이 있는 것으로 해석되어서는 안 됩니다. 어떠한 주체도 토큰, 제품, 서비스 또는 기타 자산의 매수 또는 매도 결정을 포함하여 어떠한 방식으로도 본 보고서에 의존해서는 안 됩니다.

본 감사 보고서는 특정 프로젝트나 팀에 대한 지지를 의미하지 않으며, 보고서는 특정 프로젝트의 보안을 보장하지 않습니다. 본 감사는 스마트 컨트랙트의 모든 보안 이슈를 발견한다는 어떠한 보증도 제공하지 않으며, 즉 평가 결과가 추가적인 보안 이슈가 존재하지 않음을 보장하지 않습니다. 하나의 감사가 포괄적인 것으로 간주될 수 없으므로, 스마트 컨트랙트의 보안을 보장하기 위해 독립적인 감사와 공개 버그 바운티 프로그램을 진행할 것을 항상 권장합니다.

본 감사의 범위는 섹션 1.1에 언급된 코드로 제한됩니다. 명시적으로 지정되지 않는 한, 언어 자체(예: Rust 언어), 기반 컴파일 툴체인 및 컴퓨팅 인프라의 보안은 범위에 포함되지 않습니다.

3.2 감사 절차

다음 절차에 따라 감사를 수행합니다.

  • 취약점 탐지 먼저 자동 코드 분석기로 스마트 컨트랙트를 스캔한 후, 보고된 이슈를 수동으로 검증(기각 또는 확인)합니다.

  • 의미론적 분석 스마트 컨트랙트의 비즈니스 로직을 연구하고, 자동 퍼징 도구(연구팀에서 개발)를 사용하여 가능한 취약점에 대한 추가 조사를 수행합니다. 또한 독립적인 감사자와 함께 가능한 공격 시나리오를 수동으로 분석하여 결과를 교차 검증합니다.

  • 권고 사항 가스 최적화, 코드 스타일 등을 포함한 좋은 프로그래밍 관행의 관점에서 개발자에게 유용한 조언을 제공합니다.

주요 구체적인 검사 항목은 다음과 같습니다.

3.2.1 소프트웨어 보안

  • 재진입(Reentrancy)

  • 서비스 거부(DoS)

  • 접근 제어

  • 데이터 처리 및 데이터 흐름

  • 예외 처리

  • 신뢰할 수 없는 외부 호출 및 제어 흐름

  • 초기화 일관성

  • 이벤트 운영

  • 오류 발생 가능성이 있는 무작위성

  • 프록시 시스템의 부적절한 사용

3.2.2 DeFi 보안

  • 의미론적 일관성

  • 기능 일관성

  • 접근 제어

  • 비즈니스 로직

  • 토큰 운영

  • 비상 메커니즘

  • 오라클 보안

  • 화이트리스트 및 블랙리스트

  • 경제적 영향

  • 일괄 전송

3.2.3 NFT 보안

  • 중복 항목

  • 토큰 수신자 검증

  • 오프체인 메타데이터 보안

3.2.4 추가 권고 사항

  • 가스 최적화

  • 코드 품질 및 스타일

앞서 언급한 검사 항목은 주요 항목입니다. 프로젝트의 기능에 따라 감사 과정에서 추가 검사 항목을 사용할 수 있습니다.

Sign up for the latest updates
~$410만 손실: Taiko, SecondFi 익스플로잇 | BlockSec 위클리
Security Insights

~$410만 손실: Taiko, SecondFi 익스플로잇 | BlockSec 위클리

이 주간 블록체인 보안 리포트는 2026년 6월 22~28일 발생한 주요 사건 2건을 다루며, 이더리움과 카르다노에서 약 410만 달러의 피해가 확인됐습니다. Taiko 브릿지 공격은 노출된 SGX 서명 키와 디버그 엔클레이브를 거부하지 못한 증명 정책 결함을 이용해 악성 증명자를 등록하고 L2 상태 증명을 위조했습니다. SecondFi 지갑은 Ed25519 논스 도출 시 비밀 입력이 제거되는 결함으로 공개 트랜잭션 데이터만으로 개인 키 복구가 가능했습니다.

~$18M 손실: jaredFromSubway, Aztec 등 | BlockSec 위클리
Security Insights

~$18M 손실: jaredFromSubway, Aztec 등 | BlockSec 위클리

이 주간 블록체인 보안 보고서는 2026년 6월 15일~21일을 다루며, 이더리움과 BNB 체인에서 3건의 주요 사고가 발생해 약 $18.3M의 손실이 발생했습니다. jaredFromSubway 사건은 MEV 봇이 차익거래를 위해 신뢰할 수 없는 제3자 컨트랙트에 자산을 승인한 역방향 승인 공격으로, 가짜 래퍼 토큰과 스왑 풀을 이용해 약 $15M 손실이 발생했습니다. Aztec은 이스케이프 해치 ZK 회로의 제약 누락으로 공격자가 가짜 머클 트리로 온체인 검증을 통과했습니다.

Web3 컴패니언: 오픈소스 보안 에이전틱 지갑

Web3 컴패니언: 오픈소스 보안 에이전틱 지갑

BlockSec가 Web3 Companion을 오픈소스로 공개했습니다. 이 보안 중심의 에이전트 지갑은 자체 AI 에이전트를 신뢰하지 않는 방식으로 설계되었으며, 키 격리, 강력한 정책, Passkey를 활용해 온체인 자산을 보호합니다.

Best Security Auditor for Web3

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

BlockSec Audit