Skip to content

Commit 8ef85eb

Browse files
ShasShas
authored andcommitted
feat: voter claims
1 parent c1073c1 commit 8ef85eb

File tree

2 files changed

+193
-1
lines changed

2 files changed

+193
-1
lines changed

src/PodManagerV2.sol

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ contract PodManagerV2 is
277277
uint8 ownerSharePercent = subnetManager.getPodOwnerEmissionSharePercentage(subnetId);
278278
uint8 voterSharePercent = 100 - ownerSharePercent;
279279

280+
address primaryTokenAddress = subnetManager.getSubnetPrimaryToken(subnetId);
281+
280282
// net positive scenario
281283
if (podTotalUpVotes > podTotalDownVotes && voterUpVotes > 0) {
282284
if (reppoEmissionsPerEpoch > 0) {
@@ -294,15 +296,44 @@ contract PodManagerV2 is
294296
reppo.transfer(governanceReserve, tax);
295297
emit VoterReppoEmissionsClaimed(epoch, podId, voterShareAfterTax, voter);
296298
}
299+
if (primaryTokenEmissionsPerEpoch > 0) {
300+
uint256 podEmissionsShare = (primaryTokenEmissionsPerEpoch * podTotalVotes) / subnetTotalVotes;
301+
uint256 totalVoterEmissionsShare = (podEmissionsShare * voterSharePercent) / 100;
302+
uint256 voterEmissionsShare = (totalVoterEmissionsShare * voterUpVotes) / podTotalUpVotes;
303+
if (voterEmissionsShare > primaryTokenSeedingBalance) {
304+
revert InsufficientPrimaryTokenSeedings();
305+
}
306+
$.subnetPrimaryTokenSeedingBalance[subnetId] -= voterEmissionsShare;
307+
uint256 primaryTokenTaxRate = subnetManager.getTaxRate();
308+
uint256 tax = (voterEmissionsShare * primaryTokenTaxRate) / 100;
309+
uint256 voterShareAfterTax = voterEmissionsShare - tax;
310+
IERC20(primaryTokenAddress).safeTransfer(voter, voterShareAfterTax);
311+
IERC20(primaryTokenAddress).safeTransfer(governanceReserve, tax);
312+
emit VoterPrimaryTokenEmissionsClaimed(epoch, podId, voterShareAfterTax, voter);
313+
}
297314
}
298315

