Skip to content

Commit 5e9c487

Browse files
committed
hande provers
1 parent ab1d686 commit 5e9c487

File tree

4 files changed

+217
-12
lines changed

4 files changed

+217
-12
lines changed

contracts/src/SuccinctVApp.sol

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,27 @@ contract SuccinctVApp is
220220
// Ensure the index has not been marked as claimed.
221221
if (isClaimed(_index)) revert RewardAlreadyClaimed();
222222

223+
// Mark the index as claimed.
224+
_setClaimed(_index);
225+
223226
// Verify the merkle proof.
224227
bytes32 node = keccak256(abi.encodePacked(_index, _account, _amount));
225228
if (!MerkleProof.verify(_merkleProof, rewardsRoot, node)) revert InvalidProof();
226229

227-
// Mark the index as claimed and transfer the token.
228-
_setClaimed(_index);
229-
IERC20(prove).safeTransfer(_account, _amount);
230+
// Transfer the token.
231+
//
232+
// If the `_account` is a prover vault, we need to first deposit it to get $iPROVE and then
233+
// transfer the $iPROVE to the prover vault. This splits the $PROVE amount amongst all
234+
// of the prover stakers.
235+
//
236+
// Otherwise if the `_account` is not a prover vault, we can just transfer the $PROVE directly.
237+
if (ISuccinctStaking(staking).isProver(_account)) {
238+
// Deposit $PROVE to mint $iPROVE, sending it to the prover vault.
239+
IERC4626(iProve).deposit(_amount, _account);
240+
} else {
241+
// Transfer the $PROVE from this contract to the `_account` address.
242+
IERC20(prove).safeTransfer(_account, _amount);
243+
}
230244

231245
// Emit the event.
232246
emit RewardClaimed(_index, _account, _amount);

contracts/test/SuccinctVApp.reward.t.sol

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {SuccinctVApp} from "../src/SuccinctVApp.sol";
66
import {ISuccinctVApp} from "../src/interfaces/ISuccinctVApp.sol";
77
import {Merkle} from "../lib/murky/src/Merkle.sol";
88
import {MockERC20} from "./utils/MockERC20.sol";
9+
import {MockStaking} from "../src/mocks/MockStaking.sol";
910
import {PausableUpgradeable} from
1011
"../lib/openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol";
1112

@@ -16,6 +17,9 @@ contract SuccinctVAppRewardsTest is SuccinctVAppTest {
1617
bytes32[] public rewardLeaves;
1718
bytes32 public rewardsMerkleRoot;
1819
Merkle public merkle;
20+
21+
// Prover for testing prover rewards.
22+
address public testProver;
1923

2024
// Storage slot for rewardsRoot in SuccinctVApp (slot 11 based on contract layout).
2125
uint256 constant REWARDS_ROOT_SLOT = 11;
@@ -26,13 +30,17 @@ contract SuccinctVAppRewardsTest is SuccinctVAppTest {
2630
function setUp() public override {
2731
super.setUp();
2832

29-
// Set up test rewards data.
33+
// Create a test prover.
34+
vm.prank(ALICE);
35+
testProver = MockStaking(STAKING).createProver(ALICE, STAKER_FEE_BIPS);
36+
37+
// Set up test rewards data (including a prover).
3038
rewardAccounts = [
3139
makeAddr("REWARD_1"),
3240
makeAddr("REWARD_2"),
3341
makeAddr("REWARD_3"),
3442
makeAddr("REWARD_4"),
35-
makeAddr("REWARD_5")
43+
testProver // Include the prover in rewards
3644
];
3745
rewardAmounts = [1 ether, 2 ether, 3 ether, 4 ether, 5 ether];
3846

@@ -91,7 +99,14 @@ contract SuccinctVAppRewardsTest is SuccinctVAppTest {
9199
SuccinctVApp(VAPP).rewardClaim(i, claimer, amount, proof);
92100

93101
// Check post-claim state.
94-
assertEq(MockERC20(PROVE).balanceOf(claimer), amount);
102+
if (claimer == testProver) {
103+
// For provers, PROVE is converted to iPROVE and sent to prover vault.
104+
assertEq(MockERC20(I_PROVE).balanceOf(claimer), amount);
105+
assertEq(MockERC20(PROVE).balanceOf(claimer), 0);
106+
} else {
107+
// For regular accounts, PROVE is sent directly.
108+
assertEq(MockERC20(PROVE).balanceOf(claimer), amount);
109+
}
95110
vappBalBefore -= amount;
96111
assertEq(MockERC20(PROVE).balanceOf(VAPP), vappBalBefore);
97112
assertEq(SuccinctVApp(VAPP).isClaimed(i), true);
@@ -173,18 +188,23 @@ contract SuccinctVAppRewardsTest is SuccinctVAppTest {
173188
// Set the rewards root.
174189
_setRewardsRoot(rewardsMerkleRoot);
175190

191+
// Test regular account (not a prover).
192+
uint256 regularAccountIndex = 0;
193+
address regularAccount = rewardAccounts[regularAccountIndex];
194+
uint256 amount = rewardAmounts[regularAccountIndex];
195+
176196
// Check initial balances.
177197
uint256 initialVAppBalance = MockERC20(PROVE).balanceOf(VAPP);
178-
uint256 initialClaimerBalance = MockERC20(PROVE).balanceOf(rewardAccounts[0]);
198+
uint256 initialClaimerBalance = MockERC20(PROVE).balanceOf(regularAccount);
179199
assertEq(initialClaimerBalance, 0);
180200

181201
// Claim reward.
182-
bytes32[] memory proof = merkle.getProof(rewardLeaves, 0);
183-
SuccinctVApp(VAPP).rewardClaim(0, rewardAccounts[0], rewardAmounts[0], proof);
202+
bytes32[] memory proof = merkle.getProof(rewardLeaves, regularAccountIndex);
203+
SuccinctVApp(VAPP).rewardClaim(regularAccountIndex, regularAccount, amount, proof);
184204

185205
// Verify transfer occurred.
186-
assertEq(MockERC20(PROVE).balanceOf(rewardAccounts[0]), rewardAmounts[0]);
187-
assertEq(MockERC20(PROVE).balanceOf(VAPP), initialVAppBalance - rewardAmounts[0]);
206+
assertEq(MockERC20(PROVE).balanceOf(regularAccount), amount);
207+
assertEq(MockERC20(PROVE).balanceOf(VAPP), initialVAppBalance - amount);
188208
}
189209

190210
function test_RewardClaim_DifferentCaller() public {
@@ -262,4 +282,58 @@ contract SuccinctVAppRewardsTest is SuccinctVAppTest {
262282
uint256 expectedRemainingBalance = 100 ether - totalClaimed;
263283
assertEq(MockERC20(PROVE).balanceOf(VAPP), expectedRemainingBalance);
264284
}
285+
286+
function test_RewardClaim_ToProverVault() public {
287+
// Set the rewards root.
288+
_setRewardsRoot(rewardsMerkleRoot);
289+
290+
// Get the prover index (last one in our array).
291+
uint256 proverIndex = rewardAccounts.length - 1;
292+
address proverVault = rewardAccounts[proverIndex];
293+
uint256 amount = rewardAmounts[proverIndex];
294+
295+
// Verify it's recognized as a prover.
296+
assertEq(proverVault, testProver);
297+
298+
// Check initial balances.
299+
uint256 initialVAppBalance = MockERC20(PROVE).balanceOf(VAPP);
300+
uint256 initialProverPROVEBalance = MockERC20(PROVE).balanceOf(proverVault);
301+
uint256 initialProveriPROVEBalance = MockERC20(I_PROVE).balanceOf(proverVault);
302+
assertEq(initialProverPROVEBalance, 0);
303+
assertEq(initialProveriPROVEBalance, 0);
304+
305+
// Claim reward for prover.
306+
bytes32[] memory proof = merkle.getProof(rewardLeaves, proverIndex);
307+
vm.expectEmit(true, true, true, true, VAPP);
308+
emit RewardClaimed(proverIndex, proverVault, amount);
309+
SuccinctVApp(VAPP).rewardClaim(proverIndex, proverVault, amount, proof);
310+
311+
// Verify the prover received iPROVE instead of PROVE.
312+
assertEq(MockERC20(PROVE).balanceOf(proverVault), 0);
313+
assertEq(MockERC20(I_PROVE).balanceOf(proverVault), amount);
314+
assertEq(MockERC20(PROVE).balanceOf(VAPP), initialVAppBalance - amount);
315+
assertEq(SuccinctVApp(VAPP).isClaimed(proverIndex), true);
316+
}
317+
318+
function test_RewardClaim_MixedAccountTypes() public {
319+
// Set the rewards root.
320+
_setRewardsRoot(rewardsMerkleRoot);
321+
322+
// Claim for regular account (index 0).
323+
bytes32[] memory proof0 = merkle.getProof(rewardLeaves, 0);
324+
SuccinctVApp(VAPP).rewardClaim(0, rewardAccounts[0], rewardAmounts[0], proof0);
325+
326+
// Verify regular account received PROVE.
327+
assertEq(MockERC20(PROVE).balanceOf(rewardAccounts[0]), rewardAmounts[0]);
328+
assertEq(MockERC20(I_PROVE).balanceOf(rewardAccounts[0]), 0);
329+
330+
// Claim for prover (last index).
331+
uint256 proverIndex = rewardAccounts.length - 1;
332+
bytes32[] memory proofProver = merkle.getProof(rewardLeaves, proverIndex);
333+
SuccinctVApp(VAPP).rewardClaim(proverIndex, testProver, rewardAmounts[proverIndex], proofProver);
334+
335+
// Verify prover received iPROVE.
336+
assertEq(MockERC20(PROVE).balanceOf(testProver), 0);
337+
assertEq(MockERC20(I_PROVE).balanceOf(testProver), rewardAmounts[proverIndex]);
338+
}
265339
}

contracts/test/SuccinctVApp.t.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {SuccinctGovernor} from "../src/SuccinctGovernor.sol";
1111
import {ISP1Verifier} from "../lib/sp1-contracts/contracts/src/ISP1Verifier.sol";
1212
import {FixtureLoader, Fixture, ProofFixtureJson} from "./utils/FixtureLoader.sol";
1313
import {MockERC20} from "./utils/MockERC20.sol";
14+
import {MockERC4626} from "./utils/MockERC4626.sol";
1415
import {ERC1967Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
1516
import {Initializable} from "../lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol";
1617
import {IERC20Permit} from
@@ -106,7 +107,7 @@ contract SuccinctVAppTest is Test, FixtureLoader {
106107

107108
// Deploy tokens
108109
PROVE = address(new MockERC20("Succinct", "PROVE", 18));
109-
I_PROVE = address(new MockERC20("Succinct", "iPROVE", 18));
110+
I_PROVE = address(new MockERC4626("Succinct", "iPROVE", 18, PROVE));
110111

111112
// Deploy governor
112113
GOVERNOR = address(
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {MockERC20} from "./MockERC20.sol";
5+
import {IERC4626} from "../../lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol";
6+
import {IERC20} from "../../lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol";
7+
8+
contract MockERC4626 is MockERC20, IERC4626 {
9+
address public immutable asset;
10+
11+
constructor(
12+
string memory name,
13+
string memory symbol,
14+
uint8 decimals_,
15+
address _asset
16+
) MockERC20(name, symbol, decimals_) {
17+
asset = _asset;
18+
}
19+
20+
function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
21+
// Simple 1:1 conversion for testing.
22+
shares = assets;
23+
24+
// Transfer assets from sender to this contract.
25+
IERC20(asset).transferFrom(msg.sender, address(this), assets);
26+
27+
// Mint shares to receiver.
28+
_mint(receiver, shares);
29+
30+
emit Deposit(msg.sender, receiver, assets, shares);
31+
32+
return shares;
33+
}
34+
35+
function mint(uint256 shares, address receiver) public returns (uint256 assets) {
36+
assets = shares; // 1:1 conversion
37+
IERC20(asset).transferFrom(msg.sender, address(this), assets);
38+
_mint(receiver, shares);
39+
emit Deposit(msg.sender, receiver, assets, shares);
40+
return assets;
41+
}
42+
43+
function withdraw(
44+
uint256 assets,
45+
address receiver,
46+
address owner
47+
) public returns (uint256 shares) {
48+
shares = assets; // 1:1 conversion
49+
if (msg.sender != owner) {
50+
_spendAllowance(owner, msg.sender, shares);
51+
}
52+
_burn(owner, shares);
53+
IERC20(asset).transfer(receiver, assets);
54+
emit Withdraw(msg.sender, receiver, owner, assets, shares);
55+
return shares;
56+
}
57+
58+
function redeem(
59+
uint256 shares,
60+
address receiver,
61+
address owner
62+
) public returns (uint256 assets) {
63+
assets = shares; // 1:1 conversion
64+
if (msg.sender != owner) {
65+
_spendAllowance(owner, msg.sender, shares);
66+
}
67+
_burn(owner, shares);
68+
IERC20(asset).transfer(receiver, assets);
69+
emit Withdraw(msg.sender, receiver, owner, assets, shares);
70+
return assets;
71+
}
72+
73+
function totalAssets() public view returns (uint256) {
74+
return IERC20(asset).balanceOf(address(this));
75+
}
76+
77+
function convertToShares(uint256 assets) public pure returns (uint256) {
78+
return assets; // 1:1 conversion
79+
}
80+
81+
function convertToAssets(uint256 shares) public pure returns (uint256) {
82+
return shares; // 1:1 conversion
83+
}
84+
85+
function maxDeposit(address) public pure returns (uint256) {
86+
return type(uint256).max;
87+
}
88+
89+
function previewDeposit(uint256 assets) public pure returns (uint256) {
90+
return assets; // 1:1 conversion
91+
}
92+
93+
function maxMint(address) public pure returns (uint256) {
94+
return type(uint256).max;
95+
}
96+
97+
function previewMint(uint256 shares) public pure returns (uint256) {
98+
return shares; // 1:1 conversion
99+
}
100+
101+
function maxWithdraw(address owner) public view returns (uint256) {
102+
return balanceOf(owner); // 1:1 conversion
103+
}
104+
105+
function previewWithdraw(uint256 assets) public pure returns (uint256) {
106+
return assets; // 1:1 conversion
107+
}
108+
109+
function maxRedeem(address owner) public view returns (uint256) {
110+
return balanceOf(owner);
111+
}
112+
113+
function previewRedeem(uint256 shares) public pure returns (uint256) {
114+
return shares; // 1:1 conversion
115+
}
116+
}

0 commit comments

Comments
 (0)