Debt DAO contest - HE1M'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: 13/120

Findings: 5

Award: $1,981.42

QA:
grade-b

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: Lambda

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

Labels

bug
3 (High Risk)
satisfactory
sponsor confirmed
edited-by-warden
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#L234 https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/utils/LineLib.sol#L71

Vulnerability details

Impact

If a lender would like to lend ERC20 token, the protocol works properly. The lender gives allowance to the line of credit and calls addCredit to sign the consent. Later, the borrower calls this function, and signs the consent. Since two signatures are provided, the credit will be created and the ERC20 token will be transferred from lender to the line of credit contract. Everything is working as expected.

But if the lender would like to lend ETH, and call addCredit earlier than borrower, the lender will lose the fund completely. Please read the proof of concept section regarding this scenario.

Proof of Concept

Based on the document:

To create a credit line within the Line of Credit facility, a Lender needs to make a first deposit. The deposit can only succeed if both Borrower and Lender consent and mutually sign a 2/2 transaction as evidence of agreement.

Suppose a lender decides to make a first ETH deposit. So, s/he calls addCredit and transfers some amount of ETH. Since, the borrower has not yet called this function, the lender's call only transfers the ETH and sign the consent in the mapping mutualConsents[newHash] = true. But the credit will not be created, because the modifier mutualConsent needs two signatures to proceed with credit creation.

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; }

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

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; }

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

So, the ETH is withdrawn from lender and received by LineOfCredit, but no credit is created yet. Later when borrower calls addCredit, it will be reverted, because msg.value is not equal to amount.

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; }

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

The situation can be even worse, if the borrower tricks the lenders and tell them that s/he (borrower) has already called addCredit, so the lenders will assume that it is safe to transfer ETH trough calling addCredit(...), but in reality the borrower has not called it yet. So, the lenders call this function, and will lost their ETH. They can not even withdraw it, because no credit is created for lenders.

Tools Used

There can be some solutions:

  • If the lender is going to send ETH, s/he should be after the borrower.
  • The ETH sent by lender should be tracked in the modifier by a mapping pendingDeposit[lender] = msg.value. So that if borrower later calls this function, it checks this mapping, and proceeds to the next step. It should be also claimable by the lender.
  • If lender is sending ETH, and the modifier returns false, then it should revert.

#0 - dmvt

2022-11-15T15:30:43Z

This is rather hard to understand, IMO, so I'm going to add some color. Because the modifier does not revert in a scenario where the borrower has not yet signed consent, the transaction is successful even though _mutualConsent returns false. This results in the guarded function addCredit not being executed, but the ETH is still transferred.

#1 - c4-judge

2022-11-15T15:30:54Z

dmvt marked the issue as primary issue

#2 - c4-sponsor

2022-11-30T15:07:55Z

kibagateaux marked the issue as sponsor confirmed

#3 - c4-judge

2022-12-06T17:07:37Z

dmvt marked the issue as satisfactory

#4 - C4-Staff

2022-12-20T06:32:32Z

liveactionllama marked the issue as duplicate of #125

Findings Information

🌟 Selected for report: aphak5010

Also found by: HE1M, Jeiwan, SmartSek, hansfriese, minhquanym, rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
edited-by-warden
duplicate-33

Awards

339.9637 USDC - $339.96

External Links

Lines of code

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

Vulnerability details

Impact

There is no mechanism to cancel a consent. This can happen if a lender decides to not consent on an agreement OR the parameters sent in function addCredit are not correct.

Proof of Concept

If for example a lender consented already by invoking the function addCredit, then there is no mechanism to cancel this consent. Because, any function with theses modifiers mutualConsent and mutualConsentById will set the consent in the mapping mutualConsents, and there is no way to delete it except if other party (borrower) executes this consent (that is a pity for the lender who wanted to cancel).

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; }

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

This applies to all functions that contain these modifiers.

Tools Used

A simple function just to delete a consent should be added.

#0 - c4-judge

2022-11-15T15:03:33Z

dmvt marked the issue as duplicate of #33

#1 - c4-judge

2022-12-06T16:51:15Z

dmvt marked the issue as satisfactory

Awards

4.0405 USDC - $4.04

Labels

2 (Med Risk)
partial-50
duplicate-39

External Links

Judge has assessed an item in Issue #35 as M risk. The relevant finding follows:

