Skip to content
This repository has been archived by the owner on May 26, 2023. It is now read-only.

libratus - Locked funds calculation is invalid if part of the reward pool was claimed #389

Closed
github-actions bot opened this issue Feb 21, 2023 · 7 comments
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed

Comments

@github-actions
Copy link

github-actions bot commented Feb 21, 2023

libratus

medium

Locked funds calculation is invalid if part of the reward pool was claimed

Summary

If part of the reward pool is claimed, getLockedFunds calculation in BountyCore will be invalid which may lead to errors when trying to refund a deposit

Vulnerability Detail

In getLockedFunds locked funds are calculated the following way:

        for (uint256 i = 0; i < depList.length; i++) {
            if (
                block.timestamp <
                depositTime[depList[i]] + expiration[depList[i]] &&
                tokenAddress[depList[i]] == _depositId
            ) {
                lockedFunds += volume[depList[i]];
            }
        }

Using volume[depList[i]] means always taking full deposit amount. This works for Atomic bounty but not for other types where it is possible to claim rewards in parts. If rewards were partially claimed, then only the remaining portion of the deposit should be considered locked. Current logic can lead to errors as shown in the PoC:

  • Deposit 100 DAI with expiration = 50
  • Deposit 50 DAI with expiration = 1
  • Rewind 10 seconds forward so that second deposit is expired
  • Claim a bounty (70 DAI)
  • Attempt to refund second deposit of 50 DAI
  • Despite the fact that contract has enough DAI, this will revert due to the error in locked funds calculation
diff --git a/test/DepositManager.test.js b/test/DepositManager.test.js
index 00338d3..3bccf4c 100755
--- a/test/DepositManager.test.js
+++ b/test/DepositManager.test.js
@@ -545,6 +545,42 @@ describe('DepositManager.sol', () => {
                                expect(newFunderFakeTokenBalance).to.equal('10000000000000000000000');
                        });
 
+                       it('should transfer balance to funder after partial claim', async () => {
+                               // ARRANGE
+                               await openQProxy.mintBounty(Constants.bountyId, Constants.organization, ongoingBountyInitOperation);
+
+                               const bountyAddress = await openQProxy.bountyIdToAddress(Constants.bountyId);
+                               const bounty = await OngoingBountyV1.attach(bountyAddress);
+
+                               await openQProxy.setPayout(Constants.bountyId, mockDai.address, 70);
+
+                               await mockDai.approve(bountyAddress, 10000000);
+
+                               const daiDepositId = generateDepositId(Constants.bountyId, 0);
+                               await depositManager.fundBountyToken(bountyAddress, mockDai.address, 100, 50, Constants.funderUuid);
+
+                               const daiDeposit2Id = generateDepositId(Constants.bountyId, 1);
+                               await depositManager.fundBountyToken(bountyAddress, mockDai.address, 50, 1, Constants.funderUuid);
+
+                               // ASSUME
+                               const bountyDaiBalance = (await mockDai.balanceOf(bountyAddress)).toString();
+                               expect(bountyDaiBalance).to.equal('150');
+
+                               // 10 ticks forward. Second deposit expired, first is still active.
+                               ethers.provider.send("evm_increaseTime", [10]);
+
+                               // Claim 70 tokens. 80 remain on the contract
+                               await claimManager.connect(oracle).claimBounty(bountyAddress, owner.address, abiEncodedOngoingCloserData);
+
+                               // ACT
+                               // Try refund 50 tokens. 80 remain on the contract so this shouldn't fail but it does
+                               await depositManager.refundDeposit(bountyAddress, daiDeposit2Id);
+
+                               // // ASSERT
+                               const newBountyFakeTokenBalance = (await mockDai.balanceOf(bountyAddress)).toString();
+                               expect(newBountyFakeTokenBalance).to.equal('30');
+                       });
+
                        it('should transfer NFT from bounty contract to funder', async () => {
                                // ASSUME
                                expect(await mockNft.ownerOf(1)).to.equal(owner.address);

Impact

Under certain circumstances depositor will not be able to refund an expired deposit

Code Snippet

https://github.com/sherlock-audit/2023-02-openq/blob/main/contracts/Bounty/Implementations/BountyCore.sol#L333-L352

Tool used

Manual Review

Recommendation

Subtract the amount of claimed tokens when calculating locked funds. Amount of claimed tokens can be tracked separately

Duplicate of #257

@github-actions github-actions bot added the Medium A valid Medium severity issue label Feb 21, 2023
@FlacoJones
Copy link

Will fix by simply removing crowdfunding and restricting funding to the bounty issuer

@FlacoJones FlacoJones added Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed labels Feb 23, 2023
@FlacoJones
Copy link

@IAm0x52
Copy link
Collaborator

IAm0x52 commented Mar 6, 2023

Dupe of #257

@hrishibhat hrishibhat added the Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label label Mar 7, 2023
@sherlock-admin sherlock-admin added the Reward A payout will be made for this issue label Mar 7, 2023
@kiseln
Copy link

kiseln commented Mar 9, 2023

Escalate for 20 USDC

I don't think it's a duplicate of 257. Even though both likely originate from the same design choices, the issues are different in impact and repro steps.

257 advocates for proportional distribution of remaining refunds to depositors. It describes a scenario when all refunds are expired and ready to be refunded.

The current issue, on the other hand, highlights incorrect calculations when determining the amount that can be refunded. In order to reproduce it, you need to have some deposits expired and some still ongoing. The impact is also about tokens being locked in the contract unnecessarily until the other deposit expires.

In my opinion, there is a distinction and it is possible to fix one but not the other and vice versa.

#421 is the same as this one, btw.

@sherlock-admin
Copy link
Contributor

Escalate for 20 USDC

I don't think it's a duplicate of 257. Even though both likely originate from the same design choices, the issues are different in impact and repro steps.

257 advocates for proportional distribution of remaining refunds to depositors. It describes a scenario when all refunds are expired and ready to be refunded.

The current issue, on the other hand, highlights incorrect calculations when determining the amount that can be refunded. In order to reproduce it, you need to have some deposits expired and some still ongoing. The impact is also about tokens being locked in the contract unnecessarily until the other deposit expires.

In my opinion, there is a distinction and it is possible to fix one but not the other and vice versa.

#421 is the same as this one, btw.

You've created a valid escalation for 20 USDC!

To remove the escalation from consideration: Delete your comment.
To change the amount you've staked on this escalation: Edit your comment (do not create a new comment).

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

@sherlock-admin sherlock-admin added the Escalated This issue contains a pending escalation label Mar 9, 2023
@Evert0x
Copy link

Evert0x commented Mar 16, 2023

Escalation rejected.

It's a duplicate of #257 as it originates from the same design choice. 257 might have a different scenario and recommendation but it's based on the same flaw in the code.

@sherlock-admin
Copy link
Contributor

Escalation rejected.

It's a duplicate of #257 as it originates from the same design choice. 257 might have a different scenario and recommendation but it's based on the same flaw in the code.

This issue's escalations have been rejected!

Watsons who escalated this issue will have their escalation amount deducted from their next payout.

@sherlock-admin sherlock-admin added Escalation Resolved This issue's escalations have been approved/rejected and removed Escalated This issue contains a pending escalation labels Mar 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed
Projects
None yet
Development

No branches or pull requests

6 participants