Halborn Logo

Merkle Trade Move - Taurus Labs


Prepared by:

Halborn Logo

HALBORN

Last Updated 06/26/2024

Date of Engagement by: May 22nd, 2024 - June 11th, 2024

Summary

93% of all REPORTED Findings have been addressed

All findings

14

Critical

0

High

0

Medium

0

Low

2

Informational

12


1. Introduction

Taurus Labs engaged Halborn to conduct a security assessment on their smart contracts beginning on May 22nd, 2024 and ending on June 11th, 2024. The security assessment was scoped to the smart contracts provided in the GitHub repository merkle-contract, commit hashes and further details can be found in the Scope section of this report.

2. Assessment Summary

The team at Halborn assigned two full-time security engineers to check the security of the modules. The security engineers are blockchain and smart-contract security experts with advanced penetration testing and smart-contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of this assessment is to:

    • Ensure that module functions operate as intended

    • Identify potential security issues with the modules

In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were mostly addressed by the Taurus Labs team. The main ones were the following:

    • Rectify the cancellation amount calculation when canceling a vesting plan.

    • Validate user-provided inputs during the vesting plan creation.

    • Allocate a distinct error code for each error type in the esmkl_token module.

3. Test Approach and Methodology

Halborn performed a combination of manual review of the code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of the smart contract assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of smart contracts and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the assessment: 

    • Research into the architecture, purpose, and use of the platform.

    • Manual code read and walkthrough.

    • Manual Assessment of use and safety of critical Move variables and functions in scope that could lead to arithmetic related vulnerabilities.

    • Cross module call controls.

    • Architecture related logical controls.

    • Test coverage review (aptos move test).

4. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

4.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILIY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

4.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

4.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

5. SCOPE

Files and Repository
(a) Repository: merkle-contract
(c) Items in scope:
  • merkle-token/sources/claimable_fa_store.move
  • merkle-token/sources/esmkl_token.move
  • merkle-token/sources/mkl_token.move
↓ Expand ↓
Out-of-Scope: third party dependencies, economic attacks
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

2

Informational

12

Security analysisRisk levelRemediation Date
Miscalculation of cancel amount when canceling a vesting planLowSolved - 06/20/2024
Missing input validation could result in an insolvent vesting planLowSolved - 06/20/2024
Identical error codes assigned to different errors within the same moduleInformationalSolved - 06/03/2024
Maintaining the transfer reference could potentially enable vemkl transfersInformationalAcknowledged
Canceling a vesting plan after its end date will be processed as a claim insteadInformationalSolved - 06/03/2024
Potential resource borrowing might go unusedInformationalSolved - 06/20/2024
Boolean equalityInformationalSolved - 06/20/2024
Presence of typosInformationalSolved - 06/20/2024
Employ constants to mitigate unnecessary recomputationInformationalAcknowledged
Unnecessary use of mutable referencesInformationalSolved - 06/20/2024
Usage of legacy Coin standardInformationalAcknowledged
Unused import statementInformationalSolved - 06/20/2024
Missing view decorator for read-only functionsInformationalFuture Release
Redundant check increases gas usageInformationalSolved - 06/20/2024

7. Findings & Tech Details

7.1 Miscalculation of cancel amount when canceling a vesting plan

// Low

Description

Upon cancellation of a vesting plan, the claimed asset containing the claimable amount is returned, along with the cancel amount designed to cover the remaining vesting period. This cancel amount is derived by subtracting the claimed amount from the total amount allocated within the vesting plan. The claimed amount is determined prior to the subtraction, being incremented with the output of get_claimable, which is computed through the following process:

public fun get_claimable(_vesting_plan: &VestingPlan): u64 {
    if (_vesting_plan.paused) {
        return 0
    };
    let now = timestamp::now_seconds();
    _vesting_plan.initial_amount + safe_mul_div(
        _vesting_plan.total_amount,
        min(now, _vesting_plan.end_at_sec) - min(now, _vesting_plan.start_at_sec),
        (_vesting_plan.end_at_sec - _vesting_plan.start_at_sec)
    ) - _vesting_plan.claimed_amount
}

