Platform: Code4rena
Start Date: 09/11/2021
Pot Size: $30,000 ETH
Total HM: 6
Participants: 17
Period: 3 days
Judge: pauliax
Total Solo HM: 3
Id: 50
League: ETH
Rank: 4/17
Findings: 3
Award: $3,653.72
🌟 Selected for report: 5
🚀 Solo Findings: 0
hickuphh3
The updateVestedTokens()
increases the amount of tokens to be vested for a beneficiary. There is no access restriction to the function.
The intended total vesting duration is 30 months with a 12-month cliff where 5% is immediately unlocked, and the remaining 95% linearly vested over the next 18 months. Hence, a beneficiary can prematurely unlock the full amount immediately after the 12-month cliff by specifying calling the function again with 19x the amount.
FSD.mintHatch()
, where the computed mintAmount
is 100 FSD tokens. The vesting contract is created with 100 FSD tokens vested.updateVestedTokens(ALICE_ADDRESS, 1900e18)
on the created vesting contract with the following parameters. The amount
in the contract is now 100 + 1900 = 2000 FSD tokens.calculateVestingClaim()
where her tx gets included in a block such that block.timestamp == start.add(CLIFF)
. Then, the unlock amount vestedAmount = amount.mul(5) / 100;
would be 2000 * 5 / 100 = 100, the entire FSD balance being unlocked instead of the expected 5% of 100 = 5 FSD tokens.Realistically, a user would likely call with a slightly smaller multiplier due to unpredictable block times. Nevertheless, they would be able to prematurely unlock most of the vested amount.
Restrict the updateVestedTokens()
function to only be callable by the FSD token contract.
function updateVestedTokens(address _user, uint256 _amount) external override onlyFsd { // Note: checks below are redundant require( _user == beneficiary, "FSDVesting::updateVestedTokens: Only beneficiary can update the vested amount" ); require( amount != 0, "FSDVesting::updateVestedTokens: Amount cannot be zero" ); amount = amount.add(_amount); } ... /** * @dev Throws if called by any account other than the FSD token contract */ modifier onlyFsd() { require( address(fsd) == msg.sender, "FSDVesting::updateVestedTokens: Only FSD can update the vested amount" ); _; }
#0 - pauliax
2021-11-17T12:07:39Z
A duplicate of #101
0.5895 ETH - $2,789.75
hickuphh3
The claiming of staking and governance tributes for the a beneficiary's vested tokens should be no different than other users / EOAs. However, the claimTribute()
and claimGovernanceTribute()
are missing the actual claiming calls to the corresponding functions of the FSD token contract. As a result, the accrued rewards are taken from the beneficiary's vested token while not claiming (replenishing) from the FSD token contract.
In addition to what has been mentioned above, the internal accounting for claimedTribute states can be removed because they are already performed in the FSD token contract.
// TODO: Remove _claimedTribute and _claimedGovernanceTribute mappings /** * @dev Allows claiming of staking tribute by `msg.sender` during their vesting period. * It updates the claimed status of the vest against the tribute * being claimed. * * Requirements: * - claiming amount must not be 0. */ function claimTribute(uint256 num) external onlyBeneficiary { uint256 tribute = fsd.availableTribute(num); require(tribute != 0, "FSDVesting::claimTribute: No tribute to claim"); fsd.claimTribute(num); fsd.safeTransfer(msg.sender, tribute); emit TributeClaimed(msg.sender, tribute); } /** * @dev Allows claiming of governance tribute by `msg.sender` during their vesting period. * It updates the claimed status of the vest against the tribute * being claimed. * * Requirements: * - claiming amount must not be 0. */ function claimGovernanceTribute(uint256 num) external onlyBeneficiary { uint256 tribute = fsd.availableGovernanceTribute(num); require( tribute != 0, "FSDVesting::claimGovernanceTribute: No governance tribute to claim" ); fsd.claimGovernanceTribute(num); fsd.safeTransfer(msg.sender, tribute); emit GovernanceTributeClaimed(msg.sender, tribute); }
#0 - pauliax
2021-11-17T16:41:51Z
Great find, good job warden.
🌟 Selected for report: hickuphh3
0.0155 ETH - $73.58
hickuphh3
DURATION.sub(CLIFF)
is calculated in calculateVestingClaim()
. Since both are constants, it would be better to define a new constant LINEAR_VEST_AFTER_CLIFF
that refers to the vest duration after the cliff.
uint256 private constant LINEAR_VEST_AFTER_CLIFF = 18 * ONE_MONTH;
#0 - pauliax
2021-11-16T23:44:36Z
Valid optimization.
🌟 Selected for report: hickuphh3
0.0155 ETH - $73.58
hickuphh3
Tracing the function calls, the _start
parameter in initiateVesting()
will always be block.timestamp
. Hence, this input parameter can be removed from the function.
// TODO: Modify relevant function calls function initiateVesting( address _beneficiary, uint256 _amount ) external onlyFactory { require( start == 0, "FSDVesting::initiateVesting: Vesting is already initialized" ); beneficiary = _beneficiary; start = block.timestamp; amount = _amount; }
#0 - pauliax
2021-11-16T23:46:09Z
Valid optimization.
0.007 ETH - $33.11
hickuphh3
_user
input in updateVestedTokens()
is redundant because each user will have at most 1 vesting contract, and this function should be restricted to the FSD token contract only (kindly refer to related submitted issue), which stores and retrieves the mapping of users to vesting contracts.FSD._createVesting()
./** * @dev Allows a vesting beneficiary to extend their vested token amount. */ function updateVestedTokens(uint256 _amount) external override onlyFsd { amount = amount.add(_amount); }
#0 - YunChe404
2021-11-14T11:14:06Z
The first bullet is a duplicate of #2
#1 - pauliax
2021-11-16T23:49:59Z
Valid optimization. #2 is a subset of this issue. Making this a primary issue as it contains a clear explanation and suggested solution.
🌟 Selected for report: hickuphh3
0.0155 ETH - $73.58
hickuphh3
Since the defined constants are unneeded elsewhere, it can be defined to be internal
or private
to save gas.
// One month in seconds uint256 internal constant ONE_MONTH = 30 days; // Cliff period for a vest uint256 internal constant CLIFF = 12 * ONE_MONTH; // Duration of a vest uint256 internal constant DURATION = 30 * ONE_MONTH;
#0 - pauliax
2021-11-16T23:44:00Z
While this may reduce gas usage, it also makes these variables harder to fetch from the outside for those that are interested in e.g. duration of a vest.