Sublime contest - WatchPug's results

Democratizing credit via Web3.

General Information

Platform: Code4rena

Start Date: 09/12/2021

Pot Size: $50,000 USDC

Total HM: 19

Participants: 21

Period: 7 days

Judge: 0xean

Total Solo HM: 14

Id: 61

League: ETH

Sublime

Findings Distribution

Researcher Performance

Rank: 2/21

Findings: 6

Award: $6,999.92

🌟 Selected for report: 12

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

2816.7538 USDC - $2,816.75

External Links

Handle

WatchPug

Vulnerability details

The function SavingsAccountUtil.depositFromSavingsAccount() is expected to return the number of equivalent shares for given _asset.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L225-L267

/**
 * @notice internal function used to get amount of collateral deposited to the pool
 * @param _fromSavingsAccount if true, collateral is transferred from _sender's savings account, if false, it is transferred from _sender's wallet
 * @param _toSavingsAccount if true, collateral is transferred to pool's savings account, if false, it is withdrawn from _sender's savings account
 * @param _asset address of the asset to be deposited
 * @param _amount amount of tokens to be deposited in the pool
 * @param _poolSavingsStrategy address of the saving strategy used for collateral deposit
 * @param _depositFrom address which makes the deposit
 * @param _depositTo address to which the tokens are deposited
 * @return _sharesReceived number of equivalent shares for given _asset
 */
function _deposit(
    bool _fromSavingsAccount,
    bool _toSavingsAccount,
    address _asset,
    uint256 _amount,
    address _poolSavingsStrategy,
    address _depositFrom,
    address _depositTo
) internal returns (uint256 _sharesReceived) {
    if (_fromSavingsAccount) {
        _sharesReceived = SavingsAccountUtil.depositFromSavingsAccount(
            ISavingsAccount(IPoolFactory(poolFactory).savingsAccount()),
            _depositFrom,
            _depositTo,
            _amount,
            _asset,
            _poolSavingsStrategy,
            true,
            _toSavingsAccount
        );
    } else {
        _sharesReceived = SavingsAccountUtil.directDeposit(
            ISavingsAccount(IPoolFactory(poolFactory).savingsAccount()),
            _depositFrom,
            _depositTo,
            _amount,
            _asset,
            _toSavingsAccount,
            _poolSavingsStrategy
        );
    }
}

However, since savingsAccountTransfer() does not return the result of _savingsAccount.transfer(), but returned _amount instead, which means that SavingsAccountUtil.depositFromSavingsAccount() may not return the actual shares (when pps is not 1).

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccountUtil.sol#L11-L26

    function depositFromSavingsAccount(
        ISavingsAccount _savingsAccount,
        address _from,
        address _to,
        uint256 _amount,
        address _token,
        address _strategy,
        bool _withdrawShares,
        bool _toSavingsAccount
    ) internal returns (uint256) {
        if (_toSavingsAccount) {
            return savingsAccountTransfer(_savingsAccount, _from, _to, _amount, _token, _strategy);
        } else {
            return withdrawFromSavingsAccount(_savingsAccount, _from, _to, _amount, _token, _strategy, _withdrawShares);
        }
    }

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccountUtil.sol#L66-L80

    function savingsAccountTransfer(
        ISavingsAccount _savingsAccount,
        address _from,
        address _to,
        uint256 _amount,
        address _token,
        address _strategy
    ) internal returns (uint256) {
        if (_from == address(this)) {
            _savingsAccount.transfer(_amount, _token, _strategy, _to);
        } else {
            _savingsAccount.transferFrom(_amount, _token, _strategy, _from, _to);
        }
        return _amount;
    }

As a result, the recorded _sharesReceived can be wrong.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L207-L223

    function _depositCollateral(
        address _depositor,
        uint256 _amount,
        bool _transferFromSavingsAccount
    ) internal nonReentrant {
        uint256 _sharesReceived = _deposit(
            _transferFromSavingsAccount,
            true,
            poolConstants.collateralAsset,
            _amount,
            poolConstants.poolSavingsStrategy,
            _depositor,
            address(this)
        );
        poolVariables.baseLiquidityShares = poolVariables.baseLiquidityShares.add(_sharesReceived);
        emit CollateralAdded(_depositor, _amount, _sharesReceived);
    }

PoC

Given:

  • the price per share of yearn USDC vault is 1.2
  1. Alice deposited 12,000 USDC to yearn strategy, received 10,000 share tokens;
  2. Alice created a pool, and added all the 12,000 USDC from the saving account as collateral; The recorded CollateralAdded got the wrong number: 12000 which should be 10000;
  3. Alice failed to borrow money with the pool and tries to cancelPool(), it fails as the recorded collateral shares are more than the actual collateral.

As a result, Alice has lost all the 12,000 USDC.

If Alice managed to borrow with the pool, when the loan defaults, the liquidation will also fail, and cause fund loss to the lenders.

Recommendation

Change to:

function savingsAccountTransfer(
    ISavingsAccount _savingsAccount,
    address _from,
    address _to,
    uint256 _amount,
    address _token,
    address _strategy
) internal returns (uint256) {
    if (_from == address(this)) {
        return _savingsAccount.transfer(_amount, _token, _strategy, _to);
    } else {
        return _savingsAccount.transferFrom(_amount, _token, _strategy, _from, _to);
    }
}

Findings Information

🌟 Selected for report: cmichel

Also found by: WatchPug, leastwood

Labels

bug
duplicate
3 (High Risk)

Awards

760.5235 USDC - $760.52

External Links

Handle

WatchPug

Vulnerability details

The current implementation of AaveYield.sol is taking AAVE aToken as a share token (eg, cToken and yToken).

However, AAVE's aTokens are quite different from cToken and yToken as it's always 1:1 to the underlying token, and the holder's balance will keep changing as the yield accumulates.

With the current implementation, users won't be able to get any yields for their deposits.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/AaveYield.sol#L290-L304

function _depositERC20(address asset, uint256 amount) internal returns (address aToken, uint256 sharesReceived) {
    aToken = liquidityToken(asset);
    uint256 aTokensBefore = IERC20(aToken).balanceOf(address(this));

    address lendingPool = ILendingPoolAddressesProvider(lendingPoolAddressesProvider).getLendingPool();

    //approve collateral to vault
    IERC20(asset).approve(lendingPool, 0);
    IERC20(asset).approve(lendingPool, amount);

    //lock collateral in vault
    AaveLendingPool(lendingPool).deposit(asset, amount, address(this), referralCode);

    sharesReceived = IERC20(aToken).balanceOf(address(this)).sub(aTokensBefore);
}

PoC

  1. Alice deposited 10,000 USDC to AaveYield strategy;
  2. 3 months later, the 10,000 USDC has accumulated 2,000 USDC of yields;
  3. Alice withdraws all the balance and can only get 10,000 USDC.

As a result, Alice has lost all the yields earned.

Recommendation

Use the deposited token amount / current total aToken balance as sharesReceived for _depositERC20.

#0 - ritik99

2021-12-27T10:28:03Z

Duplicate of #137

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0x1f8b

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

1267.5392 USDC - $1,267.54

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/NoYield.sol#L78-L83

function emergencyWithdraw(address _asset, address payable _wallet) external onlyOwner returns (uint256 received) {
    require(_wallet != address(0), 'cant burn');
    uint256 amount = IERC20(_asset).balanceOf(address(this));
    IERC20(_asset).safeTransfer(_wallet, received);
    received = amount;
}

received is not being assigned prior to L81, therefore, at L81, received is 0.

As a result, the emergencyWithdraw() does not work, in essence.

Recommendation

Change to:

function emergencyWithdraw(address _asset, address payable _wallet) external onlyOwner returns (uint256 received) {
    require(_wallet != address(0), 'cant burn');
    received = IERC20(_asset).balanceOf(address(this));
    IERC20(_asset).safeTransfer(_wallet, received);
}

#0 - 0xean

2022-01-21T16:34:21Z

upgrading to High sev based on assets being "lost" directly. IE the emergency function will not work.

3 — High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals). ``

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
disagree with severity
sponsor acknowledged

Awards

845.0261 USDC - $845.03

External Links

Handle

WatchPug

Vulnerability details

There are ERC20 tokens that charge fee for every transfer() or transferFrom().

In the current implementation, NoYield.sol#lockTokens() assumes that the received amount is the same as the transfer amount, and uses it to calculate sharesReceived amounts.

As a result, in unlockTokens(), later users may not be able to successfully withdraw their tokens, as it may revert at L141 for insufficient balance.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/NoYield.sol#L93-L106

    function lockTokens(
        address user,
        address asset,
        uint256 amount
    ) external payable override onlySavingsAccount nonReentrant returns (uint256 sharesReceived) {
        require(amount != 0, 'Invest: amount');
        if (asset != address(0)) {
            IERC20(asset).safeTransferFrom(user, address(this), amount);
        } else {
            require(msg.value == amount, 'Invest: ETH amount');
        }
        sharesReceived = amount;
        emit LockedTokens(user, asset, sharesReceived);
    }

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/NoYield.sol#L134-L144

    function _unlockTokens(address asset, uint256 amount) internal returns (uint256 received) {
        require(amount != 0, 'Invest: amount');
        received = amount;
        if (asset == address(0)) {
            (bool success, ) = savingsAccount.call{value: received}('');
            require(success, 'Transfer failed');
        } else {
            IERC20(asset).safeTransfer(savingsAccount, received);
        }
        emit UnlockedTokens(asset, received);
    }

Recommendation

Consider comparing before and after balance to get the actual transferred amount.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
1 (Low Risk)
sponsor disputed

Awards

281.6754 USDC - $281.68

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/PriceOracle.sol#L122-L132

function getUniswapLatestPrice(address num, address den) public view returns (uint256, uint256) {
    bytes32 _poolTokensId = getUniswapPoolTokenId(num, den);
    address _pool = uniswapPools[_poolTokensId];
    if (_pool == address(0)) {
        return (0, 0);
    }

    int24 _twapTick = OracleLibrary.consult(_pool, uniswapPriceAveragingPeriod);
    uint256 _numTokens = OracleLibrary.getQuoteAtTick(_twapTick, 10**30, num, den);
    return (_numTokens, 30);
}

The PriceOracle.sol contract can not compile due to wrong usage of OracleLibrary.getQuoteAtTick().

Recommendation

Change to:

function getUniswapLatestPrice(address num, address den) public view returns (uint256, uint256) {
    bytes32 _poolTokensId = getUniswapPoolTokenId(num, den);
    address _pool = uniswapPools[_poolTokensId];
    if (_pool == address(0)) {
        return (0, 0);
    }

    (int24 _twapTick, ) = OracleLibrary.consult(_pool, uniswapPriceAveragingPeriod);
    uint256 _numTokens = OracleLibrary.getQuoteAtTick(_twapTick, 10**30, num, den);
    return (_numTokens, 30);
}

#0 - ritik99

2021-12-24T07:47:00Z

The contracts do compile, we're just using an older version.

The getQuoteAtTick methods are unchanged from the version we use vs. now.

The change seems to be in the return value of the consult() method: what we use vs. now. Our call still matches their documentation

#1 - 0xean

2022-01-21T00:31:49Z

the contracts certainly compile on my machine based on the repo.

I do think that it is a risk that is worth highlighting that the new oracle library API is incompatible with the contracts, but marking down to low risk based on current documentation from Uniswap.

Findings Information

🌟 Selected for report: sirhashalot

Also found by: 0x1f8b, WatchPug, robee

Labels

bug
duplicate
1 (Low Risk)
sponsor disputed

Awards

51.3353 USDC - $51.34

External Links

#0 - ritik99

2021-12-25T16:56:51Z

We will be going ahead with the recommendations and references provided in #2

Findings Information

🌟 Selected for report: WatchPug

Also found by: cmichel, leastwood, robee

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

51.3353 USDC - $51.34

External Links

Handle

WatchPug

Vulnerability details

The initializer function that initializes important contract state can be called by anyone.

Impact

The attacker can initialize the contract before the legitimate deployer, hoping that the victim continues to use the same contract.

In the best case for the victim, they notice it and have to redeploy their contract costing gas.

Recommendation

Use the constructor to initialize non-proxied contracts.

For initializing proxy contracts deploy contracts using a factory contract that immediately calls initialize after deployment or make sure to call it immediately after deployment and verify the transaction succeeded.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/PoolFactory.sol#L188-L215

#0 - ritik99

2021-12-27T13:32:28Z

Cases of frontrunning initialize are easy to identify since they're only callable once. As mentioned by the warden, the only real issue would be the cost of redeployment. Hence the (0) non-critical label is apt

#1 - 0xean

2022-01-21T01:10:08Z

Upgrading to low-risk as this is really a state handling issue for the deployment

1 — Low: Low: Assets are not at risk. State handling, function incorrect as to spec, issues with comments.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

281.6754 USDC - $281.68

External Links

Handle

WatchPug

Vulnerability details

Given that Pool is deployed as a proxied contract, it should use the Upgradeable variant of OpenZeppelin Contracts.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/PoolFactory.sol#L320-L355

function _createPool(
    uint256 _poolSize,
    uint256 _borrowRate,
    address _borrowToken,
    address _collateralToken,
    uint256 _idealCollateralRatio,
    uint256 _repaymentInterval,
    uint256 _noOfRepaymentIntervals,
    address _poolSavingsStrategy,
    uint256 _collateralAmount,
    bool _transferFromSavingsAccount,
    bytes32 _salt,
    address _lenderVerifier
) internal {
    bytes memory data = _encodePoolInitCall(
        _poolSize,
        _borrowRate,
        _borrowToken,
        _collateralToken,
        _idealCollateralRatio,
        _repaymentInterval,
        _noOfRepaymentIntervals,
        _poolSavingsStrategy,
        _collateralAmount,
        _transferFromSavingsAccount,
        _lenderVerifier
    );
    bytes32 salt = keccak256(abi.encodePacked(_salt, msg.sender));
    bytes memory bytecode = abi.encodePacked(type(SublimeProxy).creationCode, abi.encode(poolImpl, address(0x01), data));
    uint256 amount = _collateralToken == address(0) ? _collateralAmount : 0;

    address pool = _deploy(amount, salt, bytecode);

    poolRegistry[pool] = true;
    emit PoolCreated(pool, msg.sender);
}

Otherwise, the constructor functions of Pool's parent contracts which may change storage at deploy time, won't work for deployed instances.

The effect may be different for different OpenZeppelin libraries.

Take ReentrancyGuard for example, the code inside ReentrancyGuard.sol#constructor won't work, should use ReentrancyGuardUpgradeable.sol instead:

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L6-L8

import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts-upgradeable/proxy/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol';

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L24-L24

contract Pool is Initializable, ERC20PausableUpgradeable, IPool, ReentrancyGuard {

Recommendation

Change to:

import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import '@openzeppelin/contracts-upgradeable/proxy/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol';
contract Pool is Initializable, ReentrancyGuardUpgradeable, ERC20PausableUpgradeable, IPool {
function initialize(
    uint256 _borrowAmountRequested,
    uint256 _borrowRate,
    address _borrower,
    address _borrowAsset,
    address _collateralAsset,
    uint256 _idealCollateralRatio,
    uint256 _repaymentInterval,
    uint256 _noOfRepaymentIntervals,
    address _poolSavingsStrategy,
    uint256 _collateralAmount,
    bool _transferFromSavingsAccount,
    address _lenderVerifier,
    uint256 _loanWithdrawalDuration,
    uint256 _collectionPeriod
) external payable initializer {
    poolFactory = msg.sender;
    poolConstants.borrowAsset = _borrowAsset;
    poolConstants.idealCollateralRatio = _idealCollateralRatio;
    poolConstants.collateralAsset = _collateralAsset;
    poolConstants.poolSavingsStrategy = _poolSavingsStrategy;
    poolConstants.borrowAmountRequested = _borrowAmountRequested;
    _initialDeposit(_borrower, _collateralAmount, _transferFromSavingsAccount);
    poolConstants.borrower = _borrower;
    poolConstants.borrowRate = _borrowRate;
    poolConstants.noOfRepaymentIntervals = _noOfRepaymentIntervals;
    poolConstants.repaymentInterval = _repaymentInterval;
    poolConstants.lenderVerifier = _lenderVerifier;

    poolConstants.loanStartTime = block.timestamp.add(_collectionPeriod);
    poolConstants.loanWithdrawalDeadline = block.timestamp.add(_collectionPeriod).add(_loanWithdrawalDuration);
    __ReentrancyGuard_init();
    __ERC20_init('Pool Tokens', 'PT');
    try ERC20Upgradeable(_borrowAsset).decimals() returns(uint8 _decimals) {
        _setupDecimals(_decimals);
    } catch(bytes memory) {}
}

Findings Information

🌟 Selected for report: 0xngndev

Also found by: 0x0x0x, Jujic, WatchPug, defsec

Labels

bug
duplicate
G (Gas Optimization)

Awards

10.9563 USDC - $10.96

External Links

Handle

WatchPug

Vulnerability details

It's a best practice to use the latest compiler version.

The specified minimum compiler version is not up to date. Older compilers might be susceptible to some bugs. We recommend changing the solidity version pragma to the latest version to enforce the use of an up to date compiler.

List of known compiler bugs and their severity can be found here: https://etherscan.io/solcbuginfo

#0 - ritik99

2021-12-26T17:34:19Z

Duplicate of #39

Findings Information

🌟 Selected for report: sirhashalot

Also found by: 0x0x0x, WatchPug, cmichel, pmerkleplant, robee

Labels

bug
duplicate
G (Gas Optimization)

Awards

8.2172 USDC - $8.22

External Links

Handle

WatchPug

Vulnerability details

Redundant code increase contract size and gas usage at deployment.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L897-L898

uint256 _tokenInStrategy = _liquidityShares;
_tokenInStrategy = IYield(_strategyList[index]).getTokensForShares(_liquidityShares, _collateralAsset);

L897, _tokenInStrategy = _liquidityShares is redundant.

#0 - ritik99

2022-01-07T22:42:54Z

This is a duplicate of #36 since both deal with unnecessary initializations. The location of the issue mentioned in the two places is however different

Findings Information

🌟 Selected for report: robee

Also found by: 0x0x0x, WatchPug

Labels

bug
duplicate
G (Gas Optimization)

Awards

22.5438 USDC - $22.54

External Links

Handle

WatchPug

Vulnerability details

Using ++i is more gas efficient than i++, especially in a loop.

For example:

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L662-L662

for (uint256 _index = 0; _index < _strategyList.length; _index++) {

#0 - ritik99

2021-12-25T18:43:00Z

Duplicate of #22

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor disputed

Awards

83.4956 USDC - $83.50

External Links

Handle

WatchPug

Vulnerability details

Based on the current implementation, shares are in aToken, which is 1:1 to the underlying token.

Therefore, getTokensForShares() and getSharesForTokens() can be much simplier.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/AaveYield.sol#L256-L265

    function getTokensForShares(uint256 shares, address asset) public view override returns (uint256 amount) {
        if (shares == 0) return 0;
        address aToken = liquidityToken(asset);

        (, , , , , , , uint256 liquidityIndex, , ) = IProtocolDataProvider(protocolDataProvider).getReserveData(asset);

        amount = IScaledBalanceToken(aToken).scaledBalanceOf(address(this)).mul(liquidityIndex).mul(shares).div(
            IERC20(aToken).balanceOf(address(this))
        );
    }

can be changed to:

    function getTokensForShares(uint256 shares, address asset) public view override returns (uint256) {
        return shares;
    }

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/AaveYield.sol#L273-L275

    function getSharesForTokens(uint256 amount, address asset) external view override returns (uint256 shares) {
        shares = (amount.mul(1e18)).div(getTokensForShares(1e18, asset));
    }

can be changed to:

    function getSharesForTokens(uint256 amount, address asset) external view override returns (uint256) {
        return amount;
    }

#0 - ritik99

2021-12-24T07:28:51Z

Instead of making the proposed changes, we plan to use Aave's wrappers for aTokens so that it conforms with the behaviour of other LP tokens

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

83.4956 USDC - $83.50

External Links

Handle

WatchPug

Vulnerability details

Check if _poolStatus and block.timestamp earlier can avoid unnecessary code execution when this check failed.

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L310-L348

    function withdrawBorrowedAmount() external override onlyBorrower(msg.sender) nonReentrant {
        LoanStatus _poolStatus = poolVariables.loanStatus;
        uint256 _tokensLent = totalSupply();
        require(
            _poolStatus == LoanStatus.COLLECTION &&
                poolConstants.loanStartTime < block.timestamp &&
                block.timestamp < poolConstants.loanWithdrawalDeadline,
            'WBA1'
        );
        IPoolFactory _poolFactory = IPoolFactory(poolFactory);
        require(_tokensLent >= _poolFactory.minBorrowFraction().mul(poolConstants.borrowAmountRequested).div(10**30), 'WBA2');

        poolVariables.loanStatus = LoanStatus.ACTIVE;
        uint256 _currentCollateralRatio = getCurrentCollateralRatio();
        require(_currentCollateralRatio >= poolConstants.idealCollateralRatio, 'WBA3');

        uint256 _noOfRepaymentIntervals = poolConstants.noOfRepaymentIntervals;
        uint256 _repaymentInterval = poolConstants.repaymentInterval;
        IRepayment(_poolFactory.repaymentImpl()).initializeRepayment(
            _noOfRepaymentIntervals,
            _repaymentInterval,
            poolConstants.borrowRate,
            poolConstants.loanStartTime,
            poolConstants.borrowAsset
        );
        IExtension(_poolFactory.extension()).initializePoolExtension(_repaymentInterval);

        address _borrowAsset = poolConstants.borrowAsset;
        (uint256 _protocolFeeFraction, address _collector) = _poolFactory.getProtocolFeeData();
        uint256 _protocolFee = _tokensLent.mul(_protocolFeeFraction).div(10**30);
        delete poolConstants.loanWithdrawalDeadline;

        uint256 _feeAdjustedWithdrawalAmount = _tokensLent.sub(_protocolFee);

        SavingsAccountUtil.transferTokens(_borrowAsset, _protocolFee, address(this), _collector);
        SavingsAccountUtil.transferTokens(_borrowAsset, _feeAdjustedWithdrawalAmount, address(this), msg.sender);

        emit AmountBorrowed(_feeAdjustedWithdrawalAmount, _protocolFee);
    }

Recommendation

Change to:

    function withdrawBorrowedAmount() external override onlyBorrower(msg.sender) nonReentrant {
        LoanStatus _poolStatus = poolVariables.loanStatus;
        require(
            _poolStatus == LoanStatus.COLLECTION &&
                poolConstants.loanStartTime < block.timestamp &&
                block.timestamp < poolConstants.loanWithdrawalDeadline,
            'WBA1'
        );
        uint256 _tokensLent = totalSupply();
        IPoolFactory _poolFactory = IPoolFactory(poolFactory);
        require(_tokensLent >= _poolFactory.minBorrowFraction().mul(poolConstants.borrowAmountRequested).div(10**30), 'WBA2');

        poolVariables.loanStatus = LoanStatus.ACTIVE;
        uint256 _currentCollateralRatio = getCurrentCollateralRatio();
        require(_currentCollateralRatio >= poolConstants.idealCollateralRatio, 'WBA3');

        uint256 _noOfRepaymentIntervals = poolConstants.noOfRepaymentIntervals;
        uint256 _repaymentInterval = poolConstants.repaymentInterval;
        IRepayment(_poolFactory.repaymentImpl()).initializeRepayment(
            _noOfRepaymentIntervals,
            _repaymentInterval,
            poolConstants.borrowRate,
            poolConstants.loanStartTime,
            poolConstants.borrowAsset
        );
        IExtension(_poolFactory.extension()).initializePoolExtension(_repaymentInterval);

        address _borrowAsset = poolConstants.borrowAsset;
        (uint256 _protocolFeeFraction, address _collector) = _poolFactory.getProtocolFeeData();
        uint256 _protocolFee = _tokensLent.mul(_protocolFeeFraction).div(10**30);
        delete poolConstants.loanWithdrawalDeadline;

        uint256 _feeAdjustedWithdrawalAmount = _tokensLent.sub(_protocolFee);

        SavingsAccountUtil.transferTokens(_borrowAsset, _protocolFee, address(this), _collector);
        SavingsAccountUtil.transferTokens(_borrowAsset, _feeAdjustedWithdrawalAmount, address(this), msg.sender);

        emit AmountBorrowed(_feeAdjustedWithdrawalAmount, _protocolFee);
    }

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

83.4956 USDC - $83.50

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L379-L383

    function _updateLiquidatorRewardFraction(uint256 _rewardFraction) internal {
        require(_rewardFraction <= 10**30, 'Fraction has to be less than 1');
        liquidatorRewardFraction = _rewardFraction;
        emit LiquidationRewardFractionUpdated(_rewardFraction);
    }

Can be changed to:

    function _updateLiquidatorRewardFraction(uint256 _rewardFraction) internal {
        require(_rewardFraction <= 1e30, 'Fraction has to be less than 1');
        liquidatorRewardFraction = _rewardFraction;
        emit LiquidationRewardFractionUpdated(_rewardFraction);
    }

and save some gas from unnecessary arithmetic operation in 10**30.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor disputed

Awards

83.4956 USDC - $83.50

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L466-L472

  function updateinterestAccruedTillLastPrincipalUpdate(uint256 _id) internal {
      require(creditLineVariables[_id].status == CreditLineStatus.ACTIVE, 'CreditLine: The credit line is not yet active.');

      uint256 _interestAccrued = calculateInterestAccrued(_id);
      uint256 _newInterestAccrued = (creditLineVariables[_id].interestAccruedTillLastPrincipalUpdate).add(_interestAccrued);
      creditLineVariables[_id].interestAccruedTillLastPrincipalUpdate = _newInterestAccrued;
  }

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L691-L727

    function borrow(uint256 _id, uint256 _amount) external payable nonReentrant onlyCreditLineBorrower(_id) {
        require(creditLineVariables[_id].status == CreditLineStatus.ACTIVE, 'CreditLine: The credit line is not yet active.');
        uint256 _borrowableAmount = calculateBorrowableAmount(_id);
        require(_amount <= _borrowableAmount, "CreditLine::borrow - The current collateral ratio doesn't allow to withdraw the amount");
        address _borrowAsset = creditLineConstants[_id].borrowAsset;
        address _lender = creditLineConstants[_id].lender;

        updateinterestAccruedTillLastPrincipalUpdate(_id);
        creditLineVariables[_id].principal = creditLineVariables[_id].principal.add(_amount);
        creditLineVariables[_id].lastPrincipalUpdateTime = block.timestamp;

        uint256 _tokenDiffBalance;
        if (_borrowAsset != address(0)) {
            uint256 _balanceBefore = IERC20(_borrowAsset).balanceOf(address(this));
            _withdrawBorrowAmount(_borrowAsset, _amount, _lender);
            uint256 _balanceAfter = IERC20(_borrowAsset).balanceOf(address(this));
            _tokenDiffBalance = _balanceAfter.sub(_balanceBefore);
        } else {
            uint256 _balanceBefore = address(this).balance;
            _withdrawBorrowAmount(_borrowAsset, _amount, _lender);
            uint256 _balanceAfter = address(this).balance;
            _tokenDiffBalance = _balanceAfter.sub(_balanceBefore);
        }
        uint256 _protocolFee = _tokenDiffBalance.mul(protocolFeeFraction).div(10**30);
        _tokenDiffBalance = _tokenDiffBalance.sub(_protocolFee);

        if (_borrowAsset == address(0)) {
            (bool feeSuccess, ) = protocolFeeCollector.call{value: _protocolFee}('');
            require(feeSuccess, 'Transfer fail');
            (bool success, ) = msg.sender.call{value: _tokenDiffBalance}('');
            require(success, 'Transfer fail');
        } else {
            IERC20(_borrowAsset).safeTransfer(protocolFeeCollector, _protocolFee);
            IERC20(_borrowAsset).safeTransfer(msg.sender, _tokenDiffBalance);
        }
        emit BorrowedFromCreditLine(_id, _tokenDiffBalance);
    }

updateinterestAccruedTillLastPrincipalUpdate() is unnecessary as it's being used only once. Therefore it can be inlined in borrow() to make the code simpler and save gas.

Recommendation

Change to:

  function borrow(uint256 _id, uint256 _amount) external payable nonReentrant onlyCreditLineBorrower(_id) {
      require(creditLineVariables[_id].status == CreditLineStatus.ACTIVE, 'CreditLine: The credit line is not yet active.');
      uint256 _borrowableAmount = calculateBorrowableAmount(_id);
      require(_amount <= _borrowableAmount, "CreditLine::borrow - The current collateral ratio doesn't allow to withdraw the amount");
      address _borrowAsset = creditLineConstants[_id].borrowAsset;
      address _lender = creditLineConstants[_id].lender;

      creditLineVariables[_id].interestAccruedTillLastPrincipalUpdate = (creditLineVariables[_id].interestAccruedTillLastPrincipalUpdate).add(calculateInterestAccrued(_id));
      creditLineVariables[_id].principal = creditLineVariables[_id].principal.add(_amount);
      creditLineVariables[_id].lastPrincipalUpdateTime = block.timestamp;

      uint256 _tokenDiffBalance;
      if (_borrowAsset != address(0)) {
          uint256 _balanceBefore = IERC20(_borrowAsset).balanceOf(address(this));
          _withdrawBorrowAmount(_borrowAsset, _amount, _lender);
          uint256 _balanceAfter = IERC20(_borrowAsset).balanceOf(address(this));
          _tokenDiffBalance = _balanceAfter.sub(_balanceBefore);
      } else {
          uint256 _balanceBefore = address(this).balance;
          _withdrawBorrowAmount(_borrowAsset, _amount, _lender);
          uint256 _balanceAfter = address(this).balance;
          _tokenDiffBalance = _balanceAfter.sub(_balanceBefore);
      }
      uint256 _protocolFee = _tokenDiffBalance.mul(protocolFeeFraction).div(10**30);
      _tokenDiffBalance = _tokenDiffBalance.sub(_protocolFee);

      if (_borrowAsset == address(0)) {
          (bool feeSuccess, ) = protocolFeeCollector.call{value: _protocolFee}('');
          require(feeSuccess, 'Transfer fail');
          (bool success, ) = msg.sender.call{value: _tokenDiffBalance}('');
          require(success, 'Transfer fail');
      } else {
          IERC20(_borrowAsset).safeTransfer(protocolFeeCollector, _protocolFee);
          IERC20(_borrowAsset).safeTransfer(msg.sender, _tokenDiffBalance);
      }
      emit BorrowedFromCreditLine(_id, _tokenDiffBalance);
  }

#0 - ritik99

2021-12-24T07:22:31Z

We believe the code separation helps with readability

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

83.4956 USDC - $83.50

External Links

Handle

WatchPug

Vulnerability details

For the storage variables that will be accessed multiple times, cache them in the stack can save ~100 gas from each extra read (SLOAD after Berlin).

For example:

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

83.4956 USDC - $83.50

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L691-L727

function borrow(uint256 _id, uint256 _amount) external payable nonReentrant onlyCreditLineBorrower(_id) {
    require(creditLineVariables[_id].status == CreditLineStatus.ACTIVE, 'CreditLine: The credit line is not yet active.');
    uint256 _borrowableAmount = calculateBorrowableAmount(_id);
    require(_amount <= _borrowableAmount, "CreditLine::borrow - The current collateral ratio doesn't allow to withdraw the amount");
    address _borrowAsset = creditLineConstants[_id].borrowAsset;
    address _lender = creditLineConstants[_id].lender;

_borrowableAmount is unnecessary. The code above can be changed to:

function borrow(uint256 _id, uint256 _amount) external payable nonReentrant onlyCreditLineBorrower(_id) {
    require(creditLineVariables[_id].status == CreditLineStatus.ACTIVE, 'CreditLine: The credit line is not yet active.');
    require(_amount <= calculateBorrowableAmount(_id), "CreditLine::borrow - The current collateral ratio doesn't allow to withdraw the amount");
    address _borrowAsset = creditLineConstants[_id].borrowAsset;
    address _lender = creditLineConstants[_id].lender;

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L391-L399

function calculateInterest(
    uint256 _principal,
    uint256 _borrowRate,
    uint256 _timeElapsed
) public pure returns (uint256) {
    uint256 _interest = _principal.mul(_borrowRate).mul(_timeElapsed).div(10**30).div(YEAR_IN_SECONDS);

    return _interest;
}

_interest is unnecessary.

Findings Information

🌟 Selected for report: Jujic

Also found by: WatchPug

Labels

bug
duplicate
G (Gas Optimization)

Awards

37.573 USDC - $37.57

External Links

Handle

WatchPug

Vulnerability details

For the arithmetic operations that will never over/underflow, using SafeMath will cost more gas.

For example:

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L460-L462

        if (_maxPossible > _currentDebt) {
            return _maxPossible.sub(_currentDebt);
        }

_maxPossible - _currentDebt will never underflow.

Recommendation

Change to:

        if (_maxPossible > _currentDebt) {
            return _maxPossible - _currentDebt;
        }

#0 - ritik99

2021-12-26T17:26:56Z

Duplicate of #92

Findings Information

🌟 Selected for report: 0xngndev

Also found by: Jujic, WatchPug, robee, sirhashalot

Labels

bug
duplicate
G (Gas Optimization)

Awards

10.9563 USDC - $10.96

External Links

Handle

WatchPug

Vulnerability details

Every reason string takes at least 32 bytes.

Use short reason strings that fits in 32 bytes or it will become more expensive.

Instances include:

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/PoolFactory.sol#L151-L151

require(poolRegistry[msg.sender], 'PoolFactory::onlyPool - Only pool can destroy itself');

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/PoolFactory.sol#L159-L162

require(
    IVerification(userRegistry).isUser(msg.sender, _verifier),
    'PoolFactory::onlyBorrower - Only a valid Borrower can create Pool'
);

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/PoolFactory.sol#L278-L278

require(_borrowToken != _collateralToken, 'PoolFactory::createPool - cant borrow the asset put in as collateralToken');

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L694-L694

require(_amount <= _borrowableAmount, "CreditLine::borrow - The current collateral ratio doesn't allow to withdraw the amount");

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccount.sol#L160-L160

require(_amount != 0, 'SavingsAccount::switchStrategy Amount must be greater than zero');

#0 - ritik99

2021-12-25T16:43:44Z

Duplicate of #47

Findings Information

🌟 Selected for report: 0x0x0x

Also found by: WatchPug, pmerkleplant, robee

Labels

bug
duplicate
G (Gas Optimization)

Awards

15.2171 USDC - $15.22

External Links

Handle

WatchPug

Vulnerability details

Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.

Caching the array length in the stack saves around 3 gas per iteration.

Instances include:

#0 - ritik99

2021-12-25T18:37:43Z

Duplicate of #157

Findings Information

🌟 Selected for report: TomFrenchBlockchain

Also found by: WatchPug

Labels

bug
duplicate
G (Gas Optimization)

Awards

37.573 USDC - $37.57

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L593-L607

function accept(uint256 _id) external {
    require(
        creditLineVariables[_id].status == CreditLineStatus.REQUESTED,
        'CreditLine::acceptCreditLineLender - CreditLine is already accepted'
    );
    bool _requestByLender = creditLineConstants[_id].requestByLender;
    require(
        (msg.sender == creditLineConstants[_id].borrower && _requestByLender) ||
            (msg.sender == creditLineConstants[_id].lender && !_requestByLender),
        "Only Borrower or Lender who hasn't requested can accept"
    );
    creditLineVariables[_id].status = CreditLineStatus.ACTIVE;
    emit CreditLineAccepted(_id);
}

L599-602 can use ternary operator to make the code simpler and save some gas.

Recommendation

Change to:

function accept(uint256 _id) external {
    require(
        creditLineVariables[_id].status == CreditLineStatus.REQUESTED,
        'CreditLine::acceptCreditLineLender - CreditLine is already accepted'
    );
    require(
        msg.sender == (creditLineConstants[_id].requestByLender ? creditLineConstants[_id].borrower : creditLineConstants[_id].lender),
        "Only Borrower or Lender who hasn't requested can accept"
    );
    creditLineVariables[_id].status = CreditLineStatus.ACTIVE;
    emit CreditLineAccepted(_id);
}

#0 - ritik99

2021-12-26T17:32:55Z

Duplicate of #77

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