This illustrates that the claimed amount will fluctuate within the range from (_vesting_plan.initial_amount - _vesting_plan.claimed_amount) to (_vesting_plan.initial_amount + _vesting_plan.total_amount - _vesting_plan.claimed_amount), wherein the claimed amount is initially set to zero upon the creation of the vesting plan.

Subsequently, it is incremented by the output of get_claimable during either a claim or cancel operation when calling claim_internal. Consequently, if_vesting_plan.initial_amountis greater than zero, the cancel amount will consistently be incorrect, holding an amount that is lesser by _vesting_plan.initial_amount, potentially leading to underflow issues because _vesting_plan.claimed_amount can hold up to _vesting_plan.total_amount + _vesting_plan.initial_amount.

Consequently, two distinct scenarios emerge:

  • Certain users may find themselves unable to cancel their vesting plans, thereby being compelled to continue with the vesting schedule against their intentions.

  • External modules relying on the cancel amount to refund users their vested tokens may inadvertently refund less than the requisite amount, potentially result in financial loss for the affected users.

Code Location

The cancel function does not incorporate the vesting plan's initial amount when calculating the cancel amount:

public fun cancel(_vesting_plan: VestingPlan, _claim_cap: ClaimCapability, _admin_cap: AdminCapability): (FungibleAsset, u64) {
    assert!(_vesting_plan.paused == false, E_PAUSED);
    assert!(_vesting_plan.uid == _claim_cap.uid && _vesting_plan.uid == _admin_cap.uid, E_NOT_AUTHORIZED);
    let claimable = get_claimable(&_vesting_plan);
    let claimed_asset = fungible_asset::zero(claimable_fa_store::get_metadata_by_uid(&_vesting_plan.claimable_fa_store_claim_cap));
    if (claimable > 0) {
        fungible_asset::merge(&mut claimed_asset, claim_internal(&mut _vesting_plan, claimable));
    };
    let cancel_amount = _vesting_plan.total_amount - _vesting_plan.claimed_amount;

    (claimed_asset, cancel_amount)
}

The claim_internal function increments the vesting plan's claimed_amount with the provided amount:

fun claim_internal(_vesting_plan: &mut VestingPlan, _amount: u64): FungibleAsset {
    _vesting_plan.claimed_amount = _vesting_plan.claimed_amount + _amount;
    claimable_fa_store::claim_funding_store(&_vesting_plan.claimable_fa_store_claim_cap, _amount)
}
BVSS
Recommendation

Consider changing the cancel_amount calculation as follows:

let cancel_amount = _vesting_plan.initial_amount + _vesting_plan.total_amount - _vesting_plan.claimed_amount;

Remediation Plan

SOLVED: The Taurus Labs team solved the issue by deducting the initial amount from the total amount within the get_claimable function:

_vesting_plan.initial_amount + safe_mul_div(
    _vesting_plan.total_amount - _vesting_plan.initial_amount,
    min(now, _vesting_plan.end_at_sec) - min(now, _vesting_plan.start_at_sec),
    (_vesting_plan.end_at_sec - _vesting_plan.start_at_sec)
) - _vesting_plan.claimed_amount
Remediation Hash
References

7.2 Missing input validation could result in an insolvent vesting plan

// Low

Description

During the creation of a vesting plan, the create function, which can be called by anyone, currently lacks validation for the accuracy of the following arguments: _start_at_sec, _end_at_sec, _initial_amount and _total_amount.

This oversight presents integrity issues, potentially leading to the following scenarios:

  • Creation of a vesting plan allowing the user to claim more than the total amount if the provided initial amount exceeds the total amount.

  • Creation of a vesting plan with a start time in the past or one that has already expired.

  • Creation of a vesting plan with start_at_sec equaling end_at_sec, resulting in a division by zero error when calling get_claimable and related functions (such as cancel and claim).

  • Creation of a vesting plan where 'start_at_sec' is greater than end_at_sec, causing an underflow error when calling get_claimable and related functions with the current timestamp exceeding the vesting plan's start time.

Code Location

The create function does not validate the user-provided arguments:

public fun create(
        _user_address: address,
        _start_at_sec: u64,
        _end_at_sec: u64,
        _initial_amount: u64,
        _total_amount: u64,
        _claimable_fa_store_claim_cap: claimable_fa_store::ClaimCapability
    ): (VestingPlan, ClaimCapability, AdminCapability)
    acquires VestingConfig {
        let config = borrow_global_mut<VestingConfig>(@merkle);
        let uid = config.next_uid;
        let vesting_plan = VestingPlan {
            uid,
            user: _user_address,
            start_at_sec: _start_at_sec,
            end_at_sec: _end_at_sec,
            initial_amount: _initial_amount,
            total_amount: _total_amount,
            claimed_amount: 0,
            paused: false,
            claimable_fa_store_claim_cap: _claimable_fa_store_claim_cap
        };
        config.next_uid = config.next_uid + 1;
        (vesting_plan, ClaimCapability { uid }, AdminCapability { uid })
    }
BVSS
Recommendation

Consider validating user-provided inputs by ensuring the following criteria:

  • The initial amount is less than the total amount.

  • The current timestamp is before the start time, and the end time is between the start time and a valid upper limit.

Remediation Plan

SOLVED: The Taurus Labs team implemented the recommended checks.

Remediation Hash
References

7.3 Identical error codes assigned to different errors within the same module

// Informational

Description

In the esmkl_token module, it was noticed that both E_NOT_AUTHORIZED and E_TOO_SMALL_VEST_AMOUNT errors are assigned the same value, which is 0. This situation can detrimentally impact user experience and complicate the process of identifying the specific cause for a transaction failure.

Code Location

The esmkl_token module exposes two different errors that share the same error code:

/// When signer is not owner of module
const E_NOT_AUTHORIZED: u64 = 0;
/// When vest too small amount
const E_TOO_SMALL_VEST_AMOUNT: u64 = 0;
BVSS
Recommendation

Consider allocating a distinct error code for each error type to enhance clarity and facilitate precise error identification.

Remediation Plan

SOLVED: The Taurus Labs team solved the issue by assigning the E_TOO_SMALL_VEST_AMOUNT error the value 1.

Remediation Hash
References

7.4 Maintaining the transfer reference could potentially enable vemkl transfers

// Informational

Description

During the locking process, a validation check determines if a user is eligible for a new vemkl token by assessing the maximum number of tokens they hold. Subsequently, when the mint_vemkl function is called, a token is minted, transferred to the user, and then rendered non-transferrable by invoking object::disable_ungated_transfer with the corresponding transfer reference. This step is intended to "soul bound" the token, as per the accompanying comment. However, since the transfer reference is stored within the VoteEscrowedMKL under the user, If the staking module introduces a new API that provides access to either the VoteEscrowedMKL or the transfer reference contained within it, it creates an avenue for the token owner to call object::enable_ungated_transfer and transfer their tokens using this reference.

The issue arises when vemkl tokens are transferred, as the UserVoteEscrowedMKL lists for both participating users fail to update. Consequently, when the new user attempts to unlock, the operation succeeds as removing a non-existent element from the vector doesn't trigger an error, returning an empty vector instead. However, the vemkl address isn't removed from the initial owner's UserVoteEscrowedMKL list. This discrepancy leads to the following consequences:

  • The original vemkl owner accumulates deprecated elements in their UserVoteEscrowedMKL list, potentially rendering them unable to lock and acquire new vemkl tokens.

  • The new owner initiating the unlock could surpass the VoteEscrowedMKLConfig.max_num_vemkl limit, as their list doesn't accurately reflect the owned vemkl tokens.

