Open Dollar - T1MOH's results

A floating $1.00 pegged stablecoin backed by Liquid Staking Tokens with NFT controlled vaults.

General Information

Platform: Code4rena

Start Date: 18/10/2023

Pot Size: $36,500 USDC

Total HM: 17

Participants: 77

Period: 7 days

Judge: MiloTruck

Total Solo HM: 5

Id: 297

League: ETH

Open Dollar

Findings Distribution

Researcher Performance

Rank: 7/77

Findings: 7

Award: $1,943.12

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: falconhoof

Also found by: 0x6d6164616e, Baki, Beosin, Krace, T1MOH, bitsurfer, immeas, pep7siup, tnquanghuy0512, twicek

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
duplicate-26

Awards

180.637 USDC - $180.64

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/AccountingEngine.sol#L215

Vulnerability details

Impact

surplusTransferPercentage is meant to be set to [0; WAD] according to comments. However it is used in calculations where 100% means 100 WAD. As a result, It will try to sell amount 100x higher than intended.

Surplus transfers and auctions will be 100 times higher, also transfer percentage is incorrect: if set surplusTransferPercentage to 0.5 WAD (50%), it will transfer 0.5% to protocol and 99.5% will sell on auction

Proof of Concept

Here it says that WAD is 100% https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/interfaces/IAccountingEngine.sol#L90-L97

/// @notice Throws when surplus surplusTransferPercentage is great than WAD (100%)
  error AccEng_surplusTransferPercentOverLimit();

  // --- Structs ---

  struct AccountingEngineParams {
    // percent of the Surplus the system transfers instead of auctioning [0/100]
    uint256 surplusTransferPercentage;
    ...
  }

In this calculation ONE_HUNDRED_WAD is used instead WAD, selling amount 100x higher than intended https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/AccountingEngine.sol#L215

    if (_params.surplusTransferPercentage < ONE_HUNDRED_WAD) {
      _id = surplusAuctionHouse.startAuction({
        _amountToSell: _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage),
        _initialBid: 0
      });

Tools Used

Manual Review

Update to use WAD:

  function auctionSurplus() external returns (uint256 _id) {
    if(_params.surplusTransferPercentage > WAD) revert AccEng_surplusTransferPercentOverLimit();
    if (_params.surplusAmount == 0) revert AccEng_NullAmount();
    if (extraSurplusReceiver == address(0)) revert AccEng_NullSurplusReceiver();
    if (block.timestamp < lastSurplusTime + _params.surplusDelay) revert AccEng_SurplusCooldown();

    uint256 _coinBalance = safeEngine.coinBalance(address(this));
    uint256 _debtBalance = safeEngine.debtBalance(address(this));
    (_coinBalance, _debtBalance) = _settleDebt(_coinBalance, _debtBalance, _unqueuedUnauctionedDebt(_debtBalance));

    if (_coinBalance < _debtBalance + _params.surplusAmount + _params.surplusBuffer) {
      revert AccEng_InsufficientSurplus();
    }

    // auction surplus percentage
-   if (_params.surplusTransferPercentage < ONE_HUNDRED_WAD) {
+   if (_params.surplusTransferPercentage < WAD) {
      _id = surplusAuctionHouse.startAuction({
-       _amountToSell: _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage),
+       _amountToSell: _params.surplusAmount.wmul(WAD - _params.surplusTransferPercentage),
        _initialBid: 0
      });

      lastSurplusTime = block.timestamp;
-     emit AuctionSurplus(_id, 0, _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage));
+     emit AuctionSurplus(_id, 0, _params.surplusAmount.wmul(WAD - _params.surplusTransferPercentage));
    }

    // transfer surplus percentage
    if (_params.surplusTransferPercentage > 0) {
      if (extraSurplusReceiver == address(0)) revert AccEng_NullSurplusReceiver();

      safeEngine.transferInternalCoins({
        _source: address(this),
        _destination: extraSurplusReceiver,
        _rad: _params.surplusAmount.wmul(_params.surplusTransferPercentage)
      });

      lastSurplusTime = block.timestamp;
      emit TransferSurplus(extraSurplusReceiver, _params.surplusAmount.wmul(_params.surplusTransferPercentage));
    }
  }

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-10-26T01:33:32Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-10-26T01:33:43Z

raymondfam marked the issue as duplicate of #26

#2 - c4-judge

2023-11-03T14:08:35Z

MiloTruck marked the issue as satisfactory

Findings Information

🌟 Selected for report: Saintcode_

Also found by: T1MOH, falconhoof

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
duplicate-24

Awards

