FairSide contest - cmichel's results

FairSide Network

General Information

Platform: Code4rena

Start Date: 20/05/2021

Pot Size: $55,000 USDC

Total HM: 19

Participants: 8

Period: 7 days

Judge: cemozer

Total Solo HM: 11

Id: 11

League: ETH

FairSide

Findings Distribution

Researcher Performance

Rank: 1/8

Findings: 10

Award: $18,416.00

🌟 Selected for report: 4

🚀 Solo Findings: 4

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: cmichel

Labels

bug
duplicate
3 (High Risk)

Awards

1297.0112 USDC - $1,297.01

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

In tokenizeConviction when locked > 0 the amount is first transferred from the user using an internal call to _transfer(msg.sender, address(fairSideConviction), locked);. It is then transferred a second time from the user in the fairSideConviction.createConvictionNFT call:

function createConvictionNFT(
    address user,
    uint256 score,
    uint256 locked,
    bool isGovernance
) external override returns (uint256) {
    if (locked > 0) {
        cs.locked = locked;
        // steals a second time
        FSD.safeTransferFrom(user, address(this), locked);
    }
}

Impact

The locked balance is transferred twice from the user instead of once, stealing their balance.

Remove the transfer in createConvictionNFT.

#0 - fairside-core

2021-05-30T13:02:16Z

Duplicate of #74

#1 - cemozerr

2021-06-07T19:42:45Z

Duplicate of #29

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed
resolved

Awards

4803.7452 USDC - $4,803.75

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The TOTAL_GOVERNANCE_SCORE is supposed to track the sum of the credit scores of all governors.

In ERC20ConvictionScore._updateConvictionScore, when the user does not fulfill the governance criteria anymore and is therefore removed, the governanceDelta should be negative but it's positive.

isGovernance[user] = false;
governanceDelta = getPriorConvictionScore(
    user,
    block.number - 1
);

It then gets added to the new total:

uint224 totalGCSNew =
    add224(
        totalGCSOld,
        governanceDelta,
        "ERC20ConvictionScore::_updateConvictionTotals: conviction score amount overflows"
    );

Impact

The TOTAL_GOVERNANCE_SCORE tracks wrong data leading to issues throughout all contracts like wrong FairSideDAO.totalVotes data which can then be used for anyone to pass proposals in the worst case. Or totalVotes can be arbitrarily inflated and break the voting mechanism as no proposals can reach the quorum (percentage of totalVotes) anymore.

Return a negative, signed integer for this case and add it to the new total.

#0 - fairside-core

2021-06-01T14:23:22Z

Fixed in PR#14.

Findings Information

🌟 Selected for report: shw

Also found by: a_delamo, cmichel, pauliax

Labels

bug
duplicate
3 (High Risk)

Awards

1297.0112 USDC - $1,297.01

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The name pendingWithdrawals indicates that this storage variable tracks the withdrawals that need yet to be paid out which also matches the behavior in _increaseWithdrawal. So it should be decreased when withdrawing in withdraw but it is not.

Impact

The getReserveBalance consistently under-reports the actual reserve balance which leads to wrong mint amounts being used in the FSD.mint calculation.

Recommendation

Decrease pendingWithdrawals by the withdrawn amount.

#0 - fairside-core

2021-05-30T13:06:24Z

Duplicate of #72

Findings Information

🌟 Selected for report: shw

Also found by: cmichel

Labels

bug
duplicate
3 (High Risk)

Awards

2161.6853 USDC - $2,161.69

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

When _reserveDelta is negative in ABC._calculateDeltaOfFSD the following branch is executed:

