Closed sherlock-admin2 closed 2 weeks ago
@WangSecurity In a contest where the requirement for medium and high severity requires quantifying the loss of funds (0.5% and 5.0%) you say that there must be zero speculation about the impact and attack cost? By your logic every single issue can be thrown out because quantifying the impact and cost of the attack is "speculation".
The fact remains, that the maximal impact of this is the complete takeover of the protocol due to the low amount of time (16 hours) to amass enough votes to outvote the attacker and this is a reasonable speculation to make. You can see sponsor also shared this opinion here - https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/63#issuecomment-2297193835.
@WangSecurity, thank you for your review.
II would like to ask you to reconsider your position on the impact. From what I can tell, the sponsor’s perspective is reflected in the following comment:
In contrast, issue 64 only needs to show that the cost wouldn't exceed the potential gain obtained from taking control of the protocol.
This is one of the arguments demonstrating that the impact involves taking control of the protocol. I have also provided data showing that such a significant vote has never occurred before in history in one of my previous comments.
We can also examine other protocols, like Compound. The total supply is 8,378,124 COMP, but only 682,191 COMP, or ~8% of the total supply, was required to ensure a malicious proposal was executed. You can refer to this article as an example, as well as the proposal.
Another example is the Balancer vs. Humpy wars. See this report by Messari. The circulating supply of Balancer is 59,433,481 BAL. Humpy won several proposals, with the most expensive being BIP-112, which required 4,201,506 veBAL, or ~7%. veBAL is 80% BAL, 20% ETH. Others were cheaper. And the war went on for months.
As calculated in my previous comment, the attacker would gain over 13% of the votes. The Compound community had 5 days to gather votes and failed, as you can see in the "Proposal History" on the proposal page. And they had all the active voters
I haven't seen any data that shows it would be possible to collect so many votes in the much shorter timeframe required for Maker (16 hours).
The decision is final. Sherlock's verdict stands, regardless of any counterarguments.
Firstly, @nobodytrue, your previous comments were deleted/hidden because they were off-topic and didn’t add any value to the escalation/discussion.
Secondly, far not every attack is “speculation” on the cost/impact and only some specific attacks require that.
Thirdly, @00xSEV thank you for these references, but it still doesn’t guarantee how it’s going to be on Maker and gaining 13% of MKR is not guaranteed to give you the control over maker. Moreover, if my calculations are correct, just buying 13% of the MKR total supply would be cheaper than executing this attack.
Hence, my decision remains that this issue has to be low-severity, planning to accept the escalation and invalidate the issue.
@nobodytrue again, you're comments are deleted for being off-topic and inappropriate.
Result: Invalid Has duplicates
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
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
eoa11
eoa12
, when used as a salt for LSUrn creation, produces an urn with an address equal toeoa11
.vat.hope(attacker)
andlsmkr.approve(attacker, max)
fromeoa11
.LSE.open(0)
fromeoa12
:LSUrn1
.LSUrn1
address ==eoa11
address.LSUrn1
retains the approvals given fromeoa11
in step 2.eoa21
,eoa22
, andLSUrn2
.LSE.lock(LSUrn1, 1000e18)
to deposit 1000 MKR intoLSUrn1
:vat.urns[LSUrn1].ink
by 1000e18.urnVoteDelegates[LSUrn1]
remainsaddress(0)
.LSUrn1
toLSUrn2
:attacker
account usingvat.fork
because both LSUrns have given approval to theattacker
address.vat.frob
can be used to move fromvat.urns[LSUrn1].ink
tovat.gem[LSUrn1]
, and thenvat.frob
to move fromvat.gem[LSUrn1]
tovat.urns[LSUrn2].ink
.attackersVD
(controlled by the attacker) usingVoteDelegateFactory.create
from theattacker
address.LSE.selectVoteDelegate(LSUrn1, victimVD)
:victimVD
is the target for fund extraction.vat.urns(ilk, urn)
.LSUrn2
in step 6,LSUrn1
has 0 .ink, so no funds are moved tovictimVD
, buturnVoteDelegates[LSUrn1]
is set tovictimVD
.LSUrn2
back toLSUrn1
(See step 6).LSE.selectVoteDelegate(LSUrn1, attackersVD)
:LSUrn1
has 1000e18 .ink.VD.withdraw
inside_selectVoteDelegate
withprevVoteDelegate
set tovictimVD
.victimVD
toattackersVD
.LSE.selectVoteDelegate(LSUrn2, victimVD)
(see step 8).LSUrn1
toLSUrn2
(See step 6).LSE.selectVoteDelegate(LSUrn2, attackersVD)
(see step 10):vat.urns[LSUrn2].ink
== 1000e18.attackersVD
balance of MKR is 2000e18.victimVD
.victimVD
.victimVD
withaddress(0)
and repeat steps 8-13 to move all funds in LSE toattackersVD
.LSE.free
850 MKR (depositing 1000 MKR - 15% withdrawal fee) to reduce the capital/cost required for the attack.hat
and vote for it with all the stolen power (almost all active voters), thereby gaining full control of the system:hat
from being selected.Result:
attackersVD
, effectively immobilizing all LSE.hat
election.Other Variations:
eoa11
can be replaced with a contract created byeoa3
. The address of the contract can be brute-forced in the same way aseoa11
. The contract performs step 2 instead ofeoa11
and self-destructs in the same transaction.LSUrn2
will be a regular urn. In step 13, the attacker must transfer LS MKR fromLSUrn1
and withdraw 85% (with a 15% withdrawal fee). They then deposit (lock
) it inLSUrn1
and repeat the process. Refer toALSEH6.testAttack1Loop1Urn
in PoC.testAttack4SendOneInk*
.LSUrn1
(testAttack5SendLsMkr).Link to create2
Impact
delay
(16 hours).chief.hat
, thereby gaining full control over the system:Line
).attackersVD
. This will brick all LSE operations.Code Snippet
test/ALockstakeEngine.sol
in the root project directory.test/ALockstakeEngine.sol
It is based on the
LockstakeEngine.t.sol
setUp
function:block.number
for caching RPC callschief
andpolling
contracts from mainnetVoteDelegateFactory
To see the diff, you can run
git diff
. Note: all other functions exceptsetUp
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(); ```Add the following
remappings.txt
to the root project directory.Run
forge test --match-path test/ALSEH5.sol -vvv
(PoCs for 2 LSUrns)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");
}
Tool used
Manual Review
Recommendation
salt
, including usingmsg.sender
.block.prevrandao
withmsg.sender
. This approach will make finding a collision practically impossible within the short timeframe thatprevrandao
is known.