Debt DAO contest - 0x1f8b'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: 25/120

Findings: 2

Award: $994.67

QA:
grade-b
Gas:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Low

1. Mixing and Outdated compiler

The pragma version used are:

pragma solidity ^0.8.9; pragma solidity 0.8.9;

Note that mixing pragma is not recommended. Because different compiler versions have different meanings and behaviors, it also significantly raises maintenance costs. As a result, depending on the compiler version selected for any given file, deployed contracts may have security issues.

The minimum required version must be 0.8.17; otherwise, contracts will be affected by the following important bug fixes:

0.8.13:

  • Code Generator: Correctly encode literals used in abi.encodeCall in place of fixed bytes arguments.

0.8.14:

  • ABI Encoder: When ABI-encoding values from calldata that contain nested arrays, correctly validate the nested array length against calldatasize() in all cases.
  • Override Checker: Allow changing data location for parameters only when overriding external functions.

0.8.15

  • Code Generation: Avoid writing dirty bytes to storage when copying bytes arrays.
  • Yul Optimizer: Keep all memory side-effects of inline assembly blocks.

0.8.16

  • Code Generation: Fix data corruption that affected ABI-encoding of calldata values represented by tuples: structs at any nesting level; argument lists of external functions, events and errors; return value lists of external functions. The 32 leading bytes of the first dynamically-encoded value in the tuple would get zeroed when the last component contained a statically-encoded array.

0.8.17

  • Yul Optimizer: Prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call.

Apart from these, there are several minor bug fixes and improvements.

2. Open TODO

The code that contains "open todos" reflects that the development is not finished and that the code can change a posteriori, prior release, with or without audit.

Affected source code:

TODO: test

3. Lack of address checks

The following methods have a lack of checks if the received argument is an address, it's good practice in order to reduce human error to check that the address specified in the constructor or initialize is different than address(0).

Also, the EIP-165 standard helps detect that a smart contract implements the expected logic, prevents human error when configuring smart contract bindings, so it is recommended to check that the received argument is a contract and supports the expected interface.

Reference:

Affected source code:

4. Don't reduce safety for gas savings

The CreditLib.accrue method says:

// interest will almost always be less than deposit // low risk of overflow unless extremely high interest rate

This low risk is not "risk free", so it doesn't make sense to have an unchecked region in this code.

  function accrue(
    ILineOfCredit.Credit memory credit,
    bytes32 id,
    address interest
  )
    public
    returns (ILineOfCredit.Credit memory)
  { unchecked {
      // interest will almost always be less than deposit
      // low risk of overflow unless extremely high interest rate

      // get token demoninated interest accrued
      uint256 accruedToken = IInterestRateCredit(interest).accrueInterest(
          id,
          credit.principal,
          credit.deposit
      );

      // update credit line balance
      credit.interestAccrued += accruedToken;

      emit InterestAccrued(id, accruedToken);
      return credit;
  } }

Affected source code:

5. Send ether in an un-compatible way

Because to transfer ether the .transfer method (which is capped at 2300 gas) is used instead of .call which is limited to the gas provided by the user. If a contract that has a fallback method more expensive than 2300 gas, creates an offer, it could be impossible for this contract to send the funds to the buyer or seller.

Reference:

  • transfer -> The receiving smart contract should have a fallback function defined or else the transfer call will throw an error. There is a gas limit of 2300 gas, which is enough to complete the transfer operation. It is hardcoded to prevent reentrancy attacks.
  • send -> It works in a similar way as to transfer call and has a gas limit of 2300 gas as well. It returns the status as a boolean.
  • call -> It is the recommended way of sending ETH to a smart contract. The empty argument triggers the fallback function of the receiving address.

Affected source code:

Recommended Mitigation Steps:

  • Use call instead.

Non critical

6. Add missing @param information

Some parameters have not been commented, which reflects that the logic has been updated but not the documentation, it is advisable that developers update both at the same time to avoid the code being out of sync with the project documentation.

  /**
    * see ILineOfCredit._createCredit
    * @notice called by LineOfCredit._createCredit during every repayment function
+   * @param id - Fill me
+   * @param amount - Fill me
+   * @param lender - Fill me
+   * @param token - Fill me
    * @param oracle - interset rate contract used by line that will calculate interest owed
   */
  function create(
      bytes32 id,
      uint256 amount,
      address lender,
      address token,
      address oracle
  )

Affected source code:

7. Wrong return

The method removePosition present inside the library CreditListLib return true when the id was not found, something that should be fixed in order to prevent a wrong use of this method.

Affected source code:

#0 - c4-judge

2022-12-06T21:47:18Z

dmvt marked the issue as grade-b

Awards

933.3247 USDC - $933.32

Labels

bug
G (Gas Optimization)
grade-a
G-19

External Links

Gas

1. Use a recent solidity version

