FairSide contest - WatchPug'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: 3/17

Findings: 5

Award: $5,600.54

🌟 Selected for report: 7

🚀 Solo Findings: 1

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

WatchPug

Vulnerability details

In the current implementation, anyone can call function updateVestedTokens() to add an arbitrary amount to the beneficiary's vesting amount without sending any of it.

This allows the attacker to make the amount type(uint256).max and call to claimVestedTokens() will always fail.

Essentially freeze all the funds in the FSDVesting.sol contract.

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/token/FSDVesting.sol#L147-L161

function updateVestedTokens(address _user, uint256 _amount)
    external
    override
{
    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);
}

Recommendation

Change to:

function updateVestedTokens(uint256 _amount)
    external
    override
{
    require(
        msg.sender == address(fsd),
        "FSDVesting::updateVestedTokens: Only FSD can update the vested amount"
    );
    require(
        amount != 0,
        "FSDVesting::updateVestedTokens: Amount cannot be zero"
    );

    amount = amount.add(_amount);
}

#0 - YunChe404

2021-11-14T11:21:19Z

#32

#1 - pauliax

2021-11-17T12:07:53Z

A duplicate of #101

Findings Information

🌟 Selected for report: WatchPug

Also found by: cmichel

Labels

bug
3 (High Risk)
disagree with severity
sponsor confirmed

Awards

0.5895 ETH - $2,789.75

External Links

Handle

WatchPug

Vulnerability details

Based on the context, once the beneficiary claimed all their vesting tokens, they should get the fairSideConviction NFT.

However, in the current implementation, if the beneficiary has claimed any amounts before it's fully vested, then they will never be able to get the fairSideConviction NFT, because at L138, it requires the tokenbClaim to be equal to the initial vesting amount.

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/token/FSDVesting.sol#L124-L142

function claimVestedTokens() external override onlyBeneficiary {
    uint256 tokenClaim = calculateVestingClaim();
    require(
        tokenClaim > 0,
        "FSDVesting::claimVestedTokens: Zero claimable tokens"
    );

    totalClaimed = totalClaimed.add(tokenClaim);
    lastClaimAt = block.timestamp;

    fsd.safeTransfer(msg.sender, tokenClaim);

    emit TokensClaimed(msg.sender, tokenClaim, block.timestamp);

    if (amount == tokenClaim) {
        uint256 tokenId = fsd.tokenizeConviction(0);
        fairSideConviction.transferFrom(address(this), msg.sender, tokenId);
    }
}

Recommendation

Change to:

function claimVestedTokens() external override onlyBeneficiary {
    uint256 tokenClaim = calculateVestingClaim();
    require(
        tokenClaim > 0,
        "FSDVesting::claimVestedTokens: Zero claimable tokens"
    );

    totalClaimed = totalClaimed.add(tokenClaim);
    lastClaimAt = block.timestamp;

    fsd.safeTransfer(msg.sender, tokenClaim);

    emit TokensClaimed(msg.sender, tokenClaim, block.timestamp);

    if (amount == totalClaimed) {
        uint256 tokenId = fsd.tokenizeConviction(0);
        fairSideConviction.transferFrom(address(this), msg.sender, tokenId);
    }
}

#0 - YunChe404

2021-11-14T14:14:17Z

The exhibit does not lead to lose of funds, so severity should be lowered to level 2.

#1 - pauliax

2021-11-19T12:11:25Z

Great find.

Disagree with the sponsor. NFTs are also assets, so a lost opportunity to claim them should be counted as a high severity issue: "High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals)."

#2 - YunChe404

2021-11-19T18:05:40Z

Based on the guideline, no assets are stolen/lost/compromised as the NFT is never minted in the first place.

#3 - pauliax

2021-11-19T18:50:41Z

If you can't mint the NFT that you are entitled to because of an issue in smart contract logic, I think it can be considered an asset that is lost forever.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
disagree with severity
sponsor confirmed