if (_reserveDelta < 0) {
    uint256 capitalPostWithdrawal =
        capitalPool.sub(uint256(_reserveDelta));

The type cast to uint256 is purely a reinterpretation of the underlying bytes, it does not compute the absolute value. Which means uint256(_reserveDelta) will be a huge value (2^256 - abs(_reserveDelta)) due to two's complement encoding. This is always greater than the capitalPool and the transaction will revert because of the underflow in SafeMath.sub.

Impact

The FSD.burn function always calls it with a negative value which would break the burn function among other functions. (Why is this not caught in a test though?)

Recommendation

Multiply _reserveDelta by -1 to get the absolute value and then the type-cast to uint256 will be safe.

#0 - fairside-core

2021-05-30T12:42:54Z

Duplicate of #77

Findings Information

🌟 Selected for report: cmichel

Labels

bug
question
3 (High Risk)
sponsor confirmed
disagree with severity
resolved

Awards

4803.7452 USDC - $4,803.75

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

In ERC20ConvictionScore._updateConvictionScore, when the user does not fulfill the governance criteria anymore, the governanceDelta is the old conviction score of the previous block.

isGovernance[user] = false;
governanceDelta = getPriorConvictionScore(
    user,
    block.number - 1
);

The user could increase their conviction / governance score first in the same block and then lose their status in a second transaction, and the total governance conviction score would only be reduced by the previous score.

Example: Block n - 10000: User is a governor and has a credit score of 1000 which was also contributed to the TOTAL_GOVERNANCE_SCORE Block n:

  • User updates their own conviction score using public updateConvictionScore function which increases the credit score by 5000 based on the accumulated time. The total governance credit score increased by 5000, making the user contribute 6000 credit score to governance in total.
  • User transfers their whole balance away, the balance drops below governanceMinimumBalance and user is not a governor anymore. The governanceDelta update of the transfer should be 6000 (user's whole credit score) but it's only 1000 because it takes the snapshot of block n - 1.

Impact

The TOTAL_GOVERNANCE_SCORE score can be inflated this way and break the voting mechanism in the worst case as no proposals can reach the quorum (percentage of totalVotes) anymore.

Use the current conviction store which should be governanceDelta = checkpoints[user][userCheckpointsLength - 1].convictionScore

#0 - fairside-core

2021-05-30T15:02:28Z

As with the other governance related issues, this would once again cause dilution of all users and would not really be a viable attack vector. As such, I believe it is better suited for a medium severity (2) label.

#1 - fairside-core

2021-06-01T13:25:35Z

This issue is actually quite deeper. When a transaction occurs in the same block, the logic paths within the if block will not execute (due to time elapsed being 0) meaning that conviction score will not be properly accounted for if I have a single normal transaction where I am still governance and consequently lose my governance in a second transaction. As such, the code needs to be adjusted to check governance eligibility outside of the if block as well (if no time has passed -> same block transaction).

The code highlighted in the finding is actually correct. The conviction score should be reduced by the previous block's as the newly accrued conviction score was never accounted for in governance and the solution proposed would actually lead to more conviction being reduced than it should. However, the finding did point out something that was wrong so not sure whether it should be nullified or not.

I believe it should be awarded as it was on the right track to find the underlying issue!

#2 - fairside-core

2021-06-01T13:33:26Z

Fixed in PR#13.

#3 - cemozerr

2021-06-08T19:19:10Z

Labeling this issue as valid, because although it wasn't 100% right on suggesting where the code was problematic, it did point out that the users could wrongfully transfer their whole balance and update their conviction score in the same block to keep their conviction score high, and then potentially do harmful things to the protocol by using their wrong conviction scores.

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: cmichel

Labels

bug
duplicate
3 (High Risk)

Awards

648.5056 USDC - $648.51

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

In tokenizeConviction, when locked == 0 the _updateConvictionScore(msg.sender, 0) function is called to update the user's conviction, however the delta is not added to the total credit / governance score.

Impact

The TOTAL_CONVICTION_SCORE and TOTAL_GOVERNANCE_SCORE track wrong data leading to issues throughout all contracts, especially with on-chain governance voting.

Call updateConvictionScore instead of _updateConvictionScore.

#0 - fairside-core

2021-05-30T14:57:08Z

Duplicate of #28

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed
disagree with severity
resolved

Awards

1441.1236 USDC - $1,441.12

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The credit score of the special address(type(uint160).max) is supposed to represent the sum of the credit scores of all users that are governors. But any user can directly transfer to this address increasing its balance and accumulating a credit score in _updateConvictionScore(to=address(uint160.max), amount). It'll first write a snapshot of this address' balance which should be very low:

// in _updateConvictionScore
_writeCheckpoint(user, userNum, userNew) = _writeCheckpoint(TOTAL_GOVERNANCE_SCORE, userNum, checkpoints[user][userNum - 1].convictionScore + convictionDelta);

This address then accumulates a score based on its balance which can be updated using updateConvictionScore(uint160.max) and breaks the invariant.

Impact

Increasing it might be useful for non-governors that don't pass the voting threshold and want to grief the proposal voting system by increasing the quorumVotes threshold required for proposals to pass. (by manipulating FairSideDAO.totalVotes). totalVotes can be arbitrarily inflated and break the voting mechanism as no proposals can reach the quorum (percentage of totalVotes) anymore.

Disallow transfers from/to this address. Or better, track the total governance credit score in a separate variable, not in an address.

#0 - fairside-core

2021-05-30T14:59:04Z

This is actually what #61 is meant to be used for. I would label this a medium level finding as it would simply dilute the voting rights of users at the expense of permanently losing FSD which should not be a viable vector.

#1 - fairside-core

2021-06-01T13:04:39Z

Indirectly fixed by PR#10.

#2 - cemozerr

2021-06-08T19:21:50Z

Labeling this as medium risk as it would not pose threat to user funds yet stall the governance process.

Findings Information

🌟 Selected for report: shw

Also found by: cmichel, pauliax, s1m0

Labels

bug
duplicate
2 (Med Risk)
disagree with severity

Awards

262.6448 USDC - $262.64

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

There is no check in FSDNetwork.getEtherPrice if the return values indicate stale data. This could lead to stale prices according to the Chainlink documentation:

Impact

Stale prices that do not reflect the current market price anymore could be used which would influence the membership and cost share pricing.

Recommendation

Add the recommended checks:

(
    uint80 roundID,
    int256 price,
    ,
    uint256 timeStamp,
    uint80 answeredInRound
) = ETH_CHAINLINK.latestRoundData();
require(
    timeStamp != 0,
    "ChainlinkOracle::getLatestAnswer: round is not complete"
);
require(
    answeredInRound >= roundID,
    "ChainlinkOracle::getLatestAnswer: stale data"
);
require(price != 0, "FSDNetwork::getEtherPrice: Chainlink Malfunction");

#0 - fairside-core

2021-05-30T13:19:36Z

I believe it should be reduced in severity. The second bullet point only applies to historical rounds, not the latest round meaning that invalidity will never occur. Staleness will occur, however, the attack vector has a low likelihood given Chainlink's reputation. Still, definitely a 1 (minor) issue!

#1 - fairside-core

2021-05-30T13:20:01Z

Duplicate of #70

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed
resolved

Awards

1441.1236 USDC - $1,441.12

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

There are two issues with the governance checks when acquiring them from an NFT:

Missing balance check

The governance checks in _updateConvictionScore are:

!isGovernance[user]
&& userConvictionScore >= governanceThreshold 
&& balanceOf(user) >= governanceMinimumBalance;

Whereas in acquireConviction, only userConvictionScore >= governanceThreshold is checked but not && balanceOf(user) >= governanceMinimumBalance.

else if (
    !isGovernance[msg.sender] && userNew >= governanceThreshold
) {
    isGovernance[msg.sender] = true;
}
the wasGovernance might be outdated

The second issue is that at the time of NFT creation, the governanceThreshold or governanceMinimumBalance was different and would not qualify for a governor now. The NFT's governance state is blindly appplied to the new user:

if (wasGovernance && !isGovernance[msg.sender]) {
    isGovernance[msg.sender] = true;
}

This allows a user to circumvent any governance parameter changes by front-running the change with an NFT creation.

Impact

It's easy to circumvent the balance check to become a governor by minting and redeeming your own NFT. One can also circumvent any governance parameter increases by front-running these actions with an NFT creation and backrunning with a redemption.

Add the missing balance check in acquireConviction. Remove the wasGovernance governance transfer from the NFT and solely recompute it based on the current governanceThreshold / governanceMinimumBalance settings.

#0 - fairside-core

2021-05-30T14:29:34Z

The latter of the two issue "types" is actually desired behaviour. If a user was historically a governance member, the NFT should boast the exact same rights and new thresholds should not retro-actively apply. The former, however, is a valid issue as it allows circumventing the balance check!

#1 - fairside-core

2021-06-01T13:17:56Z

Fixed in PR#12.

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: a_delamo, cmichel

Labels

bug
duplicate
1 (Low Risk)

Awards

129.7011 USDC - $129.70

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

Some parameters of functions are not checked:

  • FairSideConviction.constructor: _fsd
  • FairSideDAO.constructor: _timelock, _FSD, _guardian
  • FSDNetwork.constructor: _fsd, fundingPool, governance, timelock
  • FSD.constructor: _fundingPool, whitelistSigner, _timelock

Impact

A wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas.

Validate the parameters.

#0 - fairside-core

2021-05-30T13:59:29Z

Duplicate of #56

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