Debt DAO contest - Lambda's results

A cryptonative credit marketplace for fully anon and trustless loans to DAOs.

General Information

Platform: Code4rena

Start Date: 03/11/2022

Pot Size: $115,500 USDC

Total HM: 17

Participants: 120

Period: 7 days

Judge: LSDan

Total Solo HM: 1

Id: 174

League: ETH

Debt DAO

Findings Distribution

Researcher Performance

Rank: 6/120

Findings: 5

Award: $5,717.17

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Lambda

Also found by: adriro, aphak5010, berndartmueller

Labels

bug
3 (High Risk)
primary issue
satisfactory
sponsor confirmed
selected for report
H-02

Awards

3536.4311 USDC - $3,536.43

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/utils/SpigotLib.sol#L87

Vulnerability details

Impact

Neither SpigotLib.claimRevenue nor SpigotLib._claimRevenue check that the provided revenueContract was registered before. If this is not the case, SpigotLib._claimRevenue assumes that this is a revenue contract with push payments (because self.settings[revenueContract].claimFunction is 0) and just returns the difference since the last call to claimRevenue:

       if(self.settings[revenueContract].claimFunction == bytes4(0)) {
            // push payments

            // claimed = total balance - already accounted for balance
            claimed = existingBalance - self.escrowed[token]; //@audit Rebasing tokens
            // underflow revert ensures we have more tokens than we started with and actually claimed revenue
        }

SpigotLib.claimRevenue will then read self.settings[revenueContract].ownerSplit, which is 0 for non-registered revenue contracts:

uint256 escrowedAmount = claimed * self.settings[revenueContract].ownerSplit / 100;

Therefore, the whole claimed amount is sent to the treasury.

This becomes very problematic for revenue tokens that use push payments. An attacker (in practice the borrower) can just regularly call claimRevenue with this token and a non-existing revenue contract. All of the tokens that were sent to the spigot since the last call will be sent to the treasury and none to the escrow, i.e. a borrower can ensure that no revenue will be available for the lender, no matter what the configured split is.

Proof Of Concept

As mentioned above, the attack pattern works for arbitrary tokens where one (or more) revenue contracts use push payments, i.e. where the balance of the Spigot increases from time to time. Then, the attacker just calls claimRevenue with a non-existing address. This is illustrated in the following diff:

--- a/contracts/tests/Spigot.t.sol
+++ b/contracts/tests/Spigot.t.sol
@@ -174,7 +174,7 @@ contract SpigotTest is Test {
         assertEq(token.balanceOf(address(spigot)), totalRevenue);
         
         bytes memory claimData;
-        spigot.claimRevenue(revenueContract, address(token), claimData);
+        spigot.claimRevenue(address(0), address(token), claimData);

Thanks to this small modification, all of the tokens are sent to the treasury and none are sent to the escrow.

Check that a revenue contract was registered before, revert if it does not.

#0 - c4-judge

2022-11-15T21:17:23Z

dmvt marked the issue as duplicate of #70

#1 - c4-judge

2022-11-17T20:12:32Z

dmvt marked the issue as selected for report

#2 - c4-sponsor

2022-11-30T15:04:53Z

kibagateaux marked the issue as sponsor confirmed

#3 - c4-judge

2022-12-06T17:20:38Z

dmvt marked the issue as satisfactory

#4 - C4-Staff

2022-12-20T06:38:26Z

liveactionllama marked the issue as not a duplicate

#5 - C4-Staff

2022-12-20T06:38:37Z

liveactionllama marked the issue as primary issue

Findings Information

🌟 Selected for report: Lambda

Also found by: HE1M, Trust, adriro, berndartmueller, minhquanym

Labels

bug
3 (High Risk)
primary issue
satisfactory
sponsor confirmed
upgraded by judge
selected for report
H-03

Awards

1909.6728 USDC - $1,909.67

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/modules/credit/LineOfCredit.sol#L234 https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/modules/credit/LineOfCredit.sol#L270

Vulnerability details

Impact

The functions addCredit and increaseCredit both ahve a mutualConsent or mutualConsentById modifier. Furthermore, these functions are payable and the lender needs to send the corresponding ETH with each call. However, if we look at the mutual consent modifier works, we can a problem:

modifier mutualConsent(address _signerOne, address _signerTwo) {
      if(_mutualConsent(_signerOne, _signerTwo))  {
        // Run whatever code needed 2/2 consent
        _;
      }
}

function _mutualConsent(address _signerOne, address _signerTwo) internal returns(bool) {
        if(msg.sender != _signerOne && msg.sender != _signerTwo) { revert Unauthorized(); }

        address nonCaller = _getNonCaller(_signerOne, _signerTwo);

        // The consent hash is defined by the hash of the transaction call data and sender of msg,
        // which uniquely identifies the function, arguments, and sender.
        bytes32 expectedHash = keccak256(abi.encodePacked(msg.data, nonCaller));

        if (!mutualConsents[expectedHash]) {
            bytes32 newHash = keccak256(abi.encodePacked(msg.data, msg.sender));

            mutualConsents[newHash] = true;

            emit MutualConsentRegistered(newHash);

            return false;
        }

        delete mutualConsents[expectedHash];

        return true;
}

The problem is: On the first call, when the other party has not given consent to the call yet, the modifier does not revert. It sets the consent of the calling party instead.

This is very problematic in combination with sending ETH for two reasons: 1.) When the lender performs the calls first and sends ETH along with the call, the call will not revert. It will instead set the consent for him, but the sent ETH is lost. 2.) Even when the lender thinks about this and does not provide any ETH on the first call, the borrower has to perform the second call. Of course, he will not provide the ETH with this call, but this will cause the transaction to revert. There is now no way for the borrower to also grant consent, but still let the lender perform the call.