1538.6437 USDC - $1,538.64

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/AccountingEngine.sol#L181

Vulnerability details

Impact

Before starting debtAuction, current implementation checks that there is enough debt in system. However this check is performed before debt settlement. This check bypass will make totalOnAuctionDebt higher than actual debtBalance. As a result, core functions _settleDebt() and unqueuedUnauctionedDebt will revert. As well as cancelAuctionedDebtWithSurplus which means that debtAuction can't be settled.

It will introduce uncertain consequences. Because that auction with non-existant debt won't be settled in time, which means it will be restarted, every time supplying more and more tokens to mint. Somewhen it will be settled, potentially long time in future, thus minting huge amount of internalCoin to higherBidder.

Proof of Concept

Suppose following scenario:

  1. debtBalance = 300, coinBalance = 0, totalQueuedDebt = 0, totalOnAuctionDebt = 0. It means there is no bad debt in system, it shouldn't start DebtAuction
  2. debtAuctionBidSize = 300. auctionDebt() is called. It firstly ensures that debtAuctionBidSize <= debtBalance. And secondly increases totalOnAuctionDebt. And starts Auction
  function auctionDebt() external returns (uint256 _id) {
    if (_params.debtAuctionBidSize == 0) revert AccEng_DebtAuctionDisabled();

    uint256 _coinBalance = safeEngine.coinBalance(address(this));
    uint256 _debtBalance = safeEngine.debtBalance(address(this));

    if (_params.debtAuctionBidSize > _unqueuedUnauctionedDebt(_debtBalance)) revert AccEng_InsufficientDebt();

@>  (_coinBalance, _debtBalance) = _settleDebt(_coinBalance, _debtBalance, _coinBalance);
@>  totalOnAuctionDebt += _params.debtAuctionBidSize;

    _id = debtAuctionHouse.startAuction({
      _incomeReceiver: address(this),
      _amountToSell: _params.debtAuctionMintedTokens,
      _initialBid: _params.debtAuctionBidSize
    });
    ...
  }
  1. Now totalOnAuctionDebt is greater than debtBalance. Therefore functions like _unqueuedUnauctionedDebt(), _settleDebt() will revert due to underflow. As well as cancelAuctionedDebtWithSurplus because it tries to repay more debt than AccountingEngine actually has. And it will revert until system has enough debt

Tools Used

Manual Review

Change check order:

  function auctionDebt() external returns (uint256 _id) {
    ...

-   if (_params.debtAuctionBidSize > _unqueuedUnauctionedDebt(_debtBalance)) revert AccEng_InsufficientDebt();

    (_coinBalance, _debtBalance) = _settleDebt(_coinBalance, _debtBalance, _coinBalance);
    totalOnAuctionDebt += _params.debtAuctionBidSize;
+   if (_params.debtAuctionBidSize > _unqueuedUnauctionedDebt(_debtBalance)) revert AccEng_InsufficientDebt();

    ...
  }

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-10-26T01:38:32Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-10-26T01:38:40Z

raymondfam marked the issue as duplicate of #24

#2 - T1MOH593

2023-10-26T07:27:31Z

I have typo in "Proof of Concept". Correct version:

  1. debtBalance = 300, coinBalance = 300, totalQueuedDebt = 0, totalOnAuctionDebt = 0. It means there is no bad debt in system, it shouldn't start DebtAuction

#3 - c4-judge

2023-11-04T02:53:43Z

MiloTruck marked the issue as satisfactory

#4 - c4-judge

2023-11-04T02:53:50Z

MiloTruck changed the severity to 3 (High Risk)

Findings Information

🌟 Selected for report: 0xAadi

Also found by: 0xDemon, 0xlemon, 0xprinc, Arz, Giorgio, Greed, MrPotatoMagic, T1MOH, btk, ge6a, m4k2, nmirchev8, perseus, xAriextz, yashar

Awards

21.9995 USDC - $22.00

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
duplicate-429

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/proxies/ODSafeManager.sol#L235-L246

Vulnerability details

Impact

Function ODSafeManager.addSAFE() should be called when safeId was previously removed from mappings _usrSafes and _usrSafesPerCollat. ODSafeManager.addSAFE() must be called via delegateCall to BasicActions, because it uses msg.sender. However there are no such functions addSAFE() and removeSAFE() in BasicActions. It's obviously not intended because user must deploy his own library to delegateCall from proxy to call them

Proof of Concept