Awards

0.393 ETH - $1,859.83

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L274-L291

if (user.creation == 0) {
    user.creation = block.timestamp;
    user.gracePeriod =
        membership[msg.sender].creation +
        MEMBERSHIP_DURATION +
        60 days;
} else {
    uint256 elapsedDurationPercentage = ((block.timestamp -
        user.creation) * 1 ether) / MEMBERSHIP_DURATION;
    if (elapsedDurationPercentage < 1 ether) {
        uint256 durationIncrease = (costShareBenefit.mul(1 ether) /
            (totalCostShareBenefit - costShareBenefit)).mul(
                MEMBERSHIP_DURATION
            ) / 1 ether;
        user.creation += durationIncrease;
        user.gracePeriod += durationIncrease;
    }
}

PoC

  1. Alice calls function purchaseMembership() and adds 20 ether of costShareBenefit on day 1:
alice.creation = day 1 timestamp; alice.gracePeriod = day 791 timestamp;
  1. Alice calls function purchaseMembership() again and adds 20 ether of costShareBenefit on day 2:
elapsedDurationPercentage = 1/720 durationIncrease = 730 day alice.creation = day 731 timestamp; alice.gracePeriod = day 1521 timestamp;

Making Alice unable to use any membership features until two years later.

#0 - YunChe404

2021-11-14T14:13:54Z

The exhibit does not lead to lose of funds, so severity should be lowered to level 2.

#1 - pauliax

2021-11-19T12:22:40Z

Great find. Agree with the sponsor, this should be counted as a Medium severity issue: "Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements."

Also, the warden didn't mention any possible mitigation steps.

Findings Information

🌟 Selected for report: ye0lde

Also found by: WatchPug, elprofesor, pants

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.0028 ETH - $13.41

External Links

Handle

WatchPug

Vulnerability details

Every reason string takes at least 32 bytes.

Use short reason strings that fits in 32 bytes or it will become more expensive.

Instances include:

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/token/FSD.sol#L299-L307

require(
    governanceThreshold <=
        getPriorConvictionScore(
            msg.sender,
            governanceTributes[num].blockNumber
        ) &&
        isGovernance[msg.sender],
    "FSD::claimGovernanceTribute: Not a governance member"
);

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/token/FSD.sol#L314-L317

require(
    currentPhase != Phase.Final,
    "FSD::phaseAdvance: FSD is already at its final phase"
);

#0 - YunChe404

2021-11-14T11:24:23Z

#15

#1 - pauliax

2021-11-16T21:50:17Z

A duplicate of #43

Findings Information

🌟 Selected for report: WatchPug

Also found by: gzeon

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.007 ETH - $33.11

External Links

Handle

WatchPug

Vulnerability details

The check if _wallets.length <= 2 is redundant as the length of _wallets parameter must be 2.

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L520-L533

function setMembershipWallets(address[2] calldata _wallets) external {
    //todo internal
    require(
        membership[msg.sender].wallets[0] == address(0) &&
            membership[msg.sender].wallets[1] == address(0),
        "FSDNetwork::setMembershipWallets: Cannot have more than three wallets per membership"
    );
    require(
        _wallets.length <= 2,
        "FSDNetwork::setMembershipWallets: Too many wallets"
    );
    membership[msg.sender].wallets = _wallets;
}

Recommendation

Remove the redundant check.

#0 - pauliax

2021-11-17T10:42:47Z

Valid optimization.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.0155 ETH - $73.58

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L482-L495

function setAssessors(address[] calldata _assessors) external {
        require(
            msg.sender == GOVERNANCE_ADDRESS,
            "FSDNetwork::setAssessors: Insufficient Privileges"
        );

        uint256 assessorsLength = _assessors.length;
        require(
            assessorsLength == 3,
            "FSDNetwork::setAssessors: Number of assessors must be three"
        );

        assessors = _assessors;
    }