Proof Of Concept

Lender Alice calls LineOfCredit.addCredit first to add a credit with 1 ETH. She sends 1 ETH with the call. However, because borrower Bob has not performed this call yet, the function body is not executed, but the 1 ETH is still sent. Afterwards, Bob wants to give his consent, so he performs the same call. However, this call reverts, because Bob does not send any ETH with it.

Consider implementing an external function to grant consent to avoid this scenario. Also consider reverting when ETH is sent along, but the other party has not given their consent yet.

#0 - c4-judge

2022-11-15T21:32:50Z

dmvt marked the issue as duplicate of #42

#1 - c4-judge

2022-11-17T19:59:10Z

dmvt changed the severity to 3 (High Risk)

#2 - c4-judge

2022-11-17T19:59:15Z

dmvt marked the issue as selected for report

#3 - c4-sponsor

2022-11-30T15:01:40Z

kibagateaux marked the issue as sponsor confirmed

#4 - c4-judge

2022-12-06T17:07:35Z

dmvt marked the issue as satisfactory

#5 - C4-Staff

2022-12-20T06:31:35Z

liveactionllama marked the issue as not a duplicate

#6 - C4-Staff

2022-12-20T06:31:49Z

liveactionllama marked the issue as primary issue

Awards

8.0811 USDC - $8.08

Labels

bug
2 (Med Risk)
satisfactory
duplicate-39

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/d7ef66035ddf873b0c96804a1c9deeebb1f798ea/contracts/utils/LineLib.sol#L71

Vulnerability details

Impact

In LineLib.receiveTokenOrETH, it is only checked if msg.value is greater or equal than amount when the token is ETH:

if(msg.value < amount) { revert TransferFailed(); }

Therefore, when a user accidentally pays too much ETH, the additional amount will be lost. Note that this is different to ERC20 tokens, where the exact amount is enforced (by transferring the exact amount).

Proof Of Concept

Alice calls LineOfCredit.addCredit to add a credit with 1 ETH. While amount is set to 1 ETH (10**18), she accidentally sends 2 ETH with the call. The additional ETH is lost and cannot be recovered.

Either force that the amount matches exactly or reimburse the additional ETH.

#0 - c4-judge

2022-11-15T21:30:57Z

dmvt marked the issue as duplicate of #25

#1 - c4-judge

2022-12-06T16:30:16Z

dmvt marked the issue as satisfactory

#2 - C4-Staff

2022-12-20T06:44:54Z

liveactionllama marked the issue as duplicate of #39

Findings Information

🌟 Selected for report: perseverancesuccess

Also found by: 0x52, HE1M, Lambda, Trust, adriro, aphak5010, cccz, minhquanym

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-110

Awards

214.1771 USDC - $214.18

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/modules/credit/SpigotedLine.sol#L165

Vulnerability details

Impact

0x supports the filling of specific orders. Furthermore, you can post orders that can only get filled by a provided address. Moreover, the only requirement in SpigotedLineLib.claimAndTrade is that the 0x call resulted in some more tokens (even if it is only 1 wei):

