Platform: Code4rena
Start Date: 20/05/2021
Pot Size: $55,000 USDC
Total HM: 19
Participants: 8
Period: 7 days
Judge: cemozer
Total Solo HM: 11
Id: 11
League: ETH
Rank: 4/8
Findings: 4
Award: $3,011.94
🌟 Selected for report: 4
🚀 Solo Findings: 1
a_delamo
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); }
#0 - fairside-core
2021-05-30T13:07:06Z
Duplicate of #72, should be increased in severity.
🌟 Selected for report: a_delamo
1441.1236 USDC - $1,441.12
a_delamo
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
Echidna https://github.com/crytic/echidna
Use some fuzzing tool like Echidna to verify there is no edge cases
#0 - fairside-core
2021-05-30T15:09:25Z
I am slightly mixed about this finding. We did employ fuzz tests during the audit we had gone through and they were unable to pinpoint any issues in the value range we expect the curve to be utilized in. This is definitely a good suggestion and one we will assimilate, however, I am not sure how one would judge the severity of this.
🌟 Selected for report: a_delamo
a_delamo
In ERC20ConvictionScore.sol
, we store
// Conviction score based on # of days multiplied by # of FSD & NFT // @notice A record of conviction score checkpoints for each account, by index mapping(address => mapping(uint32 => Checkpoint)) public checkpoints; // @notice The number of checkpoints for each account mapping(address => uint32) public numCheckpoints; `` These two state variables are used in the following way:
function _updateConvictionScore(address user, int256 amount) internal returns (uint224 convictionDelta, uint224 governanceDelta) { if (user == address(0)) return (0, 0);
uint256 balance = balanceOf(user); uint32 userNum = numCheckpoints[user]; uint256 ts = userNum > 0 ? block.timestamp - checkpoints[user][userNum - 1].ts : 0; convictionDelta = safe224( balance.mul(ts) / 1 days, "ERC20ConvictionScore::_updateConvictionScore: Conviction score has reached maximum limit" ); bool hasMinimumGovernanceBalance = (int256(balance) + amount) >= governanceMinimumBalance; if (convictionDelta != 0) { uint224 userOld = userNum > 0 ? checkpoints[user][userNum - 1].convictionScore : 0; uint224 userNew = add224( userOld, convictionDelta, "ERC20ConvictionScore::_updateConvictionScore: conviction score amount overflows" ); _writeCheckpoint(user, userNum, userNew); if (address(fairSideConviction) == user) return (convictionDelta, 0); if ( !isGovernance[user] && userNew >= governanceThreshold && hasMinimumGovernanceBalance ) { isGovernance[user] = true; governanceDelta = userNew; } else if (isGovernance[user]) { if (hasMinimumGovernanceBalance) governanceDelta = convictionDelta; else { isGovernance[user] = false; governanceDelta = getPriorConvictionScore( user, block.number - 1 ); } } } }
Checking the contract seems like using `mapping(address => Checkpoint[]) public checkpoints;` would provide the same functionality while using less storage.
#0 - fairside-core
2021-05-30T14:16:09Z
I am unsure as to what this relates to, can we have some further information from a_delamo?
🌟 Selected for report: a_delamo
0 USDC - $0.00
a_delamo
The following methods are public and could be external. External is more gas optimize that public, so it should be used as much as possible
renounceOwnership() should be declared external: - FSOwnable.renounceOwnership() (dependencies/FSOwnable.sol#55-57) transferOwnership(address) should be declared external: - FSOwnable.transferOwnership(address) (dependencies/FSOwnable.sol#68-75) quorumVotes() should be declared external: - FairSideDAO.quorumVotes() (dao/FairSideDAO.sol#46-48) queue(uint256) should be declared external: - FairSideDAO.queue(uint256) (dao/FairSideDAO.sol#304-322) execute(uint256) should be declared external: - FairSideDAO.execute(uint256) (dao/FairSideDAO.sol#340-357) cancel(uint256) should be declared external: - FairSideDAO.cancel(uint256) (dao/FairSideDAO.sol#359-389) getActions(uint256) should be declared external: - FairSideDAO.getActions(uint256) (dao/FairSideDAO.sol#391-403) getReceipt(uint256,address) should be declared external: - FairSideDAO.getReceipt(uint256,address) (dao/FairSideDAO.sol#405-411) castVote(uint256,bool) should be declared external: - FairSideDAO.castVote(uint256,bool) (dao/FairSideDAO.sol#443-445) castVoteBySig(uint256,bool,uint8,bytes32,bytes32) should be declared external: - FairSideDAO.castVoteBySig(uint256,bool,uint8,bytes32,bytes32) (dao/FairSideDAO.sol#447-475) disableOffchainVoting() should be declared external: - FairSideDAO.disableOffchainVoting() (dao/FairSideDAO.sol#515-522) __castOffchainVotes(uint256,bytes32,uint256,uint256) should be declared external: - FairSideDAO.__castOffchainVotes(uint256,bytes32,uint256,uint256) (dao/FairSideDAO.sol#547-572) __acceptAdmin() should be declared external: - FairSideDAO.__acceptAdmin() (dao/FairSideDAO.sol#574-580) __abdicate() should be declared external: - FairSideDAO.__abdicate() (dao/FairSideDAO.sol#582-588) __queueSetTimelockPendingAdmin(address,uint256) should be declared external: - FairSideDAO.__queueSetTimelockPendingAdmin(address,uint256) (dao/FairSideDAO.sol#590-605) __executeSetTimelockPendingAdmin(address,uint256) should be declared external: - FairSideDAO.__executeSetTimelockPendingAdmin(address,uint256) (dao/FairSideDAO.sol#607-622) setDelay(uint256) should be declared external: - Timelock.setDelay(uint256) (timelock/Timelock.sol#64-80) acceptAdmin() should be declared external: - Timelock.acceptAdmin() (timelock/Timelock.sol#82-91) setPendingAdmin(address) should be declared external: - Timelock.setPendingAdmin(address) (timelock/Timelock.sol#93-101) queueTransaction(address,uint256,string,bytes,uint256) should be declared external: - Timelock.queueTransaction(address,uint256,string,bytes,uint256) (timelock/Timelock.sol#103-125) cancelTransaction(address,uint256,string,bytes,uint256) should be declared external: - Timelock.cancelTransaction(address,uint256,string,bytes,uint256) (timelock/Timelock.sol#127-144) executeTransaction(address,uint256,string,bytes,uint256) should be declared external: - Timelock.executeTransaction(address,uint256,string,bytes,uint256) (timelock/Timelock.sol#146-197) g(uint256,uint256) should be declared external: - FairSideFormula.g(uint256,uint256) (dependencies/FairSideFormula.sol#152-156) f(uint256,uint256) should be declared external: - FairSideFormula.f(uint256,uint256) (dependencies/FairSideFormula.sol#164-168)
More info: https://ethereum.stackexchange.com/a/19391
Slither
#0 - fairside-core
2021-05-30T14:14:11Z
As the Stack Overflow post indicates, the optimization is only really applicable when arrays are involved. We will retain the functions as is and adjust them as necessary further down in the development cycle.
🌟 Selected for report: a_delamo
0 USDC - $0.00
a_delamo
The method purchaseMembership
in FSDNetwork
contract contains the code below.
Inside this method, we are constantly reading from the mapping membership
, so why not use just one read Membership userMembership = membership[msg.sender]
and use this instance for everything related to memberships.
Each read we are currently doing has an impact on the gas cost.
function purchaseMembership(uint256 costShareBenefit) external { require( costShareBenefit % 10 ether == 0 && costShareBenefit > 0, "FSDNetwork::purchaseMembership: Invalid cost share benefit specified" ); if ( membership[msg.sender].creation + MEMBERSHIP_DURATION < block.timestamp ) { membership[msg.sender].creation = 0; membership[msg.sender].availableCostShareBenefits = 0; } uint256 totalCostShareBenefit = membership[msg.sender].availableCostShareBenefits.add( costShareBenefit ); require( totalCostShareBenefit <= getMaximumBenefitPerUser(), "FSDNetwork::purchaseMembership: Exceeds cost share benefit limit per account" ); totalCostShareBenefits = totalCostShareBenefits.add(costShareBenefit); // FSHARE = Total Available Cost Share Benefits / Gearing Factor uint256 fShare = totalCostShareBenefits / GEARING_FACTOR; // Floor of 7500 ETH if (fShare < 7500 ether) fShare = 7500 ether; // FSHARERatio = Capital Pool / FSHARE (scaled by 1e18) uint256 fShareRatio = (fsd.getReserveBalance() - totalOpenRequests).mul(1 ether) / fShare; // 1 ether = 100% require( fShareRatio >= 1 ether, "FSDNetwork::purchaseMembership: Insufficient Capital to Cover Membership" ); uint256 membershipFee = costShareBenefit.wmul(MEMBERSHIP_FEE); uint256 fsdSpotPrice = getFSDPrice(); uint256 fsdFee = membershipFee.wdiv(fsdSpotPrice); // Automatically locks 65% to the Network by disallowing its retrieval fsd.safeTransferFrom(msg.sender, address(this), fsdFee); if (membership[msg.sender].creation == 0) { membership[msg.sender] .availableCostShareBenefits = totalCostShareBenefit; membership[msg.sender].creation = block.timestamp; membership[msg.sender].gracePeriod = membership[msg.sender].creation + MEMBERSHIP_DURATION + 60 days; } else { membership[msg.sender] .availableCostShareBenefits = totalCostShareBenefit; uint256 elapsedDurationPercentage = ((block.timestamp - membership[msg.sender].creation) * 1 ether) / MEMBERSHIP_DURATION; if (elapsedDurationPercentage < 1 ether) { uint256 durationIncrease = (costShareBenefit.mul(1 ether) / (totalCostShareBenefit - costShareBenefit)) .mul(MEMBERSHIP_DURATION) / 1 ether; membership[msg.sender].creation += durationIncrease; } } uint256 governancePoolRewards = fsdFee.wmul(GOVERNANCE_FUNDING_POOL_REWARDS); // Staking Rewards = 20% + [FSHARERatio - 125%] (if FSHARERatio > 125%) uint256 stakingMultiplier = fShareRatio >= 1.25 ether ? STAKING_REWARDS + fShareRatio - 1.25 ether : STAKING_REWARDS; // Maximum of 75% as we have 15% distributed to governance + funding pool if (stakingMultiplier > 0.75 ether) stakingMultiplier = 0.75 ether; uint256 stakingRewards = fsdFee.wmul(stakingMultiplier); // 20% as staking rewards fsd.safeTransfer(address(fsd), stakingRewards); fsd.addRegistrationTribute(stakingRewards); // 7.5% towards governance fsd.safeTransfer(address(fsd), governancePoolRewards); fsd.addRegistrationTributeGovernance(governancePoolRewards); // 7.5% towards funding pool fsd.safeTransfer(FUNDING_POOL, governancePoolRewards); }
#0 - fairside-core
2021-06-01T13:13:18Z
Fixed in PR#11.
129.7011 USDC - $129.70
a_delamo
The following methods are missing a verification that the address is not zero. Is not a critical issue, but adding a require doesn't cost much and can same some gas in case we need to re-deploy a contract.
FSDNetwork.constructor(FSD,address,address,address).fundingPool (network/FSDNetwork.sol#94) lacks a zero-check on : - FUNDING_POOL = fundingPool (network/FSDNetwork.sol#99) FSDNetwork.constructor(FSD,address,address,address).governance (network/FSDNetwork.sol#95) lacks a zero-check on : - GOVERNANCE_ADDRESS = governance (network/FSDNetwork.sol#100) FSDNetwork.constructor(FSD,address,address,address).timelock (network/FSDNetwork.sol#96) lacks a zero-check on : - TIMELOCK = timelock (network/FSDNetwork.sol#101) SignatureWhitelist.constructor(address).signer (dependencies/SignatureWhitelist.sol#20) lacks a zero-check on : - WHITELIST_SIGNER = signer (dependencies/SignatureWhitelist.sol#21) FSD.constructor(address,address,address)._fundingPool (token/FSD.sol#60) lacks a zero-check on : - fundingPool = _fundingPool (token/FSD.sol#68) FSD.constructor(address,address,address)._timelock (token/FSD.sol#62) lacks a zero-check on : - timelock = _timelock (token/FSD.sol#69) FairSideDAO.constructor(Timelock,IERC20ConvictionScore,address)._guardian (dao/FairSideDAO.sol#211) lacks a zero-check on : - guardian = _guardian (dao/FairSideDAO.sol#214) Timelock.constructor(address,uint256).admin_ (timelock/Timelock.sol#48) lacks a zero-check on : - admin = admin_ (timelock/Timelock.sol#58) Timelock.setPendingAdmin(address).pendingAdmin_ (timelock/Timelock.sol#93) lacks a zero-check on : - pendingAdmin = pendingAdmin_ (timelock/Timelock.sol#98) Timelock.executeTransaction(address,uint256,string,bytes,uint256).target (timelock/Timelock.sol#147) lacks a zero-check on : - (success,returnData) = target.call{value: value}(callData) (timelock/Timelock.sol#187-188)
Slither
#0 - fairside-core
2021-05-30T14:00:39Z
Adding the respective require
checks significantly increase the bytecode size of the contract and all relate to privileged functions (constructor
functions or functions voted on by the DAO). As such, I believe this to be a non-critical (0) issue.
#1 - cemozerr
2021-06-07T19:34:55Z
Duplicate of #18