GithubHelp home page GithubHelp logo

2021-05-fairside-findings's Introduction

NOTE: This is a template. If you are here looking for info on current contests, you can find info on the latest contests at https://code423n4.com

FairSide Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.


Contest findings are submitted to this repo

Typically most findings come in on the last day of the contest, so don't be alarmed at all if there's nothing here but crickets until the end of the contest.

As a sponsor, you have four critical tasks in the contest process:

  1. Handle duplicate issues.
  2. Weigh in on severity.
  3. Respond to issues.
  4. Share your mitigation of findings.

Let's walk through each of these.

Handle duplicates

Because the wardens are submitting issues without seeing each others' submissions, there will always be findings that are clear duplicates. Other findings may use different language which ultimately describes the same issue but from different angles. Use your best judgement in identifying duplicates, and don't hesitate to reach out (via DM) to ask C4 for advice.

  1. Determine the best and most thorough description of the finding among the set of duplicates. (At least a portion of the content of the most useful description will be used in the audit report.)
  2. Close the other duplicate issues and label them with duplicate
  3. Mention the primary issue # when closing the issue so that duplicate issues get linked.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

If you disagree with a finding's severity, leave the original severity label set by the warden and add the label disagree with severity along with comment indicating your opinion for the judges to review. It is possible for issues to be considered 0 (Non-critical).

Feel free to use the question label to anything you would like additional C4 input on.

Respond to issues

Label each finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it.")
  • sponsor disputed, meaning either: "We either not duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

Note that it isn't necessary to dispute a finding in order to suggest it should be considered of lower or higher severity.

Add any necessary comments explaining your rationale for your evaluation of the issue. Note that when the repo is public after all issues are mitigated, wardens will read these comments.

Share your mitigation of findings

For each non-duplicate finding which you have confirmed, you will want to mitigate the issue before the contest report is made public.

As part of that process, we ask that you create a pull request in your original repo for each finding and link to the PR in the issue the PR resolves. This will allow for complete transparency in showing the work of mitigating the issues found in the contest. Rather than closing the issue, mark it as resolved with that label.

2021-05-fairside-findings's People

Contributors

code423n4 avatar ninek9 avatar c4-staff avatar joshuashort avatar

Watchers

James Cloos avatar Ashok avatar shw avatar  avatar

2021-05-fairside-findings's Issues

Bug inside ABDKMathQuad library

Handle

a_delamo

Vulnerability details

Impact

FairSideFormula library is using ABDKMathQuad library underneath.
According to the ABDKMathQuad README, the range of values is the following:

The minimum strictly positive (subnormal) value is 2^−16494 ≈ 10^−4965 and has a precision of only one bit. The minimum positive normal value is 2^−16382 ≈ 3.3621 × 10^−4932 and has a precision of 113 bits, i.e. ±2^−16494 as well. The maximum representable value is 2^16384 − 2^16271 ≈ 1.1897 × 10^4932.

Using Echidna, a fuzzing tool for smart contracts, I found some edge cases when some of the operations do not work as expected. This is the test code I run using echidna-test contracts/TestABDKMathQuad --contract TestABDKMathQuad

// SPDX-License-Identifier: Unlicense
pragma solidity >=0.6.0 <0.8.0;

import "./dependencies/ABDKMathQuad.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";

contract TestABDKMathQuad {
    uint256 internal x;
    uint256 internal x1;

    int256 internal y;

    function setX(uint256 _x) public {
        x = _x;
    }

    function setX1(uint256 _x1) public {
        x1 = _x1;
    }

    function setY(int256 _y) public {
        y = _y;
    }

    function echidna_Uint_convertion() public returns (bool) {
        bytes16 z = ABDKMathQuad.fromUInt(x);
        return ABDKMathQuad.toUInt(z) == x;
    }

    function echidna_int_convertion() public returns (bool) {
        bytes16 z = ABDKMathQuad.fromInt(y);
        return ABDKMathQuad.toInt(z) == y;
    }

    function echidna_mulUint() public returns (bool) {
        uint256 mul = SafeMath.mul(x, x1);

        bytes16 z = ABDKMathQuad.fromUInt(x);
        bytes16 z1 = ABDKMathQuad.fromUInt(x1);
        bytes16 t = ABDKMathQuad.mul(z, z1);
        return ABDKMathQuad.toUInt(t) == mul;
    }

    function echidna_divUint() public returns (bool) {
        if (x1 == 0 || x == 0 || x < x1) return true;
        uint256 div = SafeMath.div(x, x1);

        bytes16 z = ABDKMathQuad.fromUInt(x);
        bytes16 z1 = ABDKMathQuad.fromUInt(x1);
        bytes16 t = ABDKMathQuad.div(z, z1);
        return ABDKMathQuad.toUInt(t) == div;
    }

    function echidna_neg() public returns (bool) {
        bytes16 z = ABDKMathQuad.fromInt(y);
        bytes16 z_positive = ABDKMathQuad.neg(z);
        int256 result = ABDKMathQuad.toInt(z_positive);

        return result == (-y);
    }

    function echidna_ln() public returns (bool) {
        if (x == 0) return true;

        bytes16 z = ABDKMathQuad.fromUInt(x);
        bytes16 result = ABDKMathQuad.ln(z);
        if (result == ABDKMathQuad.NaN) return false;
        uint256 result_uint = ABDKMathQuad.toUInt(result);
        return result_uint < x;
    }
}

And the results are:

echidna_mulUint: failed!💥
  Call sequence:
    setX(1)
    setX1(10389074519043615041642520862277205)

echidna_Uint_convertion: failed!💥
  Call sequence:
    setX(10385528305364854446597578558364193)

echidna_neg: failed!💥
  Call sequence:
    setY(-10394149475425461937254292332080605)

echidna_int_convertion: failed!💥
  Call sequence:
    setY(-10418479581230103876421151985443129)

echidna_divUint: failed!💥
  Call sequence:
    setX1(1)
    setX(10518626300707317802075092650125337)

If we check in Remix, we can see there is a small difference when converting from UInt to Bytes16 or the opposite way. This is probably the same issue with all the other operations.

ABDKMathQuad.fromUInt(10385528305364854446597578558364193);
// 0x40700005e5e8a8c4dcb4999a17cead10
ABDKMathQuad.toUInt(0x40700005e5e8a8c4dcb4999a17cead10)
//10385528305364854446597578558364192

Tools Used

Echidna https://github.com/crytic/echidna

Recommended Mitigation Steps

Use some fuzzing tool like Echidna to verify there is no edge cases

ChainLink price data could be stale

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");

withdraw() does not decrease pendingWithdrawals

Handle

pauliax

Vulnerability details

Impact

contract Withdrawable function withdraw() does not subtract from pendingWithdrawals thus it only increases and could make function getReserveBalance() revert when the balance < pendingWithdrawals.

Recommended Mitigation Steps

Add this line to withdraw():
pendingWithdrawals = pendingWithdrawals.sub(reserveAmount);

Changing `ERC20ConvictionScore.governanceThreshold` leads to temporarily broken state

Handle

cmichel

Vulnerability details

Vulnerability Details

Changing the governanceThreshold breaks the governance credit score accounting as users who currently qualify for being a governor may not qualify anymore and this influences the quorum threshold.
It can be changed using FSD.updateGovernanceThreshold.

Impact

Imagine, governance calls updateGovernanceThreshold with a higher value disqualifying current governors but their ERC20ConvictionScore.isGovernance[user] state is not yet updated.
Someone creates a proposal using the old higher threshold.

Someone updates the user states now, for example, by transferring a single wei to them, their status is reset in _updateConvictionScore and the quorum threshold might be unreachable by the updated collective of governors.
The proposal needs to be cancelled, all users' isGovernance state needs to be updated to arrive at the correct totalVotes again.

Recommended Mitigation Steps

I don't see a good solution in the current design.
If possible make governanceThreshold a constant or manually update all users' state after a change such that the totalVotes are correct again.

non existing function returns

Handle

gpersoon

Vulnerability details

Impact

The functions castVote and castVoteBySig of FairSideDAO.sol have no "returns" parameters,
however they do call "return" at the end of the function.

This is confusing for the readers of the code.

Proof of Concept

// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dao/FairSideDAO.sol#L443
function castVote(uint256 proposalId, bool support) public {
return _castVote(msg.sender, proposalId, support);
}

function castVoteBySig( .. ) public {
...
return _castVote(signatory, proposalId, support);
}

Tools Used

Editor

Recommended Mitigation Steps

Remove the "return" statements from castVote and castVoteBySig

Conviction score is not updated during tokenization if funds are locked

Handle

0xRajeev

Vulnerability details

Impact

The _updateConvictionScore() on Line284 of tokenizeConviction() is only called if user specifies zero locked funds. This leads to loss of accounting of user’s conviction score for tokenization (since the last update for user) if non-zero amount of FSDs are specified for locking.

Proof of Concept

Alice receives FSDs and holds for 100 days but forgets to call updateConvictionScore() during this period. When Alice tries to tokenize her conviction score into a NFT, and specifies locking of 10 FSD tokens, she loses accounting for the prior 100 days of conviction and her NFT will not reflect this updated score.

Even if Alice has called updateConvictionScore() earlier, if she does not call it just before tokenizing it to a NFT (while locking non-zero FSD tokens), the last window of unaccounted conviction score (conviction deltas) is not captured in NFT tokenization leading to an effective loss of accrued fund benefits for Alice and the buyer of that NFT.

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L284

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L280-L310

Tools Used

Manual Analysis

Recommended Mitigation Steps

Updating conviction score should be done during tokenization to capture the latest conviction score irrespective of whether FSDs are being locked or not. Move _updateConvictionScore() outside the else body between Line283-Line285 of ERC20ConvictionScore.sol.

Usage of transfer

Handle

cmichel

Vulnerability details

Vulnerability Details

Withdrawable.withdraw: The address.transfer function is used to send ETH to an account. It is restricted to a low amount of GAS and might fail if GAS costs change in the future or if a smart contract's fallback function handler implements anything non-trivial.

Recommendation

Consider using the lower-level .call{value: value} instead and check it's success return value.

`ERC20ConvictionScore._updateConvictionScore` uses stale credit score for `governanceDelta`

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.

Recommended Mitigation Steps

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

Constant values used inline

Handle

gpersoon

Vulnerability details

Impact

In several locations constant values are used inline in the code.
Normally you would define those as constants to able to review and update them easier.

Proof of Concept

Examples:
.\network\FSDNetwork.sol: costShareBenefit % 10 ether == 0 && costShareBenefit > 0,
.\network\FSDNetwork.sol: if (fShare < 7500 ether) fShare = 7500 ether;
.\network\FSDNetwork.sol: (fsd.getReserveBalance() - totalOpenRequests).mul(1 ether) / fShare;
.\network\FSDNetwork.sol: fShareRatio >= 1 ether,
.\network\FSDNetwork.sol: 1 ether) / MEMBERSHIP_DURATION;
.\network\FSDNetwork.sol: if (elapsedDurationPercentage < 1 ether) {
.\network\FSDNetwork.sol: (costShareBenefit.mul(1 ether) /
.\network\FSDNetwork.sol: .mul(MEMBERSHIP_DURATION) / 1 ether;
.\network\FSDNetwork.sol: fShareRatio >= 1.25 ether
.\network\FSDNetwork.sol: ? STAKING_REWARDS + fShareRatio - 1.25 ether
.\network\FSDNetwork.sol: if (stakingMultiplier > 0.75 ether) stakingMultiplier = 0.75 ether;
.\network\FSDNetwork.sol: (requestPayout.mul(etherPrice) / 1 ether).mul(
.\network\FSDNetwork.sol: (stableAmount.mul(1 ether) / etherPrice).mul(
.\network\FSDNetwork.sol: if (fShare < 7500 ether) fShare = 7500 ether;
.\network\FSDNetwork.sol: uint256 minimumMaxBenefit = 100 ether;
.\network\FSDNetwork.sol: (fsd.getReserveBalance() - totalOpenRequests).wmul(0.05 ether); // 5% of Capital Pool
.\network\FSDNetwork.sol: user.availableCostShareBenefits.wmul(MEMBERSHIP_FEE / 10).wdiv(
.\network\FSDNetwork.sol: user.creation + 24 hours <= block.timestamp &&
.\network\FSDNetwork.sol: uint256 halfBounty = bounty / 2;
.\network\FSDNetwork.sol: csrData.creation + 7 days <= block.timestamp &&
.\token\ABC.sol: if (fShare < 7500 ether) fShare = 7500 ether;
.\token\FSD.sol: bool hatchPhase = fundingPool.balance < 2000 ether;
.\token\FSD.sol: require(bonded >= 5 ether, "FSD::mint: Low deposit amount");
.\dao\FairSideDAO.sol: return 10;
.\dao\FairSideDAO.sol: return 1;
.\dao\FairSideDAO.sol: return 3 days / SECS_PER_BLOCK;

Tools Used

grep

Recommended Mitigation Steps

Use constants for constant values

`TributeAccrual` missing out-of-bounds checks

Handle

cmichel

Vulnerability details

Vulnerability Details

The _addTribute and _addGovernanceTribute functions underflow when there are no tributes:

Tribute storage lastTribute = tributes[totalTributes - 1] = tributes[-1]; // underflow

Impact

It's bad practice and the iteration with the offset in totalAvailableTribute will miss the initial tribute unless called with strange parameters that overflow themselves.

Recommended Mitigation Steps

Add a special case totalTributes (totalGovernanceTributes) == 0.

Locked funds are debited twice from user during tokenization leading to fund loss

Handle

0xRajeev

Vulnerability details

Impact

During tokenization of conviction scores, the user can optionally provide FSDs to be locked to let it continue conviction accrual. However, the amount of FSDs specified for locking are debited twice from the user leading to fund loss for user.

This, in effect, forces the user to unknowingly and unintentionally lock twice the amount of FSD tokens, leading to a loss of the specified ‘locked’ number of tokens.

Proof of Concept

Alice decides to tokenise her conviction score into an NFT and specifies 100 FSD tokens to be locked in her call to tokenizeConviction(100). 100 FSD tokens are transferred from her FSD balance to FairSideConviction contract on Line282 of ERC20ConvictionScore.sol. However, in FairSideConviction.createConvictionNFT(), the specified locked amount is transferred again from Alice to the contract on Line50 of FairSideConviction.sol.

The impact is that Alice wanted to lock only 100 FSD token but the FairSide protocol has debited 200 tokens from her balance leading to a loss of 100 FSD tokens.

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L282

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/conviction/FairSideConviction.sol#L48-L51

Tools Used

Manual Analysis

Recommended Mitigation Steps

Remove the redundant transfer of FSD tokens on Line282 in tokenizeConviction() of ERC20ConvictionScore.sol.

Use of ecrecover is susceptible to signature malleability

Handle

0xRajeev

Vulnerability details

Impact

The ecrecover function is used in castVoteBySig() to recover the voter’s address from the signature. The built-in EVM precompile ecrecover is susceptible to signature malleability which could lead to replay attacks (references: https://swcregistry.io/docs/SWC-117, https://swcregistry.io/docs/SWC-121 and https://medium.com/cryptronics/signature-replay-vulnerabilities-in-smart-contracts-3b6f7596df57).

While this is not immediately exploitable in the DAO use case because the voter address is checked against receipt.voted to prevent re-voting, this may become a vulnerability if used elsewhere.

Proof of Concept

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dao/FairSideDAO.sol#L469

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dao/FairSideDAO.sol#L491-L495

Tools Used

Manual Analysis

Recommended Mitigation Steps

Consider using OpenZeppelin’s ECDSA library (which prevents this malleability) instead of the built-in function.

`ERC20ConvictionScore.tokenizeConviction` does not update total conviction & governance score

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.

Recommended Mitigation Steps

Call updateConvictionScore instead of _updateConvictionScore.

ERC20ConvictionScore transfer to self

Handle

gpersoon

Vulnerability details

Impact

The function _beforeTokenTransfer of ERC20ConvictionScore.sol does relatively complicated logic but does not check if from and to are the same addresses.
If the addresses are the same then the logic might get confused by making unnecessary updates.

Proof of Concept

// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/ERC20ConvictionScore.sol#L46

Tools Used

Recommended Mitigation Steps

Double check if this transfering to self is indeed unwanted and then add something like:
require(from != to,"can't transfer to self");

`FairSideDAO.SECS_PER_BLOCK` is inaccurate

Handle

cmichel

Vulnerability details

Vulnerability Details

The SECS_PER_BLOCK is currently set to 15s on Ethereum, but it's closer to 13.5s on average.

Impact

The voting period will be shorter than in reality which might lead to users not getting enough time.

Recommended Mitigation Steps

Use a more accurate representation of SECS_PER_BLOCK for the deployed chain.

`ERC20ConvictionScore` allows transfers to special TOTAL_GOVERNANCE_SCORE address

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.

Recommended Mitigation Steps

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

addRegistrationTributeGovernance shoud call_addGovernanceTribute ?

Handle

gpersoon

Vulnerability details

Impact

The function addRegistrationTributeGovernance makes a call to _addTribute, the same as addRegistrationTribute is doing
However a function _addGovernanceTribute also exists and this function is never called.

It seem more logical that addRegistrationTributeGovernance should call _addGovernanceTribute

Proof of Concept

https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/FSD.sol#L125
function addRegistrationTribute(uint256 registrationTribute) external {
...
_addTribute(registrationTribute);
}
// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/FSD.sol#L133
function addRegistrationTributeGovernance(uint256 registrationTribute)
...
_addTribute(registrationTribute);
}

// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/TributeAccrual.sol#L50
function _addGovernanceTribute(uint256 tribute) internal {

Tools Used

Editor

Recommended Mitigation Steps

Check if addRegistrationTributeGovernance should indeed call _addGovernanceTribute
If so, update the code accordingly.

`ERC20ConvictionScore.acquireConviction` implements wrong governance checks

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.

Recommended Mitigation Steps

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.

`ERC20ConvictionScore.tokenizeConviction` transfers locked balance from user twice

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.

Recommended Mitigation Steps

Remove the transfer in createConvictionNFT.

`Withdrawable.withdraw` does not decrease `pendingWithdrawals`

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.

Dangerous Solidity compiler pragma range that spans breaking versions

Handle

0xRajeev

Vulnerability details

Impact

All contracts use a Solidity compiler pragma range >=0.6.0 <0.8.0 which spans a breaking change version 0.7.0. This compiler range is very broad and includes many syntactic/semantic changes across the versions. Specifically, see silent changes in https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html#silent-changes-of-the-semantics.

This compiler range, for example, allows testing with Solidity compiler version 0.6.x but deployment with 0.7.x. While any breaking syntactic changes will be caught at compile time, there is a risk that the silent change in 0.7.0 which applies to exponentiation/shift operand types might affect the FairSide formula or other mathematical calculations, thus breaking assumptions and accounting.

The opposite scenario may also happen where testing is performed with Solidity compiler version 0.7.x but deployment with 0.6.x, which may allow bugs fixed in 0.7.x to be present in the deployed code.

Proof of Concept

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L3

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/conviction/FairSideConviction.sol#L9

https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html#silent-changes-of-the-semantics

https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html#

https://docs.soliditylang.org/en/v0.8.4/bugs.html

Tools Used

Manual Analysis

Recommended Mitigation Steps

Use the same compiler version both for testing and deployment by enforcing this in the pragma itself. An unlocked/floating pragma is risky especially one that ranges across a breaking compiler minor version.

Missing parameter validation

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.

Recommended Mitigation Steps

Validate the parameters.

NFTs can never be redeemed back to their conviction scores leading to lock/loss of funds


Handle

0xRajeev

Vulnerability details

Impact

Besides the conviction scores of users, there appears to be tracking of the FairSide protocol’s tokenized conviction score as a whole (using fscAddress = address(fairSideConviction)). This is evident in the attempted reduction of the protocol’s score when a user acquires conviction back from a NFT. However, the complementary accrual of user's conviction score to fscAddress when user tokenizes their conviction score to mint a NFT is missing in tokenizeConviction().

Because of this missing updation of conviction score to fscAddress on tokenization, there are no checkpoints written for fscAddress and there also doesn’t appear to be any initialization for bootstrapping this address’s conviction score checkpoints. As a result, the sub224() on Line350 of ERC20ConvictionScore.sol will always fail with an underflow because fscOld = 0 (because fscNum = 0) and convictionScore > 0, effectively reverting all calls to acquireConviction().

The impact is that all tokenized NFTs can never be redeemed back to their conviction scores and therefore leads to lock/loss of FSD funds for users who tokenized/sold/bought FairSide NFTs.

Proof of Concept

  1. Alice tokenizes her conviction score into a NFT. She sells that NFT to Bob who pays an amount commensurate with the conviction score captured by that NFT (as valued by the market) and any FSDs locked with the NFT.

  2. Bob then attempts to redeem the bought NFT back to the conviction score to use it on FairSide network. But the call to acquireConviction() fails. Bob is never able to redeem Alice’s NFT and has lost the funds used to buy it.

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L343-L355

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add appropriate logic to bootstrap+initialize fscAddress’s tokenized conviction score checkpoints and update it during tokenization.

setConvictionless can be front-run to prevent conviction reset

Handle

0xRajeev

Vulnerability details

Impact

The denylist convictionless is meant to deny conviction scores for certain users and is set by the privileged roles timelock/FSD-owner in setConvictionless(). The documentation says: “adjust which addresses are meant to not accrue a conviction score. The latter part is crucial and should be applied to "static" FSD token holders such as the burn address to ensure that the conviction score tribute rewards and governance quorums are correctly calculated.”

It is not clear if the addresses meant to not accrue a conviction score are a few well-known static ones or if this can be used as a denylist in general for misbehaving participants.

If this is indeed used as a general denylist for a misbehaving user say Alice, upon seeing a setConvictionless(Alice, True) call in the mempool, Alice can front-run that transaction which tries to add her to the denylist by simply transferring the tokens to another address or tokenizing it into a NFT, transferring that to another address and then re-acquiring the conviction score on that address.

The impact will be that setConvictionless will fail to achieve its denylist action.

Proof of Concept

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L251-L261

Tools Used

Manual Analysis

Recommended Mitigation Steps

Use commit/reveal scheme on the user address being made convictionless to prevent this scenario.

Wrong error message in `__castOffchainVotes`

Handle

cmichel

Vulnerability details

Vulnerability Details

The error message states:

require(
    proposal.offchain,
    "FairSideDAO::__castOffchainVotes: proposal is meant to be voted offchain"
);

But it should be "... meant to be voted onchain".

totalCostShareBenefit vs totalCostShareBenefits

Handle

gpersoon

Vulnerability details

Impact

The function purchaseMembership of FSDNetwork.sol contains a variable that is very similar to a global variable. It's easy to confuse the two, possibly introducing errors in the future.
These are the following:

  • totalCostShareBenefit
  • totalCostShareBenefits

Proof of Concept

FSDNetwork.sol:
uint256 public totalCostShareBenefits;
function purchaseMembership(uint256 costShareBenefit) external {
uint256 totalCostShareBenefit = membership[msg.sender].availableCostShareBenefits.add(costShareBenefit);
...
totalCostShareBenefits = totalCostShareBenefits.add(costShareBenefit);

Tools Used

Editor

Recommended Mitigation Steps

Change one the variables to an obviously different name.

gracePeriod not increased after membership extension

Handle

gpersoon

Vulnerability details

Impact

In the function purchaseMembership of FSDNetwork.sol, when the membership is extended then membership[msg.sender].creation is increased, however
membership[msg.sender].gracePeriod is not increased.
This might lead to a gracePeriod than is less then expected.
It seems logical to also increase the gracePeriod

Proof of Concept

FSDNetwork.sol
// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L171
function purchaseMembership(uint256 costShareBenefit) external {
...
if (membership[msg.sender].creation == 0) {
...
membership[msg.sender].creation = block.timestamp;
membership[msg.sender].gracePeriod = membership[msg.sender].creation + MEMBERSHIP_DURATION + 60 days;
} else {
....
membership[msg.sender].creation += durationIncrease;
}

Tools Used

Editor

Recommended Mitigation Steps

Check if gracePeriod has to be increased also.
When that is the case add the logic to do that.

withdraw() uses 'transfer'

Handle

pauliax

Vulnerability details

Impact

contract Withdrawable function withdraw() uses 'transfer' to send ether to the msg.sender:
msg.sender.transfer(reserveAmount);
It is no longer recommended as recipients with custom fallback functions (smart contracts) will not be able to handle that. You can read more here: https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/

Recommended Mitigation Steps

Solution (don't forget re-entrancy protection): https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L53-L59

Improvements arctan

Handle

gpersoon

Vulnerability details

Impact

The performance (gas usage) of the current arctan implementation is:
arctan(ONE) ~ 5126 Gas (with solidity 0.6.8)
The main cause of the gas usage is due to the library ABDKMathQuad which implements IEEE 754 quadruple-precision binary floating-point numbers
However the arctan approximation has a relative low precision.

The PDF higher_order_approximations.pdf in the following article: https://www.researchgate.net/publication/258792323_Full_Quadrant_Approximations_for_the_Arctangent_Function_Tips_and_Tricks
shows different formulas for the approximation for arctan, which have a higher precision than the current implementation.
https://www.researchgate.net/profile/Xavier-Girones-2/publication/258792323_Full_Quadrant_Approximations_for_the_Arctangent_Function_Tips_and_Tricks/links/5fdc7f5745851553a0c8b801/Full-Quadrant-Approximations-for-the-Arctangent-Function-Tips-and-Tricks.pdf

The third order approximation is:
arctan(x) ∼ π/2 * sgn(x)φ(abs(x))
φ(x) = { a
x + x^2 + x^3 } / { 1 + (a+1)x + (a+1)x^2 + x^3 }
a=0.6399276529

I've made an implementation (see below), which takes a lot less gas:
arctan_uint(1 * precision) ~ 574 Gas (with solidity 0.6.8)

The implementation takes a different approaches to floating points: it multiples all numbers by precision.
The precision factor can be adjusted as long as all temporary variables stay below 2^256 (the max value of an uint)

Proof of Concept

pragma solidity 0.6.8;

contract Test{
uint constant precision=10**30;
uint constant pi=3.1415926535E30;
uint constant pidiv2=pi/2;
uint constant a1=0.6399276529E30;
uint constant aplus1=1.6399276529E30;

function arctan_uint(uint x) public pure returns (uint) {
uint xsquare = xx/precision;
uint xtriple = xsquare * x/precision;
uint aplus1x = aplus1 * x/precision;
uint top = a1 * x/precision + xsquare + xtriple; // a
x + x^2 + x^3
uint bottom = precision + aplus1x + aplus1xx/precision + xtriple; // 1 + (a+1)x + (a+1)x^2 + x^3
return pidiv2
top/bottom;
}

function test_arctan_uint() public pure returns (uint){
return arctan_uint(precision);
}
}

Tools Used

Recommended Mitigation Steps

Define which resolution is required and take the necessary formula from the higher_order_approximations.pdf document.
Change the math library to a simple "precision" based implementation (as shown above). This will also require adapting other code.
Also set the "precision" constant to the required precision and adjust the constants to the required number of decimals.

Whitelist signatures can be replayed on contract redeployments on same/different chains

Handle

0xRajeev

Vulnerability details

Impact

A signature-based whitelist is used to allow authorized users to contribute during the hatch phase but the signatures include only the user address without including the contract address i.e. address(this) or the chainID. This is a common best-practice.

The impact is that if this contract is ever redeployed on the same mainnet at a different address for some reason or deployed on a Layer 2 (Optimism, Arbitrum, Polygon etc.) or any other EVM-compatible chain, the previously whitelisted signatures (which could be different from the newly whitelisted users) could still contribute to the hatch phase.

Proof of Concept

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L76

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/SignatureWhitelist.sol#L23-L35

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add address(this) and chainID to the signatures.

ChainLink price data could be stale

Handle

pauliax

Vulnerability details

Impact

function getEtherPrice() invokes ETH_CHAINLINK.latestRoundData(). However, there are no checks if the return value indicates stale data. This could lead to stale prices according to the Chainlink documentation:
“if answeredInRound < roundId could indicate stale data.”
“A timestamp with zero value means the round is not complete and should not be used.”

This issue was originally described by the leading hacker @cmichelio (kudos to him) in Maple finance contest:
code-423n4/2021-04-maple-findings#82

Recommended Mitigation Steps

Add missing checks for stale data. See example here: https://github.com/cryptexfinance/contracts/blob/master/contracts/oracles/ChainlinkOracle.sol#L58-L65

Missing use of DSMath functions may lead to underflows/overflows

Handle

0xRajeev

Vulnerability details

Impact

FairSide contracts use DappHub’s DSMath safe arithmetic library that provides overflow/underflow protection but the safe DSMath functions are not used in many places, especially in the FSD mint/burn functions.

While there do not appear to be any obvious integer overflows/underflows in the conditions envisioned, there could be exceptional paths where overflows/underflows may be triggered leading to minting/burning an unexpected number of tokens.

Proof of Concept

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L85

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L95

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L111

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L117

Tools Used

Manual Analysis

Recommended Mitigation Steps

Use DSMath add/sub functions instead of +/- in all places.

Conviction scoring fails to initialize and bootstrap

Handle

0xRajeev

Vulnerability details

Impact

Conviction scores for new addresses/users fail to initialize+bootstrap in ERC20ConvictionScore’s _updateConvictionScore() because a new user’s numCheckpoints will be zero and never gets initialized.

This effectively means that FairSide conviction scoring fails to bootstrap at all, leading to failure of the protocol’s pivotal feature.

Proof of Concept

When Alice transfers FSD tokens to Bob for the first time, _beforeTokenTransfer(Alice, Bob, 100) is triggered which calls _updateConvictionScore(Bob, 100) on Line55 of ERC20ConvictionScore.sol.

In function _updateConvictionScore(), given that this is the first time Bob is receiving FSD tokens, numCheckpoints[Bob] will be 0 (Line116) which will make ts = 0 (Line120), and Bob’s FSD balance will also be zero (Bob never has got FSD tokens prior to this) which makes convictionDelta = 0 (Line122) and not let control go past Line129.

This means that a new checkpoint never gets written, i.e. conviction score never gets initialized, for Bob or for any user for that matter.

Tools Used

Manual Analysis

Recommended Mitigation Steps

FairSide’s adjustment of Compound’s conviction scoring is based on time and so needs an initialization to take place vs. Compound’s implementation. A new checkpoint therefore needs to be created+initialized for a new user during token transfer.

Call to swapExactTokensForETH in liquidateDai() will always fail


Handle

0xRajeev

Vulnerability details

Impact

liquidateDai() calls Uniswap’s swapExactTokensForETH to swap Dai to ETH. This will work if msg.sender, i.e. FSD contract, has already given the router an allowance of at least amount on the input token Dai.

Given that there is no prior approval, the call to UniswapV2 router for swapping will fail because msg.sender has not approved UniswapV2 with an allowance for the tokens being attempted to swap.

The impact is that updateCostShareRequest() will fail and revert while working with stablecoin Dai.

Proof of Concept

https://uniswap.org/docs/v2/smart-contracts/router02/#swapexacttokensfortokens

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L191

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L182-L198

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L323

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L307-L329

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L280

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L297

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add FSD approval to UniswapV2 with an allowance for the tokens being attempted to swap.

`validateVoteHash` does not confirm the vote result

Handle

cmichel

Vulnerability details

Vulnerability Details

The validateVoteHash function only checks if the individual voting power (conviction score) is indeed correct, but it does not verify if the outcome of the vote is correct, i.e., it's possible for a guardian to submit completely different forVotes/againstVotes in __castOffchainVotes changing the proposal outcome.

Impact

The guardian needs to be trusted to submit the correct forVotes and againstVotes such that they match the votes in the voteHash.
The issue is that this cannot be easily verified.

Legitimate users can be tricked into thinking the result is correct by checking if their vote & support is contained in votes and recomputing the voteHash themselves. They then call validateVoteHash which "confirms" the guardian result.
However, in reality, the guardian could have submitted arbitrary forVotes/againstVotes values.

This makes the current validation system kind of useless.

Recommended Mitigation Steps

Sum up the for/against votes in the votes array of validateVoteHash and check if it matches the proposal.forVotes/againstVotes.

Lack of zero-address checks for immutable addresses will force contract redeployment if zero-address used accidentally

Handle

0xRajeev

Vulnerability details

Impact

Zero-address checks as input validation on address parameters is always a best practice. This is especially true for critical addresses that are immutable and set in the constructor because they cannot be changed later. Accidentally using zero addresses here will lead to failing logic or force contract redeployment and increased gas costs.

Proof of Concept

]https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dao/FairSideDAO.sol#L213-L215

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L68-L69

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/conviction/FairSideConviction.sol#L27

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L98-L101

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add zero-address input validation for these addresses in the constructor.

TESTING SUBMISSION

Handle

0xRajeev

Vulnerability details

Impact

Detailed description of the impact of this finding.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

Conviction totals not updated during tokenization

Handle

0xRajeev

Vulnerability details

Impact

_updateConvictionScore() function returns convictionDelta and governanceDelta which need to be used immediately in a call to _updateConvictionTotals(convictionDelta, governanceDelta) for updating the conviction totals of conviction and governance-enabled conviction for the entire FairSide network.

This updation of totals after a call to _updateConvictionScore() is done on Line70 in _beforeTokenTransfer() and Line367 in updateConvictionScore() of ERC20ConvictionScore.sol.

However, the return values of _updateConvictionScore() are ignored on Line284 in tokenizeConviction() and not used to update the totals using _updateConvictionTotals(convictionDelta, governanceDelta).

The impact is that when a user tokenizes their conviction score, their conviction deltas are updated and recorded (only if the funds locked are zero which is incorrect and reported separately in a different finding) but the totals are not updated. This leads to incorrect accounting of TOTAL_CONVICTION_SCORE and TOTAL_GOVERNANCE_SCORE which are used in the calculation of tributes and therefore will lead to incorrect tribute calculations.

Proof of Concept

Alice calls tokenizeConviction() to convert her conviction score into an NFT. Her conviction deltas as returned by _updateConvictionScore() are ignored and TOTAL_CONVICTION_SCORE and TOTAL_GOVERNANCE_SCORE values are not updated. As a result, the tributes rewarded are proportionally more than what should have been the case because the conviction score totals are used as the denominator in availableTribute() and availableGovernanceTribute().

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L284

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L108-L110

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L52-L70

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L365-L367

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L73-L106

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L83-L100

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L102-L123

Tools Used

Manual Analysis

Recommended Mitigation Steps

Use the return values of _updateConvictionScore() function (i.e. convictionDelta and governanceDelta) on Line284 of ERC20ConvictionScore.sol and use them in a call to _updateConvictionTotals(convictionDelta, governanceDelta).

Constant defined multiple times

Handle

gpersoon

Vulnerability details

Impact

A constant with the value address(type(uint160).max) is defined 2x in different contracts.
They seem to be used for the same purpose.
The risk of two separate definitions is that a (future) programmer changes one instance and forget to change the other instance.

Proof of Concept

// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dao/FairSideDAO.sol#L25
address private constant GOVERNANCE_CONVICTION_SCORE = address(type(uint160).max);

https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/TributeAccrual.sol#L25
address internal constant TOTAL_GOVERNANCE_SCORE =address(type(uint160).max);

Tools Used

Editor

Recommended Mitigation Steps

Define the constant once and used it in multiple contracts via imports.
Or alternatively make the constant names the same and add comments to both locations indicating they are the same constant.

Proposals might never reach quorum because of the voting threshold

Handle

cmichel

Vulnerability details

Vulnerability Details

Only voters that pass a voting threshold can vote in FairSideDAO._castVote.
The current governance threshold is used as the voting threshold, whereas the quorum votes depend on the past snapshot at proposal creation (see state).

Impact

If the FSD is more equally distributed after proposal creation among many users who then all fall below the voting threshold, the proposal might never be able to reach a quorum.

Recommended Mitigation Steps

It's unclear why a voting threshold is a good idea. Set the voting threshold to a very low value or even remove it.

`ERC20ConvictionScore`'s `governanceDelta` should be subtracted when user is not a governor anymore

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.

Recommended Mitigation Steps

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

Repetitive storage access

Handle

pauliax

Vulnerability details

Impact

function _addTribute can reuse lastTribute to reduce the numbers of storage access: tributes[totalTributes - 1].amount = add224(...) can be replaced with lastTribute.amount = add224(...) as it is already a storage pointer that can be assigned a value with no need to recalculate the index and access the array again. Same situation with function _addGovernanceTribute governanceTributes.

Recommended Mitigation Steps

lastTribute.amount = add224(...)

Incorrect use of _addTribute instead of _addGovernanceTribute

Handle

0xRajeev

Vulnerability details

Impact

The addRegistrationTributeGovernance() function is called by the FSD network to update tribute when 7.5% is contributed towards governance as part of purchaseMembership(). However, this function incorrectly calls _addTribute() (as done in addRegistrationTribute) instead of _addGovernanceTribute().

The impact is that governanceTributes never gets updated and the entire tribute accounting logic is rendered incorrect.

Proof of Concept

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L140

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L130

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L195

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L30-L48

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L50-L70

Tools Used

Manual Analysis

Recommended Mitigation Steps

Use _addGovernanceTribute() instead of _addTribute on L140 of FSD.sol

`_calculateDeltaOfFSD` fails when called with negative `_reserveDelta`

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.

SafeMath not used consistently in FSDNetwork.sol

Handle

gpersoon

Vulnerability details

to## Impact
The function _processCostShareRequest of FSDNetwork.sol updates totalOpenRequests without Safemath.
This could lead to an underflow. This could also lead to problems with getFSDPrice
In comparison the function openCostShareRequest does use Safemath to change totalOpenRequests.

Proof of Concept

FSDNetwork.sol:
// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L315
function _processCostShareRequest(
totalOpenRequests -= amount;

// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L361
function getFSDPrice() public view returns (uint256) {
...
uint256 capitalPool = fsd.getReserveBalance() - totalOpenRequests;

// https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L250
function openCostShareRequest(uint256 ethAmount, bool inStable) external {
...
totalOpenRequests = totalOpenRequests.add(requestPayout);

Tools Used

Editor

Recommended Mitigation Steps

change
totalOpenRequests -= amount;
to
totalOpenRequests = totalOpenRequests.sub(amount);

Also check for other locations where safemath should be applied.

pendingWithdrawals just increments

Handle

a_delamo

Vulnerability details

Impact

In Withdrawable.sol, every time a user wants to withdraw, the following code will get executed:

    function _increaseWithdrawal(address user, uint256 amount) internal {
        availableWithdrawal[user] = availableWithdrawal[user].add(amount);
        pendingWithdrawals = pendingWithdrawals.add(amount);
    }

Then the user will call need to call the withdraw method, to retrieve the amount he wanted to withdraw.

function withdraw() external {
        uint256 reserveAmount = availableWithdrawal[msg.sender];
        require(reserveAmount > 0, "FSD::withdraw: Insufficient Withdrawal");
        delete availableWithdrawal[msg.sender];
        msg.sender.transfer(reserveAmount);
    }

The issue is that while _increaseWithdrawal, increase the pendingWithdrawals state variable, withdraw does not reduce this property.
Meaning pendingWithdrawals will only increase, affecting the getReserveBalance method

function getReserveBalance() public view returns (uint256) {
        return address(this).balance.sub(pendingWithdrawals);
    }

This method is really important because is being used to calculate the number of tokens to burn/mint and to calculate the price of the tokens using the ABC.sol

 function getFSDPrice() public view returns (uint256) {
        // FSHARE = Total Available Cost Share Benefits / Gearing Factor
        uint256 fShare = totalCostShareBenefits / GEARING_FACTOR;
        // Floor of 7500 ETH
        if (fShare < 7500 ether) fShare = 7500 ether;

        // Capital Pool = Total Funds held in ETH – Open Cost Share Requests
        // Open Cost Share Request = Cost share request awaiting assessor consensus
        uint256 capitalPool = fsd.getReserveBalance() - totalOpenRequests;

        return FairSideFormula.f(capitalPool, fShare);
    }

Lack of input validation to enforce some minimum threshold on governanceThreshold

Handle

0xRajeev

Vulnerability details

Impact

The governanceThreshold is initialised to 30 * 1000e18 in ERC20ConvictionScore.sol. However, the updateGovernanceThreshold() function in FSD.sol lacks any input validation to enforce some minimum threshold.

This allows setting it to 0 or below any value considered as low for the threshold. The impact will be the all/many users will become eligible for governance which may affect critical protocol functioning.

Proof of Concept

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L21-L22

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L208-L214

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add a zero or minimum threshold check in updateGovernanceThreshold().

Locked funds from tokenization are credited twice to user leading to protocol fund loss

Handle

0xRajeev

Vulnerability details

Impact

The tokens optionally locked during tokenization are released twice on acquiring conviction back from a NFT. (The incorrect double debit of locked funds during tokenization has been filed as a separate finding because it is not necessarily related and also occurs in a different part of the code.)

When a user wants to acquire back the conviction score captured by a NFT, the FSD tokens locked, if any, are released to the user as well. However, this is incorrectly done twice. Released amount is transferred once on Line123 in _release() (via acquireConviction -> burn) of FairSideConviction.sol and again immediately after the burn on Line316 in acquireConviction() of ERC20ConvictionScore.sol.

This leads to loss of protocol funds.

Proof of Concept

Alice tokenizes her conviction score into a NFT and locks 100 FSDs. Bob buys the NFT from Alice and acquires the conviction score back from the NFT. But instead of 100 FSDs that were supposed to be locked with the NFT, Bob receives 100+100 = 200 FSDs from FairSide protocol.

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/conviction/FairSideConviction.sol#L123

https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L314-L316

Tools Used

Manual Analysis

Recommended Mitigation Steps

Remove the redundant transfer of FSD tokens from protocol to user on Line316 in acquireConviction() of ERC20ConvictionScore.sol.

Misleading error messages

Handle

pauliax

Vulnerability details

Impact

  • There are misleading copy-pasted error messages. For example, function liquidateEth has a misleading revert message:
    "FSD::payClaim: Insufficient Privileges"
    Same situation with functions liquidateDai, setConvictionless, _addGovernanceTribute. function _calculateDeltaOfFSD has it misspelled. contract Timelock constructor uses 'setDelay'.

Recommended Mitigation Steps

Should be 'payClaim' -> 'liquidateEth', etc to identify the real name of the function where the error happened.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.