Debt DAO contest - Trust'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: 1/120

Findings: 6

Award: $11,613.69

QA:
grade-b

๐ŸŒŸ Selected for report: 1

๐Ÿš€ Solo Findings: 0

Findings Information

๐ŸŒŸ Selected for report: Lambda

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

Labels

bug
3 (High Risk)
satisfactory
duplicate-125

Awards

1468.9791 USDC - $1,468.98

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/modules/credit/LineOfCredit.sol#L223 https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/modules/credit/LineOfCredit.sol#L265

Vulnerability details

Description

The addCredit() function transfers money from lender to a LineOfCredit contract, and opens a credit account. increaseCredit() transfers additional funds to an existing credit account contract. Both functions are payable and guarded by mutualConsent(), which guarantees that both borrower and lender approve the execution.

It is not critical to understand how the mutualConsent() modifier works, but it is important to realize both borrower and lender call this function once (the order does not matter), and in the second execution the function body is actually executed.

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

The issue is that these two functions have to verify that lender callsย after borrower. In the case of ETH credit token, when lender calls first, the message value is sent to the line without bookkeeping. At this point, it doesn't even matter if borrower will call the function as well or not, in both scenarios the previous msg.value will be lost:

function addCredit( uint128 drate, uint128 frate, uint256 amount, address token, address lender ) external payable override whileActive mutualConsent(lender, borrower) returns (bytes32) { LineLib.receiveTokenOrETH(token, lender, amount); bytes32 id = _createCredit(lender, token, amount); require(interestRate.setRate(id, drate, frate)); return id; } function receiveTokenOrETH( address token, address sender, uint256 amount ) external returns (bool) { if(token == address(0)) { revert TransferFailed(); } if(token != Denominations.ETH) { // ERC20 IERC20(token).safeTransferFrom(sender, address(this), amount); } else { // ETH // ---------- ISSUE --------- IF BORROWER CALLS SECOND msg.value will be 0 if(msg.value < amount) { revert TransferFailed(); } } return true; }

Note that this is different from the other addCredit/increaseCredit submission (borrower leaks funds) - here the root cause is the order of transactions, whereas in the other submission it's theย payability of borrower.

There is no way (to my knowledge) of retrieving funds that are not accounted for in bookkeeping. Even the sweeping function requires the sweeped funds to be in the unusedTokens array:

function sweep(address to, address token) external nonReentrant returns (uint256) { uint256 amount = unusedTokens[token]; delete unusedTokens[token]; bool success = SpigotedLineLib.sweep( to, token, amount, _updateStatus(_healthcheck()), borrower, arbiter ); return success ? amount : 0; }

Impact

When lender consents before borrower in ETH credit token, all the lent funds are permanently lost.

Tools Used

Manual audit

Two options:

1. Add a borrowerBeforeLender modifier which will guarantee correct chain of events - the downside is that the order is now forced

2. If lender consents before borrower, keep their funds in escrow and allow them to cancel the consent.

#0 - c4-judge

2022-11-17T16:16:43Z

dmvt marked the issue as duplicate of #42

#1 - trust1995

2022-11-19T16:08:59Z

Believe I have provided much more detail than the selected-for-report duplicate: https://github.com/code-423n4/2022-11-debtdao-findings/issues/125

#2 - c4-judge

2022-12-06T17:07:54Z

dmvt marked the issue as satisfactory

#3 - C4-Staff

2022-12-20T06:34:38Z

liveactionllama marked the issue as not a duplicate

#4 - C4-Staff

2022-12-20T06:34:48Z

liveactionllama marked the issue as duplicate of #125

Findings Information

๐ŸŒŸ Selected for report: Trust

Also found by: bin2chen

Labels

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

Awards

8731.9287 USDC - $8,731.93

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/modules/credit/LineOfCredit.sol#L516-L538

Vulnerability details

Description

LineOfCredit manages an array of open credit line identifiers called ids. Many interactions with the Line operate on ids[0], which is presumed to be the oldest borrow which has non zero principal. For example, borrowers must first deposit and repay to ids[0] before other credit lines.ย 

The list is managed by several functions:

  1. CreditListLib.removePosition - deletes parameter id in the ids array
  2. CreditListLib.stepQ - rotates all ids members one to the left, with the leftmost becoming the last element
  3. _sortIntoQ - most complex function, finds the smallest index which can swap identifiers with the parameter id, which satisfies the conditions:
    1. target index is not empty
    2. there is no principal owed for the target index's credit

The idea I had is that if we could corrupt the ids array so that ids[0] would be zero, but after it there would be some other active borrows, it would be a very severe situation. The whileBorrowing() modifier assumes if the first element has no principal, borrower is not borrowing.ย 

modifier whileBorrowing() { if(count == 0 || credits[ids[0]].principal == 0) { revert NotBorrowing(); } _; }

It turns out there is a simple sequence of calls which allows borrowing while ids[0] is deleted, and does not re-arrange the new borrow into ids[0]!

  1. id1 = addCredit() - add a new credit line, a new id is pushed to the end of ids array.
  2. id2 = addCredit() - called again, ids.length = 2
  3. close(id1) - calls removePosition() on id1, now ids array is [0x000000000000000000000000, id2 ]
  4. borrow(id2) - will borrow from id2 and call _sortIntoQ. The sorting loop will not find another index other than id2's existing index (id == bytes32(0) is true).

From this sequence, we achieve a borrow while ids[0] is 0! Therefore, credits[ids[0]].principal = credits[0].principal = 0, and whileBorrowing() reverts.

The impact is massive - the following functions are disabled:

  • SecureLine::liquidate()
  • LineOfCredit::depositAndClose()
  • LineOfCredit::depositAndRepay()
  • LineOfCredit::claimAndRepay()
  • LineOfCredit::claimAndTrade()

Impact

Borrower can craft a borrow that cannot be liquidated, even by arbiter. Alternatively, functionality may be completely impaired through no fault of users.

Proof of Concept

Copy the following code into LineOfCredit.t.sol

function _addCreditLender2(address token, uint256 amount) public { // Prepare lender 2 operations, does same as mintAndApprove() address lender2 = address(21); deal(lender2, mintAmount); supportedToken1.mint(lender2, mintAmount); supportedToken2.mint(lender2, mintAmount); unsupportedToken.mint(lender2, mintAmount); vm.startPrank(lender2); supportedToken1.approve(address(line), MAX_INT); supportedToken2.approve(address(line), MAX_INT); unsupportedToken.approve(address(line), MAX_INT); vm.stopPrank(); // addCredit logic vm.prank(borrower); line.addCredit(dRate, fRate, amount, token, lender2); vm.stopPrank(); vm.prank(lender2); line.addCredit(dRate, fRate, amount, token, lender2); vm.stopPrank(); } function test_attackUnliquidatable() public { bytes32 id_1; bytes32 id_2; _addCredit(address(supportedToken1), 1 ether); _addCreditLender2(address(supportedToken1), 1 ether); id_1 = line.ids(0); id_2 = line.ids(1); hoax(borrower); line.close(id_1); hoax(borrower); line.borrow(id_2, 1 ether); id_1 = line.ids(0); id_2 = line.ids(1); console.log("id1 : ", uint256(id_1)); console.log("id2 : ", uint256(id_2)); vm.warp(ttl+1); assert(line.healthcheck() == LineLib.STATUS.LIQUIDATABLE); vm.expectRevert(ILineOfCredit.NotBorrowing.selector); bool isSolvent = line.declareInsolvent(); }

Tools Used

Manual audit

When sorting new borrows into the ids queue, do not skip any elements.

#0 - c4-judge

2022-11-17T16:10:47Z

dmvt marked the issue as duplicate of #69

#1 - c4-judge

2022-11-21T20:54:50Z

dmvt marked the issue as not a duplicate

#2 - c4-judge

2022-11-21T20:55:11Z

dmvt marked the issue as duplicate of #354

#3 - c4-judge

2022-12-07T16:38:17Z

dmvt marked the issue as nullified

#4 - trust1995

2022-12-07T16:43:15Z

Unclear why this issue is nullified, I have demonstrated a POC that shows line cannot be declared insolvent. Just because the dupped to issue is wrong doesn't mean this is as well.

#5 - dmvt

2022-12-08T19:29:53Z

Kicking back to the sponsor for another look. I'm inclined to bring this one back as valid unless the sponsor can show why it isn't.

#6 - dmvt

2022-12-08T20:13:28Z

I don't want to delay post-judging QA, so for now I'm going to move forward. This ruling is subject to change pending further comment from the sponsor.

#7 - c4-judge

2022-12-08T20:13:37Z

dmvt marked the issue as not nullified

#8 - c4-judge

2022-12-08T20:13:49Z

dmvt marked the issue as not a duplicate

#9 - c4-judge

2022-12-08T20:13:57Z

dmvt marked the issue as primary issue

#10 - c4-judge

2022-12-08T20:14:02Z

dmvt marked the issue as satisfactory

#11 - c4-judge

2022-12-08T20:14:06Z

dmvt marked the issue as selected for report

#12 - c4-sponsor

2023-01-26T14:22:02Z

kibagateaux marked the issue as sponsor confirmed

Findings Information

๐ŸŒŸ Selected for report: berndartmueller

Also found by: 0xdeadbeef0x, Trust, adriro, aphak5010, hansfriese, rvierdiiev

Labels

bug
3 (High Risk)
satisfactory
duplicate-461

Awards

1133.2124 USDC - $1,133.21

External Links

Lines of code

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

Vulnerability details

Description

CreditLib's repay() function is the actual accounting of repayments in a LineOfCredit:

function repay( ILineOfCredit.Credit memory credit, bytes32 id, uint256 amount ) external returns (ILineOfCredit.Credit memory) { unchecked { if (amount <= credit.interestAccrued) { credit.interestAccrued -= amount; credit.interestRepaid += amount; emit RepayInterest(id, amount); return credit; } else { uint256 interest = credit.interestAccrued; uint256 principalPayment = amount - interest; // update individual credit line denominated in token credit.principal -= principalPayment; credit.interestRepaid += interest; credit.interestAccrued = 0; emit RepayInterest(id, interest); emit RepayPrincipal(id, principalPayment); return credit; } } }

Note that the entire function has no overflow protection. It is assumed that the caller of repay() will perform the necessary validation:

require(amount <= credit.principal + credit.interestAccrued);

If this is not the case, the following line will overflow:

ย  credit.principal -= principalPayment;

Unfortunately, there is one instance where amount is not checked, in useAndRepay():

/// see ISpigotedLine.useAndRepay function useAndRepay(uint256 amount) external whileBorrowing returns(bool) { bytes32 id = ids[0]; Credit memory credit = credits[id]; if (msg.sender != borrower && msg.sender != credit.lender) { revert CallerAccessDenied(); } require(amount <= unusedTokens[credit.token]); unusedTokens[credit.token] -= amount; credits[id] = _repay(_accrue(credit, id), id, amount); emit RevenuePayment(credit.token, amount); return true; }

When borrower tries to repay using unusedTokens, if they request a repay amount smaller than the unusedTokens total, but larger than the maximum they owe to lender, it will overflow the principal field. At this point, borrower will owe close to 2^256 tokens to lender.

Note that it is very easy for user to enter a larger amount than currently owed, as interest is ever accruing and they may estimate it to grow faster than it did in practice until the block is executed.

Impact

When borrower repays, it can overflow and make them owe 2^256 tokens to lender.

Proof of Concept

Copy the following code to SpigotedLine.t.sol:

function test_lender_can_use_and_repay_overflow() public { deal(address(lender), lentAmount + 1 ether); deal(address(revenueToken), MAX_REVENUE); address revenueC = address(0xbeef); // need new spigot for testing bytes32 id = _createCredit(address(revenueToken), Denominations.ETH, revenueC); _borrow(id, lentAmount); bytes memory tradeData = abi.encodeWithSignature( 'trade(address,address,uint256,uint256)', address(revenueToken), Denominations.ETH, 2 gwei, lentAmount + 1 ether ); uint256 new_tokens; hoax(borrower); new_tokens = line.claimAndTrade(address(revenueToken), tradeData); console.log(new_tokens); vm.prank(lender); // prank lender line.useAndRepay(lentAmount + 1 ether); (, uint256 principal,,,,,) = line.credits(line.ids(0)); console.log("Principal is", principal); //assertEq(principal, 0); }

Tools Used

Manual audit

It is recommended to check for overflow in repay() function, as there is always a risk of introducing threats when adding more calls to repay().

#0 - c4-judge

2022-11-17T16:08:02Z

dmvt marked the issue as duplicate of #82

#1 - c4-judge

2022-12-06T17:31:56Z

dmvt marked the issue as satisfactory

#2 - C4-Staff

2022-12-20T05:50:15Z

liveactionllama marked the issue as duplicate of #461

Awards

4.0405 USDC - $4.04

Labels

bug
2 (Med Risk)
downgraded by judge
partial-50
duplicate-39

External Links

Lines of code

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

Vulnerability details

Description

The receiveTokenOrETH() utility function is used multiple times the LineOfCredit contract:

  • addCredit
  • increaseCredit
  • depositAndClose
  • depositAndRepay
  • close

Payment is accepted in ETH from the contract call, or ERC20 transfer:

function receiveTokenOrETH( address token, address sender, uint256 amount ) external returns (bool) { if(token == address(0)) { revert TransferFailed(); } if(token != Denominations.ETH) { // ERC20 IERC20(token).safeTransferFrom(sender, address(this), amount); } else { // ETH if(msg.value < amount) { revert TransferFailed(); } } return true; }

The issue is that whenever the caller sends more msg.value than amount to be received, the difference would not be refunded back to them and be permanently stuck in the contract.

Usually leaks of value are treated as Med severity, but in this instance the leak is guaranteed in the close() and depositAndClose() function. This is because user does not specify an amount, only that they wish to close the credit line. The amount is calculated in real time, using the latest accrued interest. Therefore, user cannot know the accurate amount of msg.value to send and will either send too little, making the transaction revert, or send too much, and leak the difference. Because the leak is unavoidable, I believe the finding to be of high severity.

Impact

When using ETH as credit token, there will be a guaranteed leak of value.

Tools Used

Manual audit

receiveTokenOrETH() should receive a boolean refundSpare which will send spare ETH to the msg.sender. It should only be false if the caller wants to perform several receives and wishes to perform accounting in the calling function.

#0 - c4-judge

2022-11-17T16:17:08Z

dmvt marked the issue as duplicate of #25

#1 - c4-judge

2022-11-17T19:34:18Z

dmvt changed the severity to 2 (Med Risk)

#2 - c4-judge

2022-11-17T19:34:21Z

dmvt marked the issue as partial-50

#3 - 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
sponsor acknowledged
duplicate-110

Awards

214.1771 USDC - $214.18

External Links

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/utils/SpigotedLineLib.sol#L75

Vulnerability details

Description

SpigotedLineLib's claimAndTrade function allows borrower to choose when to swap their revenue tokens to credit tokens.

function claimAndTrade( address claimToken, address targetToken, address payable swapTarget, address spigot, uint256 unused, bytes calldata zeroExTradeData ) external returns(uint256, uint256) { // can't trade into same token. causes double count for unused tokens if(claimToken == targetToken) { revert BadTradingPair(); } // snapshot token balances now to diff after trade executes uint256 oldClaimTokens = LineLib.getBalance(claimToken); uint256 oldTargetTokens = LineLib.getBalance(targetToken); // @dev claim has to be called after we get balance // reverts if there are no tokens to claim uint256 claimed = ISpigot(spigot).claimEscrow(claimToken); trade( claimed + unused, claimToken, swapTarget, zeroExTradeData ); // 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 uint256 newClaimTokens = LineLib.getBalance(claimToken); // ideally we could use oracle to calculate # of tokens to receive // but sellToken might not have oracle. buyToken must have oracle emit TradeSpigotRevenue( claimToken, claimed, targetToken, tokensBought ); // used reserve revenue to repay debt if(oldClaimTokens > newClaimTokens) { uint256 diff = oldClaimTokens - newClaimTokens; // used more tokens than we had in revenue reserves. // prevent borrower from pulling idle lender funds to repay other lenders if(diff > unused) revert UsedExcessTokens(claimToken, unused); // reduce reserves by consumed amount else return ( tokensBought, unused - diff ); } else { unchecked { // excess revenue in trade. store in reserves return ( tokensBought, unused + (newClaimTokens - oldClaimTokens) ); } } }

Borrower passes calldata which is sent to 0x DEX via the trade() call:

function trade( uint256 amount, address sellToken, address payable swapTarget, bytes calldata zeroExTradeData ) public returns(bool) { if (sellToken == Denominations.ETH) { // if claiming/trading eth send as msg.value to dex (bool success, ) = swapTarget.call{value: amount}(zeroExTradeData); if(!success) { revert TradeFailed(); } } else { IERC20(sellToken).approve(swapTarget, amount); (bool success, ) = swapTarget.call(zeroExTradeData); if(!success) { revert TradeFailed(); } } return true; }

The critical issue is that borrower may use this functionality to exfiltrate their locked escrow funds out of the contract. Borrower has complete freedom on how to craft 0x DEX calldata. They have the entire escrow balance + unused balance to pass to 0x.

The only limitation is that tokensBought (credit tokens) must increase:

// 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

There are many different ways to structure the 0x call to exfiltrate the money. The simplest is probably the fillLimitOrder handler in 0x

function fillLimitOrder( LibNativeOrder.LimitOrder memory order, LibSignature.Signature memory signature, uint128 takerTokenFillAmount ) public payable returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { FillNativeOrderResults memory results = _fillLimitOrderPrivate( FillLimitOrderPrivateParams({ order: order, signature: signature, takerTokenFillAmount: takerTokenFillAmount, taker: msg.sender, sender: msg.sender }) ); LibNativeOrder.refundExcessProtocolFeeToSender(results.ethProtocolFeePaid); (takerTokenFilledAmount, makerTokenFilledAmount) = ( results.takerTokenFilledAmount, results.makerTokenFilledAmount ); }

User specifies this struct for processing:

struct LimitOrder { IERC20TokenV06 makerToken; IERC20TokenV06 takerToken; uint128 makerAmount; uint128 takerAmount; uint128 takerTokenFeeAmount; address maker; address taker; address sender; address feeRecipient; bytes32 pool; uint64 expiry; uint256 salt; }

Therefore, it is easy for borrower to prepare a trade as maker, which accepts a ton of revenue tokens, and returns a single credit token. From the trade() call, borrower will fulfill that order as a taker, and still pass the balance check performed later.

There are many more clever and devious ways to exfiltrate the money once borrower has 0x arbitrary call primitive, for example playing with the feeRecipient comes to mind.

Impact

Borrower can bypass all revenue protection for lender, essentially borrowing for free.

Tools Used

Manual audit

If it is desired to allow borrower to arbitrarily call 0x, it must be locked down very hard. Need to run balance checks on all revenue tokens, check slippage is acceptable and so on.

#0 - dmvt

2022-11-17T15:58:38Z

The warden failed to describe a vulnerability, impact, or negative result of the issue reported. There is no test proving anything the warden says, and the approach the warden is taking here doesn't look possible since it is not atomic (maker orders don't execute right away).

#1 - c4-judge

2022-11-17T15:58:42Z

dmvt marked the issue as unsatisfactory: Insufficient proof

#2 - trust1995

2022-11-19T15:24:28Z

"The warden failed to describe a vulnerability, impact, or negative result of the issue reported." - I have described a specific 0x calldata which will allow borrower to pass the locked money out to their unlocked account, which is a high severity issue.

"There is no test proving anything the warden says, and the approach the warden is taking here doesn't look possible since it is not atomic (maker orders don't execute right away)." The approach is possible because as stated, borrower will prepare a trade as maker from their own account, and then execute that trade as a taker from the Spigotted account. We don't need the maker to be atomic, we need the taker to be.

I have gone to great lengths specifying the structure passed as payload to 0x, and the function in 0x code which will facilitate this exfiltration trade. Other reports which abuse 0x calldata did not provide a POC or similar depth of explanation. The 0x contract is not integrated to the DebtDAO test suite so it is not practical to provide a test, however the explanation is technical enough so that it is clear the issue is legitimate (the calldata parameters show how we control the amount sent and received from this taker trade which will execute atomically).

#3 - dmvt

2022-11-20T19:36:03Z

You do not specifically describe how the borrower could craft this order despite your assertions to the contrary. Instead, you copy the 0x struct. The other reports show, with varying levels of clarity, how a lender could steal from a borrower using this functionality. You assert that the borrower, not the lender, would be the thief without real proof of concept. You've asserted a highly complicated scenario without much non-code description or a test to prove that what you're doing works. The code you do provide is entirely copied and pasted from the source.

The approach is possible because as stated, borrower will prepare a trade as maker from their own account, and then execute that trade as a taker from the Spigotted account. We don't need the maker to be atomic, we need the taker to be.

The lack of clarity in your report is why I misread this on the first pass. Of course, that doesn't materially matter given all of the other deficiencies.

The other report you link to will likely be marked down to 50% due to its relatively sparse condition in the final judging phase. I don't typically do that during presort. The types of comments you're making on your issues are also meant to be made during the post-judging QA phase on the discussion thread, not directly on your own issues. Sock just posted about this, and you commented in response, so I expect you already know that without my having to mention it here. Based on your comments he amended the original post. You are not "clearly providing additional facts which demonstrate inaccurate assumptions in the judge's response" but are instead copying your original post and disputing the ratings I have given.

#4 - trust1995

2022-11-20T19:57:09Z

I haven't violated a single regulation regarding response to judge's decisions. I am keeping the discussion fact based and making sure the submission is read as intended (which was important because you thought the 0x calldata is maker while it is explicitly stated it is taker). I will continue by stating the burden of proof you require from my attack is inconsistent with other judges or other submissions you have accepted in this specific contest.

You are saying I did not provide a POC that borrower could be the thief rather than lender, but the function claimAndTrade() is clearly callable by both parties. As a warden I have no idea what other submissions will state and it seems like you are putting the blame on me for coming up with an attack on a code that others found a different way to attack.

The attack is not "highly complicated scenario" because I've shown the code flow to the trade call with 0x calldata and references documentation which details the different parameters available to be provided. These parameters include taker, taker token, taker amount, maker, maker token and maker amount, which is suffice to be convinced that the attack would work. Pre-marking the finding as unsatisfactory, without giving sponsor a chance to look and make up their mind about it, is as strict a ruling as I've seen in my 10+ contests in the past 3 months.

#5 - c4-sponsor

2022-12-04T19:32:05Z

kibagateaux marked the issue as sponsor confirmed

#6 - c4-sponsor

2022-12-04T19:32:13Z

kibagateaux marked the issue as sponsor acknowledged

#7 - c4-judge

2022-12-08T19:34:25Z

dmvt marked the issue as duplicate of #88

#8 - c4-judge

2022-12-08T19:34:32Z

dmvt marked the issue as satisfactory

#9 - dmvt

2022-12-08T19:37:00Z

This is effectively the same issue but with a different actor. If it is valid, so is #363, but the issue is the same and can be summarized as "nothing prevents loan participants from crafting malicious 0x trades"

#10 - c4-judge

2022-12-08T19:37:29Z

dmvt changed the severity to 2 (Med Risk)

#11 - dmvt

2022-12-08T19:39:10Z

Additionally, I believe that the external requirements with regard to 0x, the relatively unlikely nature of this attack happening, and the fact that there is very little to gain relative to the potential reputational loss makes this set of issues medium severity.

#12 - C4-Staff

2022-12-16T23:20:47Z

captainmangoC4 marked the issue as duplicate of #110

Lines of code

https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/modules/factories/LineFactory.sol#L135

Vulnerability details

Description

LineFactory is in charge of creating new lines. deploySecuredLineWithModules() creates a new secure line using an existing spigot and escrow, and new CoreLineParams parameters. There is a dev note hinting to this issue:

* @dev The `cratio` in the CoreParams are not used, due to the fact * they're passed in when the Escrow is created separately. */

The problem is that caller passes a cratio in CoreLineParams, but it is completely ignored because the one in the existing escrow is used. There isย no verification that the intended cratio == mSpigot's cratio. User could easily think the new ratio will be applied, but actually, the old one would be reused.

Since they are not even warned about this at any stage, they may suffer a surprising liquidation when their collateral does not suffice for the re-used cratio.

Impact

Borrowers may unknowingly use a previous collateral ratio when deploying a secured line with existing modules.

Tools Used

Manual audit

add a requirement:

require(coreParams.cratio == Escrow(mEscrow).minimumCollateralRatio())

#0 - c4-judge

2022-11-17T16:14:04Z

dmvt marked the issue as unsatisfactory: Insufficient proof

#1 - trust1995

2022-11-19T16:02:44Z

Factually disagree with insufficient proof. I've provided the lines where coreParams is used, but it's cratio field is never used. Instead, they are passed in the mEscrow. I've also listed the developer doc showing that it is not used.

#2 - dmvt

2022-12-07T16:48:25Z

You haven't shown how funds are compromised. At best, this is documentation. I'll downgrade to QA.

#3 - c4-judge

2022-12-07T16:48:34Z

dmvt changed the severity to QA (Quality Assurance)

#4 - c4-judge

2022-12-07T16:48:39Z

dmvt marked the issue as grade-b

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