FairSide contest - hickuphh3's results

Decentralized Cost Sharing Network.

General Information

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

FairSide

Findings Distribution

Researcher Performance

Rank: 4/17

Findings: 3

Award: $3,653.72

🌟 Selected for report: 5

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: leastwood

Also found by: WatchPug, cmichel, hickuphh3, hyh, rfa

Labels

bug
duplicate
3 (High Risk)

Awards

0.1289 ETH - $610.12

External Links

Handle

hickuphh3

Vulnerability details

Impact

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.

Example

  1. Alice calls FSD.mintHatch(), where the computed mintAmount is 100 FSD tokens. The vesting contract is created with 100 FSD tokens vested.
  2. Alice calls 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.
  3. Alice calls 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

Findings Information

🌟 Selected for report: hickuphh3

Also found by: leastwood

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

0.5895 ETH - $2,789.75

External Links

Handle

hickuphh3

Vulnerability details

Impact

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.

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.0155 ETH - $73.58

External Links

Handle

hickuphh3

Vulnerability details

Impact

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.

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.0155 ETH - $73.58

External Links

Handle

hickuphh3

Vulnerability details

Impact

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.

Findings Information

🌟 Selected for report: hickuphh3

Also found by: nathaniel

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.007 ETH - $33.11

External Links

Handle

hickuphh3

Vulnerability details

Impact

  1. The _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.
  2. The zero amount check is redundant because it is already checked in 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.

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.0155 ETH - $73.58

External Links

Handle

hickuphh3

Vulnerability details

Impact

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.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter