Omniscia CloudFunding Audit

CrowdFunding Manual Review Findings

CrowdFunding Manual Review Findings

CFG-01M: Inexistent Graceful Handling of Rewards

Description:

The distributeFtsoRewardsToProject function gracefully handles an error in the claimReward function by emitting an error, however, the identical implementation of distributeFtsoRewardsToStakers does not do this.

Impact:

Should a raise have been unsuccessful and one of the reward managers reported by the manager be misbehaving, it will be impossible to claim the rewards required for the system to distribute FTSO rewards to stakers properly.

Example:

contracts/CrowdFunding.sol
204function distributeFtsoRewardsToProject(uint256[] calldata epochs) external {
205 require(raiseCompletedAtBlock > 0, 'Raise not completed');
206 IFtsoRewardManager[] memory ftsoRewardManagers = manager.getFtsoRewardManagers();
207 uint256 epochsRewards;
208 for (uint256 j; j < ftsoRewardManagers.length; j++) {
209 try ftsoRewardManagers[j].claimReward(payable(address(this)), epochs) returns (uint256 rewards) {
210 epochsRewards += rewards;
211 } catch {
212 emit ClaimRewardError(address(ftsoRewardManagers[j]), epochs);
213 }
214 }
215 // split between project and platform and send
216 uint256 platformFee = (epochsRewards * platformFeeBips) / 100_00;
217 uint256 projectReward = epochsRewards - platformFee;
218 if (projectReward > 0) {
219 safeTransferNAT(projectRewardAccount, projectReward);
220 emit ProjectReward(projectRewardAccount, projectReward);
221 }
222 if (platformFee > 0) {
223 address platformFeeAccount = manager.platformFeeAccount();
224 safeTransferNAT(platformFeeAccount, platformFee);
225 emit PlatformFee(platformFeeAccount, platformFee);
226 }
227}
228
229function distributeFtsoRewardsToStakers(uint256[] calldata epochs) external {
230 require(block.timestamp >= raiseDeadline && raiseCompletedAtBlock == 0, 'Raise not completed or successful');
231 IFtsoManager ftsoManager = FlareLibrary.getFtsoManager();
232 IFtsoRewardManager[] memory ftsoRewardManagers = manager.getFtsoRewardManagers();
233 uint256 totalRewards;
234 uint256 extraBalance;
235 for (uint256 i; i < epochs.length; i++) {
236 uint256 epoch = epochs[i];
237 uint256 votePowerBlock = ftsoManager.getRewardEpochVotePowerBlock(epoch);
238 uint256 votePower = totalCapitalAt(votePowerBlock);
239 uint256[] memory singleEpoch = new uint256[](1);
240 singleEpoch[0] = epoch;
241 uint256 epochRewards;
242 for (uint256 j; j < ftsoRewardManagers.length; j++) {
243 if (ftsoRewardManagers[j].active()) {
244 epochRewards += ftsoRewardManagers[j].claimReward(payable(address(this)), singleEpoch);
245 }
246 }
247 if (epochRewards > 0) {
248 if (votePower == 0) {
249 extraBalance += epochRewards;
250 } else {
251 totalRewards += epochRewards;
252 pendingStakersRewards[epoch] = FtsoReward(votePowerBlock, epochRewards, votePower);
253 emit StakersRewards(epoch, epochRewards);
254 }
255 }
256 }
257 // wrap FTSO rewards
258 if (totalRewards > 0) {
259 assert(address(this).balance >= totalRewards);
260 wNat.deposit{value: address(this).balance}();
261 }
262 if (extraBalance > 0) {
263 address platformAccount = manager.platformFeeAccount();
264 if (platformAccount != address(0)) {
265 safeTransferNAT(platformAccount, extraBalance);
266 emit ExtraBalance(platformAccount, extraBalance);
267 }
268 }
269}

Recommendation:

We advise graceful handling to be introduced to distributeFtsoRewardsToStakers as well, ensuring that staker rewards can also be properly claimed and distributed.

Alleviation:

The CloudFunding team has stated that the distributeFtsoRewardsToProject and distributeFtsoRewardsToStakers functions behave differently purposefully as the claim failure in the former instance is ignore-able and can be retried whilst the staker reward distribution function makes irreversible storage changes and as such should fatally fail on claim failure. As a result, we consider this exhibit nullified.

CFG-02M: Potentially Incorrect Epoch Assumption

Description:

The expectedWrappedBalance validation code that calculates how much wrapped balance should remain at rest may be incorrect as it will retrieve the current reward epoch and reward epoch to expire next whilst the rewards that may have already been stored for stakers may be have been claimed for an already-expired epoch.

Impact:

Should the assumption be true, the reward claim process will break causing funds to be diverted to the platform fee account incorrectly.

Example:

contracts/CrowdFunding.sol
107function expectedWrappedBalance() private view returns (uint256 balance) {
108 balance = totalCapital();
109 IFtsoManager ftsoManager = FlareLibrary.getFtsoManager();
110 uint256 currentRewardEpoch = ftsoManager.getCurrentRewardEpoch();
111 uint256 firstRewardEpoch = ftsoManager.getRewardEpochToExpireNext();
112 for (uint256 epoch = firstRewardEpoch; epoch < currentRewardEpoch; epoch++) {
113 balance += pendingStakersRewards[epoch].remainingAmount;
114 }
115 IDistributionToDelegators[] memory distributions = manager.getDistributions();
116 if (distributions.length > 0) {
117 (, uint256 claimableUntil) = airdropClaimRange();
118 if (block.timestamp < claimableUntil) {
119 uint256 scanLen = airdropLastMonth(distributions);
120 for (uint256 month; month < scanLen; month++) {
121 Airdrop[] storage airdrops = pendingAirdrops[month];
122 for (uint256 j; j < airdrops.length; j++) {
123 balance += airdrops[j].remainingAmount;
124 }
125 }
126 }
127 }
128}

Recommendation:

We advise this trait of the system to be evaluated and potentially remediated as it would allow wrapped funds that are owed to be withdrawn thus breaking the epoch reward withdrawal system.

Alleviation:

The CloudFunding team has stated that this is intended behaviour as any expired rewards are meant to be diverted to the platform fee account. We advise this trait to be properly documented as part of the function's definition. Based on this specification, we consider this exhibit nullified.