299316
// net negative scenario
300317
if (podTotalDownVotes > podTotalUpVotes && voterDownVotes > 0) {
318+
if (reppoEmissionsPerEpoch > 0) {
319+
uint256 podEmissionsShare = (reppoEmissionsPerEpoch * podTotalVotes) / subnetTotalVotes;
320+
uint256 totalVoterEmissionsShare = (podEmissionsShare * voterSharePercent) / 100;
321+
uint256 voterEmissionsShare = (totalVoterEmissionsShare * voterDownVotes) / podTotalDownVotes;
322+
if (voterEmissionsShare > reppoSeedingBalance) {
323+
revert InsufficientReppoSeedings();
324+
}
325+
$.subnetREPPOSeedingBalance[subnetId] -= voterEmissionsShare;
326+
uint256 reppoTaxRate = subnetManager.getTaxRate();
327+
uint256 tax = (voterEmissionsShare * reppoTaxRate) / 100;
328+
uint256 voterShareAfterTax = voterEmissionsShare - tax;
329+
reppo.transfer(voter, voterShareAfterTax);
330+
reppo.transfer(governanceReserve, tax);
331+
emit VoterReppoEmissionsClaimed(epoch, podId, voterShareAfterTax, voter);
332+
}
301333
if (primaryTokenEmissionsPerEpoch > 0) {
302334
uint256 podEmissionsShare = (primaryTokenEmissionsPerEpoch * podTotalVotes) / subnetTotalVotes;
303335
uint256 totalVoterEmissionsShare = (podEmissionsShare * voterSharePercent) / 100;
304336
uint256 voterEmissionsShare = (totalVoterEmissionsShare * voterDownVotes) / podTotalDownVotes;
305-
address primaryTokenAddress = subnetManager.getSubnetPrimaryToken(subnetId);
306337
if (voterEmissionsShare > primaryTokenSeedingBalance) {
307338
revert InsufficientPrimaryTokenSeedings();
308339
}

test/PodManagerV2/PodManagerV2.PodVoterClaims.t.sol

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,165 @@ contract PodManagerPodVoterClaimsTest is PodManagerV2TestBase {
5656

5757
assertEq(afterBal - beforeBal, expected);
5858
}
59+
60+
function test_voterDownVotesCanClaimEmissions() public {
61+
uint256 epoch = veReppo.currentEpoch();
62+
uint256 podOne = _mintPod(user1, 1);
63+
64+
uint256 votes = 100;
65+
vm.prank(user2);
66+
podManager.vote(podOne, votes, false);
67+
68+
// Seed enough for the claim to succeed
69+
uint256 seeded = 100;
70+
deal(address(reppo), admin, seeded);
71+
vm.startPrank(admin);
72+
reppo.approve(address(podManager), seeded);
73+
podManager.seedREPPOEmissionsBySubnetOwner(1, seeded);
74+
vm.stopPrank();
75+
76+
// Advance epoch so claim is valid
77+
veReppo.setCurrentEpoch(epoch + 1);
78+
79+
uint256 beforeBal = reppo.balanceOf(user2);
80+
vm.prank(user2);
81+
podManager.claimVoterEmissions(user2, podOne, epoch);
82+
uint256 afterBal = reppo.balanceOf(user2);
83+
84+
// --- Compute expected payout ---
85+
uint256 reppoPerEpoch = subnetManager.getReppoEmissionsPerEpoch(1); // 100
86+
uint256 subnetVotes = podManager.totalVotesOfSubnetOfEpoch(1, epoch);
87+
uint256 podEmissionsShare = (reppoPerEpoch * votes) / subnetVotes;
88+
uint256 ownerSharePercent = subnetManager.getPodOwnerEmissionSharePercentage(1); // 50
89+
uint256 voterSharePercent = 100 - ownerSharePercent;
90+
uint256 voterEmissionsShare = (podEmissionsShare * voterSharePercent) / 100;
91+
uint256 taxRate = subnetManager.getTaxRate(); // 10
92+
uint256 tax = (voterEmissionsShare * taxRate) / 100;
93+
uint256 expected = voterEmissionsShare - tax;
94+
95+
assertEq(afterBal - beforeBal, expected);
96+
}
97+
98+
function test_voterGetsNothingWhenPodHasNetUpVotesButVoterOnlyHasDownVotes() public {
99+
uint256 epoch = veReppo.currentEpoch();
100+
uint256 podOne = _mintPod(admin, 1);
101+
102+
uint256 upVotes = 100;
103+
vm.prank(user1);
104+
podManager.vote(podOne, upVotes, true);
105+
106+
uint256 downVotes = 50;
107+
vm.prank(user2);
108+
podManager.vote(podOne, downVotes, false);
109+
110+
// Seed enough for the claim to succeed
111+
uint256 seeded = 100;
112+
deal(address(reppo), admin, seeded);
113+
vm.startPrank(admin);
114+
reppo.approve(address(podManager), seeded);
115+
podManager.seedREPPOEmissionsBySubnetOwner(1, seeded);
116+
vm.stopPrank();
117+
118+
// Advance epoch so claim is valid
119+
veReppo.setCurrentEpoch(epoch + 1);
120+
121+
vm.prank(user2);
122+
podManager.claimVoterEmissions(user2, podOne, epoch);
123+
uint256 afterBal = reppo.balanceOf(user2);
124+
125+
assertEq(afterBal, 0);
126+
}
127+
128+
function test_voterGetsNothingWhenPodHasNetDownVotesButVoterOnlyHasUpVotes() public {
129+
uint256 epoch = veReppo.currentEpoch();
130+
uint256 podOne = _mintPod(admin, 1);
131+
132+
uint256 upVotes = 50;
133+
vm.prank(user1);
134+
podManager.vote(podOne, upVotes, true);
135+
136+
uint256 downVotes = 100;
137+
vm.prank(user2);
138+
podManager.vote(podOne, downVotes, false);
139+
140+
// Seed enough for the claim to succeed
141+
uint256 seeded = 100;
142+
deal(address(reppo), admin, seeded);
143+
vm.startPrank(admin);
144+
reppo.approve(address(podManager), seeded);
145+
podManager.seedREPPOEmissionsBySubnetOwner(1, seeded);
146+
vm.stopPrank();
147+
148+
// Advance epoch so claim is valid
149+
veReppo.setCurrentEpoch(epoch + 1);
150+
151+
vm.prank(user1);
152+
podManager.claimVoterEmissions(user1, podOne, epoch);
153+
uint256 afterBal = reppo.balanceOf(user1);
154+
155+
assertEq(afterBal, 0);
156+
}
157+
158+
function test_voterGetsNothingWhenPodHasNeutralVotes() public {
159+
uint256 epoch = veReppo.currentEpoch();
160+
uint256 podOne = _mintPod(admin, 1);
161+
162+
uint256 votes = 100;
163+
vm.prank(user1);
164+
podManager.vote(podOne, votes, true);
165+
166+
vm.prank(user2);
167+
podManager.vote(podOne, votes, false);
168+
169+
// Seed enough for the claim to succeed
170+
uint256 seeded = 100;
171+
deal(address(reppo), admin, seeded);
172+
vm.startPrank(admin);
173+
reppo.approve(address(podManager), seeded);
174+
podManager.seedREPPOEmissionsBySubnetOwner(1, seeded);
175+
vm.stopPrank();
176+
177+
// Advance epoch so claim is valid
178+
veReppo.setCurrentEpoch(epoch + 1);
179+
180+
vm.prank(user1);
181+
vm.expectRevert(bytes("PodNetNeutralNoEmissions()"));
182+
podManager.claimVoterEmissions(user1, podOne, epoch);
183+
uint256 afterBalUser1 = reppo.balanceOf(user1);
184+
185+
vm.prank(user2);
186+
vm.expectRevert(bytes("PodNetNeutralNoEmissions()"));
187+
podManager.claimVoterEmissions(user2, podOne, epoch);
188+
uint256 afterBalUser2 = reppo.balanceOf(user2);
189+
190+
assertEq(afterBalUser1, 0);
191+
assertEq(afterBalUser2, 0);
192+
}
193+
194+
function testRevert_whenVoterTriesToClaimMultipleTimesForSameEpoch() public {
195+
uint256 epoch = veReppo.currentEpoch();
196+
uint256 podOne = _mintPod(user1, 1);
197+
198+
uint256 votes = 100;
199+
vm.prank(user2);
200+
podManager.vote(podOne, votes, true);
201+
202+
// Seed enough for the claim to succeed
203+
uint256 seeded = 100;
204+
deal(address(reppo), admin, seeded);
205+
vm.startPrank(admin);
206+
reppo.approve(address(podManager), seeded);
207+
podManager.seedREPPOEmissionsBySubnetOwner(1, seeded);
208+
vm.stopPrank();
209+
210+
// Advance epoch so claim is valid
211+
veReppo.setCurrentEpoch(epoch + 1);
212+
213+
vm.prank(user2);
214+
podManager.claimVoterEmissions(user2, podOne, epoch);
215+
216+
vm.prank(user2);
217+
vm.expectRevert(bytes("EmissionsAlreadyClaimed()"));
218+
podManager.claimVoterEmissions(user2, podOne, epoch);
219+
}
59220
}

0 commit comments

Comments
 (0)