Reward tokens corresponding to removed milestones are locked
Description
In the addMetavestMilestone and createMetavest functions, tokens are transferred to _grant as a milestone award. However, the removeMetavestMilestone function does not return the tokens associated with the removed milestone.
Impact
The grantee cannot get awards in removed milestones. The authority is also unable to recover these tokens because the calculation of tokensToRecover uses the value in storage instead of the token balance.
The following proof-of-concept script demonstrates that removed milestones' awards are left in the contract after the authority and grantee complete the withdrawal.
function testAuditRemainingBalanceFromRemoveMilestone() public {
address vestingAllocation = createDummyVestingAllocationSlowUnlock();
uint256 snapshot = token.balanceOf(authority);
VestingAllocation(vestingAllocation).confirmMilestone(0);
// add a new milestone and remove it
BaseAllocation.Milestone[] memory milestones = new BaseAllocation.Milestone[](1);
milestones[0] = BaseAllocation.Milestone({
milestoneAward: 1337,
unlockOnCompletion: false,
complete: true,
conditionContracts: new address[](0)
});
token.approve(address(controller), 1337);
controller.addMetavestMilestone(vestingAllocation, milestones[0]);
bytes4 selector = bytes4(keccak256("removeMetavestMilestone(address,uint256)"));
bytes memory msgData = abi.encodeWithSelector(selector, vestingAllocation, 1);
controller.proposeMetavestAmendment(vestingAllocation, controller.removeMetavestMilestone.selector, msgData);
vm.prank(grantee);
controller.consentToMetavestAmendment(vestingAllocation, controller.removeMetavestMilestone.selector, true);
controller.removeMetavestMilestone(vestingAllocation, 1);
vm.warp(block.timestamp + 25 seconds);
controller.terminateMetavestVesting(vestingAllocation);
// withdraw all vested tokens
vm.startPrank(grantee);
vm.warp(block.timestamp + 25 seconds);
VestingAllocation(vestingAllocation).withdraw(
VestingAllocation(vestingAllocation)
.getAmountWithdrawable()
);
vm.stopPrank();
assertNotEq(token.balanceOf(vestingAllocation), 0);
console.log('remaining balance: ', token.balanceOf(vestingAllocation));
}The following text is the result of the proof-of-concept script:
[PASS] testAuditRemainingBalanceFromRemoveMilestone() (gas: 2007632)
Logs:
remaining balance: 1337Recommendations
When removing a milestone, return the corresponding milestone award to the authority.
Remediation
This issue has been acknowledged by MetaLeX Labs, Inc, and a fix was implemented in commit dfe46863↗.