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: 2/17
Findings: 4
Award: $5,960.99
π Selected for report: 3
π Solo Findings: 1
0.1289 ETH - $610.12
leastwood
The updateVestedTokens()
function is intended to be called by the FSD.sol
contract when updating a user's vested token amount. A check is performed to ensure that _user == beneficiary
, however, as _user
is a user controlled argument, it is possible to spoof calls to updateVestedTokens()
such that anyone can arbitrarily add any amount to the vested contract. Additionally, there is no check to ensure that the call originated from a trusted/whitelisted source.
There are two main reasons as to why the beneficiary or an attacker would want to call this function:
calculateVestingClaim()
allows them to withdraw their entire vested amount without waiting the entire duration.claimVestedTokens()
by the beneficiary account. This can be done by increasing the vested amount such that safeTransfer()
calls fail due to insufficient token balance within the contract.https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/token/FSDVesting.sol#L147-L161 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/token/FSDVesting.sol#L100-L115 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/token/FSDVesting.sol#L125 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/token/FSDVesting.sol#L134
Manual code review. Discussions with dev.
Ensure that the updateVestedTokens()
function is only callable from the FSD.sol
contract. This can be done by implementing an onlyFSD
role.
#0 - YunChe404
2021-11-14T11:32:08Z
#32
#1 - pauliax
2021-11-17T12:06:18Z
Great find, an unauthorized call is of paramount importance. Opening this issue as primary, as it contains a clear and concise explanation.
leastwood
FSDVesting.claimTribute()
attempts to claim staking rewards which are stored as tributes and generated through membership purchases by the FSD network. The FSDVesting.sol
contract accrues a conviction score which generates a percentage claim over tributes locked in FSD.sol
. However, this function does not directly call TributeAccrual.claimTribute()
and as a result, will instead transfer the tribute directly out the contract's vested holdings. This reduces the vested amount in FSDVesting.sol
, breaking the contract as the beneficiary account is unable to claim all vested tokens without their call failing.
Therefore, this issue is of high severity as any honest tribute claim will potentially result in locked vested tokens due to a bricked FSDVesting.sol
contract.
https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/token/FSDVesting.sol#L212-L226 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dependencies/TributeAccrual.sol#L155-L168
Manual code review.
Consider updating the FSDVesting.claimTribute()
function to directly call TributeAccrual.claimTribute()
and then transfer the tribute to the beneficiary account. This will ensure that the tribute amount is transferred to the FSDVesting.sol
contract before being transferred to the beneficiary.
This could look like the following:
function claimTribute(uint256 num) external onlyBeneficiary { uint256 tribute = fsd.availableTribute(num); fsd.claimTribute(num); fsd.safeTransfer(msg.sender, tribute); emit TributeClaimed(msg.sender, tribute); }
#0 - YunChe404
2021-11-14T11:32:27Z
#28
#1 - pauliax
2021-11-17T16:42:10Z
A duplicate of #28
π Selected for report: leastwood
0.393 ETH - $1,859.83
leastwood
Conviction scores are calculating by taking the user's balance and multiplying it by the time elapsed. This score is updated upon each token transfer, or alternatively by directly calling ERC20ConvictionScore.updateConvictionScore()
. The availableTribute()
and availableGovernanceTribute()
functions calculate a user's share by calculating their percentage of the total conviction score to find the amount owed to them.
This is shown in the following statement where userCS
is the user's conviction score and totalCS
is the protocol's total conviction score:
return amount.mul(userCS).div(totalCS);
The tribute amount that can be successfully claimed disproportionally favours users who have updated their conviction score more recently and who are early to claim their allocated tribute.
Consider the following POC:
updateConvictionScore()
which sets his conviction score to 5 * time = 10
where time = 2.claimTribute()
where the tribute to claim is of size 15 tokens. As a result, Bob receives 10 tokens according to the calculation mentioned above ((15*10)/15
).claimTribute()
, receiving 7.5 tokens in the process.As you can see from the above example, the amount of tokens transferred favours user's who claim early and update their conviction score. It is entirely possible that due to the finite amount of tokens stored in the contract, a number of user's won't be able to successfully claim their allocated tribute.
https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dependencies/TributeAccrual.sol#L93-L112 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dependencies/TributeAccrual.sol#L117-L136 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dependencies/TributeAccrual.sol#L156 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dependencies/TributeAccrual.sol#L179 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dependencies/ERC20ConvictionScore.sol#L476-L479
Manual code review.
There is no easy solution to the mentioned issue. Ideally, all users should have an up to date conviction score whenever claimTribute()
or claimGovernanceTribute()
are called, although this would be an incredibly gas intensive mitigation. Alternatively, the protocol's total conviction score can be calculated by tracking the total minted balance of FSD.sol
's token and multiplying this by a running total of time elapsed. This will need to be adjusted whenever tokens are minted or burned and updated during tribute claims.
#0 - YunChe404
2021-11-14T14:28:26Z
The exhibit does not lead to lose of funds, so severity should be lowered to level 2.
#1 - YunChe404
2021-11-14T17:26:17Z
As with any system with a finite supply, the FairSide project is not an exception (and does not intend to be).
#2 - pauliax
2021-11-19T12:53:08Z
Great observation, but I am afraid this issue falls under the nature of blockchains as mentioned by the warden it will practically be impossible to update all the users at once. Because a hypothetical attack path exists that leads to favoring an exploiter over other users, I am assigning this issue a severity of Medium.
π Selected for report: leastwood
0.131 ETH - $619.94
leastwood
The FSD
token price is calculated using spot price directly on the curve. These price results are useful when calculating the membership fee which is distributed to protocol users and in calculating the bounty amount upon opening Cross Share Requests (CSRs). Spot prices can be easily manipulated by well-funded individuals or via flash loan attacks to generate increased membership and claim fees for the recipient users.
This issue is of medium severity as its possible for user's to extract increased fees from membership purchases.
https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/network/FSDNetwork.sol#L178-L189 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/network/FSDNetwork.sol#L268 https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/network/FSDNetwork.sol#L365
https://shouldiusespotpriceasmyoracle.com/ Manual code review
Consider avoid using spot price and adhere to the guide mentioned above in order to avoid price manipulation via flash loans. It could be useful to implement a TWAP oracle which avoids using the current block in its calculations. This should limit flash loan attacks and make it increasingly costly for well-funded individuals to manipulate the token price (assuming token liquidity is substantial). A useful TWAP flash-loan resistant implementation can be found here.
#0 - YunChe404
2021-11-14T17:27:39Z
There is no viable vulnerability by the exhibit described due to the flashloan fees/curve exit tributes.
#1 - pauliax
2021-11-19T13:02:50Z
Based on official recommendations, I think this issue falls under the non-critical category: "Unless there is something uniquely novel created by combining vectors, most submissions regarding vulnerabilities that are inherent to a particular system or the Ethereum network as a whole should be considered non-critical. Examples of such vulnerabilities include front running, sandwich attacks, and MEV. One important caveat to all of the above: unless otherwise specified by the contest sponsor or intended to be handled by the code. For example, flash loans are generally unavoidable, but since MarginSwap had a safeguard against them, we considered these findings relevant in their contest."
However, because lately we have seen some major hacks related to price manipulation (e.g. Rari Fuse VUSD pool), where even TWAP was exploited, I would like to leave this issue with a low severity score, as a reminder that spot prices and oracles are a subject of manipulation and need special attention.
0.0172 ETH - $81.35
leastwood
The FairSideDAO.state()
function validates the current state of a target proposal and is used to restrict certain proposal actions. The function defines an active proposal as block.number > proposal.startBlock
& block.number <= proposal.endBlock
when the first statement should rather be block.number >= proposal.startBlock
to better reflect the intended functionality.
https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dao/FairSideDAO.sol#L312-L315
Manual code review.
Consider updating the condition for ProposalState.Pending
in FairSideDAO.state()
to block.number < proposal.startBlock
such that the aforementioned conditions are accurately reflected when determining whether a proposal is active.
#0 - YunChe404
2021-11-14T14:30:50Z
The exhibit does not pose an actual security vulnerability, hence the severity should be lowered to level zero.
#1 - pauliax
2021-11-17T15:03:31Z
A duplicate of #41.