Skip to content

Commit

Permalink
feat: introduce proxy clones
Browse files Browse the repository at this point in the history
This commit introduces proxy clones to make create `StakeVault`s as
cheap as possible.

Major changes here are:

- Introduce `VaultFactory` which takes care of creating clones and
  registering them with the stake manager
- Make `StakeVault` and `Initializable` so it can be used as a
  "template" contract to later have proxies point to it
- Adjust the deployment script to also deploy `VaultFactory` and ensure
  The proxy is whitelisted in the stake manager
- Make use of the new proxy clones in tests
- Add a test for `TrustedCodehashAccess` that ensures the proxy
  whitelisting works and setting up a (malicious) proxy is not going to
  work

Closes #101
  • Loading branch information
0x-r4bbit committed Feb 3, 2025
1 parent a47d46d commit 9b65913
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 130 deletions.
72 changes: 43 additions & 29 deletions .gas-report

Large diffs are not rendered by default.

121 changes: 61 additions & 60 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,75 +1,76 @@
EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92757)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 300544)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 387340)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 667427)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 395139)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 394708)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 380241)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 306012)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 392806)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 678359)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 400605)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 400174)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 385707)
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39471)
IntegrationTest:testStakeFoo() (gas: 1218594)
LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 6214173)
LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 297675)
LeaveTest:test_TrustNewStakeManager() (gas: 6269901)
LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1000, μ: 344783, ~: 344801)
LockTest:test_LockFailsWithNoStake() (gas: 102637)
LockTest:test_LockFailsWithZero() (gas: 315022)
LockTest:test_LockWithoutPriorLock() (gas: 393335)
MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1752531)
IntegrationTest:testStakeFoo() (gas: 1232258)
LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 6218572)
LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 303138)
LeaveTest:test_TrustNewStakeManager() (gas: 6286362)
LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1000, μ: 350272, ~: 350297)
LockTest:test_LockFailsWithNoStake() (gas: 105377)
LockTest:test_LockFailsWithZero() (gas: 320506)
LockTest:test_LockWithoutPriorLock() (gas: 398817)
MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1757478)
MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 4996)
MathTest:test_CalcAccrueMP() (gas: 7990)
MathTest:test_CalcBonusMP() (gas: 18676)
MathTest:test_CalcInitialMP() (gas: 5352)
MathTest:test_CalcAccrueMP() (gas: 8013)
MathTest:test_CalcBonusMP() (gas: 18644)
MathTest:test_CalcInitialMP() (gas: 5375)
MathTest:test_CalcMaxAccruedMP() (gas: 4642)
MathTest:test_CalcMaxTotalMP() (gas: 19449)
MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 731369)
MathTest:test_CalcMaxTotalMP() (gas: 19411)
MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 739601)
NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 85934)
NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 58332)
NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35804)
NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 102512)
NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 49555)
NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35979)
RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 490632)
RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 493376)
RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160880)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39384)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39407)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39442)
RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 620722)
StakeTest:test_StakeMultipleAccounts() (gas: 502561)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 508596)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 847390)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517705)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539649)
StakeTest:test_StakeOneAccount() (gas: 279841)
StakeTest:test_StakeOneAccountAndRewards() (gas: 285896)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510467)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500009)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300111)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 300696)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 300763)
StakingTokenTest:testStakeToken() (gas: 10422)
UnstakeTest:test_StakeMultipleAccounts() (gas: 502560)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 508640)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 847367)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517704)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539693)
UnstakeTest:test_StakeOneAccount() (gas: 279841)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 285874)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510511)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500008)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300111)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 300718)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 300762)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 546541)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 707663)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 803659)
UnstakeTest:test_UnstakeOneAccount() (gas: 481480)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 505028)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 410671)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 530083)
UpgradeTest:test_RevertWhenNotOwner() (gas: 2897740)
UpgradeTest:test_UpgradeStakeManager() (gas: 6114750)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39407)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39385)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39420)
RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 623466)
StakeTest:test_StakeMultipleAccounts() (gas: 508049)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 514084)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 852890)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 523193)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 545137)
StakeTest:test_StakeOneAccount() (gas: 282585)
StakeTest:test_StakeOneAccountAndRewards() (gas: 288640)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 513211)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 502753)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 302900)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 303440)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 303507)
StakingTokenTest:testStakeToken() (gas: 13140)
TrustedCodehashAccessTest:test_RevertWhenProxyCloneCodehashNotTrusted() (gas: 1896237)
UnstakeTest:test_StakeMultipleAccounts() (gas: 508026)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 514106)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 852867)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 523170)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 545159)
UnstakeTest:test_StakeOneAccount() (gas: 282585)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 288662)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 513233)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 502797)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 302900)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 303462)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 303551)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 551979)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 718583)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 816209)
UnstakeTest:test_UnstakeOneAccount() (gas: 488546)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 510466)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 416131)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 535543)
UpgradeTest:test_RevertWhenNotOwner() (gas: 2897728)
UpgradeTest:test_UpgradeStakeManager() (gas: 6117494)
VaultRegistrationTest:test_VaultRegistration() (gas: 62154)
WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 313397)
WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 318812)
XPNFTTokenTest:testApproveNotAllowed() (gas: 10500)
XPNFTTokenTest:testGetApproved() (gas: 10523)
XPNFTTokenTest:testIsApprovedForAll() (gas: 10698)
Expand Down
25 changes: 16 additions & 9 deletions script/DeployRewardsStreamerMP.s.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";

