Closed sherlock-admin4 closed 1 month ago
1 comment(s) were left on this issue during the judging contest.
Audittens commented:
Attack cost is the same as for the issue 64
The protocol team comments:
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 aVoteDelegate
could be use to grief (but not steal) funds deposited to aVoteDelegate
, 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."
Escalate
if https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/64 is a medium
this issue should be a medium as well.
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
but the cost of attack will decrease as shown in the report and the loss of fund have no upper side because user will keep lock voting power fund.
function lock(uint256 wad) external {
require(block.number == hatchTrigger || block.number > hatchTrigger + HATCH_SIZE,
"VoteDelegate/no-lock-during-hatch");
gov.transferFrom(msg.sender, address(this), wad);
chief.lock(wad);
stake[msg.sender] += wad;
emit Lock(msg.sender, wad);
}
Escalate
if https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/64 is a medium
this issue should be a medium as well.
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
but the cost of attack will decrease as shown in the report and the loss of fund have no upper side because user will keep lock voting power fund.
function lock(uint256 wad) external { require(block.number == hatchTrigger || block.number > hatchTrigger + HATCH_SIZE, "VoteDelegate/no-lock-during-hatch"); gov.transferFrom(msg.sender, address(this), wad); chief.lock(wad); stake[msg.sender] += wad; emit Lock(msg.sender, wad); }
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.
other evidence:
issue https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/109 which is a duplicate of valid issue
https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/63
has the following the text:
Similarly a create2 collision found for votedelegate contract will allow a user to lock the funds of all the delegators by setting an approval for the IOU token early and then moving it later to another address causing the withdraw function of chief to revert since that much amount of IOU tokens are not present to burn
and
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
I think as outlined by a lot of report, the attacker can approve the token allowance and then self-destruct so any fund that deposit into the contract can be transfered out and get stolen.
The rules state that a valid duplicate must "Identify at least a Medium impact" (https://docs.sherlock.xyz/audits/judging/judging#ix.-duplication-rules).
I can see that my issue (https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/63), which has the following impacts:
The attacker can create non-liquidatable positions. <@1
All users who select the attacker's VD can lose their funds.
All votes are permanently locked on the attacker's VD <@2
and can be used by the attacker for voting.
Instead of eoa2, a contract can be used to allow others to vote and sell voting power, similar to Curve bribing or other governance attacks.
has the same impact as https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/42
prevent the protocol from liquidating the collateral. <@1
and https://github.com/sherlock-audit/2024-06-makerdao-endgame-judging/issues/109
can avoid future liquidations <@1
and also DOS other user's in withdrawing funds <@2
If impacts 1 and 2 are enough to be considered Medium, this issue should be valid. If they are not, then issues 42 and 109 should not be considered valid at all, right?
If this issue is valid, please also consider marking #37 as invalid since it lacks a sufficient proof of an attack path. No mentions about approves or selfdestruct, impossible to reproduce.
If an attacker can predict the user's address, they can pre-calculate the VoteDelegate contract address and deploy a malicious contract at that address before the user initiates the transaction.
Impact If an attacker successfully executes a salt attack, they can modify the behavior of the newly created VoteDelegate contract. This could lead to a loss of control over the voting delegation process, allowing the attacker to manipulate votes or perform other unauthorized actions.
The CREATE2
collision is valid, but this report and #64 describe the same vulnerability—CREATE2 address collisions. The different impacts they describe are simply variations of how an attacker might exploit this vulnerability. Since the core issue is identical, having two separate reports from a single Watson is redundant, as they share the same root cause and the same fix.
I see how the problem with this report is the cost of the attack for #64. Hence, will provide my decision on this escalation, once the discussion on #64 is settled.
Computationally, issue 63 requires 3 keccak's per generated create2 address while issue 64 requires only 2 of those.
There is a social aspect to the IOU griefing attack. It requires convincing users to choose the attacker's delegate. This is particularly challenging considering that the "approval + self-destruct" transaction will be visible on-chain. Any attempt to entice users to lock their MKR will attract more attention to the transaction history of the delegate.
Regarding the auction blocking attack, as per the contest rules, issue 63 cannot be considered medium if the cost of the attack is more than 150m$. This is because the maximum theoretical bad debt cannot exceed the debt ceiling, which per contest rules is capped at 100m$ and valid griefing attacks are requested to have an attack cost that does not exceed the damage by more than 50%.
In practice given a reasonable mat
value, and considering that governance can unilaterally grab
an offending vault, governance will be able to manually liquidate the position and retrieve some amount of collateral value. As a result, the damage to the protocol will be much less than 100m$ and hence this submission should show that the attack cost would be much less than 150m$.
Note that if the auction blocking attack is carried out in order to avoid paying liquidation and exit fees, then it should be shown that the cost of the attack is less than ~30m$ given a debt ceiling of 100m$, liquidation penalty of 13% and exit fee of 15%.
In contrast, issue 64 only needs to show that the cost wouldn't exceed the potential gain obtained from taking control of the protocol.
Based on the discussion under #64 about the cost of the attack, I believe this finding is low-severity as well. Planning to reject the escalation and leave the issue as it is.
Result: Invalid Has duplicates
00xSEV
Medium
An attacker can exploit VD address collisions using create2 to lock some liquidations and withdrawals in Maker protocol
Summary
An attacker can use brute-force to find two private keys that create EOAs with the following properties:
eoa1
.eoa1
.Since a VD (VoteDelegate) address depends solely on
msg.sender
. 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.The attacker can approve IOU tokens to an EOA,
attacker
, and then create a VD. By transferring IOUs to another address, the attacker can lock liquidations and withdrawals for anyone using this VD.Vulnerability Detail
Examples of previous issues with the same root cause:
Summary
The current cost of this attack is less than $1.5 million with current prices.
An attacker can find a single address collision between (1) and (2) with a high probability of success using a 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:
The attacker finds two private keys that generate EOAs with the following properties:
eoa1
.eoa2
, when used as a salt for VD creation, produces a VD with the same address aseoa1
.Call
IOU.approve(attacker, max)
fromeoa1
.Call
voteDelegateFactory.create()
fromeoa2
:VD1
.VD1
address ==eoa1
address.VD1
retains the approvals given fromeoa1
in step 2.Call
LSE.open
to createLSUrn1
.Call
LSE.lock(LSUrn1, 1000e18)
to deposit funds.Call
LSE.draw(LSUrn1, attacker, maxPossible)
to borrow the maximum amount.Call
LSE.selectVoteDelegate(LSUrn1, VD1)
to transfer MKR tochief
and getIOU
onVD1
.Call
IOU.transferFrom(VD1, attacker, maxPossible)
.Now all liquidations will revert because
dog.bark
=>LSClipper.kick
=>LSE.onKick
=>LSE._selectVoteDelegate
=>VoteDelegateLike(prevVoteDelegate).free(wad);
=>chief.free(wad);
=>IOU.burn
will revert.The same is true for withdrawals of users who trusted this VD and delegated their funds to it, starting from
_selectVoteDelegate
, which is called onfree
and will revert:hat
by voting and gain full control of the protocol.Link to create2
Variations:
eoa1
can be replaced with a contract created byeoa3
. The address of the contract can be brute-forced in the same way aseoa1
. The contract performs step 2 instead ofeoa1
and self-destructs in the same transaction.eoa2
in step 1, the attacker can use a contract. A brute-forced EOA creates a contract that will create a VD such that the VD address equalseoa1
.Impact
The attacker can create non-liquidatable positions. All users who select the attacker's VD can lose their funds. All votes are permanently locked on the attacker's VD and can be used by the attacker for voting. Instead of
eoa2
, a contract can be used to allow others to vote and sell voting power, similar to Curve bribing or other governance attacks.If the attacker could acquire a substantial amount of funds, they could select a
hat
by voting and gain full control of the protocol.Code Snippet
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/ALSEH3.sol
from the root project directory.pragma solidity ^0.8.21;
import "./ALockstakeEngine.sol";
contract VoteDelegateLike { mapping(address => uint256) public stake; }
interface GemLike { function approve(address, uint256) external; function transfer(address, uint256) external; function transferFrom(address, address, uint256) external; function balanceOf(address) external view returns (uint256); }
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; function deposits(address) external returns (uint); }
contract ALSEH3 is ALockstakeEngineTest { // Address used by the attacker, a regular EOA address attacker = makeAddr("attacker"); // Address brute-forced by the attacker to make a VD that matches an EOA controlled by the attacker address minedVDCreator = makeAddr("minedUrnCreator");
}