Skip to content

Commit ab7a713

Browse files
authored
feat(protocol-contracts): make OperatorStaking UUPS upgradeable (#1562)
1 parent 4071494 commit ab7a713

10 files changed

Lines changed: 149 additions & 96 deletions

File tree

protocol-contracts/staking/contracts/OperatorStaking.sol

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
pragma solidity ^0.8.27;
44

5+
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
56
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
6-
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7-
import {ERC1363} from "@openzeppelin/contracts/token/ERC20/extensions/ERC1363.sol";
7+
import {ERC1363Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC1363Upgradeable.sol";
88
import {ERC4626, IERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
99
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
1010
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
@@ -27,17 +27,24 @@ import {ProtocolStaking} from "./ProtocolStaking.sol";
2727
* may decrease due to slashing. These losses are symmetrically passed to delegators on the `OperatorStaking` level.
2828
* Slashing must first decrease the `ProtocolStaking` balance of this contract before affecting pending withdrawals.
2929
*/
30-
contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
30+
contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUpgradeable {
3131
using Math for uint256;
3232
using Checkpoints for Checkpoints.Trace208;
3333

34-
ProtocolStaking private immutable _protocolStaking;
35-
IERC20 private immutable _asset;
36-
address private _rewarder;
37-
uint256 private _totalSharesInRedemption;
38-
mapping(address => uint256) private _sharesReleased;
39-
mapping(address => Checkpoints.Trace208) private _redeemRequests;
40-
mapping(address => mapping(address => bool)) private _operator;
34+
/// @custom:storage-location erc7201:fhevm_protocol.storage.OperatorStaking
35+
struct OperatorStakingStorage {
36+
ProtocolStaking _protocolStaking;
37+
IERC20 _asset;
38+
address _rewarder;
39+
uint256 _totalSharesInRedemption;
40+
mapping(address => uint256) _sharesReleased;
41+
mapping(address => Checkpoints.Trace208) _redeemRequests;
42+
mapping(address => mapping(address => bool)) _operator;
43+
}
44+
45+
// keccak256(abi.encode(uint256(keccak256("fhevm_protocol.storage.OperatorStaking")) - 1)) & ~bytes32(uint256(0xff))
46+
bytes32 private constant OPERATOR_STAKING_STORAGE_LOCATION =
47+
0x7fc851282090a0d8832502c48739eac98a0856539351f17cb5d5950c860fd200;
4148

4249
/// @dev Emitted when an operator is set or unset for a controller.
4350
event OperatorSet(address indexed controller, address indexed operator, bool approved);
@@ -72,6 +79,11 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
7279
_;
7380
}
7481

82+
/// @custom:oz-upgrades-unsafe-allow constructor
83+
constructor() {
84+
_disableInitializers();
85+
}
86+
7587
/**
7688
* @notice Initializes the OperatorStaking contract.
7789
* @param name The name of the ERC20 token.
@@ -81,24 +93,34 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
8193
* @param initialMaxFeeBasisPoints_ The initial maximum fee basis points for the OperatorRewarder contract.
8294
* @param initialFeeBasisPoints_ The initial fee basis points for the OperatorRewarder contract.
8395
*/
84-
constructor(
96+
function initialize(
8597
string memory name,
8698
string memory symbol,
8799
ProtocolStaking protocolStaking_,
88100
address beneficiary_,
89101
uint16 initialMaxFeeBasisPoints_,
90102
uint16 initialFeeBasisPoints_
91-
) ERC20(name, symbol) {
92-
_asset = IERC20(protocolStaking_.stakingToken());
93-
_protocolStaking = protocolStaking_;
103+
) public initializer {
104+
__ERC20_init(name, symbol);
105+
106+
OperatorStakingStorage storage $ = _getOperatorStakingStorage();
107+
108+
$._asset = IERC20(protocolStaking_.stakingToken());
109+
$._protocolStaking = protocolStaking_;
94110

95111
IERC20(asset()).approve(address(protocolStaking_), type(uint256).max);
96112

97113
address rewarder_ = address(
98-
new OperatorRewarder(beneficiary_, protocolStaking_, this, initialMaxFeeBasisPoints_, initialFeeBasisPoints_)
114+
new OperatorRewarder(
115+
beneficiary_,
116+
protocolStaking_,
117+
this,
118+
initialMaxFeeBasisPoints_,
119+
initialFeeBasisPoints_
120+
)
99121
);
100122
protocolStaking_.setRewardsRecipient(rewarder_);
101-
_rewarder = rewarder_;
123+
$._rewarder = rewarder_;
102124

103125
emit RewarderSet(address(0), rewarder_);
104126
}
@@ -133,19 +155,21 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
133155
}
134156
_burn(ownerRedeem, shares);
135157

158+
OperatorStakingStorage storage $ = _getOperatorStakingStorage();
159+
136160
uint256 newTotalSharesInRedemption = totalSharesInRedemption() + shares;
137-
_totalSharesInRedemption = newTotalSharesInRedemption;
161+
$._totalSharesInRedemption = newTotalSharesInRedemption;
138162

139163
ProtocolStaking protocolStaking_ = protocolStaking();
140164
int256 assetsToWithdraw = SafeCast.toInt256(previewRedeem(newTotalSharesInRedemption)) -
141165
SafeCast.toInt256(
142166
IERC20(asset()).balanceOf(address(this)) + protocolStaking_.awaitingRelease(address(this))
143167
);
144168

145-
(, uint48 lastReleaseTime, uint208 controllerSharesRedeemed) = _redeemRequests[controller].latestCheckpoint();
169+
(, uint48 lastReleaseTime, uint208 controllerSharesRedeemed) = $._redeemRequests[controller].latestCheckpoint();
146170
uint48 releaseTime = protocolStaking_.unstake(SafeCast.toUint256(SignedMath.max(assetsToWithdraw, 0)));
147171
assert(releaseTime >= lastReleaseTime); // should never happen
148-
_redeemRequests[controller].push(releaseTime, controllerSharesRedeemed + shares);
172+
$._redeemRequests[controller].push(releaseTime, controllerSharesRedeemed + shares);
149173

150174
emit RedeemRequest(controller, ownerRedeem, 0, msg.sender, shares, releaseTime);
151175
}
@@ -174,8 +198,9 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
174198
uint256 assets = previewRedeem(shares);
175199

176200
if (assets > 0) {
177-
_totalSharesInRedemption -= shares;
178-
_sharesReleased[controller] += shares;
201+
OperatorStakingStorage storage $ = _getOperatorStakingStorage();
202+
$._totalSharesInRedemption -= shares;
203+
$._sharesReleased[controller] += shares;
179204
_doTransferOut(receiver, assets);
180205

181206
emit IERC4626.Withdraw(msg.sender, receiver, controller, assets, shares);
@@ -200,15 +225,15 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
200225
}
201226

202227
/**
203-
* @dev Set a new rewarder contract.
228+
* @dev Set a new rewarder contract. Only callable by the owner.
204229
* @param newRewarder The new rewarder contract address. This contract must not be the same as the current
205230
* and must have code.
206231
*/
207232
function setRewarder(address newRewarder) public virtual onlyOwner {
208233
address oldRewarder = rewarder();
209234
require(newRewarder != oldRewarder && newRewarder.code.length > 0, InvalidRewarder(newRewarder));
210235
OperatorRewarder(oldRewarder).shutdown();
211-
_rewarder = newRewarder;
236+
_getOperatorStakingStorage()._rewarder = newRewarder;
212237
protocolStaking().setRewardsRecipient(newRewarder);
213238

214239
emit RewarderSet(oldRewarder, newRewarder);
@@ -220,7 +245,7 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
220245
* @param approved True to approve, false to revoke.
221246
*/
222247
function setOperator(address operator, bool approved) public virtual {
223-
_operator[msg.sender][operator] = approved;
248+
_getOperatorStakingStorage()._operator[msg.sender][operator] = approved;
224249

225250
emit OperatorSet(msg.sender, operator, approved);
226251
}
@@ -238,23 +263,23 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
238263
* @return The asset address.
239264
*/
240265
function asset() public view virtual returns (address) {
241-
return address(_asset);
266+
return address(_getOperatorStakingStorage()._asset);
242267
}
243268

244269
/**
245270
* @notice Returns the ProtocolStaking contract address.
246271
* @return The ProtocolStaking contract address.
247272
*/
248273
function protocolStaking() public view virtual returns (ProtocolStaking) {
249-
return _protocolStaking;
274+
return _getOperatorStakingStorage()._protocolStaking;
250275
}
251276

252277
/**
253278
* @notice Returns the rewarder contract address.
254279
* @return The rewarder contract address.
255280
*/
256281
function rewarder() public view virtual returns (address) {
257-
return _rewarder;
282+
return _getOperatorStakingStorage()._rewarder;
258283
}
259284

260285
/**
@@ -275,7 +300,8 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
275300
* @return Amount of shares pending redeem.
276301
*/
277302
function pendingRedeemRequest(uint256, address controller) public view virtual returns (uint256) {
278-
return _redeemRequests[controller].latest() - _redeemRequests[controller].upperLookup(Time.timestamp());
303+
OperatorStakingStorage storage $ = _getOperatorStakingStorage();
304+
return $._redeemRequests[controller].latest() - $._redeemRequests[controller].upperLookup(Time.timestamp());
279305
}
280306

281307
/**
@@ -284,15 +310,16 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
284310
* @return Amount of claimable shares.
285311
*/
286312
function claimableRedeemRequest(uint256, address controller) public view virtual returns (uint256) {
287-
return _redeemRequests[controller].upperLookup(Time.timestamp()) - _sharesReleased[controller];
313+
OperatorStakingStorage storage $ = _getOperatorStakingStorage();
314+
return $._redeemRequests[controller].upperLookup(Time.timestamp()) - $._sharesReleased[controller];
288315
}
289316

290317
/**
291318
* @notice Returns the total shares in redemption.
292319
* @return The total shares in redemption.
293320
*/
294321
function totalSharesInRedemption() public view virtual returns (uint256) {
295-
return _totalSharesInRedemption;
322+
return _getOperatorStakingStorage()._totalSharesInRedemption;
296323
}
297324

298325
/**
@@ -337,7 +364,7 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
337364
* @return True if operator is approved, false otherwise.
338365
*/
339366
function isOperator(address controller, address operator) public view virtual returns (bool) {
340-
return _operator[controller][operator];
367+
return _getOperatorStakingStorage()._operator[controller][operator];
341368
}
342369

343370
function _doTransferOut(address to, uint256 amount) internal {
@@ -371,6 +398,8 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
371398
emit IERC4626.Deposit(caller, receiver, assets, shares);
372399
}
373400

401+
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
402+
374403
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
375404
// Shares in redemption have not yet received assets, so we need to account for them in the conversion.
376405
return
@@ -394,4 +423,10 @@ contract OperatorStaking is ERC1363, ReentrancyGuardTransient {
394423
function _decimalsOffset() internal view virtual returns (uint8) {
395424
return 0;
396425
}
426+
427+
function _getOperatorStakingStorage() private pure returns (OperatorStakingStorage storage $) {
428+
assembly {
429+
$.slot := OPERATOR_STAKING_STORAGE_LOCATION
430+
}
431+
}
397432
}

protocol-contracts/staking/contracts/ProtocolStaking.sol

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
2828
using SafeERC20 for IERC20;
2929
using Math for uint256;
3030

31-
/// @custom:storage-location erc7201:zama.storage.ProtocolStaking
31+
/// @custom:storage-location erc7201:fhevm_protocol.storage.ProtocolStaking
3232
struct ProtocolStakingStorage {
3333
// Stake - general
3434
address _stakingToken;
@@ -48,10 +48,9 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
4848
int256 _totalVirtualPaid;
4949
}
5050

51-
// keccak256(abi.encode(uint256(keccak256("zama.storage.ProtocolStaking")) - 1)) & ~bytes32(uint256(0xff))
51+
// keccak256(abi.encode(uint256(keccak256("fhevm_protocol.storage.ProtocolStaking")) - 1)) & ~bytes32(uint256(0xff))
5252
bytes32 private constant PROTOCOL_STAKING_STORAGE_LOCATION =
53-
0x6867237db38693700f305f18dff1dbf600e282237f7d452b4c792e6b019c6b00;
54-
bytes32 private constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
53+
0xd955b2342c0487c5e5b5f50f5620ec67dcb16d94462ba5d080d7b7472b67b900;
5554
bytes32 private constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
5655
bytes32 private constant ELIGIBLE_ACCOUNT_ROLE = keccak256("ELIGIBLE_ACCOUNT_ROLE");
5756

@@ -89,13 +88,11 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
8988
string memory version,
9089
address stakingToken_,
9190
address governor,
92-
address upgrader,
9391
address manager,
9492
uint48 initialUnstakeCooldownPeriod,
9593
uint256 initialRewardRate
9694
) public initializer {
9795
__AccessControlDefaultAdminRules_init(0, governor);
98-
_grantRole(UPGRADER_ROLE, upgrader);
9996
_grantRole(MANAGER_ROLE, manager);
10097
_setRoleAdmin(ELIGIBLE_ACCOUNT_ROLE, MANAGER_ROLE);
10198
__ERC20_init(name, symbol);
@@ -370,7 +367,7 @@ contract ProtocolStaking is AccessControlDefaultAdminRulesUpgradeable, ERC20Vote
370367
super._update(from, to, value);
371368
}
372369

373-
function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {}
370+
function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
374371

375372
function _historicalReward() internal view returns (uint256) {
376373
ProtocolStakingStorage storage $ = _getProtocolStakingStorage();

protocol-contracts/staking/contracts/mocks/ProtocolStakingSlashingMock.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ contract ProtocolStakingSlashingMock is ProtocolStaking {
1313
using SafeERC20 for IERC20;
1414

1515
bytes32 private constant PROTOCOL_STAKING_STORAGE_LOCATION =
16-
0x6867237db38693700f305f18dff1dbf600e282237f7d452b4c792e6b019c6b00;
16+
0xd955b2342c0487c5e5b5f50f5620ec67dcb16d94462ba5d080d7b7472b67b900;
1717

1818
mapping(address => uint256) private _slashedAmount;
1919

protocol-contracts/staking/tasks/deployment/operatorStaking.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ export const OPERATOR_REWARDER_CONTRACT_NAME = 'OperatorRewarder';
88

99
// Get the name of the operator staking contract to save in the deployments
1010
export function getOperatorStakingName(tokenName: string): string {
11-
return tokenName + '_Staking';
11+
return tokenName + '_Staking_Proxy';
12+
}
13+
14+
// Get the name of the implementation contract to save in the deployments
15+
export function getOperatorStakingImplName(tokenName: string): string {
16+
return tokenName + '_Staking_Impl';
1217
}
1318

1419
// Get the name of the operator rewarder contract to save in the deployments
@@ -26,43 +31,47 @@ async function deployOperatorStaking(
2631
initialFeeBasisPoints: number,
2732
hre: HardhatRuntimeEnvironment,
2833
) {
29-
const { getNamedAccounts, ethers, deployments, network } = hre;
34+
const { getNamedAccounts, ethers, deployments, network, upgrades } = hre;
3035
const { save, getArtifact } = deployments;
3136

3237
// Get the deployer account
3338
const { deployer } = await getNamedAccounts();
3439
const deployerSigner = await ethers.getSigner(deployer);
3540

36-
// Get the contract factory and deploy the operator staking and rewarder contracts
37-
const operatorStakingFactory = await ethers.getContractFactory(OPERATOR_STAKING_CONTRACT_NAME, deployerSigner);
38-
const operatorStaking = await operatorStakingFactory.deploy(
39-
tokenName,
40-
symbol,
41-
protocolStakingAddress,
42-
beneficiaryAddress,
43-
initialMaxFeeBasisPoints,
44-
initialFeeBasisPoints,
41+
// Get the contract factory and deploy the proxy + the implementation + the rewarder contract
42+
const protocolStakingFactory = await ethers.getContractFactory(OPERATOR_STAKING_CONTRACT_NAME, deployerSigner);
43+
const proxy = await upgrades.deployProxy(
44+
protocolStakingFactory,
45+
[tokenName, symbol, protocolStakingAddress, beneficiaryAddress, initialMaxFeeBasisPoints, initialFeeBasisPoints],
46+
{ kind: 'uups', initializer: 'initialize' },
4547
);
46-
await operatorStaking.waitForDeployment();
48+
await proxy.waitForDeployment();
4749

48-
// Get the operator staking and rewarder addresses
49-
const operatorStakingAddress = await operatorStaking.getAddress();
50-
const operatorRewarderAddress = await operatorStaking.rewarder();
50+
// Get the operator staking proxy and rewarder addresses
51+
const operatorStakingProxyAddress = await proxy.getAddress();
52+
const operatorRewarderAddress = await proxy.rewarder();
5153

5254
console.log(
5355
[
5456
`✅ Deployed ${tokenName} OperatorStaking:`,
55-
` - Operator staking address: ${operatorStakingAddress}`,
57+
` - Operator staking proxy address: ${operatorStakingProxyAddress}`,
5658
` - Operator rewarder address: ${operatorRewarderAddress}`,
5759
` - Deployed by deployer account: ${deployer}`,
5860
` - Network: ${network.name}`,
5961
'',
6062
].join('\n'),
6163
);
6264

63-
// Save the OperatorStaking and OperatorRewarder contract artifacts
65+
// Save the OperatorStaking proxy and implementation contract artifacts
6466
const operatorStakingArtifact = await getArtifact(OPERATOR_STAKING_CONTRACT_NAME);
65-
await save(getOperatorStakingName(tokenName), { address: operatorStakingAddress, abi: operatorStakingArtifact.abi });
67+
const implAddress = await upgrades.erc1967.getImplementationAddress(operatorStakingProxyAddress);
68+
await save(getOperatorStakingName(tokenName), {
69+
address: operatorStakingProxyAddress,
70+
abi: operatorStakingArtifact.abi,
71+
});
72+
await save(getOperatorStakingImplName(tokenName), { address: implAddress, abi: operatorStakingArtifact.abi });
73+
74+
// Save the OperatorRewarder contract artifacts
6675
const operatorRewarderArtifact = await getArtifact(OPERATOR_REWARDER_CONTRACT_NAME);
6776
await save(getOperatorRewarderName(tokenName), {
6877
address: operatorRewarderAddress,

0 commit comments

Comments
 (0)