import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";

import { TransparentProxy } from "../src/TransparentProxy.sol";
import { IStakeManagerProxy } from "../src/interfaces/IStakeManagerProxy.sol";
import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol";
import { StakeVault } from "../src/StakeVault.sol";
import { VaultFactory } from "../src/VaultFactory.sol";

contract DeployRewardsStreamerMPScript is BaseScript {
function run() public returns (RewardsStreamerMP, DeploymentConfig) {
function run() public returns (RewardsStreamerMP, VaultFactory, DeploymentConfig) {
DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster);
(address deployer, address stakingToken) = deploymentConfig.activeNetworkConfig();

Expand All @@ -21,16 +25,19 @@ contract DeployRewardsStreamerMPScript is BaseScript {
address impl = address(new RewardsStreamerMP());
// Create upgradeable proxy
address proxy = address(new TransparentProxy(impl, initializeData));
vm.stopBroadcast();

RewardsStreamerMP stakeManager = RewardsStreamerMP(proxy);
StakeVault tempVault = new StakeVault(address(this), IStakeManagerProxy(proxy));
bytes32 vaultCodeHash = address(tempVault).codehash;
// Create vault implementation for proxy clones
address vaultImplementation = address(new StakeVault(IERC20(stakingToken)));
address proxyClone = Clones.clone(vaultImplementation);

// Whitelist vault implementation codehash
RewardsStreamerMP(proxy).setTrustedCodehash(proxyClone.codehash, true);

// Create vault factory
VaultFactory vaultFactory = new VaultFactory(deployer, proxy, vaultImplementation);

vm.startBroadcast(deployer);
stakeManager.setTrustedCodehash(vaultCodeHash, true);
vm.stopBroadcast();

return (stakeManager, deploymentConfig);
return (RewardsStreamerMP(proxy), vaultFactory, deploymentConfig);
}
}
24 changes: 17 additions & 7 deletions src/StakeVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

pragma solidity ^0.8.26;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IStakeManagerProxy } from "./interfaces/IStakeManagerProxy.sol";
import { IStakeVault } from "./interfaces/IStakeVault.sol";
Expand All @@ -12,8 +13,10 @@ import { IStakeVault } from "./interfaces/IStakeVault.sol";
* @author Ricardo Guilherme Schmidt <ricardo3@status.im>
* @notice A contract to secure user stakes and manage staking with IStakeManager.
* @dev This contract is owned by the user and allows staking, unstaking, and withdrawing tokens.
* @dev The only reason this is `OwnableUpgradeable` is because we use proxy clones
* to create stake vault instances. Hence, we need to use `Initializeable` to set the owner.
*/
contract StakeVault is IStakeVault, Ownable {
contract StakeVault is IStakeVault, Initializable, OwnableUpgradeable {
error StakeVault__NotEnoughAvailableBalance();
error StakeVault__InvalidDestinationAddress();
error StakeVault__UpdateNotAvailable();
Expand Down Expand Up @@ -55,13 +58,20 @@ contract StakeVault is IStakeVault, Ownable {

/**
* @notice Initializes the contract with the owner, staked token, and stake manager.
*/
constructor(IERC20 token) {
STAKING_TOKEN = token;
_disableInitializers();
}

/**
* @param _owner The address of the owner.
* @param _stakeManager The address of the StakeManager contract.
*/
constructor(address _owner, IStakeManagerProxy _stakeManager) Ownable(_owner) {
STAKING_TOKEN = _stakeManager.STAKING_TOKEN();
stakeManager = _stakeManager;
stakeManagerImplementationAddress = _stakeManager.implementation();
function initialize(address _owner, address _stakeManager) public initializer {
__Ownable_init(_owner);
stakeManager = IStakeManagerProxy(_stakeManager);
stakeManagerImplementationAddress = stakeManager.implementation();
}

/**
Expand All @@ -82,7 +92,7 @@ contract StakeVault is IStakeVault, Ownable {
/**
* @notice Returns the address of the current owner.
*/
function owner() public view override(Ownable, IStakeVault) returns (address) {
function owner() public view override(OwnableUpgradeable, IStakeVault) returns (address) {
return super.owner();
}

Expand Down
76 changes: 76 additions & 0 deletions src/VaultFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
// import { TransparentClones } from "./TransparentClones.sol";
import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";
import { StakeVault } from "./StakeVault.sol";

/**
* @title VaultFactory
* @author 0x-r4bbit
*
* This contract is reponsible for creating staking vaults for users.
* A user of the staking protocol is able to create multiple vaults to facilitate
* different strategies. For example, a user may want to create a vault for
* a long-term lock period, while also creating a vault that has no lock period
* at all.
*
* @notice This contract is used by users to create staking vaults.
* @dev This contract will be deployed by Status, making Status the owner of the contract.
* @dev A contract address for a `StakeManager` has to be provided to create this contract.
* @dev Reverts with {VaultFactory__InvalidStakeManagerAddress} if the provided
* `StakeManager` address is zero.
* @dev The `StakeManager` contract address can be changed by the owner.
*/
contract VaultFactory is Ownable {
error VaultFactory__InvalidStakeManagerAddress();

event VaultCreated(address indexed vault, address indexed owner);
event StakeManagerAddressChanged(address indexed newStakeManagerAddress);
event VaultImplementationChanged(address indexed newVaultImplementation);

/// @dev Address of the `StakeManager` contract instance.
address public stakeManager;
/// @dev Address of the `StakeVault` implementation contract.
address public vaultImplementation;

/// @param _stakeManager Address of the `StakeManager` contract instance.
constructor(address _owner, address _stakeManager, address _vaultImplementation) Ownable(_owner) {
if (_stakeManager == address(0)) {
revert VaultFactory__InvalidStakeManagerAddress();
}
stakeManager = _stakeManager;
vaultImplementation = _vaultImplementation;
}

/// @notice Sets the `StakeManager` contract address.
/// @dev Only the owner can call this function.
/// @dev Emits a {StakeManagerAddressChanged} event.
/// @param _stakeManager Address of the `StakeManager` contract instance.
function setStakeManager(address _stakeManager) external onlyOwner {
stakeManager = _stakeManager;
emit StakeManagerAddressChanged(_stakeManager);
}

/// @notice Sets the `StakeVault` implementation contract address.
/// @dev Only the owner can call this function.
/// @dev Emits a {VaultImplementationChanged} event.
/// @param _vaultImplementation Address of the `StakeVault` implementation contract.
/// @dev This function is used to change the implementation of the `StakeVault` contract.
function setVaultImplementation(address _vaultImplementation) external onlyOwner {
vaultImplementation = _vaultImplementation;
emit VaultImplementationChanged(_vaultImplementation);
}

/// @notice Creates an instance of a `StakeVault` contract.
/// @dev Anyone can call this function.
/// @dev Emits a {VaultCreated} event.
function createVault() external returns (StakeVault clone) {
clone = StakeVault(Clones.clone(vaultImplementation));
clone.initialize(msg.sender, stakeManager);
clone.register();
emit VaultCreated(address(clone), msg.sender);
}
}
Loading

0 comments on commit 9b65913

Please sign in to comment.