diff --git a/.gas-snapshot b/.gas-snapshot index f8fb744..c3409fd 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,18 +1,24 @@ +CreateVaultTest:testDeployment() (gas: 9774) +CreateVaultTest:test_createVault() (gas: 650992) ExecuteAccountTest:testDeployment() (gas: 26400) -ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 982104) +ExecuteAccountTest:test_RevertWhen_InvalidLimitEpoch() (gas: 991602) LeaveTest:testDeployment() (gas: 26172) -LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 670554) +LeaveTest:test_RevertWhen_NoPendingMigration() (gas: 678051) LeaveTest:test_RevertWhen_SenderIsNotVault() (gas: 10562) LockTest:testDeployment() (gas: 26400) -LockTest:test_RevertWhen_DecreasingLockTime() (gas: 985034) +LockTest:test_RevertWhen_DecreasingLockTime() (gas: 994528) LockTest:test_RevertWhen_SenderIsNotVault() (gas: 10607) MigrateTest:testDeployment() (gas: 26172) -MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 670393) +MigrateTest:test_RevertWhen_NoPendingMigration() (gas: 677890) MigrateTest:test_RevertWhen_SenderIsNotVault() (gas: 10629) +SetStakeManagerTest:testDeployment() (gas: 9774) +SetStakeManagerTest:test_RevertWhen_InvalidStakeManagerAddress() (gas: 20481) +SetStakeManagerTest:test_SetStakeManager() (gas: 19869) StakeManagerTest:testDeployment() (gas: 26172) StakeTest:testDeployment() (gas: 26172) StakeTest:test_RevertWhen_SenderIsNotVault() (gas: 10638) StakedTokenTest:testStakeToken() (gas: 7638) UnstakeTest:testDeployment() (gas: 26355) -UnstakeTest:test_RevertWhen_FundsLocked() (gas: 981497) -UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 10609) \ No newline at end of file +UnstakeTest:test_RevertWhen_FundsLocked() (gas: 990991) +UnstakeTest:test_RevertWhen_SenderIsNotVault() (gas: 10609) +VaultFactoryTest:testDeployment() (gas: 9774) \ No newline at end of file diff --git a/contracts/VaultFactory.sol b/contracts/VaultFactory.sol new file mode 100644 index 0000000..a4802c0 --- /dev/null +++ b/contracts/VaultFactory.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { StakeManager } from "./StakeManager.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 Ownable2Step { + error VaultFactory__InvalidStakeManagerAddress(); + + event VaultCreated(address indexed vault, address indexed owner); + event StakeManagerAddressChanged(address indexed newStakeManagerAddress); + + /// @dev Address of the `StakeManager` contract instance. + StakeManager public stakeManager; + + /// @param _stakeManager Address of the `StakeManager` contract instance. + constructor(address _stakeManager) { + if (_stakeManager == address(0)) { + revert VaultFactory__InvalidStakeManagerAddress(); + } + stakeManager = StakeManager(_stakeManager); + } + + /// @notice Sets the `StakeManager` contract address. + /// @dev Only the owner can call this function. + /// @dev Reverts if the provided `StakeManager` address is zero. + /// @dev Emits a {StakeManagerAddressChanged} event. + /// @param _stakeManager Address of the `StakeManager` contract instance. + function setStakeManager(address _stakeManager) external onlyOwner { + if (_stakeManager == address(0) || _stakeManager == address(stakeManager)) { + revert VaultFactory__InvalidStakeManagerAddress(); + } + stakeManager = StakeManager(_stakeManager); + emit StakeManagerAddressChanged(_stakeManager); + } + + /// @notice Creates an instance of a `StakeVault` contract. + /// @dev Anyone can call this function. + /// @dev Emits a {VaultCreated} event. + function createVault() external returns (StakeVault) { + StakeVault vault = new StakeVault(msg.sender, stakeManager.stakedToken(), stakeManager); + emit VaultCreated(address(vault), msg.sender); + return vault; + } +} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 6f4c810..5130227 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -4,16 +4,18 @@ pragma solidity >=0.8.19 <=0.9.0; import { BaseScript } from "./Base.s.sol"; import { DeploymentConfig } from "./DeploymentConfig.s.sol"; import { StakeManager } from "../contracts/StakeManager.sol"; +import { VaultFactory } from "../contracts/VaultFactory.sol"; contract Deploy is BaseScript { - function run() public returns (StakeManager, DeploymentConfig) { + function run() public returns (VaultFactory, StakeManager, DeploymentConfig) { DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); (, address token) = deploymentConfig.activeNetworkConfig(); vm.startBroadcast(broadcaster); StakeManager stakeManager = new StakeManager(token, address(0)); + VaultFactory vaultFactory = new VaultFactory(address(stakeManager)); vm.stopBroadcast(); - return (stakeManager, deploymentConfig); + return (vaultFactory, stakeManager, deploymentConfig); } } diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index 0ce2768..8ea23d2 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -8,10 +8,12 @@ import { Deploy } from "../script/Deploy.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import { StakeManager } from "../contracts/StakeManager.sol"; import { StakeVault } from "../contracts/StakeVault.sol"; +import { VaultFactory } from "../contracts/VaultFactory.sol"; contract StakeManagerTest is Test { DeploymentConfig internal deploymentConfig; StakeManager internal stakeManager; + VaultFactory internal vaultFactory; address internal stakeToken; address internal deployer; @@ -19,7 +21,7 @@ contract StakeManagerTest is Test { function setUp() public virtual { Deploy deployment = new Deploy(); - (stakeManager, deploymentConfig) = deployment.run(); + (vaultFactory, stakeManager, deploymentConfig) = deployment.run(); (deployer, stakeToken) = deploymentConfig.activeNetworkConfig(); } @@ -36,7 +38,7 @@ contract StakeManagerTest is Test { function _createTestVault(address owner) internal returns (StakeVault vault) { vm.prank(owner); - vault = new StakeVault(owner, ERC20(stakeToken), stakeManager); + vault = vaultFactory.createVault(); vm.prank(deployer); stakeManager.setVault(address(vault).codehash); diff --git a/test/StakeVault.t.sol b/test/StakeVault.t.sol index 2389ae7..441e45a 100644 --- a/test/StakeVault.t.sol +++ b/test/StakeVault.t.sol @@ -1,19 +1,20 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - import { Test } from "forge-std/Test.sol"; import { Deploy } from "../script/Deploy.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; import { StakeManager } from "../contracts/StakeManager.sol"; import { StakeVault } from "../contracts/StakeVault.sol"; +import { VaultFactory } from "../contracts/VaultFactory.sol"; contract StakeVaultTest is Test { StakeManager internal stakeManager; DeploymentConfig internal deploymentConfig; + VaultFactory internal vaultFactory; + StakeVault internal stakeVault; address internal deployer; @@ -24,11 +25,11 @@ contract StakeVaultTest is Test { function setUp() public virtual { Deploy deployment = new Deploy(); - (stakeManager, deploymentConfig) = deployment.run(); + (vaultFactory, stakeManager, deploymentConfig) = deployment.run(); (deployer, stakeToken) = deploymentConfig.activeNetworkConfig(); vm.prank(testUser); - stakeVault = new StakeVault(testUser, ERC20(stakeToken), stakeManager); + stakeVault = vaultFactory.createVault(); } } diff --git a/test/VaultFactory.t.sol b/test/VaultFactory.t.sol new file mode 100644 index 0000000..9bad2a2 --- /dev/null +++ b/test/VaultFactory.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import { Test } from "forge-std/Test.sol"; +import { Deploy } from "../script/Deploy.s.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; + +import { StakeManager } from "../contracts/StakeManager.sol"; +import { StakeVault } from "../contracts/StakeVault.sol"; +import { VaultFactory } from "../contracts/VaultFactory.sol"; + +contract VaultFactoryTest is Test { + DeploymentConfig internal deploymentConfig; + + StakeManager internal stakeManager; + + VaultFactory internal vaultFactory; + + address internal deployer; + + address internal stakedToken; + + address internal testUser = makeAddr("testUser"); + + function setUp() public virtual { + Deploy deployment = new Deploy(); + (vaultFactory, stakeManager, deploymentConfig) = deployment.run(); + (deployer, stakedToken) = deploymentConfig.activeNetworkConfig(); + } + + function testDeployment() public { + assertEq(address(vaultFactory.stakeManager()), address(stakeManager)); + } +} + +contract SetStakeManagerTest is VaultFactoryTest { + function setUp() public override { + VaultFactoryTest.setUp(); + } + + function test_RevertWhen_InvalidStakeManagerAddress() public { + vm.startPrank(deployer); + vm.expectRevert(VaultFactory.VaultFactory__InvalidStakeManagerAddress.selector); + vaultFactory.setStakeManager(address(0)); + + vm.expectRevert(VaultFactory.VaultFactory__InvalidStakeManagerAddress.selector); + vaultFactory.setStakeManager(address(stakeManager)); + } + + function test_SetStakeManager() public { + vm.prank(deployer); + vaultFactory.setStakeManager(address(this)); + assertEq(address(vaultFactory.stakeManager()), address(this)); + } +} + +contract CreateVaultTest is VaultFactoryTest { + event VaultCreated(address indexed vault, address indexed owner); + + function setUp() public override { + VaultFactoryTest.setUp(); + } + + function test_createVault() public { + vm.prank(testUser); + vm.expectEmit(false, false, false, false); + emit VaultCreated(makeAddr("some address"), testUser); + StakeVault vault = vaultFactory.createVault(); + assertEq(vault.owner(), testUser); + assertEq(address(vault.stakedToken()), address(stakedToken)); + } +}