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
Rank: 1/120
Findings: 6
Award: $11,613.69
๐ Selected for report: 1
๐ Solo Findings: 0
๐ Selected for report: Lambda
Also found by: HE1M, Trust, adriro, berndartmueller, minhquanym
1468.9791 USDC - $1,468.98
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
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; }
When lender consents before borrower in ETH credit token, all the lent funds are permanently lost.
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
8731.9287 USDC - $8,731.93
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:
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]!
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:
Borrower can craft a borrow that cannot be liquidated, even by arbiter. Alternatively, functionality may be completely impaired through no fault of users.
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(); }
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
๐ Selected for report: berndartmueller
Also found by: 0xdeadbeef0x, Trust, adriro, aphak5010, hansfriese, rvierdiiev
1133.2124 USDC - $1,133.21
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.
When borrower repays, it can overflow and make them owe 2^256 tokens to lender.
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); }
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
๐ Selected for report: 0xdeadbeef0x
Also found by: 8olidity, Ch_301, HE1M, Koolex, Lambda, Nyx, RedOneN, Ruhum, Tomo, Trust, adriro, aphak5010, ayeslick, berndartmueller, brgltd, carlitox477, cccz, codexploder, d3e4, eierina, eighty, immeas, joestakey, lotux, minhquanym, perseverancesuccess, rbserver, rvierdiiev
4.0405 USDC - $4.04
The receiveTokenOrETH() utility function is used multiple times the LineOfCredit contract:
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.
When using ETH as credit token, there will be a guaranteed leak of value.
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
๐ Selected for report: perseverancesuccess
Also found by: 0x52, HE1M, Lambda, Trust, adriro, aphak5010, cccz, minhquanym
214.1771 USDC - $214.18
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.
Borrower can bypass all revenue protection for lender, essentially borrowing for free.
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
๐ Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xRoxas, 0xSmartContract, Awesome, Aymen0909, B2, BClabs, Bnke0x0, Deekshith99, Deivitto, Diana, Dinesh11G, Funen, HE1M, HardlyCodeMan, Josiah, Nyx, Rahoz, RaymondFam, RedOneN, ReyAdmirado, Rolezn, Saintcode_, TomJ, Trust, __141345__, a12jmx, adriro, ajtra, aphak5010, apostle0x01, brgltd, btk, bulej93, c3phas, carlitox477, catwhiskeys, ch0bu, chaduke, chrisdior4, cryptonue, cryptostellar5, csanuragjain, ctf_sec, delfin454000, djxploit, durianSausage, erictee, fatherOfBlocks, gogo, i_got_hacked, immeas, joestakey, jumpdest7d, lukris02, martin, mcwildy, merlin, minhquanym, oyc_109, pashov, peanuts, pedr02b2, rbserver, rotcivegaf, rvierdiiev, sakman, saneryee, seyni, shark, slowmoses, tnevler, trustindistrust, w0Lfrum, yurahod, zaskoh
61.3462 USDC - $61.35
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.
Borrowers may unknowingly use a previous collateral ratio when deploying a secured line with existing modules.
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