This scenario also exposes the possibility for malicious users to exploit the system by creating multiple accounts, acquiring the maximum allowed vemkl tokens for each account, and then transferring all tokens to their primary accounts. This manipulation results in surpassing the upper bound limit for allowed vemkl tokens, granting a disproportionately strong voting power that could significantly impact the protocol's future.

Code Location

The mint_vemkl function stores the transfer reference for the created vemkl token under the VoteEscrowedMKL resource:

let vemkl = VoteEscrowedMKL {
    lock_time,
    unlock_time: _unlock_time,
    mutator_ref,
    burn_ref,
    transfer_ref,
    mkl_token: mkl_store,
    mkl_delete_ref,
    esmkl_token: esmkl_store, // object address (FungibleStore)
    esmkl_delete_ref,
};
BVSS
Recommendation

Consider removing the transfer reference from the VoteEscrowedMKL resource to prevent vemkl tokens from being transferred.

Remediation Plan

ACKNOWLEDGED: The Taurus Labs team acknowledged this issue but opted to retain the TransferRef in the user's VoteEscrowedMKL resource for potential future extensibility.

References

7.5 Canceling a vesting plan after its end date will be processed as a claim instead

// Informational

Description

After creating a vesting plan, users have the option to either claim or cancel them. If a user attempts to call cancel before the vesting plan's end date, the current claimable amount will be distributed, and the cancel amount, intended to cover the remaining vesting period, will be returned. This cancel amount can then be utilized, for instance, in esmkl_token::cancel to refund any unused esmkl tokens.

However, it's worth noting that the cancel function can still be invoked even after the vesting plan's conclusion. In such cases, the logic mirrors that of the claim function, albeit with an additional AdminCapability. This introduces an ambiguous behavior where a user can effectively claim their tokens through the cancel function, despite the vesting plan not being canceled in reality.

Code Location

The cancel function enables users to cancel their vesting plans upon conclusion, resulting in a claim instead.

public fun cancel(_vesting_plan: VestingPlan, _claim_cap: ClaimCapability, _admin_cap: AdminCapability): (FungibleAsset, u64) {
    assert!(_vesting_plan.paused == false, E_PAUSED);
    assert!(_vesting_plan.uid == _claim_cap.uid && _vesting_plan.uid == _admin_cap.uid, E_NOT_AUTHORIZED);
    let claimable = get_claimable(&_vesting_plan);
    let claimed_asset = fungible_asset::zero(claimable_fa_store::get_metadata_by_uid(&_vesting_plan.claimable_fa_store_claim_cap));
    if (claimable > 0) {
        fungible_asset::merge(&mut claimed_asset, claim_internal(&mut _vesting_plan, claimable));
    };
    let cancel_amount = _vesting_plan.total_amount - _vesting_plan.claimed_amount;

    (claimed_asset, cancel_amount)
}
BVSS
Recommendation

Consider implementing a check to ensure that the vesting plan's end date has not been reached before allowing cancellation. This requirement will compel users to claim their plans through the claim function, as intended.

Remediation Plan

SOLVED: The Taurus Labs team revised the logic to ensure that a vesting plan cancellation is permissible only when the cancellation amount exceeds zero.

Remediation Hash
References

7.6 Potential resource borrowing might go unused

// Informational

Description

In the pmkl_token::get_user_season_claimable function, there's a risk that the borrowed RewardInfo resource might not be utilized if the subsequent if clause is triggered. This could lead to unnecessary gas consumption.

Code Location

The get_user_season_claimable function borrows the RewardInfo resource too early, possibly leading to its underutilization.

let reward_info = borrow_global<RewardInfo>(@merkle);
if (!table::contains(&pmkl_info.season_pmkl, _season_number)) {
    return 0
};
Score
Recommendation

Consider borrowing the RewardInfo resource just after the if-clause to ensure its use in the intended context.

if (!table::contains(&pmkl_info.season_pmkl, _season_number)) {
    return 0
};
let reward_info = borrow_global<RewardInfo>(@merkle);

Remediation Plan

SOLVED: The Taurus Labs team implemented the recommended solution.

Remediation Hash
References

7.7 Boolean equality

// Informational

Description

In the vesting module, both the claim and cancel functions include assertions that compare a boolean expression to false, leading to unnecessary gas consumption.

Code Location

The claim function includes an assertion that compares a boolean expression to false:

public fun claim(_vesting_plan: &mut VestingPlan, _claim_cap: &ClaimCapability): FungibleAsset {
    assert!(_vesting_plan.paused == false, E_PAUSED);

The cancel function includes an assertion that compares a boolean expression to false:

public fun cancel(_vesting_plan: VestingPlan, _claim_cap: ClaimCapability, _admin_cap: AdminCapability): (FungibleAsset, u64) {
    assert!(_vesting_plan.paused == false, E_PAUSED);
Score
Recommendation

Consider implementing the necessary adjustments by adhering to the following principle:

assert!(x == true) -> assert!(x)
assert!(x == false) -> assert!(!x)

Remediation Plan

SOLVED: The Taurus Labs team implemented the recommended adjustments.

Remediation Hash
References

7.8 Presence of typos

// Informational

Description

Within the modules in scope, several typos were identified.

Code Location

In the staking module, the following typos were identified:

const E_INVALIAD_EPOCH_START_AT: u64 = 5;
string::utf8(b""), // decsription

In the pre_tge_reward module, the following typo was identified:

let seasoun_end_sec = mkl_token::mkl_tge_at() - (DAY_SECONDS * 14 * (tge_season - 3));
Score
Recommendation

Consider implementing the following changes:

E_INVALIAD_EPOCH_START_AT -> E_INVALID_EPOCH_START_AT
decsription -> description
seasoun -> season

Remediation Plan

SOLVED: The Taurus Labs team implemented the recommended changes.

Remediation Hash
References

7.9 Employ constants to mitigate unnecessary recomputation

// Informational

Description

In the get_unlock_amount function within the mkl_token module, there is an unnecessary computation of one month's duration in seconds as DAY_SECONDS * 365 / 12 each time the function is called.

Code Location

The get_unlock_amount function recalculates the duration of one month in seconds every time it is invoked:

let idx = elapsed_time / (DAY_SECONDS * 365 / 12);
Score
Recommendation

Consider adding a new constant named MONTH_SECONDS within the mkl_token module, with a value of 2628000, calculated as DAY_SECONDS * 365 / 12, to avoid redundant computation.

Remediation Plan

ACKNOWLEDGED: The Taurus Labs team acknowledged this issue but decided that using an additional const is an unnecessary use of gas.

References

7.10 Unnecessary use of mutable references

// Informational

Description

Borrowing a resource should ideally be reserved for cases where modification is necessary. However, in the modules in scope, several unnecessary mutable borrows were identified.

Code Location

In the claimable_fa_store module, the following unnecessary mutable borrows were identified:

let claimable_fa_store = borrow_global_mut<ClaimableFaStore>(resource_account);
let claimable_fa_store = borrow_global_mut<ClaimableFaStore>(_claim_cap.resource_account);
let claimable_fa_store = borrow_global_mut<ClaimableFaStore>(_claim_cap.resource_account);

In the mkl_token module, the following unnecessary mutable borrow was identified:

let pool_store = borrow_global_mut<PoolStore<PoolType>>(@merkle);

In the pmkl_token module, the following unnecessary mutable borrow was identified:

let reward_info = borrow_global_mut<RewardInfo>(@merkle);

In the staking module, the following unnecessary mutable borrows were identified:

let config = borrow_global_mut<VoteEscrowedMKLConfig>(@merkle);
let ve_powers = borrow_global_mut<VoteEscrowedPowers>(@merkle);
let user_vemkl = borrow_global_mut<UserVoteEscrowedMKL>(_user_address);

In the protocol_reward module, the following unnecessary mutable borrow was identified:

let user_reward_info = borrow_global_mut<UserRewardInfo<AssetType>>(_user_address);
Score
Recommendation

Consider using borrow_global instead.

Remediation Plan

SOLVED: The Taurus Labs team implemented the recommended changes.

Remediation Hash
References

7.11 Usage of legacy Coin standard

// Informational

Description

Following AIP-63, the Aptos team is transitioning from the Coin standard to the Fungible Asset (FA) standard. It's advisable to adopt the FA standard from the outset to circumvent potential migration complexities post-contract deployment.

Code Location

The protocol_reward module uses the legacy Coin standard:

use aptos_framework::coin::{Self, Coin};
struct Reward<phantom AssetType> has store {
    registered_at: u64,
    registered_reward_amount: u64,
    coin: Coin<AssetType>
}
let reward = coin::extract(&mut reward.coin, reward_amount);
coin: coin::withdraw<AssetType>(_admin, _amount)
aptos_account::deposit_coins(address_of(_admin), coin::extract_all(&mut reward.coin));
coin::merge(&mut reward.coin, coin::withdraw<AssetType>(_admin, _amount));
Score
Recommendation

Consider using the Fungible Asset (FA) standard instead of the legacy Coin standard.

Remediation Plan

ACKNOWLEDGED: The Taurus Labs team acknowledged this issue but decided against fixing it considering their plan to distribute rewards in zUSDC, which is a USD Coin bridged to Aptos by LayerZero.

References

7.12 Unused import statement

// Informational

Description

In the pmkl_token module, an unused import was identified.

Code Location

The merkle::claimable_fa_store module was imported but never used during the pmkl_token module's unit tests.

#[test_only]
use merkle::claimable_fa_store;
Score
Recommendation

Consider removing the unused import statement.

Remediation Plan

SOLVED: The Taurus Labs team removed the unused import statement.

Remediation Hash
References

7.13 Missing view decorator for read-only functions

// Informational

Description

#[view] decorated functions cannot alter storage; they only read data, providing a safe way to access information without risking state modification. Utilizing the #[view] macro to safeguard read-only functions ensures that any attempt to modify storage inadvertently is prevented, thereby minimizing potential security vulnerabilities. The following read-only functions lack the #[view]decorator:

  • pmkl_token::get_current_season_info

  • pmkl_token::get_season_user_pmkl

  • pmkl_token::get_season_info

  • pmkl_token::get_user_season_claimable

  • staking::get_epoch_user_vote_power

  • mkl_token::get_metadata

  • mkl_token::get_unlock_amount

  • mkl_token::get_allocation

  • esmkl_token::get_metadata

  • protocol_reward::user_reward_amount

Score
Recommendation

Consider using the #[view] decorator for the mentioned functions.

Remediation Plan

PENDING: The Taurus Labs team appreciated the recommendation and is open to implementing it in the future.

References

7.14 Redundant check increases gas usage

// Informational

Description

In the pmkl_token module, the get_season_user_pmkl function unnecessarily contains an assertion that always holds true.

Initially, the function verifies if the provided season number has an entry in the season season_pmkl, returning a default SeasonUserPMKLInfoView resource if not.
After this check, it redundantly verifies again if the season_pmkl table contains an entry for the season number, which is always ensured by the previous return statement.

Code Location

The get_season_user_pmkl function unnecessarily contains an assertion that always holds true.

if (!table::contains(&season_pmkl_info.season_pmkl, _season_number)) {
    return SeasonUserPMKLInfoView {
        season_number: _season_number,
        total_supply: 0,
        user_balance: 0
    }
};
assert!(table::contains(&season_pmkl_info.season_pmkl, _season_number), E_INVALID_SEASON_NUMBER);
Score
Recommendation

Consider removing the redundant check.

Remediation Plan

SOLVED: The Taurus Labs team removed the redundant check.

Remediation Hash
References

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.