No. 2 Any smart contract that uses transfer() or send() is taking a hard dependency on gas costs by forwarding a fixed amount of gas: 2300. If gas costs are subject to change, then smart contracts can’t depend on any particular gas costs.

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; }

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

#0 - c4-judge

2022-12-06T16:52:41Z

dmvt marked the issue as duplicate of #25

#1 - c4-judge

2022-12-06T16:52:47Z

dmvt marked the issue as partial-50

#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
partial-50
edited-by-warden
duplicate-110

Awards

107.0886 USDC - $107.09

External Links

Lines of code

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

Vulnerability details

Impact

During trading claimToken with targetToken, the calldata provided for the 0x router is an arbitrary data that can be provided by the borrower or lender. https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/utils/SpigotedLineLib.sol#L135

If lender provides the calldata so that that more claimToken is consumed than expected, the borrower is impacted (loses some fund).

Proof of Concept

If a lender (malicious) would like to claim and repay, the flow is as follows: claimAndRepay(address claimToken, bytes calldata zeroExTradeData) => function _claimAndTrade(address claimToken, address targetToken, bytes calldata zeroExTradeData) => function claimAndTrade(address claimToken, address targetToken, address payable swapTarget, address spigot, uint256 unused, bytes calldata zeroExTradeData) => function trade(uint256 amount, address sellToken, address payable swapTarget, bytes calldata zeroExTradeData)

https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/modules/credit/SpigotedLine.sol#L93 https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/modules/credit/SpigotedLine.sol#L187 https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/utils/SpigotedLineLib.sol#L53 https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/utils/SpigotedLineLib.sol#L120

Assuming the targetToken is DAI, and claimToken is USDC, if the calldata zeroExTradeData is something that calls the function buy 100 DAI for any amount of USDC instead of buy 100 DAI for maximum amount of 105 USDC, it is highly possible that more than 105 USDC will be consumed to trade for 100 DAI. In this case, the borrower loses more claimToken than expected.

In summary, since the calldata is arbitrary, the lender can put these data buy X targetToken for any amount of claim token or sell X claimToken for any amount of targetToken, in both cases the borrower may lose fund. While the correct call data is: buy X targetToken for max Y claimToken or sell X claimToken for min Y targetToken.

Tools Used

The calldata zeroExTradeData should be set in advance that which function of the router is going to be called. For example in case of uniswap router, it should be abi.encodeWithSignature("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)", amountIn, amountOutMin, path, to, dedline). In which the amountIn and amountOutMin should be a factor of each other so the borrower will not lose revenue token a lot.

#0 - c4-judge

2022-11-17T15:46:51Z

dmvt marked the issue as duplicate of #88

#1 - c4-judge

2022-11-17T20:47:15Z

dmvt changed the severity to 2 (Med Risk)

#2 - c4-judge

2022-12-08T19:53:11Z

dmvt marked the issue as partial-50

#3 - C4-Staff

2022-12-16T23:20:48Z

captainmangoC4 marked the issue as duplicate of #110

No. 1

Better to check that if an ERC20 token is going to be transferred to the protocol, no ETH should be sent:

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

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

No. 2

Any smart contract that uses transfer() or send() is taking a hard dependency on gas costs by forwarding a fixed amount of gas: 2300. If gas costs are subject to change, then smart contracts can’t depend on any particular gas costs.

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; }

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

No. 3

It is possible to apply grieving attack. When a borrower is going to call rollover(address newLine), the attacker calls the function init() in the address of new line, so the status of the new line will change to LineLib.STATUS.ACTIVE. https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/modules/credit/SecuredLine.sol#L48

Now, the borrower transaction for rolling over will be reverted, because the status of new line is not LineLib.STATUS.UNINITIALIZED. https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/modules/credit/SecuredLine.sol#L57

No. 4

Line 234 will never be called, it can be removed: https://github.com/debtdao/Line-of-Credit/blob/c1037d7f873cfb2af48fdaf00e44cd55c7737fc1/contracts/utils/SpigotedLineLib.sol#L234

No. 5

Maybe better to use staticcall to get decimals: https://github.com/debtdao/Line-of-Credit/blob/f32cb3eeb08663f2456bf6e2fba21e964da3e8ae/contracts/utils/CreditLib.sol#L142

#0 - c4-judge

2022-12-06T16:51:46Z

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