Halborn Logo

LMCV part 1 - DAMfinance


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/26/2024

Date of Engagement by: August 5th, 2022 - September 6th, 2022

Summary

88% of all REPORTED Findings have been addressed

All findings

16

Critical

0

High

1

Medium

2

Low

7

Informational

6


1. INTRODUCTION

DAMfinance engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-08-05 and ending on 2022-09-06. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. AUDIT SUMMARY

The team at Halborn was provided four weeks for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to:

    • Ensure that smart contract functions operate as intended

    • Identify potential security issues with the smart contracts

In summary, Halborn identified some security risks that were mostly addressed by the DAMfinance team.

3. TEST APPROACH & METHODOLOGY

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the bridge code and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:

    • Research into architecture and purpose

    • Smart contract manual code review and walk-through

    • Graphing out functionality and contract logic/connectivity/functions (solgraph)

    • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes

    • Manual testing by custom scripts

    • Static Analysis of security for scoped contract, and imported functions. (Slither)

    • Local deployment (Hardhat, Remix IDE, Brownie)

4. SCOPE

IN-SCOPE: The security assessment was scoped to the following smart contracts:

    • CollateralJoinDecimals.sol

    • CollateralJoin.sol

    • dPrimeJoin.sol

    • dPrime.sol

    • LMCVProxy.sol

    • LMCV.sol

    • PSM.sol

    • WGLMR.sol

Commit ID: 3391f49ca23e67b2dbb39d35ff7d665dc5769661

Remediation plan:

Pull Request: 30 Branch: secondRoundAuditFixes Commit ID 9798fb6f03aab96d8702116e6bef394b2e501d59

OUT-OF-SCOPE: Other smart contracts in the repository, external libraries and economical attacks.

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

1

Medium

2

Low

7

Informational

6

Impact x Likelihood

HAL-02

HAL-01

HAL-05

HAL-06

HAL-07

HAL-03

HAL-11

HAL-16

HAL-04

HAL-09

HAL-08

HAL-10

HAL-12

HAL-13

HAL-14

HAL-15

Security analysisRisk levelRemediation Date
USER FUNDS MAY GET LOCKEDHighSolved - 10/10/2022
ADMINS ARE ALLOWED TO BURN USERS DPRIME TOKENS WITHOUT AUTHORIZATIONMediumFuture Release
MULTIPLE INTEGER UNDERFLOWS IN LMCV MODULEMediumSolved - 10/11/2022
INTEGER UNDERFLOW PSM MODULELowSolved - 10/10/2022
CONTRACTS MIGHT LOSE ADMIN FUNCTIONALITYLowSolved - 10/10/2022
MISSING PAUSE/UNPAUSE FUNCTIONALITYLowSolved - 10/10/2022
IMPROPER ROLE-BASED ACCESS CONTROLLowRisk Accepted
DIVISION BY ZERO IN ISWITHINCREDITLIMITLowSolved - 10/10/2022
EDITCOLLATERALLIST BEHAVIOUR MAY BE MISLEADINGLowSolved - 10/10/2022
MISSING ZERO ADDRESS CHECKLowSolved - 10/10/2022
MISSING EVENTS ON CHANGESInformationalSolved - 10/10/2022
FUNCTIONS COULD BE DECLARED AS EXTERNALInformationalSolved - 10/10/2022
VARIABLES COULD BE DEFINED AS CONSTANTInformationalSolved - 10/10/2022
COLLATERALLIST SEEMS TO BE UNUSEDInformationalSolved - 10/11/2022
MISSING NATSPEC DOCUMENTATIONInformationalFuture Release
CHANGE MEMORY TO CALLDATAInformationalSolved - 10/10/2022

8. Findings & Tech Details

8.1 USER FUNDS MAY GET LOCKED

// High

Description

When admin calls thecage function of the CollateralJoin contract, the live flag is set to zero, which means the contract is stopped.

The user can repay the loan, but will not be able to exit (withdraw) assets from the CollateralJoin contract.

Code Location

cage function:

contracts/CollateralJoin.sol

function cage() external auth {
    live = 0;
    emit Cage();
}

The exit function, requires contract to be live:

contracts/CollateralJoin.sol

function exit(address usr, uint256 wad) external {
    require(live == 1, "CollateralJoin/not-live");
    lmcv.pullCollateral(collateralName, msg.sender, wad);
    require(collateralContract.transfer(usr, wad), "CollateralJoin/failed-transfer");
    emit Exit(usr, wad);
}

Hardhat test case:

it("HAL-01 User collateral gets locked", async function () {

    // initial collateralJoin balance: 2 users joined with 100 tokens each
    expect(await mockToken.balanceOf(collateralJoin.address)).to.equal(fwad("200"));

    //Total value of collateral: $6000
    //Total loanable amount: $3000
    await userLMCV.loan(collateralBytesList, [fwad("50"), fwad("100"), fwad("200")], fwad("2000"), addr1.address);

    expect(await userLMCV.lockedCollateral(addr1.address, mockTokenBytes)).to.equal(fwad("50"));
    expect(await userLMCV.lockedCollateral(addr1.address, mockToken2Bytes)).to.equal(fwad("100"));
    expect(await userLMCV.lockedCollateral(addr1.address, mockToken3Bytes)).to.equal(fwad("200"));

    expect(await userLMCV.normalizedDebt(addr1.address)).to.equal(fwad("2000"));
    expect(await lmcv.totalDPrime()).to.equal(frad("2000"));
    expect(await lmcv.totalNormalizedDebt()).to.equal(fwad("2000"));

    // lock join contract
    await collateralJoin.cage();

    expect(await await collateralJoin.live()).to.equal(0);

    // repay the loan
    await userLMCV.repay(collateralBytesList, [fwad("50"), fwad("100"), fwad("200")], fwad("2000"), addr1.address);

    // validate the loan was paid - there is no locked collateral, debt nor issued dPrime
    expect(await userLMCV.lockedCollateral(addr1.address, mockTokenBytes)).to.equal(fwad("0"));
    expect(await userLMCV.lockedCollateral(addr1.address, mockToken2Bytes)).to.equal(fwad("0"));
    expect(await userLMCV.lockedCollateral(addr1.address, mockToken3Bytes)).to.equal(fwad("0"));
    expect(await userLMCV.normalizedDebt(addr1.address)).to.equal(fwad("0"));
    expect(await lmcv.totalDPrime()).to.equal(frad("0"));
    expect(await lmcv.totalNormalizedDebt()).to.equal(fwad("0"));

    // try to exit from join contract
    let collatJoinConnect = collateralJoin.connect(addr1);
    await expect(collatJoinConnect.exit(addr1.address, fwad("100")))
        .to.be.revertedWith("CollateralJoin/not-live");

    // balance does not change
    expect(await mockToken.balanceOf(collateralJoin.address)).to.equal(fwad("200"));
});
Score
Impact: 5
Likelihood: 3
Recommendation

SOLVED: Updated the cage function to allow an admin to re-make the contract. When the administrator resumes the contract, users can withdraw their assets successfully.

Reference: CollateralJoin.sol

8.2 ADMINS ARE ALLOWED TO BURN USERS DPRIME TOKENS WITHOUT AUTHORIZATION

// Medium

Description

An administrator of the dPrime token can burn users' tokens. Admin burning dPrime might break the LMCV contract as there might be less supply than recorded in the LMCV contract.

Code Location

contracts/CollateralJoin.sol

function burn(address from, uint256 value) external {
    uint256 balance = balanceOf[from];
    require(balance >= value, "dPrime/insufficient-balance");

    if (from != msg.sender && admins[msg.sender] != 1) {
        uint256 allowed = allowance[from][msg.sender];
[...]

Hardhat test scenario:

it("HAL-02 admin burns user dPrime", async function () {
    await setupUser(addr1, ["1000", "1000", "1000"]);
    await userLMCV.approveMultiple([lmcvProxy.address, dPrimeJoin.address]);

    // lock some collataral of each token
    await userLMCVProxy.createLoan(collateralBytesList, [fwad("100"), fwad("200"), fwad("300")], fwad("1000"));
    expect(await lmcv.totalNormalizedDebt()).to.equal(fwad("1000"));
    expect(await lmcv.normalizedDebt(addr1.address)).to.equal(fwad("1000"));

    expect(await dPrime.balanceOf(addr1.address)).to.eq(fwad("990"));
    expect(await dPrime.totalSupply()).to.eq(fwad("990"));

    dPrime.burn(addr1.address, fwad("500"));

    expect(await lmcv.totalNormalizedDebt()).to.equal(fwad("1000"));
    expect(await lmcv.normalizedDebt(addr1.address)).to.equal(fwad("1000"));
    expect(await dPrime.balanceOf(addr1.address)).to.be.eq(fwad("490"));
    expect(await dPrime.totalSupply()).to.eq(fwad("490"));
});
Score
Impact: 5
Likelihood: 2
Recommendation

PENDING: The DAMfinance team stated that they are planning to implement the recommended fix with the governance module in the future.

8.3 MULTIPLE INTEGER UNDERFLOWS IN LMCV MODULE

// Medium

Description

There are multiple cases in the LMCV contract when subtracting balances without checks. Such behavior may cause the transaction to fail due to arithmetic errors (integer underflow).

Code Location

The repay function does not perform boundary checks; when the user tries to repay a loan after the interest rate has changed, the transaction may fail with an arithmetic error due to underflow:

Calculating the dPrime amounts and debt:

contracts/LMCV.sol

dPrime[user]            -= normalizedDebtChange * rateMult;
totalDPrime             -= normalizedDebtChange * rateMult;
normalizedDebt[user]    -= normalizedDebtChange;
totalNormalizedDebt     -= normalizedDebtChange;

Calculating new collateral amount:

contracts/LMCV.sol

// Debit locked collateral amount and credit unlocked collateral amount.
uint256 newLockedCollateralAmount   = lockedCollateral[user][collateralList[i]]     -= collateralChange[i];
uint256 newUnlockedCollateralAmount = unlockedCollateral[user][collateralList[i]]   += collateralChange[i];

deflate function calculating protocol deficit:

contracts/LMCV.sol

function deflate(uint256 rad) external {
    address u = msg.sender;
    protocolDeficit[u]      -= rad;
    totalProtocoldeficit    -= rad;
    dPrime[u]               -= rad;
    totalDPrime             -= rad;

    emit Deflate(msg.sender, rad);
}

pullCollateral function updating unlocked collateral:

contracts/LMCV.sol

function pullCollateral(bytes32 collat, address user, uint256 wad) external auth {
    unlockedCollateral[user][collat] -= wad;
    emit PullCollateral(collat, user, wad);
}

moveCollateral function updating unlocked collateral:

contracts/LMCV.sol

function moveCollateral(bytes32 collat, address src, address dst, uint256 wad) external {
    require(approval(src, msg.sender), "LMCV/collateral move not allowed");
    unlockedCollateral[src][collat] -= wad;
    unlockedCollateral[dst][collat] += wad;
    emit MoveCollateral(collat, src, dst, wad);
}

moveDPrime function:

contracts/LMCV.sol

function moveDPrime(address src, address dst, uint256 rad) external {
    require(approval(src, msg.sender), "LMCV/dPrime move not allowed");
    dPrime[src] -= rad;
    dPrime[dst] += rad;
    emit MoveDPrime(src, dst, rad);
}

Below are the Hardhat test cases for underflow issues:

Issues
  ✔ HAL-03 Integer underflow in LMCV pullCollateral function - exit twice (38ms)
  ✔ HAL-03 Integer underflow in LMCV pullCollateral function - exit with more than joined
  ✔ HAL-03 Integer underflow in LMCV moveCollateral - move more than deposited
  ✔ HAL-03 Integer underflow in LMCV moveDPrime (62ms)
  ✔ HAL-03 Integer underflow in LMCV repay - rate updated after loan (98ms)

Source code of test cases:

it("HAL-03 Integer underflow in LMCV pullCollateral function - exit twice", async function () {

    expect(await userLMCV.unlockedCollateral(addr1.address, mockTokenBytes)).to.equal(fwad("100"));

    // exit from CollateralJoin
    let collatJoinConnect = collateralJoin.connect(addr1);
    await collatJoinConnect.exit(addr1.address, fwad("100"));

    expect(await userLMCV.unlockedCollateral(addr1.address, mockTokenBytes)).to.equal(fwad("0"));

    // try to exit for the second time
    await expect(collatJoinConnect.exit(addr1.address, fwad("100")))
        .to.be.revertedWith("Arithmetic operation underflowed or overflowed outside of an unchecked block");
});

it("HAL-03 Integer underflow in LMCV pullCollateral function - exit with more than joined", async function () {

    let collatJoinConnect = collateralJoin.connect(addr1);
    expect(await userLMCV.unlockedCollateral(addr1.address, mockTokenBytes)).to.equal(fwad("100"));

    // exit with more than joined
    await expect(collatJoinConnect.exit(addr1.address, fwad("101")))
        .to.be.revertedWith("Arithmetic operation underflowed or overflowed outside of an unchecked block");

    expect(await userLMCV.unlockedCollateral(addr1.address, mockTokenBytes)).to.equal(fwad("100"));
});

it("HAL-03 Integer underflow in LMCV moveCollateral - move more than deposited", async function () {

    expect(await userLMCV.unlockedCollateral(addr1.address, mockTokenBytes)).to.equal(fwad("100"));
    expect(await userLMCV.unlockedCollateral(addr1.address, mockToken2Bytes)).to.equal(fwad("200"));
    expect(await userLMCV.unlockedCollateral(addr1.address, mockToken3Bytes)).to.equal(fwad("300"));

    await expect(userLMCV.moveCollateral(mockTokenBytes, addr1.address, addr1.address, fwad("200")))
        .to.be.revertedWith("Arithmetic operation underflowed or overflowed outside of an unchecked block");
});

it("HAL-03 Integer underflow in LMCV moveDPrime", async function () {

    await userLMCV.approve(dPrimeJoin.address);
    await userLMCV.loan([mockTokenBytes], [fwad("50")], fwad("1000"), addr1.address);
    let dPrimeJoinConnect = dPrimeJoin.connect(addr1);
    await expect(dPrimeJoinConnect.exit(addr1.address, fwad("1001")))
        .to.be.revertedWith("Arithmetic operation underflowed or overflowed outside of an unchecked block");

    expect(await dPrime.balanceOf(addr1.address)).to.equal(fwad("0"));
    expect(await lmcv.totalDPrime()).to.equal(frad("1000"));
});

it("HAL-03 Integer underflow in LMCV repay - rate updated after loan", async function () {

    // take a loan
    await userLMCV.loan(collateralBytesList, [fwad("50"), fwad("100"), fwad("200")], fwad("2000"), addr1.address);
    expect(await lmcv.dPrime(addr1.address)).to.equal(frad("2000"));
    expect(await userLMCV.normalizedDebt(addr1.address)).to.equal(fwad("2000"));
    expect(await userLMCV.dPrime(addr1.address)).to.equal(frad("2000"));

    // update rate
    await lmcv.updateRate(fray(".1"));

    // repay
    await expect(userLMCV.repay(collateralBytesList, [fwad("50"), fwad("100"), fwad("200")], fwad("2000"), addr1.address))
        .to.be.revertedWith("Arithmetic operation underflowed or overflowed outside of an unchecked block");
});
Score
Impact: 3
Likelihood: 3
Recommendation

SOLVED: Added additional require statements to ensure that an underflow does not occur:

LMCV.sol: #275, 282, 294, 409, 420, 530

8.4 INTEGER UNDERFLOW PSM MODULE

// Low

Description

PSM contract may revert due to an arithmetic error caused by integer underflow when mintFee is set to a large value.

Code Location

contracts/PSM.sol

function createDPrime(address usr, bytes32[] memory collateral, uint256[] memory collatAmount) external {
    require(collateral.length == 1 && collatAmount.length == 1 && collateral[0] == collateralName, "PSM/Incorrect setup");
    uint256 collatAmount18 = collatAmount[0] * to18ConversionFactor; // [wad]
    uint256 fee = _rmul(collatAmount18, mintFee); // rmul(wad, ray) = wad
    uint256 dPrimeAmt = collatAmount18 - fee;

    collateralJoin.join(address(this), collatAmount[0], msg.sender);
[...]
Score
Impact: 2
Likelihood: 2
Recommendation

SOLVED: The require statement was added to ensure that mintFee is less than 100%.

Reference: PSM.sol

8.5 CONTRACTS MIGHT LOSE ADMIN FUNCTIONALITY

// Low

Description

The deny function is not checking if there are any other active wards before setting wards[usr] = 0. If the user denies himself, when they are the only ward, the contract will lose admin functionality.

Code Location

LMCV module:

contracts/LMCVProxy.sol

function deny(address usr) external auth {
    wards[usr] = 0;
    emit Deny(usr);
}

PSM module:

contracts/PSM.sol

function deny(address usr) external auth {
    wards[usr] = 0;
    emit Deny(usr);
}

dPrime module:

contracts/dPrime.sol

function deny(address usr) external auth {
    admins[usr] = 0;
    emit Deny(usr);
}

CollateralJoin module:

contracts/CollateralJoin.sol

function deny(address usr) external auth {
    wards[usr] = 0;
    emit Deny(usr);
}

CollateralJoinDecimals module:

contracts/CollateralJoinDecimals.sol

function deny(address usr) external auth {
    wards[usr] = 0;
    emit Deny(usr);
}
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The ArchAdmin variable was added to the contract. The address assigned to this field cannot be removed from wards/admins mapping via administrate or deny functions, ensuring there is at least one administrator on the contract. To update this address, a new ArchAdmin must be set; then the address can be removed from admins mapping.

Reference:

8.6 MISSING PAUSE/UNPAUSE FUNCTIONALITY

// Low

Description

In case a hack occurs, or an exploit is discovered, the team should be able to pause functionality until the necessary changes are made to the system. To use a THORchain example again, the team behind THOR chain noticed an attack was going to occur well before the system transferred funds to the hacker. However, they were unable to shut the system down fast enough (According to the incident report).

In case of the contracts in scope, only LMCV and LMCVProxy can be stopped/resumed. Other contracts can only be disabled by the cage function (CollateralJoin and CollateralJoinDecimals) or do not have such possibility at all (PSM, dPrimeJoin).

Score
Impact: 3
Likelihood: 2
Recommendation

SOLVED: The cage function was modified, and the setLive function was added to the contracts. Now all contracts except dPrime and dPrimeJoin can be stopped/resumed in case of an attack.

8.7 IMPROPER ROLE-BASED ACCESS CONTROL

// Low

Description

The smart contracts, in scope, do not implement granular access control. All the privileged functionality is assigned to one role. This could lead to serious consequences if, for example, a malicious admin decides to take over the platform.

Score
Impact: 3
Likelihood: 2
Recommendation

RISK ACCEPTED: The DAMfinance team accepted the risk of this finding and stated that in this role-based admin structure, only smart contracts would have access to specific roles, and a person-controlled owner address would be able to set all of these roles. Since a smart contract can only call functions it has interfaces for and admin access to in this setup, and an owner-level admin hack would have the ability to set itself as any other level admin, this does not seem like a useful check.

8.8 DIVISION BY ZERO IN ISWITHINCREDITLIMIT

// Low

Description

The isWithinCreditLimit function does not handle leveraged-only collateral properly. When leveraged-only collateral is passed, and the credit value exceeds the collateral value, the transaction fails with a Division or modulo division by zero error.

Code Location

LMCV module:

contracts/LMCV.sol

function isWithinCreditLimit(address user, uint256 rate) private view returns (bool) {
    bytes32[] storage lockedList = lockedCollateralList[user];
    uint256 creditLimit             = 0; // [rad]
    uint256 leverTokenCreditLimit   = 0; // [rad]
    uint256 noLeverageTotal         = 0; // [wad]
    uint256 leverageTotal           = 0; // [rad]
    for (uint256 i = 0; i < lockedList.length; i++) {
        Collateral memory collateralData = CollateralData[lockedList[i]];

        if(lockedCollateral[user][lockedList[i]] > collateralData.dustLevel){
            uint256 collateralValue = lockedCollateral[user][lockedList[i]] * collateralData.spotPrice; // wad*ray -> rad

            if(!collateralData.leveraged){
                creditLimit += _rmul(collateralValue, collateralData.creditRatio);
                noLeverageTotal += collateralValue / RAY;
            } else {
                leverageTotal += collateralValue;
                leverTokenCreditLimit += _rmul(collateralValue, collateralData.creditRatio);
            }
        }
    }

    // If only leverage tokens exist, just return their credit limit
    // Keep credit ratio low on levered tokens (60% or lower) to incentivize having non levered collateral in the vault
    if(noLeverageTotal == 0 && leverageTotal > 0 && leverTokenCreditLimit >= normalizedDebt[user] * rate){
        return true;
    }

    uint256 leverageMultiple = noLeverageTotal == 0 && leverageTotal == 0 ? RAY : RAY + leverageTotal / noLeverageTotal;

    if (_rmul(creditLimit, leverageMultiple) >= (normalizedDebt[user] * rate)) {
        return true;
    }
    return false;
}
Score
Impact: 2
Likelihood: 3
Recommendation

SOLVED: The condition in the isWithinLimit function of the LMCV.sol contract was modified to handle correctly limits for leveraged tokens.

Reference: LMCV.sol

8.9 EDITCOLLATERALLIST BEHAVIOUR MAY BE MISLEADING

// Low

Description

The editCollateralList function takes three arguments: bytes32 collateralName, bool accepted, uint256 position. When adding collateral, only collateraName is used, and position is ignored. When removing collateral, only position is used; the function is not validated if a given collateralName is located in a specified position. Moreover, the function does not check if the given collateral is already added to the list.

Code Location

LMCV module:

contracts/LMCV.sol

function editCollateralList(bytes32 collateralName, bool accepted, uint256 position) external auth {
    if(accepted){
        CollateralList.push(collateralName);
    }else{
        deleteElement(CollateralList, position);
    }
}
Score
Impact: 2
Likelihood: 2
Recommendation

SOLVED: The editCollateralList function was removed.

Reference: LMCV.sol

8.10 MISSING ZERO ADDRESS CHECK

// Low

Description

Code Location

Following functions are not validating, that given address is different from zero:

Score
Impact: 2
Likelihood: 3
Recommendation

SOLVED: Zero-address checks were added:

8.11 MISSING EVENTS ON CHANGES

// Informational

Description

Functions performing important changes on the contract: setLMCV, setDPrimeJoin, setDPrime, and editCollateral are not emitting events to facilitate monitoring of the protocol.

Code Location

LMCVProxy contract, lines: 75, 80, 85, 106

Score
Impact: 2
Likelihood: 1
Recommendation

SOLVED: New events were added:

8.12 FUNCTIONS COULD BE DECLARED AS EXTERNAL

// Informational

Description

The following functions could be declared as external:

  • WGLMR.withdraw(uint256)
  • WGLMR.totalSupply()
  • WGLMR.approve(address,uint256)
  • WGLMR.transfer(address,uint256)
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: Function definitions were updated from public to external:

8.13 VARIABLES COULD BE DEFINED AS CONSTANT

// Informational

Description

The following variables could be defined as constant:

  • WGLMR.decimals
  • WGLMR.name
  • WGLMR.symbol
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: Variable definitions were updated to constant:

8.14 COLLATERALLIST SEEMS TO BE UNUSED

// Informational

Description

The CollateralList array seems to be used only in setup tests. There is no use of this array in any contract functions.

Code Location

LMCV.sol, line 40

Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The editCollateralList function and the CollateralList array were removed.

8.15 MISSING NATSPEC DOCUMENTATION

// Informational

Description

Solidity contracts can use a special form of comments to provide rich documentation for functions, return variables, and more. This special form is named the Ethereum Natural Language Specification Format (NatSpec).

Code Location

LMCV.sol, LMCVProxy.sol, PSM.sol, WGLMR.sol, dPrimeJoin.sol, dPrime.sol, CollateralJoin.sol, CollateralJoinDecimals.sol

Score
Impact: 1
Likelihood: 1
Recommendation

PENDING: Natspec documentation is planned for future releases.

8.16 CHANGE MEMORY TO CALLDATA

// Informational

Description

It is often more optimal to define parameters as calldata instead of memory for external functions when the parameter is only read.

The following parameters of external functions are stored in memory:

  • collateralList, collateralChange parameters of loan, repay and liquidate functions in LMCV contract
  • collaterals and amounts in createLoan and repayLoan functions of LMCVProxy contract

After changing those parameters from memory to calldata, gas usage in unit tests was reduced by around 1000.

Code Location

LMCV.sol, 316, 389, 462

LMCVProxy.sol 112, 123

Score
Impact: 2
Likelihood: 1
Recommendation

SOLVED: Function definitions were updated with calldata:

9. Automated Testing

STATIC ANALYSIS REPORT

Description

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contract in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contract in the repository and was able to compile it correctly into its ABI and binary format, Slither was run against the contract. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.

Slither results

dPrimeLike is re-used:
        - dPrimeLike (contracts/PSM.sol#31-36)
        - dPrimeLike (contracts/dPrimeJoin.sol#7-10)
LMCVLike is re-used:
        - LMCVLike (contracts/LMCVProxy.sol#24-38)
        - LMCVLike (contracts/PSM.sol#12-29)
        - LMCVLike (contracts/dPrimeJoin.sol#12-15)
CollateralJoinLike is re-used:
        - CollateralJoinLike (contracts/LMCVProxy.sol#14-17)
        - CollateralJoinLike (contracts/PSM.sol#38-44)
dPrimeJoinLike is re-used:
        - dPrimeJoinLike (contracts/LMCVProxy.sol#19-22)
        - dPrimeJoinLike (contracts/PSM.sol#6-10)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#name-reused

dPrimeJoinLike.dPrime().dPrime (contracts/PSM.sol#9) shadows:
        - dPrimeJoinLike.dPrime() (contracts/PSM.sol#9) (function)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#local-variable-shadowing

CollateralJoinDecimals.constructor(address,address,bytes32,address)._lmcvProxy (contracts/CollateralJoinDecimals.sol#81) lacks a zero-check on :
                - lmcvProxy = _lmcvProxy (contracts/CollateralJoinDecimals.sol#89)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#missing-zero-address-validation

CollateralJoin.constructor(address,address,bytes32,address)._lmcvProxy (contracts/CollateralJoin.sol#104) lacks a zero-check on :
                - lmcvProxy = _lmcvProxy (contracts/CollateralJoin.sol#108)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#missing-zero-address-validation

PSM.constructor(address,address,address).treasury_ (contracts/PSM.sol#111) lacks a zero-check on :
                - treasury = treasury_ (contracts/PSM.sol#119)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#missing-zero-address-validation

LMCVProxy.createLoan(bytes32[],uint256[],uint256) (contracts/LMCVProxy.sol#112-121) has external calls inside a loop: require(bool,string)(ERC20Like(collateralContracts[collaterals[i]]).trans
ferFrom(msg.sender,address(this),amounts[i]),LMCVProxy/collateral transfer failed) (contracts/LMCVProxy.sol#116)
LMCVProxy.createLoan(bytes32[],uint256[],uint256) (contracts/LMCVProxy.sol#112-121) has external calls inside a loop: CollateralJoinLike(collateralJoins[collaterals[i]]).join(msg.sender,amoun
ts[i]) (contracts/LMCVProxy.sol#117)
LMCVProxy.repayLoan(bytes32[],uint256[],uint256) (contracts/LMCVProxy.sol#123-133) has external calls inside a loop: CollateralJoinLike(collateralJoins[collaterals[i]]).proxyExit(msg.sender,a
mounts[i]) (contracts/LMCVProxy.sol#131)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation/#calls-inside-a-loop

Reentrancy in CollateralJoin.exit(address,uint256) (contracts/CollateralJoin.sol#125-130):
        External calls:
        - lmcv.pullCollateral(collateralName,msg.sender,wad) (contracts/CollateralJoin.sol#127)
        - require(bool,string)(collateralContract.transfer(usr,wad),CollateralJoin/failed-transfer) (contracts/CollateralJoin.sol#128)
        Event emitted after the call(s):
        - Exit(usr,wad) (contracts/CollateralJoin.sol#129)
Reentrancy in CollateralJoin.join(address,uint256) (contracts/CollateralJoin.sol#118-123):
        External calls:
        - require(bool,string)(collateralContract.transferFrom(msg.sender,address(this),wad),CollateralJoin/failed-transfer) (contracts/CollateralJoin.sol#120)
        - lmcv.pushCollateral(collateralName,usr,wad) (contracts/CollateralJoin.sol#121)
        Event emitted after the call(s):
        - Join(usr,wad) (contracts/CollateralJoin.sol#122)
Reentrancy in CollateralJoin.proxyExit(address,uint256) (contracts/CollateralJoin.sol#132-137):
        External calls:
        - lmcv.pullCollateral(collateralName,usr,wad) (contracts/CollateralJoin.sol#134)
        - require(bool,string)(collateralContract.transfer(usr,wad),CollateralJoin/failed-transfer) (contracts/CollateralJoin.sol#135)
        Event emitted after the call(s):
        - Exit(usr,wad) (contracts/CollateralJoin.sol#136)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-3

Reentrancy in dPrimeJoin.exit(address,uint256) (contracts/dPrimeJoin.sol#69-73):
        External calls:
        - lmcv.moveDPrime(msg.sender,address(this),RAY * wad) (contracts/dPrimeJoin.sol#70)
        - dPrime.mint(usr,wad) (contracts/dPrimeJoin.sol#71)
        Event emitted after the call(s):
        - Exit(usr,wad) (contracts/dPrimeJoin.sol#72)
Reentrancy in dPrimeJoin.join(address,uint256) (contracts/dPrimeJoin.sol#63-67):
        External calls:
        - lmcv.moveDPrime(address(this),usr,RAY * wad) (contracts/dPrimeJoin.sol#64)
        - dPrime.burn(msg.sender,wad) (contracts/dPrimeJoin.sol#65)
        Event emitted after the call(s):
        - Join(usr,wad) (contracts/dPrimeJoin.sol#66)
Reentrancy in dPrimeJoin.proxyExit(address,uint256) (contracts/dPrimeJoin.sol#75-79):
        External calls:
        - lmcv.moveDPrime(usr,address(this),RAY * wad) (contracts/dPrimeJoin.sol#76)
        - dPrime.mint(usr,wad) (contracts/dPrimeJoin.sol#77)
        Event emitted after the call(s):
        - Exit(usr,wad) (contracts/dPrimeJoin.sol#78)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-3

dPrime.permit(address,address,uint256,uint256,uint8,bytes32,bytes32) (contracts/dPrime.sol#179-203) uses timestamp for comparisons
        Dangerous comparisons:
        - require(bool,string)(block.timestamp <= deadline,dPrime/permit-expired) (contracts/dPrime.sol#180)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#block-timestamp

LMCV.either(bool,bool) (contracts/LMCV.sol#615-617) uses assembly
        - INLINE ASM (contracts/LMCV.sol#616)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#assembly-usage

console._sendLogPayload(bytes) (node_modules/hardhat/console.sol#7-14) uses assembly
        - INLINE ASM (node_modules/hardhat/console.sol#10-13)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#assembly-usage

Different versions of Solidity are used:
        - Version used: ['^0.8.0', '^0.8.7']
        - ^0.8.0 (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#4)
        - ^0.8.0 (node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol#4)
        - ^0.8.0 (node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol#4)
        - ^0.8.0 (node_modules/@openzeppelin/contracts/utils/Context.sol#4)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#different-pragma-directives-are-used

Different versions of Solidity are used:
        - Version used: ['0.8.7', '>=0.4.22<0.9.0']
        - 0.8.7 (contracts/LMCVProxy.sol#3)
        - 0.8.7 (contracts/PSM.sol#2)
        - 0.8.7 (contracts/dPrimeJoin.sol#3)
        - >=0.4.22<0.9.0 (node_modules/hardhat/console.sol#2)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#different-pragma-directives-are-used

Pragma version^0.8.0 (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#4) allows old versions
Pragma version^0.8.0 (node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol#4) allows old versions
Pragma version^0.8.0 (node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol#4) allows old versions
Pragma version^0.8.0 (node_modules/@openzeppelin/contracts/utils/Context.sol#4) allows old versions
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity

Pragma version>=0.4.22<0.9.0 (node_modules/hardhat/console.sol#2) is too complex
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity

dPrimeJoin (contracts/dPrimeJoin.sol#17-80) should inherit from CollateralJoinLike (contracts/LMCVProxy.sol#14-17)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#missing-inheritance

Parameter CollateralJoinDecimals.join(address,uint256,address)._msgSender (contracts/CollateralJoinDecimals.sol#96) is not in mixedCase
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions

Contract dPrime (contracts/dPrime.sol#7-204) is not in CapWords
Function dPrime.DOMAIN_SEPARATOR() (contracts/dPrime.sol#57-59) is not in mixedCase
Constant dPrime.version (contracts/dPrime.sol#13) is not in UPPER_CASE_WITH_UNDERSCORES
Variable dPrime._DOMAIN_SEPARATOR (contracts/dPrime.sol#29) is not in mixedCase
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions

Parameter LMCV.setTreasury(address)._treasury (contracts/LMCV.sol#195) is not in mixedCase
Parameter LMCV.editLeverageStatus(bytes32,bool)._leveraged (contracts/LMCV.sol#229) is not in mixedCase
Parameter LMCV.editAcceptedCollateralType(bytes32,uint256,uint256,uint256,uint256,bool)._lockedAmountLimit (contracts/LMCV.sol#248) is not in mixedCase
Parameter LMCV.editAcceptedCollateralType(bytes32,uint256,uint256,uint256,uint256,bool)._dustLevel (contracts/LMCV.sol#249) is not in mixedCase
Parameter LMCV.editAcceptedCollateralType(bytes32,uint256,uint256,uint256,uint256,bool)._creditRatio (contracts/LMCV.sol#250) is not in mixedCase
Parameter LMCV.editAcceptedCollateralType(bytes32,uint256,uint256,uint256,uint256,bool)._liqBonusMult (contracts/LMCV.sol#251) is not in mixedCase
Parameter LMCV.editAcceptedCollateralType(bytes32,uint256,uint256,uint256,uint256,bool)._leveraged (contracts/LMCV.sol#252) is not in mixedCase
Variable LMCV.PSMAddresses (contracts/LMCV.sol#24) is not in mixedCase
Variable LMCV.CollateralList (contracts/LMCV.sol#40) is not in mixedCase
Variable LMCV.CollateralData (contracts/LMCV.sol#41) is not in mixedCase
Variable LMCV.ProtocolDebtCeiling (contracts/LMCV.sol#60) is not in mixedCase
Variable LMCV.MintFee (contracts/LMCV.sol#61) is not in mixedCase
Variable LMCV.AccumulatedRate (contracts/LMCV.sol#62) is not in mixedCase
Variable LMCV.Treasury (contracts/LMCV.sol#69) is not in mixedCase
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions

Contract dPrimeJoinLike (contracts/LMCVProxy.sol#19-22) is not in CapWords
Parameter LMCVProxy.setLMCV(address)._lmcv (contracts/LMCVProxy.sol#75) is not in mixedCase
Parameter LMCVProxy.setDPrimeJoin(address)._dPrimeJoin (contracts/LMCVProxy.sol#80) is not in mixedCase
Parameter LMCVProxy.setDPrime(address)._dPrime (contracts/LMCVProxy.sol#85) is not in mixedCase
Contract dPrimeLike (contracts/PSM.sol#31-36) is not in CapWords
Contract dPrimeJoin (contracts/dPrimeJoin.sol#17-80) is not in CapWords
Contract console (node_modules/hardhat/console.sol#4-1532) is not in CapWords
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions

Reentrancy in WGLMR.withdraw(uint256) (contracts/WGLMR.sol#38-43):
        External calls:
        - address(msg.sender).transfer(wad) (contracts/WGLMR.sol#41)
        Event emitted after the call(s):
        - Withdrawal(msg.sender,wad) (contracts/WGLMR.sol#42)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-4

console.slitherConstructorConstantVariables() (node_modules/hardhat/console.sol#4-1532) uses literals with too many digits:
        - CONSOLE_ADDRESS = address(0x000000000000000000636F6e736F6c652e6c6f67) (node_modules/hardhat/console.sol#5)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#too-many-digits

WGLMR.decimals (contracts/WGLMR.sol#21) should be constant
WGLMR.name (contracts/WGLMR.sol#19) should be constant
WGLMR.symbol (contracts/WGLMR.sol#20) should be constant
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant

name() should be declared external:
        - ERC20.name() (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#62-64)
symbol() should be declared external:
        - ERC20.symbol() (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#70-72)
decimals() should be declared external:
        - ERC20.decimals() (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#87-89)
totalSupply() should be declared external:
        - ERC20.totalSupply() (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#94-96)
balanceOf(address) should be declared external:
        - ERC20.balanceOf(address) (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#101-103)
transfer(address,uint256) should be declared external:
        - ERC20.transfer(address,uint256) (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#113-117)
approve(address,uint256) should be declared external:
        - ERC20.approve(address,uint256) (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#136-140)
transferFrom(address,address,uint256) should be declared external:
        - ERC20.transferFrom(address,address,uint256) (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#158-167)
increaseAllowance(address,uint256) should be declared external:
        - ERC20.increaseAllowance(address,uint256) (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#181-185)
decreaseAllowance(address,uint256) should be declared external:
        - ERC20.decreaseAllowance(address,uint256) (node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol#201-210)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external

withdraw(uint256) should be declared external:
        - WGLMR.withdraw(uint256) (contracts/WGLMR.sol#38-43)
totalSupply() should be declared external:
        - WGLMR.totalSupply() (contracts/WGLMR.sol#45-47)
approve(address,uint256) should be declared external:
        - WGLMR.approve(address,uint256) (contracts/WGLMR.sol#49-53)
transfer(address,uint256) should be declared external:
        - WGLMR.transfer(address,uint256) (contracts/WGLMR.sol#55-57)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external
. analyzed (31 contracts with 78 detectors), 82 result(s) found

Slither correctly flagged that:

  • some state variables could be declared as constant.

  • some functions can be defined as external

  • usage of external calls inside a loop

  • missing zero address checks

Those issues are included in the findings section of the report.

No major issues found by Slither.

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

© Halborn 2024. All rights reserved.