Skip to content

Commit 60bbab6

Browse files
authored
feat(contracts): staking upgradeable (#193)
1 parent 9de5def commit 60bbab6

File tree

11 files changed

+271
-222
lines changed

11 files changed

+271
-222
lines changed

contracts/.gas-snapshot

Lines changed: 153 additions & 153 deletions
Large diffs are not rendered by default.

contracts/README.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,39 +106,37 @@ Fill out `{CHAIN_ID}.json` with any pre-deployed contracts. For example, if ther
106106

107107
#### Deploy each contract
108108

109+
The broadcast flag is intentially removed from these scripts so that the predicted addresses are written to the `{CHAIN_ID}.json` file without actually deploying them. This is so that `initialize` functions can be atomically called to avoid [frontrunning attacks](https://dedaub.com/blog/the-cpimp-attack-an-insanely-far-reaching-vulnerability-successfully-mitigated/) on the `initialize` function.
110+
109111
Deploy the SuccinctStaking contract:
110112

111113
```sh
112-
FOUNDRY_PROFILE=deploy forge script SuccinctStakingScript --private-key $PRIVATE_KEY --broadcast --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
114+
FOUNDRY_PROFILE=deploy forge script SuccinctStakingScript --private-key $PRIVATE_KEY --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
113115
```
114116

115117
This DOES NOT initalize the contract - this will be done in a later step once references to other contracts are available.
116118

117119
Deploy the $iPROVE contract (assumes $PROVE is already deployed):
118120

119121
```sh
120-
FOUNDRY_PROFILE=deploy forge script IntermediateSuccinctScript --private-key $PRIVATE_KEY --broadcast --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
122+
FOUNDRY_PROFILE=deploy forge script IntermediateSuccinctScript --private-key $PRIVATE_KEY --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
121123
```
122124

123125
Deploy the SuccinctGovernor contract:
124126

125127
```sh
126-
FOUNDRY_PROFILE=deploy forge script SuccinctGovernorScript --private-key $PRIVATE_KEY --broadcast --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
128+
FOUNDRY_PROFILE=deploy forge script SuccinctGovernorScript --private-key $PRIVATE_KEY --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
127129
```
128130

129131
Deploy the SuccinctVApp implementation and proxy contracts (assumes verifier is already deployed):
130132

131133
```sh
132-
FOUNDRY_PROFILE=deploy forge script SuccinctVAppScript --private-key $PRIVATE_KEY --broadcast --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
134+
FOUNDRY_PROFILE=deploy forge script SuccinctVAppScript --private-key $PRIVATE_KEY --rpc-url $ETH_RPC_URL --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY
133135
```
134136

135137
If the SP1VerifierGateway is not already deployed, follow steps in [sp1-contracts](https://github.com/succinctlabs/sp1-contracts) to deploy it and fill out the address in your `{CHAIN_ID}.json` file.
136138

137-
Initalize the SuccinctStaking contract:
138-
139-
```sh
140-
FOUNDRY_PROFILE=deploy forge script SuccinctStakingScript --sig "initialize()" --private-key $PRIVATE_KEY --broadcast --rpc-url $ETH_RPC_URL
141-
```
139+
At this point, you should have all the predicted addresses in your `{CHAIN_ID}.json` file. You should now re-run these with the `--broadcast` flag to actually deploy the contracts.
142140

143141
Run the integrity check:
144142

contracts/script/deploy/All.s.sol

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@ contract AllScript is BaseScript, FixtureLoader {
2121
address OWNER = readAddress("OWNER");
2222

2323
// Deploy contracts
24-
address STAKING = address(new SuccinctStaking{salt: salt}(OWNER));
24+
(address STAKING, address STAKING_IMPL) = _deployStakingAsProxy(salt);
2525
address PROVE = address(new Succinct{salt: salt}(OWNER));
2626
address I_PROVE = address(new IntermediateSuccinct{salt: salt}(PROVE, STAKING));
2727
address GOVERNOR = _deployGovernor(salt, I_PROVE);
2828
(address VERIFIER, address VAPP, address VAPP_IMPL) =
2929
_deployVAppAsProxy(salt, OWNER, PROVE, I_PROVE, STAKING);
3030

3131
// Initialize staking contract
32-
_initializeStaking(STAKING, GOVERNOR, VAPP, PROVE, I_PROVE);
32+
_initializeStaking(OWNER, STAKING, GOVERNOR, VAPP, PROVE, I_PROVE);
3333

3434
// Write addresses
3535
writeAddress("STAKING", STAKING);
36+
writeAddress("STAKING_IMPL", STAKING_IMPL);
3637
writeAddress("VERIFIER", VERIFIER);
3738
writeAddress("VAPP", VAPP);
3839
writeAddress("VAPP_IMPL", VAPP_IMPL);
@@ -102,8 +103,18 @@ contract AllScript is BaseScript, FixtureLoader {
102103
return (VERIFIER, VAPP, VAPP_IMPL);
103104
}
104105

106+
/// @dev Deploys the staking contract as a proxy but does not initialize it.
107+
function _deployStakingAsProxy(bytes32 salt) internal returns (address, address) {
108+
address STAKING_IMPL = address(new SuccinctStaking{salt: salt}());
109+
address STAKING = address(
110+
SuccinctStaking(payable(address(new ERC1967Proxy{salt: salt}(STAKING_IMPL, ""))))
111+
);
112+
return (STAKING, STAKING_IMPL);
113+
}
114+
105115
/// @dev This is a stack-too-deep workaround.
106116
function _initializeStaking(
117+
address OWNER,
107118
address STAKING,
108119
address GOVERNOR,
109120
address VAPP,
@@ -115,9 +126,9 @@ contract AllScript is BaseScript, FixtureLoader {
115126
uint256 MAX_UNSTAKE_REQUESTS = readUint256("MAX_UNSTAKE_REQUESTS");
116127
uint256 UNSTAKE_PERIOD = readUint256("UNSTAKE_PERIOD");
117128
uint256 SLASH_CANCELLATION_PERIOD = readUint256("SLASH_CANCELLATION_PERIOD");
118-
uint256 DISPENSE_RATE = readUint256("DISPENSE_RATE");
119129

120130
SuccinctStaking(STAKING).initialize(
131+
OWNER,
121132
GOVERNOR,
122133
VAPP,
123134
PROVE,
@@ -126,8 +137,7 @@ contract AllScript is BaseScript, FixtureLoader {
126137
MIN_STAKE_AMOUNT,
127138
MAX_UNSTAKE_REQUESTS,
128139
UNSTAKE_PERIOD,
129-
SLASH_CANCELLATION_PERIOD,
130-
DISPENSE_RATE
140+
SLASH_CANCELLATION_PERIOD
131141
);
132142
}
133143
}

contracts/script/deploy/SuccinctStaking.s.sol

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ pragma solidity ^0.8.28;
33

44
import {BaseScript} from "../utils/Base.s.sol";
55
import {SuccinctStaking} from "../../src/SuccinctStaking.sol";
6+
import {ERC1967Proxy} from
7+
"../../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
68

79
contract SuccinctStakingScript is BaseScript {
810
string internal constant KEY = "STAKING";
@@ -11,17 +13,6 @@ contract SuccinctStakingScript is BaseScript {
1113
// Read config
1214
bytes32 salt = readBytes32("CREATE2_SALT");
1315
address OWNER = readAddress("OWNER");
14-
15-
// Deploy contract
16-
SuccinctStaking deployed = new SuccinctStaking{salt: salt}(OWNER);
17-
18-
// Write address
19-
writeAddress(KEY, address(deployed));
20-
}
21-
22-
/// @dev Only run this once all of the other contracts are deployed. Script must be ran with OWNER's private key.
23-
function initialize() external broadcaster {
24-
address STAKING = readAddress(KEY);
2516
address GOVERNOR = readAddress("GOVERNOR");
2617
address VAPP = readAddress("VAPP");
2718
address PROVE = readAddress("PROVE");
@@ -31,19 +22,32 @@ contract SuccinctStakingScript is BaseScript {
3122
uint256 MAX_UNSTAKE_REQUESTS = readUint256("MAX_UNSTAKE_REQUESTS");
3223
uint256 UNSTAKE_PERIOD = readUint256("UNSTAKE_PERIOD");
3324
uint256 SLASH_CANCELLATION_PERIOD = readUint256("SLASH_CANCELLATION_PERIOD");
34-
uint256 DISPENSE_RATE = readUint256("DISPENSE_RATE");
3525

36-
SuccinctStaking(STAKING).initialize(
37-
GOVERNOR,
38-
VAPP,
39-
PROVE,
40-
I_PROVE,
41-
DISPENSER,
42-
MIN_STAKE_AMOUNT,
43-
MAX_UNSTAKE_REQUESTS,
44-
UNSTAKE_PERIOD,
45-
SLASH_CANCELLATION_PERIOD,
46-
DISPENSE_RATE
26+
// Encode the initialize function call data
27+
bytes memory initData = abi.encodeCall(
28+
SuccinctStaking.initialize,
29+
(
30+
OWNER,
31+
GOVERNOR,
32+
VAPP,
33+
PROVE,
34+
I_PROVE,
35+
DISPENSER,
36+
MIN_STAKE_AMOUNT,
37+
MAX_UNSTAKE_REQUESTS,
38+
UNSTAKE_PERIOD,
39+
SLASH_CANCELLATION_PERIOD
40+
)
4741
);
42+
43+
// Deploy contract
44+
address STAKING_IMPL = address(new SuccinctStaking{salt: salt}());
45+
address STAKING = address(
46+
SuccinctStaking(payable(address(new ERC1967Proxy{salt: salt}(STAKING_IMPL, initData))))
47+
);
48+
49+
// Write address
50+
writeAddress(KEY, STAKING);
51+
writeAddress(string.concat(KEY, "_IMPL"), STAKING_IMPL);
4852
}
4953
}

contracts/script/utils/PostDeployment.s.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ contract PostDeploymentScript is BaseScript, Test {
2121
address VAPP = readAddress("VAPP");
2222
address VAPP_IMPL = readAddress("VAPP_IMPL");
2323
address STAKING = readAddress("STAKING");
24+
address STAKING_IMPL = readAddress("STAKING_IMPL");
2425
address PROVE = readAddress("PROVE");
2526
address I_PROVE = readAddress("I_PROVE");
2627
address GOVERNOR = readAddress("GOVERNOR");
2728

2829
_checkVAppImpl(VAPP, VAPP_IMPL);
2930
_checkVApp(VAPP, STAKING, PROVE, I_PROVE);
31+
_checkStakingImpl(STAKING, STAKING_IMPL);
3032
_checkStaking(STAKING, VAPP, PROVE, I_PROVE);
3133
_checkGovernor(GOVERNOR, I_PROVE);
3234
_checkIProve(I_PROVE, STAKING, PROVE);
@@ -69,6 +71,16 @@ contract PostDeploymentScript is BaseScript, Test {
6971
assertEq(vapp.timestamp(), 0);
7072
}
7173

74+
function _checkStakingImpl(address _staking, address _stakingImpl) internal view {
75+
bytes32 implRaw = vm.load(_staking, _IMPL_SLOT);
76+
address currentImpl = address(uint160(uint256(implRaw)));
77+
assertEq(
78+
currentImpl,
79+
_stakingImpl,
80+
"staking implementation address mismatch (may have been upgraded)"
81+
);
82+
}
83+
7284
function _checkStaking(address _staking, address _vapp, address _prove, address _iProve)
7385
internal
7486
view

contracts/src/SuccinctStaking.sol

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,28 @@ import {ISuccinctStaking} from "./interfaces/ISuccinctStaking.sol";
77
import {IIntermediateSuccinct} from "./interfaces/IIntermediateSuccinct.sol";
88
import {IProver} from "./interfaces/IProver.sol";
99
import {SuccinctGovernor} from "./SuccinctGovernor.sol";
10-
import {Initializable} from "../lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol";
11-
import {Ownable} from "../lib/openzeppelin-contracts/contracts/access/Ownable.sol";
10+
import {Initializable} from
11+
"../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
12+
import {OwnableUpgradeable} from
13+
"../lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
1214
import {IERC20} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol";
1315
import {IERC20Permit} from
1416
"../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol";
1517
import {IERC4626} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol";
1618
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
1719
import {Math} from "../lib/openzeppelin-contracts/contracts/utils/math/Math.sol";
20+
import {UUPSUpgradeable} from
21+
"../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
1822

1923
/// @title SuccinctStaking
2024
/// @author Succinct Labs
2125
/// @notice Manages staking, unstaking, dispensing, and slashing for the Succinct Prover Network.
2226
contract SuccinctStaking is
2327
Initializable,
24-
Ownable,
28+
OwnableUpgradeable,
2529
ProverRegistry,
2630
StakedSuccinct,
31+
UUPSUpgradeable,
2732
ISuccinctStaking
2833
{
2934
using SafeERC20 for IERC20;
@@ -89,14 +94,15 @@ contract SuccinctStaking is
8994
INITIALIZER
9095
//////////////////////////////////////////////////////////////*/
9196

92-
/// @dev Only the owner is set in the constructor. This is done because other contracts
93-
/// (e.g. SuccinctVApp) need a reference to this contract, and this contract needs a
94-
/// reference to it. So we deploy this first, then initialize it later.
95-
constructor(address _owner) Ownable(_owner) {}
97+
/// @custom:oz-upgrades-unsafe-allow constructor
98+
constructor() {
99+
_disableInitializers();
100+
}
96101

97102
/// @dev We don't do this in the constructor because we must deploy this contract
98103
/// first.
99104
function initialize(
105+
address _owner,
100106
address _governor,
101107
address _vApp,
102108
address _prove,
@@ -105,13 +111,12 @@ contract SuccinctStaking is
105111
uint256 _minStakeAmount,
106112
uint256 _maxUnstakeRequests,
107113
uint256 _unstakePeriod,
108-
uint256 _slashCancellationPeriod,
109-
uint256 _dispenseRate
110-
) external onlyOwner initializer {
114+
uint256 _slashCancellationPeriod
115+
) external initializer {
111116
// Ensure that parameters critical for functionality are non-zero.
112117
if (
113-
_governor == address(0) || _vApp == address(0) || _prove == address(0)
114-
|| _intermediateProve == address(0) || _dispenser == address(0)
118+
_owner == address(0) || _governor == address(0) || _vApp == address(0)
119+
|| _prove == address(0) || _intermediateProve == address(0) || _dispenser == address(0)
115120
) {
116121
revert ZeroAddress();
117122
}
@@ -120,16 +125,16 @@ contract SuccinctStaking is
120125
}
121126

122127
// Setup the initial state.
128+
__UUPSUpgradeable_init();
129+
__Ownable_init(_owner);
130+
__StakedSuccinct_init();
123131
__ProverRegistry_init(_governor, _vApp, _prove, _intermediateProve);
124132
dispenser = _dispenser;
125133
minStakeAmount = _minStakeAmount;
126134
maxUnstakeRequests = _maxUnstakeRequests;
127135
unstakePeriod = _unstakePeriod;
128136
slashCancellationPeriod = _slashCancellationPeriod;
129137

130-
// Setup the dispense rate.
131-
_updateDispenseRate(_dispenseRate);
132-
133138
// Approve the $iPROVE contract to transfer $PROVE from this contract during stake().
134139
IERC20(prove).approve(iProve, type(uint256).max);
135140
}
@@ -657,4 +662,7 @@ contract SuccinctStaking is
657662

658663
dispenseRate = _dispenseRate;
659664
}
665+
666+
/// @dev Authorizes an ERC1967 proxy upgrade to a new implementation contract.
667+
function _authorizeUpgrade(address _newImplementation) internal override onlyOwner {}
660668
}

contracts/src/SuccinctVApp.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ contract SuccinctVApp is
130130
}
131131

132132
// Set the state variables.
133+
__UUPSUpgradeable_init();
133134
__Ownable_init(_owner);
134135
vkey = _vkey;
135136
prove = _prove;

contracts/src/libraries/ProverRegistry.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ abstract contract ProverRegistry is IProverRegistry {
4545
/// @dev A mapping from prover vault to whether it is deactivated.
4646
mapping(address => bool) internal deactivatedProvers;
4747

48+
/// @dev This empty reserved space to add new variables without shifting down storage.
49+
uint256[10] private __gap;
50+
4851
/// @dev This call must be sent by the VApp contract. This also acts as a check to ensure that the contract
4952
/// has been initialized.
5053
modifier onlyVApp() {

contracts/src/tokens/StakedSuccinct.sol

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.28;
33

4-
import {ERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
4+
import {ERC20Upgradeable} from
5+
"../../lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
56

67
string constant NAME = "StakedSuccinct";
78
string constant SYMBOL = "stPROVE";
@@ -12,19 +13,24 @@ string constant SYMBOL = "stPROVE";
1213
/// @dev This contract balance stays 1:1 with $PROVER-N vaults to give one unified
1314
/// source of truth to track staked $PROVE. It is non-transferable outside of
1415
/// staking operations.
15-
abstract contract StakedSuccinct is ERC20 {
16+
abstract contract StakedSuccinct is ERC20Upgradeable {
1617
error NonTransferable();
1718

1819
/// @dev Only true if in the process of staking or unstaking.
1920
bool internal transient isStakingOperation;
2021

22+
/// @dev This empty reserved space to add new variables without shifting down storage.
23+
uint256[10] private __gap;
24+
2125
modifier stakingOperation() {
2226
isStakingOperation = true;
2327
_;
2428
isStakingOperation = false;
2529
}
2630

27-
constructor() ERC20(NAME, SYMBOL) {}
31+
function __StakedSuccinct_init() internal onlyInitializing {
32+
__ERC20_init(NAME, SYMBOL);
33+
}
2834

2935
function name() public pure virtual override returns (string memory) {
3036
return NAME;
@@ -36,7 +42,10 @@ abstract contract StakedSuccinct is ERC20 {
3642

3743
/// @dev Only can update balances when staking operations are occuring. This is equivalent to
3844
/// the only staking checks that we have on $iPROVE and $PROVER-N tokens.
39-
function _update(address _from, address _to, uint256 _value) internal override(ERC20) {
45+
function _update(address _from, address _to, uint256 _value)
46+
internal
47+
override(ERC20Upgradeable)
48+
{
4049
if (!isStakingOperation) {
4150
revert NonTransferable();
4251
}

0 commit comments

Comments
 (0)