Omniscia Mantissa Finance Audit

Vesting Manual Review Findings

Vesting Manual Review Findings

VGN-01M: Inexistent Claim of Existing Funds

TypeSeverityLocation
Logical FaultVesting.sol:L100-L104

Description:

The Vesting::transferRecipient function will not claim any pending funds before transferring, indicating potentially undesirable behaviour.

Impact:

The severity and validity of this exhibit will be based on the action that the Mantissa team takes to alleviate it.

Example:

contracts/Vesting.sol
100function transferRecipient(address _newRecipient) external onlyRecipient {
101 emit RecipientTransferred(recipient, _newRecipient);
102 require(_newRecipient != address(0), "recipient zero address");
103 recipient = _newRecipient;
104}

Recommendation:

We advise the Mantissa team to evaluate this trait and either automatically claim any pending vest or retain the current behaviour in the codebase.

Alleviation (418ee413ad8e26f7eea383764c19953ff31b2bf3):

The Mantissa Finance team evaluated this exhibit and stated that the Vesting::transferRecipient function is a fail-safe and that the onus of any issues arising from it is on the sender and recipient.

As such, we consider this exhibit acknowledged.

VGN-02M: Potentially Unsafe Transfer of Vest

TypeSeverityLocation
Logical FaultVesting.sol:L56-L60, L100-L104

Description:

The Vesting::transferRecipient function is meant to be utilized to transfer ownership of a particular vest, however, the Vesting::revoke and Vesting::withdraw functions can be utilized to cancel the vest and acquire any funds within it.

Impact:

It is presently possible to advertise a Vesting for "sale" with the owner able to recoup any funds associated with it after the transfer of it concludes.

Example:

contracts/Vesting.sol
56function revoke() external onlyOwner {
57 require(canBeRevoked, "Cannot be revoked");
58 emit AmountRevoked(amountRemaining);
59 amountRemaining = 0;
60}
61
62function withdraw(address _token) external onlyOwner {
63 uint256 contractBalance = IERC20(_token).balanceOf(address(this));
64 require(contractBalance > 0, "Contract balance is already 0");
65 if (_token == mnt) {
66 require(contractBalance > amountRemaining, "Nothing to withdraw");
67 uint256 safeAmount = contractBalance - amountRemaining;
68 IERC20(_token).safeTransfer(msg.sender, safeAmount);
69 } else {
70 IERC20(_token).safeTransfer(msg.sender, contractBalance);
71 }
72}
73
74function getTotalAmountVested() public view returns (uint256) {
75 if (cliffTimestamp >= block.timestamp) {
76 return 0;
77 }
78 uint256 vestTime = block.timestamp - cliffTimestamp;
79 uint256 amountVested = amount * vestTime / vestDuration;
80 if (amountVested > amount) {
81 amountVested = amount;
82 }
83 return amountVested;
84}
85
86function getAmountClaimable() public view returns (uint256) {
87 uint256 totalVested = getTotalAmountVested();
88 uint256 amountPaid = amount - amountRemaining;
89 return totalVested - amountPaid;
90}
91
92function claim() external onlyRecipient {
93 uint256 claimable = getAmountClaimable();
94 require(claimable > 0, "Nothing to claim");
95 amountRemaining -= claimable;
96 IERC20(mnt).safeTransfer(recipient, claimable);
97 emit AmountClaimed(claimable, amountRemaining);
98}
99
100function transferRecipient(address _newRecipient) external onlyRecipient {
101 emit RecipientTransferred(recipient, _newRecipient);
102 require(_newRecipient != address(0), "recipient zero address");
103 recipient = _newRecipient;
104}

Recommendation:

We advise the code to disallow a transfer of the recipient if the Vesting can be revoked to prevent potential social engineering attacks, such as "selling" a Vesting that is revoked once the recipient acquires it.

Alleviation (418ee413ad8e26f7eea383764c19953ff31b2bf3):

The Mantissa Finance team stated that the responsible party for invoking the Vesting::revoke and Vesting::withdraw functions will be a multi-signature wallet owned by the Mantissa Finance team.

Additionally, they state that if a vesting is marked as revoke-able it implies that the Mantissa Finance team will be capable of updating it at any moment.

We would like to note that certain on-chain edge-case race conditions can still materialize (i.e. the multi-signature wallet issues a revocation but the user "sells" their vest before the revocation goes through) and as such, we consider this exhibit valid and acknowledged.