Recommendation

Change to:

function setAssessors(address[3] calldata _assessors) external {
        require(
            msg.sender == GOVERNANCE_ADDRESS,
            "FSDNetwork::setAssessors: Insufficient Privileges"
        );

        assessors = _assessors;
    }

#0 - pauliax

2021-11-17T10:45:20Z

Great find, valid optimization.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.0155 ETH - $73.58

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L310-L315

// 20% as staking rewards
fsd.safeTransfer(address(fsd), stakingRewards);
fsd.addRegistrationTribute(stakingRewards);

// 7.5% towards governance
fsd.safeTransfer(address(fsd), governancePoolRewards);

The 2 transfers to the same address can be done in one external call to save gas

Recommendation

Change to:

// 20% as staking rewards
// 7.5% towards governance
fsd.safeTransfer(address(fsd), governancePoolRewards + stakingRewards);
fsd.addRegistrationTribute(stakingRewards);

#0 - pauliax

2021-11-17T10:46:51Z

Great find, valid optimization.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.0155 ETH - $73.58

External Links

Handle

WatchPug

Vulnerability details

The checks in the for loop can be changed to else if to save gas and make sure msg.sender != sigAssessor.

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L616-L649

function _isApprovedByAssessors(
        bytes memory sig,
        uint256 id,
        Action action
    ) private view returns (bool) {
        bytes32 digest = _hashTypedDataV4(
            keccak256(abi.encode(CSR_ACTION, id, action))
        );

        address sigAssessor = ECDSA.recover(digest, sig);
        uint256 assessorsLength = assessors.length;
        bool assessorOne;
        bool assessorTwo;

        for (uint256 i = 0; i < assessorsLength; i++) {
            if (msg.sender == assessors[i]) {
                assessorOne = true;
            }
            if (sigAssessor == assessors[i]) {
                assessorTwo = true;
            }
        }

        require(
            assessorOne && assessorTwo,
            "FSDNetwork::_isApprovedByAssessors: Not an Assessor"
        );
        require(
            msg.sender != sigAssessor,
            "FSDNetwork::_isApprovedByAssessors: Cannot be the single Assessor"
        );

        return true;
    }

Recommendation

Change to:

function _isApprovedByAssessors(
        bytes memory sig,
        uint256 id,
        Action action
    ) private view returns (bool) {
        bytes32 digest = _hashTypedDataV4(
            keccak256(abi.encode(CSR_ACTION, id, action))
        );

        address sigAssessor = ECDSA.recover(digest, sig);
        uint256 assessorsLength = assessors.length;
        bool assessorOne;
        bool assessorTwo;

        for (uint256 i = 0; i < assessorsLength; i++) {
            if (msg.sender == assessors[i]) {
                assessorOne = true;
            } else if (sigAssessor == assessors[i]) {
                assessorTwo = true;
            }
        }

        require(
            assessorOne && assessorTwo,
            "FSDNetwork::_isApprovedByAssessors: Not an Assessor"
        );

        return true;
    }

#0 - pauliax

2021-11-17T10:53:02Z

Great find, valid optimization.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0.0155 ETH - $73.58

External Links

Handle

WatchPug

Vulnerability details

For the storage variables that will be accessed multiple times, especially in for loops, cache and read from the stack can save ~100 gas from each extra read (SLOAD after Berlin).

For example:

https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/dependencies/TributeAccrual.sol#L77-L88

    function totalAvailableTribute(uint256 offset)
        external
        view
        override
        returns (uint256 total)
    {
        for (uint256 i = offset; i < totalTributes; i++)
            total = total.add(availableTribute(i));

        for (uint256 i = offset; i < totalGovernanceTributes; i++)
            total = total.add(availableGovernanceTribute(i));
    }

totalTributes and totalGovernanceTributes can be cached.

#0 - pauliax

2021-11-17T11:43:56Z

Valid optimization.

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