// underflow revert ensures we have more tokens than we started with
uint256 tokensBought = LineLib.getBalance(targetToken) - oldTargetTokens;

if(tokensBought == 0) { revert TradeFailed(); } // ensure tokens bought

Furthermore, a borrower can call arbitrary 0x functions by providing the corresponding zeroExTradeData (which is passed as calldata to 0x).

A malicious borrower can exploit this in the following way: 1.) Post a 0x order to buy the whole revenue amount that has accrued in the Spigot contract for an extremely low amount of the target token (e.g., 1 cent). 2.) Call SpigotedLine.claimAndTrade to fill this order (by providing the corresponding call data) and get the revenue tokens for free.

An attacker can use this attack pattern continously to siphon all revenue tokens out of the spigot. Like that, he never has to pay back the spigoted line of credit.

Proof Of Concept

Let's say there is a spigoted line of credit with some revenue token TOK with a value of 1,000 USD. The borrower has taken out a loan in USDC, so the token for the credit is USDC. Since the last spigot.claimEscrow call, 100 TOK were accrued (with a value of 100,000 USD). The attacker uses the attack pattern that is described above to sell all 100 TOK (to himself via an order that he posted earlier) for 1 USDC. This leads to a loss of 100,000 USD for the lender.

Consider implementing an external function to grant consent to avoid this scenario. Also consider reverting when ETH is sent along, but the other party has not given their consent yet.

#0 - dmvt

2022-11-15T21:34:41Z

#1 - c4-judge

2022-11-15T21:34:48Z

dmvt marked the issue as unsatisfactory: Out of scope

#2 - OpenCoreCH

2022-12-08T22:30:41Z

@dmvt I think there was a mistake here, the issue is not about trading the revenue token for a fake token and I do not mention a fake token anywhere (the TOK in the PoC might have been confusing, but this is the revenue token that I use for the example as I mention there). It is about trading it for the credit token, but using malicious zeroExTradeData (that executes a trade which is only executable by this address) to sell them for an extremely unfavorable rate. It looks like #411 describes the same issue.

#3 - dmvt

2022-12-09T00:37:00Z

Thanks for catching that. On further review, I agree. Thank you for waiting for post-judging qa to point this out.

#4 - c4-judge

2022-12-09T00:37:13Z

dmvt marked the issue as duplicate of #88

#5 - c4-judge

2022-12-09T00:37:22Z

dmvt marked the issue as satisfactory

#6 - c4-judge

2022-12-09T00:37:27Z

dmvt changed the severity to 2 (Med Risk)

#7 - C4-Staff

2022-12-16T23:20:48Z

captainmangoC4 marked the issue as duplicate of #110

Findings Information

Awards

48.8098 USDC - $48.81

Labels

bug
2 (Med Risk)
satisfactory
duplicate-367

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/d7ef66035ddf873b0c96804a1c9deeebb1f798ea/contracts/utils/LineLib.sol#L69 https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/utils/SpigotLib.sol#L38

Vulnerability details

Impact & Proof Of Concept

The function LineLib.receiveTokenOrETH that is used in multiple places assumes that the requested transfer amount is also equal to the actual transferred amount:

if(token != Denominations.ETH) { // ERC20
            IERC20(token).safeTransferFrom(sender, address(this), amount);
}

This is not true for fee-on-transfer tokens, where the change in balance is smaller than the requested amount. This leads to problems in multiple places. For instance, in LineOfCredit.addCredit, the amount parameter is used to create the credit:

LineLib.receiveTokenOrETH(token, lender, amount);

bytes32 id = _createCredit(lender, token, amount);

When the contract does not contain this amount, there are two options: 1.) Payouts that should be possible are not possible (because the balance is not sufficient). 2.) Funds from other users are used for payouts.

Another big problem for the system are rebasing tokens. This can cause an underflow in SpigotLib._claimRevenue (because the balance is now smaller than the previously cached balance), making all calls to claim revenue revert:

uint256 existingBalance = LineLib.getBalance(token);
...

claimed = existingBalance - self.escrowed[token];

Check the actual amount that was transferred (i.e., the difference of the balances).

#0 - c4-judge

2022-11-15T21:30:39Z

dmvt marked the issue as duplicate of #26

#1 - c4-judge

2022-12-06T16:47:51Z

dmvt marked the issue as satisfactory

#2 - C4-Staff

2022-12-20T06:01:34Z

liveactionllama marked the issue as duplicate of #367

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter