Ethos Reserve contest - bin2chen's results

A CDP-backed stablecoin platform designed to generate yield on underlying assets to establish a sustainable DeFi stable interest rate.

General Information

Platform: Code4rena

Start Date: 16/02/2023

Pot Size: $144,750 USDC

Total HM: 17

Participants: 154

Period: 19 days

Judge: Trust

Total Solo HM: 5

Id: 216

League: ETH

Ethos Reserve

Findings Distribution

Researcher Performance

Rank: 4/154

Findings: 4

Award: $9,600.63

QA:
grade-b

๐ŸŒŸ Selected for report: 1

๐Ÿš€ Solo Findings: 0

Findings Information

๐ŸŒŸ Selected for report: 0xBeirao

Also found by: bin2chen

Labels

bug
3 (High Risk)
judge review requested
satisfactory
upgraded by judge
duplicate-481

Awards

6760.0877 USDC - $6,760.09

External Links

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/StabilityPool.sol#L531

Vulnerability details

Impact

no judgment that lastLUSDLossError_Offset!=0, but _debtToOffset is 0 _computeRewardsPerUnitStaked() may overflow

Proof of Concept

Use lastLUSDLossError_Offset in the _computeRewardsPerUnitStaked() method to store the last excess The code is as follows:

    function _computeRewardsPerUnitStaked(
        address _collateral,
        uint _collToAdd,
        uint _debtToOffset,
        uint _totalLUSDDeposits
    )
        internal
        returns (uint collGainPerUnitStaked, uint LUSDLossPerUnitStaked)
    {
...

       if (_debtToOffset == _totalLUSDDeposits) {
            LUSDLossPerUnitStaked = DECIMAL_PRECISION;  // When the Pool depletes to 0, so does each deposit 
            lastLUSDLossError_Offset = 0;
        } else {

            uint LUSDLossNumerator = _debtToOffset.mul(DECIMAL_PRECISION).sub(lastLUSDLossError_Offset);//<----------If _debtToOffset==0,but lastLUSDLossError_Offset!=0 will overflow
            /*    

Here is a problem, there is no judgment that lastLUSDLossError_Offset!=0, but _debtToOffset is 0, so when this method is executed will overflow is it possible for _debtToOffset to be 0? This is possible updateRewardSum() is to pass _debtToOffset=0 The code is as follows:

    function updateRewardSum(address _collateral, uint _collToAdd) external override {
        _requireCallerIsActivePool();
        uint totalLUSD = totalLUSDDeposits; // cached to save an SLOAD
        if (totalLUSD == 0) { return; }

        _triggerLQTYIssuance(communityIssuance);

        (uint collGainPerUnitStaked, ) = _computeRewardsPerUnitStaked(_collateral, _collToAdd, 0, totalLUSD);//<---------_debtToOffset==0


    }

In ActivePool._rebalance() will call StabilityPool.updateRewardSum()

This will cause the ActivePool to overflow and fail if there is a residual lastLUSDLossError_Offset after the execution of StabilityPool.offset().

Tools Used

_debtToOffset < lastLUSDLossError_Offset then return LUSDLossPerUnitStaked =0

#0 - c4-judge

2023-03-08T15:32:13Z

trust1995 marked the issue as primary issue

#1 - c4-judge

2023-03-08T15:32:17Z

trust1995 marked the issue as satisfactory

#2 - trust1995

2023-03-08T15:32:27Z

Waiting for sponsor to weigh in.

#3 - tess3rac7

2023-03-14T14:46:40Z

Duplicate of #481 -- which is a valid high issue (and was communicated to us directly by 0xBeirao during the contest). Leaving to judge how to deal with this issue.

#4 - c4-sponsor

2023-03-14T14:46:47Z

tess3rac7 requested judge review

#5 - c4-judge

2023-03-20T10:33:51Z

trust1995 marked the issue as duplicate of #481

#6 - c4-judge

2023-03-20T10:34:18Z

trust1995 changed the severity to 3 (High Risk)

Findings Information

๐ŸŒŸ Selected for report: bin2chen

Also found by: rbserver

Labels

bug
2 (Med Risk)
disagree with severity
primary issue
satisfactory
selected for report
sponsor confirmed
M-02

Awards

2636.4342 USDC - $2,636.43

External Links

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L141

Vulnerability details

Impact

_harvestCore() roi calculation error,may double

Proof of Concept

The _harvestCore() will calculate the roi and repayment values The implementation code is as follows:

    function _harvestCore(uint256 _debt) internal override returns (int256 roi, uint256 repayment) {
        _claimRewards();
        uint256 numSteps = steps.length;
        for (uint256 i = 0; i < numSteps; i = i.uncheckedInc()) {
            address[2] storage step = steps[i];
            IERC20Upgradeable startToken = IERC20Upgradeable(step[0]);
            uint256 amount = startToken.balanceOf(address(this));
            if (amount == 0) {
                continue;
            }
            _swapVelo(step[0], step[1], amount, VELO_ROUTER);
        }

        uint256 allocated = IVault(vault).strategies(address(this)).allocated;
        uint256 totalAssets = balanceOf();
        uint256 toFree = _debt;

        if (totalAssets > allocated) {
            uint256 profit = totalAssets - allocated;
            toFree += profit;
            roi = int256(profit);
        } else if (totalAssets < allocated) {
            roi = -int256(allocated - totalAssets);
        }

        (uint256 amountFreed, uint256 loss) = _liquidatePosition(toFree);
        repayment = MathUpgradeable.min(_debt, amountFreed);
        roi -= int256(loss);//<------่ฟ™ไธชๅœฐๆ–นๅฏ่ƒฝไผšๅฏผ่‡ด้‡ๅค
    }

The last line may cause double counting of losses For example, the current: vault.allocated = 9 vault.strategy.allocBPS = 9000 strategy.totalAssets = 9

Suppose that after some time, strategy loses 2, then: strategy.totalAssets = 9 - 2 = 7 Also the administrator sets vault.strategy.allocBPS = 0

This executes harvest()->_harvestCore(9) to get roi = 4 repayment = 7

The actual loss of 2, but roi = 4 (double), test code as follows:

add to test/starter-test.js 'Vault Tests'

    it.only('test_roi', async function () {
      const {vault, strategy, wantHolder, strategist} = await loadFixture(deployVaultAndStrategyAndGetSigners);
      const depositAmount = toWantUnit('10');
      await vault.connect(wantHolder)['deposit(uint256)'](depositAmount);
      await strategy.harvest();

      const balanceOf = await strategy.balanceOf();
      console.log(`strategy balanceOf: ${balanceOf}`);
      // allocated = 9
      // 1.loss 2, left 7
      await strategy.lossFortest(toWantUnit('2'));
      // 2.modify bps=>0
      await vault.connect(strategist).updateStrategyAllocBPS(strategy.address, 0);
      // 3.so debt = 9
      await strategy.harvest();

      const {allocated, losses, allocBPS} = await vault.strategies(strategy.address);
      console.log(`losses: ${losses}`);
      console.log(`allocated: ${allocated}`);
      console.log(`allocBPS: ${allocBPS}`);
    });

add to ReaperStrategyGranarySupplyOnly.sol

    function lossFortest(uint256 amout) external{
        ILendingPool(ADDRESSES_PROVIDER.getLendingPool()).withdraw(address(want), amout, address(1));        
    }
$ npx hardhat test test/starter-test.js

  Vaults
    Vault Tests
strategy balanceOf: 900000000
losses: 400000000     <--------will double
allocated: 0
allocBPS: 0

The last vault's allocated is correct, but the loss is wrong Statistics and bpsChange of _reportLoss() will be wrong

Tools Used

remove roi -= int256(loss);

#0 - c4-judge

2023-03-08T15:27:08Z

trust1995 marked the issue as satisfactory

#1 - c4-judge

2023-03-08T15:27:12Z

trust1995 marked the issue as primary issue

#2 - tess3rac7

2023-03-14T14:42:08Z

Similar to #488

Recommend low priority since it's an edge case that would only affect reporting data.

#3 - c4-sponsor

2023-03-14T14:42:15Z

tess3rac7 marked the issue as disagree with severity

#4 - trust1995

2023-03-20T10:29:26Z

Reporting data is handled by the vault at: debt = IVault(vault).report(roi, repayment); We cannot rule out damage that may occur due to miscalculations on report / debt, so med severity seems appropriate.

#5 - c4-judge

2023-03-20T15:47:49Z

trust1995 marked the issue as selected for report

#6 - c4-sponsor

2023-03-20T16:06:49Z

tess3rac7 marked the issue as sponsor confirmed

Findings Information

Labels

bug
2 (Med Risk)
satisfactory
sponsor disputed
duplicate-632

Awards

142.8544 USDC - $142.85

External Links

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L251

Vulnerability details

Impact

There may be a loss of precision in the vault deposit, resulting in _rebalance overflow

Proof of Concept

After executing ActivePool.rebalance() it will first calculate the current profit The code is as follows:

    function _rebalance(address _collateral, uint256 _amountLeavingPool) internal {
...
        vars.currentAllocated = yieldingAmount[_collateral];
        

        vars.yieldGenerator = IERC4626(yieldGenerator[_collateral]);
        vars.ownedShares = vars.yieldGenerator.balanceOf(address(this));
        vars.sharesToAssets = vars.yieldGenerator.convertToAssets(vars.ownedShares);

        vars.profit = vars.sharesToAssets.sub(vars.currentAllocated); //<-------vars.profit is uint256
        if (vars.profit < yieldClaimThreshold[_collateral]) {
            vars.profit = 0;
        }    

where vars.fit is uint256, the default is not negative The documentation is described below: The vault will be farming the lowest-risk yield possible, so you can assume that the principal will be protected from loss

But there is a problem, if the vault is not exclusive (only one depositor), when making a deposit is generally a loss of precision If you deposit 100 (* e18), you will get 99.99999999999999999999 (*e18) if you take it immediately Simplify ReaperVaultV2.sol as follows:

  function test() external {
    uint256 totalSupply = 333*10**18;
    uint256 totalAsset = 340*10**18;
    uint256 depositAsset = 100*10**18;

    //1.get shares
    uint256 getShares = depositAsset * totalSupply / totalAsset; //round down
    totalAsset  += depositAsset;
    totalSupply += getShares;
    //2.convertToAssets use round down
    uint256 getAsset = getShares * totalAsset / totalSupply; //round down
    console.log("depositAsset:",depositAsset);
    console.log("getAsset:",getAsset);
  }
$ forge test -vvv

[PASS] test() (gas: 4593)
Logs:
  depositAsset: 100000000000000000000
  getAsset: 99999999999999999999

This will result in vars.profit = vars.sharesToAssets.sub(vars.currentAllocated); overflow ActivePool.sendCollateral()/ActivePool.pullCollateralFromBorrowerOperationsOrDefaultPool() will revert Until there are gains or other depositors

Tools Used

vars.sharesToAssets.sub(vars.currentAllocated) If less than 0, then set profit=0

#0 - c4-judge

2023-03-08T15:43:45Z

trust1995 marked the issue as primary issue

#1 - c4-judge

2023-03-08T15:47:06Z

trust1995 marked the issue as satisfactory

#2 - tess3rac7

2023-03-14T14:57:55Z

Within this test https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/test/starter-test.js#L332 one can add console logs for userBalance and userBalanceAfterWithdraw and verify that they are same.

Moreover,

  • System is not designed to handle losses as described in the docs:

The vault will be farming the lowest-risk yield possible, so you can assume that the principal will be protected from loss

  • Only ActivePool will be given DEPOSITOR role

#3 - c4-sponsor

2023-03-14T14:58:02Z

tess3rac7 marked the issue as sponsor disputed

#4 - trust1995

2023-03-20T10:42:17Z

I don't think it is a fair certainty that strategies will not yield losses. In light of that, med severity seems appropriate.

#5 - c4-judge

2023-03-20T15:51:46Z

trust1995 marked issue #632 as primary and marked this issue as a duplicate of 632

L-1: CollateralConfig.updateCollateralRatios() will modify the MCR and CCR, these two parameters will affect whether the user is liquidated, related to the user's collateral. Although the administrator is a multi-signature wallet, but still need to have an effective time, to give the user sufficient time to choose whether to keep trove or repayment

L-2: increaseLUSDDebt()/decreaseLUSDDebt() Event missing emit

    function increaseLUSDDebt(address _collateral, uint _amount) external override {
        _requireValidCollateralAddress(_collateral);
        _requireCallerIsBOorTroveM();
        LUSDDebt[_collateral] = LUSDDebt[_collateral].add(_amount);
-       ActivePoolLUSDDebtUpdated(_collateral, LUSDDebt[_collateral]);
+       emit ActivePoolLUSDDebtUpdated(_collateral, LUSDDebt[_collateral]);
    }

L-3: TroveManager._removeTroveOwner() Forgot settings Troves[_borrower][_collateral].arrayIndex = 0 when clearing Trove Owner

function _removeTroveOwner(address _borrower, address _collateral, uint TroveOwnersArrayLength) internal { Status troveStatus = Troves[_borrower][_collateral].status; // Itโ€™s set in caller function `_closeTrove` assert(troveStatus != Status.nonExistent && troveStatus != Status.active); uint128 index = Troves[_borrower][_collateral].arrayIndex; uint length = TroveOwnersArrayLength; uint idxLast = length.sub(1); assert(index <= idxLast); address addressToMove = TroveOwners[_collateral][idxLast]; TroveOwners[_collateral][index] = addressToMove; Troves[addressToMove][_collateral].arrayIndex = index; emit TroveIndexUpdated(addressToMove, _collateral, index);
  • Troves[_borrower][_collateral].arrayIndex = 0; TroveOwners[_collateral].pop();
    }

#0 - c4-judge

2023-03-08T15:19:07Z

trust1995 marked the issue as grade-b

#1 - c4-sponsor

2023-03-17T04:04:14Z

0xBebis marked the issue as sponsor acknowledged

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