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: 4/120
Findings: 8
Award: $7,858.73
π Selected for report: 1
π Solo Findings: 1
π Selected for report: berndartmueller
Also found by: 0xdeadbeef0x, Trust, adriro, aphak5010, hansfriese, rvierdiiev
1133.2124 USDC - $1,133.21
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/modules/credit/SpigotedLine.sol#L137-L151 https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/CreditLib.sol#L168-L195
Borrower debt can be increased due to overflow error. Malicious lender or borrower by mistake can increase debt of borrower to very huge amount.
SpigotedLine.useAndRepay
function allows to repay credit using unsued amount of credit tokens that is controlled by SpigotedLine.
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; }
Note, that there is no any check that amount provided by caller is less or equal to credit.principal + credit.interests.
That means that borrower or lender can provide it by mistake or maliciously.
Then function LineOfCredit._repay
will be called that will just pass amount to the CreditLib.repay
.
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; } } }
As you can see this function uses unchecked
for calculation. That's why in case when the provided value is bigger than credit.principal + credit.interestAccrued
we will have overflow here.
As a result now, users debt became super big, while it should be 0.
How this can be used? First of all by lender. He can call this function and provide needed amount for overflow. But there is one condition that should be met. Unused amount of tokens should be bigger then credit.principal + credit.interestAccrued
.
This is the test that will show how it works. Copy it to SpigotedLine.t.sol.
function test_principal_become_very_huge() 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); uint256 principal; uint256 interestAccrued; (, principal, interestAccrued,,,,) = line.credits(line.ids(0)); //amount is bigger than principal and interestAccrued to make oferflow uint256 amountThatShouldBeUnused = principal + interestAccrued + 1; bytes memory tradeData = abi.encodeWithSignature( 'trade(address,address,uint256,uint256)', address(revenueToken), Denominations.ETH, 1 gwei, amountThatShouldBeUnused ); hoax(borrower); line.claimAndTrade(address(revenueToken), tradeData); uint256 unusedAmount = line.unused(address(revenueToken)); //unused amount should be bigger then amount we provide to useAndRepay console.log("unused:", unusedAmount); (, principal, interestAccrued,,,,) = line.credits(line.ids(0)); //now principal is lentAmount console.log("principal:", principal); console.log("interest:", interestAccrued); vm.prank(lender); // prank lender //here is the attack line.useAndRepay(amountThatShouldBeUnused); (, principal, interestAccrued,,,,) = line.credits(line.ids(0)); //principal is too big now console.log("princ:", principal); console.log("interest:", interestAccrued); }
VsCode
Do not pass amount that is bigger than credit.principal + credit.interestAccrued
to _repay
function.
#0 - c4-judge
2022-11-15T20:32:38Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2022-11-30T15:07:20Z
kibagateaux marked the issue as sponsor confirmed
#2 - c4-judge
2022-12-06T17:31:49Z
dmvt marked the issue as satisfactory
#3 - C4-Staff
2022-12-20T05:50:16Z
liveactionllama marked the issue as duplicate of #461
π Selected for report: rvierdiiev
5821.2858 USDC - $5,821.29
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/modules/credit/LineOfCredit.sol#L223-L244 https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/LineLib.sol#L59-L74
Borrower can by mistake add own money to credit if credit is in ETH.
Function LineOfCredit.addCredit
is used to create new credit.
It can be called only after contest of another party.
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; }
LineLib.receiveTokenOrETH(token, lender, amount)
is responsible for getting payment.
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/LineLib.sol#L59-L74
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; }
As you can see in case of native token payment, sender
is not checked to be msg.sender
, so this makes it's possible that borrower can by mistake pay instead of lender. It sounds funny, but it's possible. What is needed is that lender call addCredit
first and then borrower calls addCredit
and provides value.
VsCode
Check that if payment in ETH then lender == msg.sender
in addCredit
function.
#0 - c4-judge
2022-11-14T18:21:07Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2022-11-30T18:05:39Z
kibagateaux marked the issue as sponsor confirmed
#2 - c4-judge
2022-12-06T15:00:57Z
dmvt marked the issue as satisfactory
#3 - c4-judge
2022-12-06T15:01:01Z
dmvt marked the issue as selected for report
π Selected for report: aphak5010
Also found by: HE1M, Jeiwan, SmartSek, hansfriese, minhquanym, rvierdiiev
339.9637 USDC - $339.96
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/MutualConsent.sol#L38-L60 https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/MutualConsent.sol#L31-L36
Borrower or Lender can't remove their consent from MutualConsent
if they need.
MutualConsent
has mutualConsent
modifier which is used to protect functions that need to have consent of 2 parties before call. Example of such function is LineOfCredit.addCredit
.
This is how mutual consent checking works https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/MutualConsent.sol#L38-L60
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; }
As you can see the function store consent of first caller and then waits when another caller will call same function. There is no any ability to remove this contest.
How this can be used?
For example at t1 lender and borrower aggreed on some credit for amount of tokens and specific fee rates. Then borrower calls createCredit
and his consent is stored.
But lender do not call createCredit
.
After some time smth happened and rates changed, so it's not good credit conditions for a borrower anymore, but very good for lender.
And lender calls createCredit
. As borrower consent already stored, this credit will be created and fees will start accumulate.
VsCode
Add ability to remove consent, or think about some expiration time for consents.
#0 - c4-judge
2022-11-15T16:51:30Z
dmvt marked the issue as duplicate of #33
#1 - c4-judge
2022-12-06T16:51:06Z
dmvt marked the issue as satisfactory
π 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
8.0811 USDC - $8.08
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/modules/credit/LineOfCredit.sol#L223-L244 https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/LineLib.sol#L59-L74
Function LineOfCredit.addCredit
do not return overpaid in ETH amount and do not add it to credit. As a result if lender overpaid by mistake, he loses those funds.
Function LineOfCredit.addCredit
allows to create new credit and fund it with ERC20 token or ETHER.
LineLib.receiveTokenOrETH
function is used to receive payment.
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; }
This function do not return overpaid amount to lender and also this overpaid amount is not added to credit line(this is correct). As result lender can lose funds by mistake.
VsCode
Return overpaid amount back to lender.
#0 - c4-judge
2022-11-14T18:21:56Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2022-11-30T18:02:12Z
kibagateaux marked the issue as sponsor confirmed
#2 - c4-judge
2022-12-06T15:05:00Z
dmvt marked the issue as satisfactory
#3 - C4-Staff
2022-12-20T06:44:54Z
liveactionllama marked the issue as duplicate of #39
π Selected for report: adriro
Also found by: Jeiwan, Ruhum, berndartmueller, bin2chen, rvierdiiev
440.6937 USDC - $440.69
Currently SpigotLib
stores whitelisted functions per all revenue contracts, but it should store them for each revenue contract separately as whitelisted function in one revenue contract can be malicious in another.
When Spigot
is deployed for the line, then borrower with approve of arbiter can add revenue contract to Spigot
. This revenue contract should have some functions to work with it through the Spigot
(transferOwnership, claimRevenue). It's not allowed to use all functions of revenue contract, because of security reasons(as it can transfer ownership or claim revenue). That's why there is ability to provide whitelisted functions using SpigotedLine.updateWhitelist
function.
The problem is that when whitelisted function selector is stored it is stored for all revenue contracts, not only for specific one. https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/SpigotLib.sol#L208-L213
function updateWhitelistedFunction(SpigotState storage self, bytes4 func, bool allowed) external returns (bool) { if(msg.sender != self.owner) { revert CallerAccessDenied(); } self.whitelistedFunctions[func] = allowed; emit UpdateWhitelistFunction(func, allowed); return true; }
And then when SpigotLib.operate
is called by operator, no matter which revenue contract he wants to call, function just should be whitelisted. If it is whitelisted then call is possible to any revenue contract.
How attacker can use it.
1.Attacker creates simple revenue contract with function that is called doSomeJob()
.
2.Arbiter checks that revenue contract contains transferOwnership
,claimRevenue
functions that are not harmful.
3.They call addSpigot
and add this revenue contract to Spigot
.
4.Operator asks Arbiter to include doSomeJob()
function as it is needed to do revenue.
5.After the check arbiter calls updateWhitelist
and now function updateWhitelist
is possible to call for every revenue contract.
6.Some time later attacker creates another contract with all valid functions and doSomeJob()
function which does smth harmful(like claim revenue and mess up all calculations or transfer ownership to attacker).
7.Arbiter checks that new revenue contract contains transferOwnership
,claimRevenue
functions that are not harmful.
8.They call addSpigot
and add this revenue contract to Spigot
.
9.Now attacker is possible to call this function on new revenue contract to break things.
Also it's possible that harmful function is provided in the first revenue contract and it's allowance to call will be whitelisted with the second revenue contract which implements that function without bad actions, just to get it whitelisted. Then when function is whitelisted, attacker calls it on first revenue contract.
VsCode
Consider to have whitelisted functions per revenue contract.
#0 - c4-judge
2022-11-15T20:03:34Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2022-11-30T16:41:17Z
kibagateaux marked the issue as sponsor acknowledged
#2 - kibagateaux
2022-11-30T16:42:09Z
definitely valid. Owner has control over contracts that can be called and the whitelisted functions so its expected they will not allow any upgradeable contracts (many security risk) and will check functions across contracts before enabling
#3 - c4-judge
2022-12-06T17:21:54Z
dmvt marked the issue as satisfactory
#4 - C4-Staff
2022-12-20T06:09:29Z
liveactionllama marked the issue as duplicate of #312
π Selected for report: __141345__
Also found by: Bnke0x0, Ch_301, Jeiwan, Lambda, Ruhum, aphak5010, ayeslick, cccz, codexploder, everyanykey, hansfriese, ladboy233, minhquanym, pashov, rbserver, rvierdiiev
48.8098 USDC - $48.81
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/modules/credit/LineOfCredit.sol#L223-L244 https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/LineLib.sol#L59-L74
Function LineOfCredit.addCredit
do not support non standard ERC20 tokens. This leads to incorrect calculations and lose of funds.
Function LineOfCredit.addCredit
allows to create new credit and fund it with ERC20 token or ETHER.
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; }
This is how transfer is handled.
LineLib.receiveTokenOrETH
function is used to receive payment.
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; }
As you can see amount was just transferred without check of balance of contract before and after.
Later the amount provided in addCredit
is set to the credit info. In case of non standard erc20 token the provided amount will be wrong and all interest calculations will be also incorrect.
That means that current implementation can't handle different non standard tokens, like fee on transfer, rebase tokens.
VsCode
Calculate transferred amount by checking balance before and after the transfer.
#0 - c4-judge
2022-11-15T14:25:15Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2022-11-30T18:00:31Z
kibagateaux marked the issue as sponsor disputed
#2 - kibagateaux
2022-11-30T18:01:52Z
not our responsibility to support non-standard ERC-20, thats why they are non-standard.
Up to borrower/lender to approve tokens, Debt DAO is not responsible.
#3 - c4-judge
2022-12-06T16:34:46Z
dmvt marked the issue as satisfactory
#4 - C4-Staff
2022-12-20T06:01:34Z
liveactionllama marked the issue as duplicate of #367
π Selected for report: __141345__
Also found by: 0xdeadbeef0x, 8olidity, Amithuddar, Bnke0x0, Ch_301, Deivitto, IllIllI, KingNFT, Nyx, RaymondFam, RedOneN, Satyam_Sharma, SmartSek, Tomo, adriro, bananasboys, carlitox477, cccz, cloudjunky, codexploder, corerouter, cryptonue, d3e4, datapunk, joestakey, martin, merlin, minhquanym, pashov, peanuts, rvierdiiev
5.3388 USDC - $5.34
LineLib.sendOutTokenOrETH function use transfer to send ether. If receiver is not EOA then the call can revert because of not enough amount of gas.
function sendOutTokenOrETH( address token, address receiver, uint256 amount ) external returns (bool) { if(token == address(0)) { revert TransferFailed(); } // both branches revert if call failed if(token!= Denominations.ETH) { // ERC20 IERC20(token).safeTransfer(receiver, amount); } else { // ETH payable(receiver).transfer(amount); } return true; }
If payment should be done in ether, then transfer
is called which provide 2300 amount of gas to the call. If receiver is not EOA this amount can be not enough and this will cause all payments to revert and user will not be able to get his funds.
Actually all loan repayment will fail if all conditions are met.
VsCode
Use call
instead of `transfer.
#0 - c4-judge
2022-11-15T14:31:16Z
dmvt marked the issue as duplicate of #14
#1 - c4-judge
2022-12-06T14:58:22Z
dmvt marked the issue as satisfactory
#2 - C4-Staff
2022-12-20T05:56:44Z
liveactionllama marked the issue as duplicate of #369
π 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
SecuredLine.rollover
function allows user to transfer the Escrow and Spigot from repaid secured line to another line. In case if line is CreditOfLine which do not know how to work with Spigot, ownersip of Spigot will be lost.
SecuredLine.rollover
function changes line for the Escrow
and transfers ownership of Spigot
to the new line.
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/modules/credit/SecuredLine.sol#L48-L66
function rollover(address newLine) external onlyBorrower override returns(bool) { // require all debt successfully paid already if(status != LineLib.STATUS.REPAID) { revert DebtOwed(); } // require new line isn't activated yet if(ILineOfCredit(newLine).status() != LineLib.STATUS.UNINITIALIZED) { revert BadNewLine(); } // we dont check borrower is same on both lines because borrower might want new address managing new line EscrowedLine._rollover(newLine); SpigotedLineLib.rollover(address(spigot), newLine); // ensure that line we are sending can accept them. There is no recovery option. if(ILineOfCredit(newLine).init() != LineLib.STATUS.ACTIVE) { revert BadRollover(); } return true; }
function rollover(address spigot, address newLine) external returns(bool) { require(ISpigot(spigot).updateOwner(newLine)); return true; }
As result ownership of Spigot
will be completely lost and can't be recovered as LineOfCredit
do not have such function.
Also all functions that rely on owner will not possible to call.
Regarding to the Escrow it also can make some problems. After rollover Escrow will have new line and this line will never be changed, because LineOfCredit
do not have such function.
In case if rollover was called for SpigotedLine
then only Escrow
will have problems. Borrower will not be able to withdraw his collateral funds while credit is not repaid.
And in case if this SpigotedLine
will become liquidateable and borrower didn't withdraw collateral before, then he will not be able to withdraw collateral anymore, even if SpigotedLine
is not suppose to use any Escrow.
This is simple test that shows, that you can rollover to CreditOfLine.
function test_rollover_to_credit_of_line() public { _addCredit(address(supportedToken1), 1 ether); bytes32 id = line.ids(0); hoax(borrower); line.borrow(id, 1 ether); hoax(borrower); line.depositAndClose(); LineOfCredit lineOfCredit = new LineOfCredit( address(oracle), arbiter, borrower, 150 days); hoax(borrower); line.rollover(address(lineOfCredit)); assertEq(address(lineOfCredit), spigot.owner()); assertEq(address(lineOfCredit), escrow.line()); }
VsCode
You could add some function that indicates that line is SecuredLine and then allow rollover only to such function.
#0 - c4-judge
2022-11-15T20:23:46Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2022-11-30T16:31:29Z
kibagateaux marked the issue as sponsor acknowledged
#2 - c4-sponsor
2022-11-30T16:31:39Z
kibagateaux marked the issue as disagree with severity
#3 - kibagateaux
2022-11-30T16:33:20Z
Technically not possible since we only deploy SecuredLine in our system, if someone was deploying contracts on their own, of course they could misconfigure it like any contract but impossible on our deployed system.
#4 - c4-judge
2022-12-06T17:27:47Z
dmvt changed the severity to QA (Quality Assurance)
#5 - c4-judge
2022-12-06T17:27:59Z
dmvt marked the issue as grade-b