Note that msg.sender is used. To msg.sender be proxy, proxy must delegateCall to another function which calls addSAFE(). That's why BasicActions.sol exists. However BasicActions.sol doesn't call addSAFE() and removeSAFE()

  function addSAFE(uint256 _safe) external {
    SAFEData memory _sData = _safeData[_safe];
    _usrSafes[msg.sender].add(_safe);
    _usrSafesPerCollat[msg.sender][_sData.collateralType].add(_safe);
  }

Tools Used

Manual Review

Add calls to addSAFE() and removeSAFE() in BasicActions.sol

Assessed type

DoS

#0 - c4-pre-sort

2023-10-26T01:21:05Z

raymondfam marked the issue as sufficient quality report

#1 - raymondfam

2023-10-26T01:22:07Z

Could have had a coded POC.

#2 - c4-pre-sort

2023-10-26T01:22:47Z

raymondfam marked the issue as primary issue

#3 - c4-pre-sort

2023-10-26T19:05:19Z

raymondfam marked the issue as duplicate of #380

#4 - c4-judge

2023-11-02T18:17:18Z

MiloTruck marked the issue as not a duplicate

#5 - c4-judge

2023-11-02T18:17:30Z

MiloTruck marked the issue as duplicate of #294

#6 - c4-judge

2023-11-08T00:27:09Z

MiloTruck marked the issue as satisfactory

Findings Information

🌟 Selected for report: Kaysoft

Also found by: Arz, T1MOH, btk, fibonacci, hals, holydevoti0n, immeas, perseus, spark, tnquanghuy0512

Labels

bug
2 (Med Risk)
downgraded by judge
low quality report
satisfactory
duplicate-202

Awards

54.1911 USDC - $54.19

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/gov/ODGovernor.sol#L41

Vulnerability details

Impact

3 minutes to vote is obviously too low to vote against proposal. Especially when VotingDelay set to 1 block

Proof of Concept

votingDelay is set to 1 block, votingPeriod is set to 15 blocks: https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/gov/ODGovernor.sol#L41

abstract contract GovernorSettings is Governor {
    ...

    constructor(
        uint256 initialVotingDelay,
        uint256 initialVotingPeriod,
        uint256 initialProposalThreshold
    ) {
        _setVotingDelay(initialVotingDelay);
        _setVotingPeriod(initialVotingPeriod);
        _setProposalThreshold(initialProposalThreshold);
    }

    ...
}

Tools Used

Manual Review

Set votingPeriod and votingDelay to real values

Assessed type

Governance

#0 - c4-pre-sort

2023-10-26T01:12:08Z

raymondfam marked the issue as low quality report

#1 - c4-pre-sort

2023-10-26T01:12:37Z

raymondfam marked the issue as duplicate of #73

#2 - c4-judge

2023-11-02T07:06:55Z

MiloTruck changed the severity to 2 (Med Risk)

#3 - c4-judge

2023-11-02T08:47:14Z

MiloTruck marked the issue as satisfactory

Findings Information

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
duplicate-171

Awards

37.1417 USDC - $37.14

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/proxies/ODSafeManager.sol#L105-L109

Vulnerability details

Impact

Current implementation allows approved user to give permissions. As a result, malicious approved user can't be deleted from approved, because always can give approval to another malicious address

Proof of Concept

safeAllowed() checks that msg.sender is owner or approved:

  function allowSAFE(uint256 _safe, address _usr, uint256 _ok) external safeAllowed(_safe) {
    address _owner = _safeData[_safe].owner;
    safeCan[_owner][_safe][_usr] = _ok;
    emit AllowSAFE(msg.sender, _safe, _usr, _ok);
  }

  modifier safeAllowed(uint256 _safe) {
    address _owner = _safeData[_safe].owner;
    if (msg.sender != _owner && safeCan[_owner][_safe][msg.sender] == 0) revert SafeNotAllowed();
    _;
  }

So when user decides to remove approval, malicious user can frontrun and give permission to another address

Tools Used

Manual Review

Permit only owner to give permission:

- function allowSAFE(uint256 _safe, address _usr, uint256 _ok) external safeAllowed(_safe) {
+ function allowSAFE(uint256 _safe, address _usr, uint256 _ok) external {
    address _owner = _safeData[_safe].owner;
+   if (msg.sender != _owner) revert SafeNotAllowed();
    safeCan[_owner][_safe][_usr] = _ok;
    emit AllowSAFE(msg.sender, _safe, _usr, _ok);
  }

Assessed type

MEV

#0 - c4-pre-sort

2023-10-26T01:26:16Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-10-26T01:26:24Z

raymondfam marked the issue as primary issue

#2 - raymondfam

2023-10-26T01:26:34Z

Good catch.

#3 - c4-pre-sort

2023-10-26T06:20:37Z

raymondfam marked the issue as duplicate of #171

#4 - c4-judge

2023-11-02T08:44:25Z

MiloTruck marked the issue as satisfactory

Findings Information

🌟 Selected for report: klau5

Also found by: 0x6d6164616e, Arz, T1MOH, immeas, josephdara, nican0r, tnquanghuy0512

Labels

bug
2 (Med Risk)
high quality report
satisfactory
sponsor confirmed
duplicate-156

Awards

102.2123 USDC - $102.21

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/oracles/CamelotRelayer.sol#L68-L101

Vulnerability details

Impact

CamelotRelayer reverts when tries to get price from Camelot Pool. It uses Uniswap's OracleLibrary, however Camelot Pool is not compatible with it.

Proof of Concept

OracleLibrary's functions consult() and getOldestObservationSecondsAgo() are utilized. They call functions observe() and slot0() on CamelotPool contract

    function consult(address pool, uint32 secondsAgo)
        internal
        view
        returns (int24 arithmeticMeanTick, uint128 harmonicMeanLiquidity)
    {
        ...

@>      (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = IUniswapV3Pool(pool)
            .observe(secondsAgos);

        ...
    }

    function getOldestObservationSecondsAgo(address pool) internal view returns (uint32 secondsAgo) {
@>      (, , uint16 observationIndex, uint16 observationCardinality, , , ) = IUniswapV3Pool(pool).slot0();
        
        ...
    }

However CamelotPool doesn't have such functions. Insert this file to test/

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;

import "./e2e/Common.t.sol";
import "../src/contracts/oracles/CamelotRelayer.sol";

import "forge-std/console.sol";
import "forge-std/Test.sol";

contract MyTest is Test {

    function test_CamelotRelayerDontWork() public {
        // fork Arb Mainnet
        vm.createSelectFork("https://arbitrum-one.publicnode.com");

        // https://docs.camelot.exchange/contracts/amm-v2/factory
        // Get random pair - index 2
        ICamelotPair camelotPair = ICamelotPair(ICamelotFactory(0x6EcCab422D763aC031210895C81787E87B43A652).allPairs(2));
        

        // Token addresses from pair https://arbiscan.io/address/0x9Fa7314525d5C706213468220C7c9820Cfe629cb
        CamelotRelayer camelotRelayer = new CamelotRelayer(camelotPair.token0(), camelotPair.token1(), 100);

        // reverts because Camelot pair doesn't have `slot0()` and `observe()`
        camelotRelayer.getResultWithValidity();
    }
}

Tools Used

Manual Review

Develop custom OracleLibrary for CamelotPool

Assessed type

Oracle

#0 - c4-pre-sort

2023-10-26T01:16:17Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-10-26T01:16:20Z

raymondfam marked the issue as primary issue

#2 - raymondfam

2023-10-26T01:18:05Z

Could have provided links on OracleLibrary doesn't work with Camelot Pools.

#3 - c4-pre-sort

2023-10-27T05:22:56Z

raymondfam marked the issue as high quality report

#4 - c4-sponsor

2023-10-27T22:55:35Z

pi0neerpat (sponsor) confirmed

#5 - c4-judge

2023-11-02T04:49:03Z

MiloTruck marked issue #156 as primary and marked this issue as a duplicate of 156

#6 - c4-judge

2023-11-02T08:46:13Z

MiloTruck marked the issue as satisfactory

Awards

8.3007 USDC - $8.30

Labels

bug
grade-b
low quality report
QA (Quality Assurance)
Q-22

External Links

1. Low. Consider disabling ability to deploy proxy for proxy

https://github.com/open-dollar/od-contracts/blob/67e5917e7dc0c16324aff3fde0298cd218a15152/src/contracts/proxies/Vault721.sol#L154-L156

Perform additional check to ensure that user is not registered proxy

  function _isNotProxy(address _user) internal view returns (bool) {
-   return _userRegistry[_user] == address(0) || ODProxy(_userRegistry[_user]).OWNER() != _user;
+   return (_userRegistry[_user] == address(0) || ODProxy(_userRegistry[_user]).OWNER() != _user) && 
+     _proxyRegistry[_user] == address(0);
  }

#0 - c4-pre-sort

2023-10-27T01:27:30Z

raymondfam marked the issue as low quality report

#1 - c4-judge

2023-11-03T16:56:04Z

MiloTruck marked the issue as grade-c

#2 - c4-judge

2023-11-03T16:56:14Z

MiloTruck marked the issue as grade-a

#3 - c4-judge

2023-11-03T16:56:19Z

MiloTruck marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter