Platform: Code4rena
Start Date: 09/11/2021
Pot Size: $30,000 ETH
Total HM: 6
Participants: 17
Period: 3 days
Judge: pauliax
Total Solo HM: 3
Id: 50
League: ETH
Rank: 3/17
Findings: 5
Award: $5,600.54
🌟 Selected for report: 7
🚀 Solo Findings: 1
WatchPug
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.
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); }
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
0.5895 ETH - $2,789.75
WatchPug
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.
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); } }
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.
🌟 Selected for report: WatchPug
0.393 ETH - $1,859.83
WatchPug
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; } }
function purchaseMembership()
and adds 20 ether of costShareBenefit
on day 1:alice.creation = day 1 timestamp; alice.gracePeriod = day 791 timestamp;
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.
🌟 Selected for report: ye0lde
Also found by: WatchPug, elprofesor, pants
WatchPug
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:
require( governanceThreshold <= getPriorConvictionScore( msg.sender, governanceTributes[num].blockNumber ) && isGovernance[msg.sender], "FSD::claimGovernanceTribute: Not a governance member" );
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
0.007 ETH - $33.11
WatchPug
The check if _wallets.length <= 2
is redundant as the length of _wallets
parameter must be 2.
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; }
Remove the redundant check.
#0 - pauliax
2021-11-17T10:42:47Z
Valid optimization.
🌟 Selected for report: WatchPug
0.0155 ETH - $73.58
WatchPug
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; }
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.
🌟 Selected for report: WatchPug
0.0155 ETH - $73.58
WatchPug
// 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
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.
🌟 Selected for report: WatchPug
0.0155 ETH - $73.58
WatchPug
The checks in the for loop can be changed to else if
to save gas and make sure msg.sender != sigAssessor
.
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; }
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.
🌟 Selected for report: WatchPug
0.0155 ETH - $73.58
WatchPug
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:
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.