Using an updated version of solidity you can achieve significant gas savings, like the one mentioned above:

  • With at least 0.8.10 to skip existence check for external contract if return data is expected. In this case, the ABI decoder will revert if the contract does not exist.
  • With at least 0.8.16 to have a more efficient code for checked addition and subtraction.
  • With at least 0.8.17 to have a more efficient overflow checks for multiplication.

2. Avoid compound assignment operator in state variables

Using compound assignment operators for state variables (like State += X or State -= X ...) it's more expensive than using operator assignment (like State = State + X or State = State - X ...).

Proof of concept (without optimizations):

pragma solidity 0.8.15;

contract TesterA {
uint256 private _a;
function testShort() public {
_a += 1;
}
}

contract TesterB {
Uint256 private _a;
function testLong() public {
_a = _a + 1;
}
}

Gas saving executing: 13 per entry

TesterA.testShort: 43507 TesterB.testLong: 43494

Affected source code:

Total gas saved: 13 * 16 = 256

3. Remove empty blocks

An empty block or an empty method generally indicates a lack of logic that it’s unnecessary and should be eliminated, call a method that literally does nothing only consumes gas unnecessarily, if it is a virtual method which is expects it to be filled by the class that implements it, it is better to use abstract contracts with just the definition.

Sample code:

pragma solidity >=0.4.0 <0.7.0;

abstract contract BaseContract {
    function totalSupply() public virtual returns (uint256);
}

Reference:

Affected source code:

4. Reduce math operations

Several methods have been found in which it is possible to reduce the number of mathematical operations, the cases are detailed below.

Affected source code:

Use scientific notation rather than exponentiation:

5. Reduce the size of error messages (Long revert Strings)

Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.

Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.

Proof of concept (without optimizations):

pragma solidity 0.8.15;

contract TesterA {
function testShortRevert(bool path) public view {
require(path, "test error");
}
}

contract TesterB {
function testLongRevert(bool path) public view {
require(path, "test big error message, more than 32 bytes");
}
}

Gas saving executing: 18 per entry

TesterA.testShortRevert: 21886 TesterB.testLongRevert: 21904

Affected source code:

Total gas saved: 18 * 1 = 18

6. Use Custom Errors instead of Revert Strings to save Gas

Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)

Source Custom Errors in Solidity:

Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.

Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).

Proof of concept (without optimizations):

pragma solidity 0.8.15;

contract TesterA {
function testRevert(bool path) public view {
 require(path, "test error");
}
}

contract TesterB {
error MyError(string msg);
function testError(bool path) public view {
 if(path) revert MyError("test error");
}
}

Gas saving executing: 9 per entry

TesterA.testRevert: 21611 TesterB.testError: 21602

Affected source code:

Total gas saved: 9 * 24 = 216

7. Avoid unused returns

Having a method that always returns the same value is not correct in terms of consumption, if you want to modify a value, and the method will perform a revert in case of failure, it is not necessary to return a true, since it will never be false. It is less expensive not to return anything, and it also eliminates the need to double-check the returned value by the caller.

Affected source code:

8. delete optimization

Use delete instead of set to default value (false or 0).

5 gas could be saved per entry in the following affected lines:

Affected source code:

Total gas saved: 5 * 1 = 5

9. Use the unchecked keyword

When an underflow or overflow cannot occur, one might conserve gas by using the unchecked keyword to prevent unnecessary arithmetic underflow/overflow tests.

Affected source code:

10. Avoid index for values

There is no need to create index for amount or values, these values will be different each time, and there are no reason to have an index on them.

    event AddCredit(
        address indexed lender,
        address indexed token,
        uint256 indexed deposit,
        bytes32 id
    );

  /// @notice Emits data re Lender removes funds (principal) - there is no corresponding function, just withdraw()
  event WithdrawDeposit(bytes32 indexed id, uint256 indexed amount);
  
  /// @notice Emits data re Lender withdraws interest - there is no corresponding function, just withdraw()
  // Bob - consider changing event name to WithdrawInterest
  event WithdrawProfit(bytes32 indexed id, uint256 indexed amount);
  

  /// @notice After accrueInterest runs, emits the amount of interest added to a Borrower's outstanding balance of interest due 
  // but not yet repaid to the Line of Credit contract
  event InterestAccrued(bytes32 indexed id, uint256 indexed amount);


  // Borrower Events

  event Borrow(bytes32 indexed id, uint256 indexed amount);
  // Emits notice that a Borrower has drawn down an amount on a credit line

  event RepayInterest(bytes32 indexed id, uint256 indexed amount);
  /** Emits that a Borrower has repaid an amount of interest 
  (N.B. results in an increase in interestRepaid, i.e. interest not yet withdrawn by a Lender). There is no corresponding function
  */
  
  event RepayPrincipal(bytes32 indexed id, uint256 indexed amount);
  // Emits that a Borrower has repaid an amount of principal - there is no corresponding function

Affected source code:

#0 - c4-judge

2022-11-17T22:58:26Z

dmvt marked the issue as grade-b

#1 - c4-judge

2022-11-17T22:59:35Z

dmvt marked the issue as grade-a

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