From 8f0a83eeb9bdbed6c8d7347c69f42451f2ee375d Mon Sep 17 00:00:00 2001 From: Dmitri Tsumak Date: Wed, 11 Oct 2023 22:55:29 +0300 Subject: [PATCH] Add initialize to Keeper, VaultsRegistry --- abi/Errors.json | 10 ++-- abi/IKeeper.json | 13 +++++ abi/IVaultsRegistry.json | 13 +++++ contracts/interfaces/IKeeper.sol | 6 ++- contracts/interfaces/IVaultsRegistry.sol | 6 +++ contracts/keeper/Keeper.sol | 13 +++++ contracts/libraries/Errors.sol | 2 +- contracts/misc/CumulativeMerkleDrop.sol | 3 +- contracts/osToken/OsToken.sol | 4 ++ contracts/vaults/VaultsRegistry.sol | 12 +++++ contracts/vaults/ethereum/EthGenesisVault.sol | 10 ++-- helpers/constants.ts | 2 + helpers/types.ts | 1 + helpers/utils.ts | 2 +- package.json | 2 +- tasks/eth-full-deploy-local.ts | 47 ++++++++++++------- tasks/eth-full-deploy.ts | 47 +++++++++++++------ test/EthGenesisVault.spec.ts | 44 ++++++++++++++++- test/EthVault.withdraw.spec.ts | 7 +++ test/KeeperOracles.spec.ts | 25 +++++++++- test/VaultsRegistry.spec.ts | 23 +++++++++ .../EthGenesisVault.spec.ts.snap | 16 +++---- .../EthVault.upgrade.spec.ts.snap | 2 +- test/__snapshots__/KeeperOracles.spec.ts.snap | 4 +- test/__snapshots__/KeeperRewards.spec.ts.snap | 2 +- .../__snapshots__/VaultsRegistry.spec.ts.snap | 6 +-- test/shared/fixtures.ts | 16 +++---- 27 files changed, 267 insertions(+), 71 deletions(-) diff --git a/abi/Errors.json b/abi/Errors.json index 06d0c302..57c162a1 100644 --- a/abi/Errors.json +++ b/abi/Errors.json @@ -84,6 +84,11 @@ "name": "InvalidHealthFactor", "type": "error" }, + { + "inputs": [], + "name": "InvalidInitialHarvest", + "type": "error" + }, { "inputs": [], "name": "InvalidLiqBonusPercent", @@ -184,11 +189,6 @@ "name": "MaxOraclesExceeded", "type": "error" }, - { - "inputs": [], - "name": "NegativeAssetsDelta", - "type": "error" - }, { "inputs": [], "name": "NotCollateralized", diff --git a/abi/IKeeper.json b/abi/IKeeper.json index 03df6233..3f89a724 100644 --- a/abi/IKeeper.json +++ b/abi/IKeeper.json @@ -392,6 +392,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/IVaultsRegistry.json b/abi/IVaultsRegistry.json index 063eba77..1e05bfb3 100644 --- a/abi/IVaultsRegistry.json +++ b/abi/IVaultsRegistry.json @@ -128,6 +128,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/interfaces/IKeeper.sol b/contracts/interfaces/IKeeper.sol index 1fe922d2..da5e5390 100644 --- a/contracts/interfaces/IKeeper.sol +++ b/contracts/interfaces/IKeeper.sol @@ -12,5 +12,9 @@ import {IKeeperRewards} from './IKeeperRewards.sol'; * @notice Defines the interface for the Keeper contract */ interface IKeeper is IKeeperOracles, IKeeperRewards, IKeeperValidators { - + /** + * @notice Initializes the Keeper contract. Can only be called once. + * @param _owner The address of the owner + */ + function initialize(address _owner) external; } diff --git a/contracts/interfaces/IVaultsRegistry.sol b/contracts/interfaces/IVaultsRegistry.sol index 072fa029..4708a31a 100644 --- a/contracts/interfaces/IVaultsRegistry.sol +++ b/contracts/interfaces/IVaultsRegistry.sol @@ -89,4 +89,10 @@ interface IVaultsRegistry { * @param factory The address of the factory to remove from the whitelist */ function removeFactory(address factory) external; + + /** + * @notice Function for initializing the registry. Can only be called once during the deployment. + * @param _owner The address of the owner of the contract + */ + function initialize(address _owner) external; } diff --git a/contracts/keeper/Keeper.sol b/contracts/keeper/Keeper.sol index 49ac6b9f..eadfc513 100644 --- a/contracts/keeper/Keeper.sol +++ b/contracts/keeper/Keeper.sol @@ -9,6 +9,7 @@ import {IKeeper} from '../interfaces/IKeeper.sol'; import {KeeperValidators} from './KeeperValidators.sol'; import {KeeperRewards} from './KeeperRewards.sol'; import {KeeperOracles} from './KeeperOracles.sol'; +import {Errors} from '../libraries/Errors.sol'; /** * @title Keeper @@ -16,6 +17,8 @@ import {KeeperOracles} from './KeeperOracles.sol'; * @notice Defines the functionality for updating Vaults' rewards and approving validators registrations */ contract Keeper is KeeperOracles, KeeperRewards, KeeperValidators, IKeeper { + bool private _initialized; + /** * @dev Constructor * @param sharedMevEscrow The address of the shared MEV escrow contract @@ -37,4 +40,14 @@ contract Keeper is KeeperOracles, KeeperRewards, KeeperValidators, IKeeper { KeeperRewards(sharedMevEscrow, vaultsRegistry, osToken, _rewardsDelay, maxAvgRewardPerSecond) KeeperValidators(validatorsRegistry) {} + + /// @inheritdoc IKeeper + function initialize(address _owner) external override onlyOwner { + if (_owner == address(0)) revert Errors.ZeroAddress(); + if (_initialized) revert Errors.AccessDenied(); + + // transfer ownership + _transferOwnership(_owner); + _initialized = true; + } } diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index a3254a45..2e62f5e9 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -53,6 +53,6 @@ library Errors { error InvalidCheckpointIndex(); error InvalidCheckpointValue(); error MaxOraclesExceeded(); - error NegativeAssetsDelta(); + error InvalidInitialHarvest(); error ClaimTooEarly(); } diff --git a/contracts/misc/CumulativeMerkleDrop.sol b/contracts/misc/CumulativeMerkleDrop.sol index 098c3213..dd241c16 100644 --- a/contracts/misc/CumulativeMerkleDrop.sol +++ b/contracts/misc/CumulativeMerkleDrop.sol @@ -22,8 +22,7 @@ contract CumulativeMerkleDrop is Ownable2Step, ICumulativeMerkleDrop { * @param _owner The address of the owner of the contract * @param _token The address of the token contract */ - constructor(address _owner, address _token) Ownable(msg.sender) { - _transferOwnership(_owner); + constructor(address _owner, address _token) Ownable(_owner) { token = IERC20(_token); } diff --git a/contracts/osToken/OsToken.sol b/contracts/osToken/OsToken.sol index 7e71949e..881546fb 100644 --- a/contracts/osToken/OsToken.sol +++ b/contracts/osToken/OsToken.sol @@ -51,6 +51,7 @@ contract OsToken is ERC20, Ownable2Step, IOsToken { * @param _keeper The address of the Keeper contract * @param _checker The address of the OsTokenChecker contract * @param _treasury The address of the DAO treasury + * @param _owner The address of the owner of the contract * @param _feePercent The fee percent applied on the rewards * @param _capacity The amount after which the osToken stops accepting deposits * @param _name The name of the ERC20 token @@ -60,11 +61,13 @@ contract OsToken is ERC20, Ownable2Step, IOsToken { address _keeper, address _checker, address _treasury, + address _owner, uint16 _feePercent, uint256 _capacity, string memory _name, string memory _symbol ) ERC20(_name, _symbol) Ownable(msg.sender) { + if (_owner == address(0)) revert Errors.ZeroAddress(); keeper = _keeper; checker = _checker; _lastUpdateTimestamp = uint64(block.timestamp); @@ -72,6 +75,7 @@ contract OsToken is ERC20, Ownable2Step, IOsToken { setCapacity(_capacity); setTreasury(_treasury); setFeePercent(_feePercent); + _transferOwnership(_owner); } /// @inheritdoc IERC20 diff --git a/contracts/vaults/VaultsRegistry.sol b/contracts/vaults/VaultsRegistry.sol index 0d76fb9c..b00813ae 100644 --- a/contracts/vaults/VaultsRegistry.sol +++ b/contracts/vaults/VaultsRegistry.sol @@ -21,6 +21,8 @@ contract VaultsRegistry is Ownable2Step, IVaultsRegistry { /// @inheritdoc IVaultsRegistry mapping(address => bool) public override vaultImpls; + bool private _initialized; + /** * @dev Constructor */ @@ -61,4 +63,14 @@ contract VaultsRegistry is Ownable2Step, IVaultsRegistry { factories[factory] = false; emit FactoryRemoved(factory); } + + /// @inheritdoc IVaultsRegistry + function initialize(address _owner) external override onlyOwner { + if (_owner == address(0)) revert Errors.ZeroAddress(); + if (_initialized) revert Errors.AccessDenied(); + + // transfer ownership + _transferOwnership(_owner); + _initialized = true; + } } diff --git a/contracts/vaults/ethereum/EthGenesisVault.sol b/contracts/vaults/ethereum/EthGenesisVault.sol index 72653dde..9c45b320 100644 --- a/contracts/vaults/ethereum/EthGenesisVault.sol +++ b/contracts/vaults/ethereum/EthGenesisVault.sol @@ -118,7 +118,9 @@ contract EthGenesisVault is Initializable, EthVault, IEthGenesisVault { // it's the first harvest, deduct rewards accumulated so far in legacy pool totalAssetsDelta -= SafeCast.toInt256(_rewardEthToken.totalRewards()); // the first state update must be with positive delta - if (totalAssetsDelta < 0) revert Errors.NegativeAssetsDelta(); + if (_poolEscrow.owner() != address(this) || totalAssetsDelta < 0) { + revert Errors.InvalidInitialHarvest(); + } } // fetch total assets controlled by legacy pool @@ -153,7 +155,9 @@ contract EthGenesisVault is Initializable, EthVault, IEthGenesisVault { /// @inheritdoc IEthGenesisVault function migrate(address receiver, uint256 assets) external override returns (uint256 shares) { - if (msg.sender != address(_rewardEthToken)) revert Errors.AccessDenied(); + if (msg.sender != address(_rewardEthToken) || _poolEscrow.owner() != address(this)) { + revert Errors.AccessDenied(); + } _checkCollateralized(); _checkHarvested(); @@ -184,7 +188,7 @@ contract EthGenesisVault is Initializable, EthVault, IEthGenesisVault { address receiver, uint256 assets ) internal virtual override(VaultEnterExit, VaultEthStaking) { - _pullAssets(); + if (assets > super._vaultAssets()) _pullAssets(); return super._transferVaultAssets(receiver, assets); } diff --git a/helpers/constants.ts b/helpers/constants.ts index 252ee228..1ca46a7d 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -11,6 +11,7 @@ export const NETWORKS: { governor: '0xFF2B6d2d5c205b99E2e6f607B6aFA3127B9957B6', validatorsRegistry: '0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b', securityDeposit: 1000000000, + exitedAssetsClaimDelay: 24 * 60 * 60, // 24 hours // Keeper oracles: [ @@ -65,6 +66,7 @@ export const NETWORKS: { governor: '0x144a98cb1CdBb23610501fE6108858D9B7D24934', validatorsRegistry: '0x00000000219ab540356cBB839Cbe05303d7705Fa', securityDeposit: 1000000000, + exitedAssetsClaimDelay: 24 * 60 * 60, // 24 hours // Keeper oracles: [], // TODO: update with oracles' addresses diff --git a/helpers/types.ts b/helpers/types.ts index fdecc2a0..683fde84 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -13,6 +13,7 @@ export type NetworkConfig = { governor: string validatorsRegistry: string securityDeposit: BigNumberish + exitedAssetsClaimDelay: number // Keeper oracles: string[] diff --git a/helpers/utils.ts b/helpers/utils.ts index e54a83f8..fc7caca6 100644 --- a/helpers/utils.ts +++ b/helpers/utils.ts @@ -3,7 +3,7 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types/runtime' export async function deployContract(tx: any): Promise { const result = await tx - await result.deployTransaction.wait() + await result.waitForDeployment() return result } diff --git a/package.json b/package.json index 577a331f..ce4051bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stakewise/v3-core", - "version": "0.1.0", + "version": "1.0.0", "description": "Liquid staking protocol for Ethereum", "main": "index.js", "scripts": { diff --git a/tasks/eth-full-deploy-local.ts b/tasks/eth-full-deploy-local.ts index c860b41a..12477337 100644 --- a/tasks/eth-full-deploy-local.ts +++ b/tasks/eth-full-deploy-local.ts @@ -12,6 +12,7 @@ import { RewardSplitter__factory, RewardSplitterFactory__factory, CumulativeMerkleDrop__factory, + OsTokenChecker__factory, } from '../typechain-types' import { deployContract } from '../helpers/utils' import { NetworkConfig, Networks } from '../helpers/types' @@ -55,6 +56,12 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ const sharedMevEscrowAddress = await sharedMevEscrow.getAddress() console.log('SharedMevEscrow deployed at', sharedMevEscrowAddress) + const osTokenChecker = await deployContract( + new OsTokenChecker__factory(deployer).deploy(vaultsRegistryAddress) + ) + const osTokenCheckerAddress = await osTokenChecker.getAddress() + console.log('OsTokenChecker deployed at', osTokenCheckerAddress) + const keeperCalculatedAddress = ethers.getCreateAddress({ from: deployer.address, nonce: (await ethers.provider.getTransactionCount(deployer.address)) + 1, @@ -62,8 +69,9 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ const osToken = await deployContract( new OsToken__factory(deployer).deploy( keeperCalculatedAddress, - vaultsRegistryAddress, + osTokenCheckerAddress, treasury.address, + governor.address, goerliConfig.osTokenFeePercent, goerliConfig.osTokenCapacity, goerliConfig.osTokenName, @@ -107,7 +115,8 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ ltvPercent: goerliConfig.ltvPercent, }) ) - console.log('OsTokenConfig deployed at', osTokenConfig.address) + const osTokenConfigAddress = await osTokenConfig.getAddress() + console.log('OsTokenConfig deployed at', osTokenConfigAddress) const factories: string[] = [] for (const vaultType of ['EthVault', 'EthPrivVault', 'EthErc20Vault', 'EthPrivErc20Vault']) { @@ -119,8 +128,9 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ vaultsRegistryAddress, validatorsRegistryAddress, osTokenAddress, - osTokenConfig.address, + osTokenConfigAddress, sharedMevEscrowAddress, + goerliConfig.exitedAssetsClaimDelay, ], })) as string console.log(`${vaultType} implementation deployed at`, vaultImpl) @@ -134,15 +144,16 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ await vaultsRegistry.addFactory(ethVaultFactoryAddress) console.log(`Added ${vaultType}Factory to VaultsRegistry`) - await osToken.setVaultImplementation(vaultImpl, true) - console.log(`Added ${vaultType} implementation to OsToken`) + await vaultsRegistry.addVaultImpl(vaultImpl) + console.log(`Added ${vaultType} implementation to VaultsRegistry`) factories.push(ethVaultFactoryAddress) } const priceFeed = await deployContract( new PriceFeed__factory(deployer).deploy(osTokenAddress, goerliConfig.priceFeedDescription) ) - console.log('PriceFeed deployed at', priceFeed.address) + const priceFeedAddress = await priceFeed.getAddress() + console.log('PriceFeed deployed at', priceFeedAddress) const rewardSplitterImpl = await deployContract(new RewardSplitter__factory(deployer).deploy()) const rewardSplitterImplAddress = await rewardSplitterImpl.getAddress() @@ -151,7 +162,8 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ const rewardSplitterFactory = await deployContract( new RewardSplitterFactory__factory(deployer).deploy(rewardSplitterImplAddress) ) - console.log('RewardSplitterFactory deployed at', rewardSplitterFactory.address) + const rewardSplitterFactoryAddress = await rewardSplitterFactory.getAddress() + console.log('RewardSplitterFactory deployed at', rewardSplitterFactoryAddress) const cumulativeMerkleDrop = await deployContract( new CumulativeMerkleDrop__factory(deployer).deploy( @@ -159,7 +171,8 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ goerliConfig.swiseToken ) ) - console.log('CumulativeMerkleDrop deployed at', cumulativeMerkleDrop.address) + const cumulativeMerkleDropAddress = await cumulativeMerkleDrop.getAddress() + console.log('CumulativeMerkleDrop deployed at', cumulativeMerkleDropAddress) // pass ownership to governor await vaultsRegistry.transferOwnership(governor.address) @@ -167,11 +180,12 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ await osToken.transferOwnership(governor.address) console.log('Ownership transferred to governor') - // accept ownership from governor - await Keeper__factory.connect(keeperAddress, governor).acceptOwnership() - await OsToken__factory.connect(osTokenAddress, governor).acceptOwnership() - await VaultsRegistry__factory.connect(vaultsRegistryAddress, governor).acceptOwnership() - console.log('Ownership accepted from governor') + // transfer ownership to governor + await vaultsRegistry.initialize(governor.address) + console.log('VaultsRegistry ownership transferred to', governor.address) + + await keeper.initialize(governor.address) + console.log('Keeper ownership transferred to', governor.address) // Save the addresses const addresses = { @@ -183,9 +197,10 @@ task('eth-full-deploy-local', 'deploys StakeWise V3 for Ethereum to local networ EthPrivErc20VaultFactory: factories[3], SharedMevEscrow: sharedMevEscrowAddress, OsToken: osTokenAddress, - OsTokenConfig: osTokenConfig.address, - PriceFeed: priceFeed.address, - RewardSplitterFactory: rewardSplitterFactory.address, + OsTokenConfig: osTokenConfigAddress, + OsTokenChecker: osTokenCheckerAddress, + PriceFeed: priceFeedAddress, + RewardSplitterFactory: rewardSplitterFactoryAddress, } const json = JSON.stringify(addresses, null, 2) const fileName = `${DEPLOYMENTS_DIR}/${networkName}.json` diff --git a/tasks/eth-full-deploy.ts b/tasks/eth-full-deploy.ts index 5603d1a4..def40f7f 100644 --- a/tasks/eth-full-deploy.ts +++ b/tasks/eth-full-deploy.ts @@ -12,6 +12,7 @@ import { RewardSplitter__factory, RewardSplitterFactory__factory, CumulativeMerkleDrop__factory, + OsTokenChecker__factory, } from '../typechain-types' import { deployContract, verify } from '../helpers/utils' import { NETWORKS } from '../helpers/constants' @@ -45,6 +46,18 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta 'contracts/vaults/ethereum/mev/SharedMevEscrow.sol:SharedMevEscrow' ) + const osTokenChecker = await deployContract( + new OsTokenChecker__factory(deployer).deploy(vaultsRegistryAddress) + ) + const osTokenCheckerAddress = await osTokenChecker.getAddress() + console.log('OsTokenChecker deployed at', osTokenCheckerAddress) + await verify( + hre, + osTokenCheckerAddress, + [vaultsRegistryAddress], + 'contracts/osToken/OsTokenChecker.sol:OsTokenChecker' + ) + const keeperCalculatedAddress = ethers.getCreateAddress({ from: deployer.address, nonce: (await ethers.provider.getTransactionCount(deployer.address)) + 1, @@ -52,8 +65,9 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta const osToken = await deployContract( new OsToken__factory(deployer).deploy( keeperCalculatedAddress, - vaultsRegistryAddress, + osTokenCheckerAddress, networkConfig.treasury, + networkConfig.governor, networkConfig.osTokenFeePercent, networkConfig.osTokenCapacity, networkConfig.osTokenName, @@ -67,8 +81,9 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta osTokenAddress, [ keeperCalculatedAddress, - vaultsRegistryAddress, + osTokenCheckerAddress, networkConfig.treasury, + networkConfig.governor, networkConfig.osTokenFeePercent, networkConfig.osTokenCapacity, networkConfig.osTokenName, @@ -152,6 +167,7 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta osTokenAddress, osTokenConfigAddress, sharedMevEscrowAddress, + networkConfig.exitedAssetsClaimDelay, ], })) as string console.log(`${vaultType} implementation deployed at`, vaultImpl) @@ -165,6 +181,7 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta osTokenAddress, osTokenConfigAddress, sharedMevEscrowAddress, + networkConfig.exitedAssetsClaimDelay, ], `contracts/vaults/ethereum/${vaultType}.sol:${vaultType}` ) @@ -184,8 +201,8 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta await vaultsRegistry.addFactory(ethVaultFactoryAddress) console.log(`Added ${vaultType}Factory to VaultsRegistry`) - await osToken.setVaultImplementation(vaultImpl, true) - console.log(`Added ${vaultType} implementation to OsToken`) + await vaultsRegistry.addVaultImpl(vaultImpl) + console.log(`Added ${vaultType} implementation to VaultsRegistry`) factories.push(ethVaultFactoryAddress) } @@ -202,10 +219,11 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta sharedMevEscrowAddress, networkConfig.genesisVault.poolEscrow, networkConfig.genesisVault.rewardEthToken, + networkConfig.exitedAssetsClaimDelay, ], }) const ethGenesisVaultAddress = await ethGenesisVault.getAddress() - await ethGenesisVault.deployed() + await ethGenesisVault.waitForDeployment() const tx = await ethGenesisVault.initialize( ethers.AbiCoder.defaultAbiCoder().encode( ['address', 'tuple(uint256 capacity, uint16 feePercent, string metadataIpfsHash)'], @@ -223,8 +241,8 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta console.log('Added EthGenesisVault to VaultsRegistry') const ethGenesisVaultImpl = await ethGenesisVault.implementation() - await osToken.setVaultImplementation(ethGenesisVaultImpl, true) - console.log(`Added EthGenesisVault implementation to OsToken`) + await vaultsRegistry.addVaultImpl(ethGenesisVaultImpl) + console.log(`Added EthGenesisVault implementation to VaultsRegistry`) await verify( hre, ethGenesisVaultAddress, @@ -237,6 +255,7 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta sharedMevEscrowAddress, networkConfig.genesisVault.poolEscrow, networkConfig.genesisVault.rewardEthToken, + networkConfig.exitedAssetsClaimDelay, ], 'contracts/vaults/ethereum/EthGenesisVault.sol:EthGenesisVault' ) @@ -290,10 +309,12 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta 'contracts/misc/CumulativeMerkleDrop.sol:CumulativeMerkleDrop' ) - // pass ownership to governor - await vaultsRegistry.transferOwnership(networkConfig.governor) - await keeper.transferOwnership(networkConfig.governor) - await osToken.transferOwnership(networkConfig.governor) + // transfer ownership to governor + await vaultsRegistry.initialize(networkConfig.governor) + console.log('VaultsRegistry ownership transferred to', networkConfig.governor) + + await keeper.initialize(networkConfig.governor) + console.log('Keeper ownership transferred to', networkConfig.governor) // Save the addresses const addresses = { @@ -307,6 +328,7 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta SharedMevEscrow: sharedMevEscrowAddress, OsToken: osTokenAddress, OsTokenConfig: osTokenConfigAddress, + OsTokenChecker: osTokenCheckerAddress, PriceFeed: priceFeedAddress, RewardSplitterFactory: rewardSplitterFactoryAddress, } @@ -320,8 +342,5 @@ task('eth-full-deploy', 'deploys StakeWise V3 for Ethereum').setAction(async (ta fs.writeFileSync(fileName, json, 'utf-8') console.log('Saved to', fileName) - console.log('NB! Accept ownership of Keeper from', networkConfig.governor) - console.log('NB! Accept ownership of OsToken from', networkConfig.governor) - console.log('NB! Accept ownership of VaultsRegistry from', networkConfig.governor) console.log('NB! Commit and accept StakeWise V2 PoolEscrow ownership to EthGenesisVault') }) diff --git a/test/EthGenesisVault.spec.ts b/test/EthGenesisVault.spec.ts index aed1bc57..e78aa767 100644 --- a/test/EthGenesisVault.spec.ts +++ b/test/EthGenesisVault.spec.ts @@ -90,7 +90,6 @@ describe('EthGenesisVault', () => { ), { value: SECURITY_DEPOSIT } ) - await vault.connect(admin).acceptPoolEscrowOwnership() await expect(tx).to.emit(vault, 'MetadataUpdated').withArgs(dao.address, metadataIpfsHash) await expect(tx).to.emit(vault, 'FeeRecipientUpdated').withArgs(dao.address, admin.address) await expect(tx) @@ -125,10 +124,12 @@ describe('EthGenesisVault', () => { }) it('applies ownership transfer', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() expect(await poolEscrow.owner()).to.eq(await vault.getAddress()) }) it('apply ownership cannot be called second time', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await expect(vault.connect(other).acceptPoolEscrowOwnership()).to.be.revertedWithCustomError( vault, 'AccessDenied' @@ -145,7 +146,15 @@ describe('EthGenesisVault', () => { ).to.be.revertedWithCustomError(vault, 'AccessDenied') }) + it('fails when pool escrow ownership is not accepted', async () => { + const assets = ethers.parseEther('10') + await expect( + rewardEthToken.connect(other).migrate(other.address, assets, 0) + ).to.be.revertedWithCustomError(vault, 'AccessDenied') + }) + it('fails with zero receiver', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) const assets = ethers.parseEther('1') await expect( @@ -154,6 +163,7 @@ describe('EthGenesisVault', () => { }) it('fails with zero assets', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) await expect( rewardEthToken.connect(other).migrate(other.address, 0, 0) @@ -161,6 +171,7 @@ describe('EthGenesisVault', () => { }) it('fails when not collateralized', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() const assets = ethers.parseEther('1') await expect( rewardEthToken.connect(other).migrate(other.address, assets, assets) @@ -168,6 +179,7 @@ describe('EthGenesisVault', () => { }) it('fails when not harvested', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) await updateRewards(keeper, [ { @@ -190,6 +202,7 @@ describe('EthGenesisVault', () => { }) it('migrates from rewardEthToken', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) const assets = ethers.parseEther('10') const expectedShares = ethers.parseEther('10') @@ -206,6 +219,7 @@ describe('EthGenesisVault', () => { }) it('pulls assets on claim exited assets', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) const shares = ethers.parseEther('10') @@ -254,6 +268,7 @@ describe('EthGenesisVault', () => { }) it('pulls assets on redeem', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() const shares = ethers.parseEther('10') await vault.connect(other).deposit(other.address, ZERO_ADDRESS, { value: shares }) @@ -274,6 +289,7 @@ describe('EthGenesisVault', () => { }) it('pulls assets on single validator registration', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) const validatorDeposit = ethers.parseEther('32') await rewardEthToken.connect(other).migrate(other.address, validatorDeposit, validatorDeposit) @@ -288,6 +304,7 @@ describe('EthGenesisVault', () => { }) it('pulls assets on multiple validators registration', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) const validatorsData = await createEthValidatorsData(vault) const validatorsRegistryRoot = await validatorsRegistry.get_deposit_root() @@ -365,6 +382,7 @@ describe('EthGenesisVault', () => { }) it('splits reward between rewardEthToken and vault', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() const totalRewards = ethers.parseEther('30') const expectedVaultDelta = (totalRewards * totalVaultAssets) / (totalLegacyAssets + totalVaultAssets) @@ -388,7 +406,27 @@ describe('EthGenesisVault', () => { await snapshotGasCost(receipt) }) + it('fails when pool escrow ownership not accepted', async () => { + const totalRewards = ethers.parseEther('30') + const vaultReward = { + reward: totalRewards, + unlockedMevReward: 0n, + vault: await vault.getAddress(), + } + const rewardsTree = await updateRewards(keeper, [vaultReward]) + const proof = getRewardsRootProof(rewardsTree, vaultReward) + await expect( + vault.updateState({ + rewardsRoot: rewardsTree.root, + reward: vaultReward.reward, + unlockedMevReward: vaultReward.unlockedMevReward, + proof, + }) + ).to.be.revertedWithCustomError(vault, 'InvalidInitialHarvest') + }) + it('fails with negative first update', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() const totalPenalty = ethers.parseEther('-5') const vaultReward = { reward: totalPenalty, @@ -404,10 +442,11 @@ describe('EthGenesisVault', () => { unlockedMevReward: vaultReward.unlockedMevReward, proof, }) - ).to.revertedWithCustomError(vault, 'NegativeAssetsDelta') + ).to.revertedWithCustomError(vault, 'InvalidInitialHarvest') }) it('splits penalty between rewardEthToken and vault', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() await collateralizeEthVault(vault, keeper, validatorsRegistry, admin) const totalPenalty = ethers.parseEther('-5') const expectedVaultDelta = @@ -435,6 +474,7 @@ describe('EthGenesisVault', () => { }) it('deducts rewards on first state update', async () => { + await vault.connect(admin).acceptPoolEscrowOwnership() const totalRewards = ethers.parseEther('25') const legacyRewards = ethers.parseEther('5') await rewardEthToken.connect(other).setTotalRewards(legacyRewards) diff --git a/test/EthVault.withdraw.spec.ts b/test/EthVault.withdraw.spec.ts index b57a8a88..43517646 100644 --- a/test/EthVault.withdraw.spec.ts +++ b/test/EthVault.withdraw.spec.ts @@ -90,6 +90,13 @@ describe('EthVault - withdraw', () => { ).to.be.revertedWithCustomError(vault, 'ZeroAddress') }) + it('fails for zero shares', async () => { + await expect(vault.connect(holder).redeem(0, holder.address)).to.be.revertedWithCustomError( + vault, + 'InvalidShares' + ) + }) + it('does not overflow', async () => { const vault: EthVaultMock = await createVaultMock(admin, { capacity, diff --git a/test/KeeperOracles.spec.ts b/test/KeeperOracles.spec.ts index 1f31ae2b..8eb835f0 100644 --- a/test/KeeperOracles.spec.ts +++ b/test/KeeperOracles.spec.ts @@ -5,7 +5,7 @@ import { Keeper } from '../typechain-types' import { ethVaultFixture } from './shared/fixtures' import { expect } from './shared/expect' import snapshotGasCost from './shared/snapshotGasCost' -import { ORACLES, ORACLES_CONFIG } from './shared/constants' +import { ORACLES, ORACLES_CONFIG, ZERO_ADDRESS } from './shared/constants' describe('KeeperOracles', () => { const maxOracles = 30 @@ -99,4 +99,27 @@ describe('KeeperOracles', () => { await snapshotGasCost(receipt) }) }) + + describe('initialize', () => { + it('cannot initialize twice', async () => { + await expect(keeper.connect(dao).initialize(other.address)).revertedWithCustomError( + keeper, + 'AccessDenied' + ) + }) + + it('not owner cannot initialize', async () => { + await expect(keeper.connect(other).initialize(other.address)).revertedWithCustomError( + keeper, + 'OwnableUnauthorizedAccount' + ) + }) + + it('cannot initialize to zero address', async () => { + await expect(keeper.connect(dao).initialize(ZERO_ADDRESS)).revertedWithCustomError( + keeper, + 'ZeroAddress' + ) + }) + }) }) diff --git a/test/VaultsRegistry.spec.ts b/test/VaultsRegistry.spec.ts index 7a00138d..6aa24b4f 100644 --- a/test/VaultsRegistry.spec.ts +++ b/test/VaultsRegistry.spec.ts @@ -163,4 +163,27 @@ describe('VaultsRegistry', () => { ).revertedWithCustomError(vaultsRegistry, 'AlreadyRemoved') expect(await vaultsRegistry.factories(await ethVaultFactory.getAddress())).to.be.eq(false) }) + + describe('initialize', () => { + it('cannot initialize twice', async () => { + await expect(vaultsRegistry.connect(dao).initialize(admin.address)).revertedWithCustomError( + vaultsRegistry, + 'AccessDenied' + ) + }) + + it('not owner cannot initialize', async () => { + await expect(vaultsRegistry.connect(admin).initialize(admin.address)).revertedWithCustomError( + vaultsRegistry, + 'OwnableUnauthorizedAccount' + ) + }) + + it('cannot initialize to zero address', async () => { + await expect(vaultsRegistry.connect(dao).initialize(ZERO_ADDRESS)).revertedWithCustomError( + vaultsRegistry, + 'ZeroAddress' + ) + }) + }) }) diff --git a/test/__snapshots__/EthGenesisVault.spec.ts.snap b/test/__snapshots__/EthGenesisVault.spec.ts.snap index 2be43f07..da8c689f 100644 --- a/test/__snapshots__/EthGenesisVault.spec.ts.snap +++ b/test/__snapshots__/EthGenesisVault.spec.ts.snap @@ -10,48 +10,48 @@ Object { exports[`EthGenesisVault migrate migrates from rewardEthToken 1`] = ` Object { "calldataByteLength": 100, - "gasUsed": 74436, + "gasUsed": 79779, } `; exports[`EthGenesisVault pulls assets on claim exited assets 1`] = ` Object { "calldataByteLength": 100, - "gasUsed": 62475, + "gasUsed": 62518, } `; exports[`EthGenesisVault pulls assets on multiple validators registration 1`] = ` Object { "calldataByteLength": 3716, - "gasUsed": 667548, + "gasUsed": 667690, } `; exports[`EthGenesisVault pulls assets on single validator registration 1`] = ` Object { - "calldataByteLength": 1444, - "gasUsed": 335898, + "calldataByteLength": 1476, + "gasUsed": 336694, } `; exports[`EthGenesisVault update state deducts rewards on first state update 1`] = ` Object { "calldataByteLength": 196, - "gasUsed": 145157, + "gasUsed": 150496, } `; exports[`EthGenesisVault update state splits penalty between rewardEthToken and vault 1`] = ` Object { "calldataByteLength": 196, - "gasUsed": 100555, + "gasUsed": 100543, } `; exports[`EthGenesisVault update state splits reward between rewardEthToken and vault 1`] = ` Object { "calldataByteLength": 196, - "gasUsed": 162257, + "gasUsed": 167596, } `; diff --git a/test/__snapshots__/EthVault.upgrade.spec.ts.snap b/test/__snapshots__/EthVault.upgrade.spec.ts.snap index 3f354df3..47a6868b 100644 --- a/test/__snapshots__/EthVault.upgrade.spec.ts.snap +++ b/test/__snapshots__/EthVault.upgrade.spec.ts.snap @@ -3,6 +3,6 @@ exports[`EthVault - upgrade works with valid call data 1`] = ` Object { "calldataByteLength": 132, - "gasUsed": 76105, + "gasUsed": 76117, } `; diff --git a/test/__snapshots__/KeeperOracles.spec.ts.snap b/test/__snapshots__/KeeperOracles.spec.ts.snap index 0d6c99b1..1e517d0f 100644 --- a/test/__snapshots__/KeeperOracles.spec.ts.snap +++ b/test/__snapshots__/KeeperOracles.spec.ts.snap @@ -3,14 +3,14 @@ exports[`KeeperOracles add oracle succeeds 1`] = ` Object { "calldataByteLength": 36, - "gasUsed": 52928, + "gasUsed": 52950, } `; exports[`KeeperOracles remove oracle succeeds 1`] = ` Object { "calldataByteLength": 36, - "gasUsed": 31111, + "gasUsed": 31133, } `; diff --git a/test/__snapshots__/KeeperRewards.spec.ts.snap b/test/__snapshots__/KeeperRewards.spec.ts.snap index a2dd5aed..981ad704 100644 --- a/test/__snapshots__/KeeperRewards.spec.ts.snap +++ b/test/__snapshots__/KeeperRewards.spec.ts.snap @@ -45,7 +45,7 @@ Object { exports[`KeeperRewards set min rewards oracles succeeds 1`] = ` Object { "calldataByteLength": 36, - "gasUsed": 32256, + "gasUsed": 32278, } `; diff --git a/test/__snapshots__/VaultsRegistry.spec.ts.snap b/test/__snapshots__/VaultsRegistry.spec.ts.snap index e0e96f8a..ab5154fb 100644 --- a/test/__snapshots__/VaultsRegistry.spec.ts.snap +++ b/test/__snapshots__/VaultsRegistry.spec.ts.snap @@ -17,14 +17,14 @@ Object { exports[`VaultsRegistry owner can add vault 1`] = ` Object { "calldataByteLength": 36, - "gasUsed": 49674, + "gasUsed": 49686, } `; exports[`VaultsRegistry owner can register implementation contract 1`] = ` Object { "calldataByteLength": 36, - "gasUsed": 47487, + "gasUsed": 47499, } `; @@ -38,6 +38,6 @@ Object { exports[`VaultsRegistry owner can remove implementation 1`] = ` Object { "calldataByteLength": 36, - "gasUsed": 25430, + "gasUsed": 25442, } `; diff --git a/test/shared/fixtures.ts b/test/shared/fixtures.ts index 0a276ec4..498f4647 100644 --- a/test/shared/fixtures.ts +++ b/test/shared/fixtures.ts @@ -159,6 +159,7 @@ export const createOsToken = async function ( keeperAddress: string, checker: OsTokenChecker, treasury: Wallet, + governor: Wallet, feePercent: BigNumberish, capacity: BigNumberish, name: string, @@ -169,6 +170,7 @@ export const createOsToken = async function ( keeperAddress, await checker.getAddress(), treasury.address, + governor.address, feePercent, capacity, name, @@ -377,10 +379,9 @@ export const ethVaultFixture = async function (): Promise { const osTokenChecker = await createOsTokenChecker(vaultsRegistry) // 2. calc keeper address - const [_deployer] = await ethers.getSigners() const _keeperAddress = ethers.getCreateAddress({ - from: _deployer.address, - nonce: (await ethers.provider.getTransactionCount(_deployer.address)) + 1, + from: dao.address, + nonce: (await ethers.provider.getTransactionCount(dao.address)) + 1, }) // 3. deploy ostoken @@ -388,6 +389,7 @@ export const ethVaultFixture = async function (): Promise { _keeperAddress, osTokenChecker, dao, + dao, OSTOKEN_FEE, OSTOKEN_CAPACITY, OSTOKEN_NAME, @@ -456,12 +458,8 @@ export const ethVaultFixture = async function (): Promise { } // change ownership - await vaultsRegistry.transferOwnership(dao.address) - await vaultsRegistry.connect(dao).acceptOwnership() - await keeper.transferOwnership(dao.address) - await keeper.connect(dao).acceptOwnership() - await osToken.transferOwnership(dao.address) - await osToken.connect(dao).acceptOwnership() + await vaultsRegistry.initialize(dao.address) + await keeper.initialize(dao.address) return { vaultsRegistry,