diff --git a/.gitmodules b/.gitmodules index d6fe983..4257d04 100755 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,15 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/murky"] - path = lib/murky - url = https://github.com/dmfxyz/murky +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "lib/contracts"] + path = lib/contracts + url = https://github.com/Universal-Page/contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 0b199ec..0000000 --- a/.prettierrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "plugins": ["prettier-plugin-solidity"], - "overrides": [ - { - "files": "*.sol", - "options": { - "tabWidth": 4, - "printWidth": 80, - "compiler": "0.8.17" - } - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b7e2454 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "solidity.formatter": "forge" +} \ No newline at end of file diff --git a/README.md b/README.md index c31fbc6..2311299 100755 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ Repository for the Stakingverse contracts. This repository includes the followin - Stakingverse Vault (based on [Universal Page vault's implementation](https://github.com/Universal-Page/contracts/blob/main/src/pool/Vault.sol)) - Liquid Staking Token (sLYX) based on the LUKSO LSP7 standard (linked to the Vault contract below deployed on LUKSO Mainnet). -| Contract | Address on LUKSO Mainnet | -| :-------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | -| Staking Vault Proxy | [`0x9F49a95b0c3c9e2A6c77a16C177928294c0F6F04`](https://explorer.lukso.network/address/0x9F49a95b0c3c9e2A6c77a16C177928294c0F6F04?tab=contract) | -| Staking Vault `Vault.sol` Implementation (old) | [`0x2Cb02ef26aDDAB15686ed634d70699ab64F195f4`](https://explorer.lukso.network/address/0x2Cb02ef26aDDAB15686ed634d70699ab64F195f4?tab=contract) | -| Staking Vault `StakingverseVault.sol` Implementation (upgraded) | _To be deployed_ | -| SLYX Token Proxy | [`0x8a3982f0a7d154d11a5f43eec7f50e52ebbc8f7d`](https://explorer.lukso.network/address/0x8a3982f0a7d154d11a5f43eec7f50e52ebbc8f7d?tab=contract) | -| SLYX Token Implementation | _To be deployed_ | +| Contract | Address on LUKSO Mainnet | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------- | +| Staking Vault Proxy | [`0x9F49a95b0c3c9e2A6c77a16C177928294c0F6F04`](https://explorer.lukso.network/address/0x9F49a95b0c3c9e2A6c77a16C177928294c0F6F04?tab=contract) | +| Staking Vault `Vault.sol` Implementation
(commit [`33d1619` on Universal.Page repository](https://github.com/Universal-Page/contracts/tree/33d1619a19162444c870b8a5a4bf42eb4532818c)) | [`0x2Cb02ef26aDDAB15686ed634d70699ab64F195f4`](https://explorer.lukso.network/address/0x2Cb02ef26aDDAB15686ed634d70699ab64F195f4?tab=contract) | +| Staking Vault `StakingverseVault.sol` Implementation (upgraded) | _To be deployed_ | +| SLYX Token Proxy | [`0x8a3982f0a7d154d11a5f43eec7f50e52ebbc8f7d`](https://explorer.lukso.network/address/0x8a3982f0a7d154d11a5f43eec7f50e52ebbc8f7d?tab=contract) | +| SLYX Token Implementation | _To be deployed_ | - [Stakingverse Contracts](#stakingverse-contracts) - [Installation](#installation) @@ -34,11 +34,11 @@ Repository for the Stakingverse contracts. This repository includes the followin ## Installation ```bash -# Install LUKSO and OpenZeppelin contracts dependencies +# Install LUKSO LSP7 dependencies npm install -# Install forge contracts testing library -forge install https://github.com/foundry-rs/forge-std --no-commit --no-git +# Install git submodule dependencies OZ contracts + forge library +git submodule update --init --recursive ``` ## Build diff --git a/foundry.toml b/foundry.toml index 2dfd10f..816ad05 100755 --- a/foundry.toml +++ b/foundry.toml @@ -8,9 +8,18 @@ gas_reports = ["SLYXToken", "StakingverseVault"] extra_output_files = ["metadata"] fs_permissions = [ { access = "read", path = "./scripts/" }, + { access = "read", path = "./test/" }, + { access = "read", path = "./build/" }, { access = "read-write", path = "./artifacts/" }, ] + +# For foundry upgrades +ffi = true +ast = true +build_info = true +extra_output = ["storageLayout"] + # solidity compiler solc_version = "0.8.22" diff --git a/lib/contracts b/lib/contracts new file mode 160000 index 0000000..91893d7 --- /dev/null +++ b/lib/contracts @@ -0,0 +1 @@ +Subproject commit 91893d701ef041a8a4f9d83b69d5b04da4dc9789 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..dc44c9f --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dc44c9f1a4c3b10af99492eed84f83ed244203f6 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..2d081f2 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..326d96b --- /dev/null +++ b/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit 326d96b5d9b5fa87b8cdb734f02a8f73324dad3e diff --git a/package-lock.json b/package-lock.json index 0dc1f28..04b1b69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@lukso/lsp6-contracts": "^0.15.0", "@lukso/universalprofile-contracts": "^0.15.0", "@openzeppelin/contracts-v4.9.0": "npm:@openzeppelin/contracts@4.9.0", - "prettier-plugin-solidity": "^1.4.2", "solhint": "^5.0.5" } }, @@ -5040,38 +5039,6 @@ "node": ">= 0.6" } }, - "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", - "dev": true, - "peer": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-solidity": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.4.2.tgz", - "integrity": "sha512-VVD/4XlDjSzyPWWCPW8JEleFa8JNKFYac5kNlMjVXemQyQZKfpekPMhFZSePuXB6L+RixlFvWe20iacGjFYrLw==", - "dev": true, - "dependencies": { - "@solidity-parser/parser": "^0.19.0", - "semver": "^7.6.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "prettier": ">=2.3.0" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/package.json b/package.json index 7fe9dc8..29c644b 100644 --- a/package.json +++ b/package.json @@ -23,23 +23,24 @@ "build:sizes": "forge build --sizes", "build:storage": "forge build --extra-output storageLayout", "build:ir": "forge build --extra-output ir", - "test": "forge test --no-match-contract ForkTest", - "test:debug": "forge test -vvv --no-match-contract ForkTest", - "test:invariant": "forge test --match-test invariant", - "test:invariant:debug": "forge test --match-test invariant --no-match-contract ForkTest -vvv", - "test:coverage": "forge coverage --report summary --no-match-test ^invariant --no-match-contract ForkTest", - "test:gas": "forge test --no-match-contract ForkTest --gas-report", - "test:fork": "forge test --fork-url https://rpc.mainnet.lukso.network --match-contract ForkTest" + "test": "forge test --no-match-contract '^Fork|LocalUpgradeTest'", + "test:debug": "forge test -vvv --no-match-contract '^Fork|LocalUpgradeTest'", + "test:invariant": "forge test --match-test ^invariant", + "test:invariant:debug": "forge test --match-test ^invariant --no-match-contract ^Fork -vvv", + "test:coverage": "forge coverage --report summary --no-match-test ^invariant --no-match-contract '^Fork|LocalUpgradeTest'", + "test:gas": "forge test --no-match-contract '^Fork|LocalUpgradeTest' --gas-report", + "test:upgrade": "FOUNDRY_OUT=build forge clean && forge test --match-contract LocalUpgradeTest", + "test:fork": "forge test --fork-url https://rpc.mainnet.lukso.network --match-contract ForkTest", + "test:upgrade:fork": "FOUNDRY_OUT=build forge clean && forge test --fork-url https://rpc.mainnet.lukso.network --match-contract ForkMainnetUpgradeTest" }, "dependencies": { "@lukso/lsp7-contracts": "~0.16.3" }, "devDependencies": { - "prettier-plugin-solidity": "^1.4.2", "@lukso/lsp1delegate-contracts": "^0.15.0", "@lukso/lsp6-contracts": "^0.15.0", "@lukso/universalprofile-contracts": "^0.15.0", - "solhint": "^5.0.5", - "@openzeppelin/contracts-v4.9.0": "npm:@openzeppelin/contracts@4.9.0" + "@openzeppelin/contracts-v4.9.0": "npm:@openzeppelin/contracts@4.9.0", + "solhint": "^5.0.5" } -} \ No newline at end of file +} diff --git a/remappings.txt b/remappings.txt index 8c2fc4e..4b9d94c 100755 --- a/remappings.txt +++ b/remappings.txt @@ -1,8 +1,13 @@ @erc725/smart-contracts/=node_modules/@erc725/smart-contracts/ @erc725/smart-contracts-v8/=node_modules/@erc725/smart-contracts-v8/ @lukso/=node_modules/@lukso/ -@openzeppelin/=node_modules/@openzeppelin/ +@openzeppelin/contracts-v4.9.0/=node_modules/@openzeppelin/contracts-v4.9.0/ solidity-bytes-utils/=node_modules/solidity-bytes-utils/ + ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ murky/=lib/murky/src/ + +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +UniversalPage/contracts=lib/contracts/ \ No newline at end of file diff --git a/script/SLYXTokenScriptMainnet.s.sol b/script/SLYXTokenScriptMainnet.s.sol index 4b77077..ab0f7d0 100644 --- a/script/SLYXTokenScriptMainnet.s.sol +++ b/script/SLYXTokenScriptMainnet.s.sol @@ -13,10 +13,7 @@ import {SLYXToken} from "../src/SLYXToken.sol"; import {IVault} from "../src/IVault.sol"; -import { - PROXY_ADMIN_MAINNET, - SLYX_TOKEN_PROXY_MAINNET -} from "./MainnetConstants.sol"; +import {PROXY_ADMIN_MAINNET, SLYX_TOKEN_PROXY_MAINNET} from "./MainnetConstants.sol"; contract DeploySLYXTokenImplementation is Script { function run() external { @@ -24,12 +21,7 @@ contract DeploySLYXTokenImplementation is Script { SLYXToken slyxToken = new SLYXToken(); vm.stopBroadcast(); - console.log( - string.concat( - "SLYXToken implementation deployed at ", - Strings.toHexString(address(slyxToken)) - ) - ); + console.log(string.concat("SLYXToken implementation deployed at ", Strings.toHexString(address(slyxToken)))); } } @@ -37,9 +29,7 @@ contract DeploySLYXTokenProxy is Script { function run() external { // Proxy deployment parameters for the SLYXToken address proxyAdmin = vm.envAddress("SLYX_PROXY_ADMIN_ADDRESS"); - address sLyxTokenImplementation = vm.envAddress( - "SLYX_TOKEN_IMPLEMENTATION_ADDRESS" - ); + address sLyxTokenImplementation = vm.envAddress("SLYX_TOKEN_IMPLEMENTATION_ADDRESS"); // Parameters to initialize the SLYX Token contract address owner = vm.envAddress("SLYX_TOKEN_CONTRACT_OWNER_ADDRESS"); @@ -53,34 +43,19 @@ contract DeploySLYXTokenProxy is Script { // proxy admin proxyAdmin, // `initialize(address,IVault)` calldata - abi.encodeCall( - SLYXToken.initialize, - (owner, IVault(linkedVault)) - ) + abi.encodeCall(SLYXToken.initialize, (owner, IVault(linkedVault))) ) ); vm.stopBroadcast(); - console.log( - string.concat( - "SLYXToken proxy deployed at ", - Strings.toHexString(proxy) - ) - ); - console.log( - string.concat( - "Linked to implementation at address ", - Strings.toHexString(sLyxTokenImplementation) - ) - ); + console.log(string.concat("SLYXToken proxy deployed at ", Strings.toHexString(proxy))); + console.log(string.concat("Linked to implementation at address ", Strings.toHexString(sLyxTokenImplementation))); } } contract UpgradeSLYXToken is Script { function run() external { - address newSLYXTokenImplementation = vm.envAddress( - "NEW_SLYX_TOKEN_IMPLEMENTATION_ADDRESS" - ); + address newSLYXTokenImplementation = vm.envAddress("NEW_SLYX_TOKEN_IMPLEMENTATION_ADDRESS"); vm.startBroadcast(); IProxy(SLYX_TOKEN_PROXY_MAINNET).upgradeTo( @@ -91,8 +66,7 @@ contract UpgradeSLYXToken is Script { console.log( string.concat( - "SLYXToken proxy upgraded to implementation at ", - Strings.toHexString(newSLYXTokenImplementation) + "SLYXToken proxy upgraded to implementation at ", Strings.toHexString(newSLYXTokenImplementation) ) ); } @@ -106,11 +80,6 @@ contract ChangeAdmin is Script { IProxy(SLYX_TOKEN_PROXY_MAINNET).changeAdmin(newProxyAdmin); vm.stopBroadcast(); - console.log( - string.concat( - "SLYXToken proxy admin changed to ", - Strings.toHexString(newProxyAdmin) - ) - ); + console.log(string.concat("SLYXToken proxy admin changed to ", Strings.toHexString(newProxyAdmin))); } } diff --git a/script/SLYXTokenScriptTestnet.s.sol b/script/SLYXTokenScriptTestnet.s.sol index e51aa33..86fd3c9 100644 --- a/script/SLYXTokenScriptTestnet.s.sol +++ b/script/SLYXTokenScriptTestnet.s.sol @@ -13,10 +13,7 @@ import {SLYXToken} from "../src/SLYXToken.sol"; import {IVault} from "../src/IVault.sol"; -import { - PROXY_ADMIN_TESTNET, - SLYX_TOKEN_PROXY_TESTNET -} from "./TestnetConstants.sol"; +import {PROXY_ADMIN_TESTNET, SLYX_TOKEN_PROXY_TESTNET} from "./TestnetConstants.sol"; contract DeploySLYXTokenImplementation is Script { function run() external { @@ -24,12 +21,7 @@ contract DeploySLYXTokenImplementation is Script { vm.broadcast(deployerPrivateKey); SLYXToken slyxToken = new SLYXToken(); - console.log( - string.concat( - "SLYXToken implementation deployed at ", - Strings.toHexString(address(slyxToken)) - ) - ); + console.log(string.concat("SLYXToken implementation deployed at ", Strings.toHexString(address(slyxToken)))); } } @@ -39,9 +31,7 @@ contract DeploySLYXTokenProxy is Script { // Proxy deployment parameters for the SLYXToken address proxyAdmin = vm.envAddress("SLYX_PROXY_ADMIN_ADDRESS"); - address sLyxTokenImplementation = vm.envAddress( - "SLYX_TOKEN_IMPLEMENTATION_ADDRESS" - ); + address sLyxTokenImplementation = vm.envAddress("SLYX_TOKEN_IMPLEMENTATION_ADDRESS"); // Parameters to initialize the SLYX Token contract address owner = vm.envAddress("SLYX_TOKEN_CONTRACT_OWNER_ADDRESS"); @@ -55,25 +45,12 @@ contract DeploySLYXTokenProxy is Script { // proxy admin proxyAdmin, // `initialize(address,IVault)` calldata - abi.encodeCall( - SLYXToken.initialize, - (owner, IVault(linkedVault)) - ) + abi.encodeCall(SLYXToken.initialize, (owner, IVault(linkedVault))) ) ); - console.log( - string.concat( - "SLYXToken proxy deployed at ", - Strings.toHexString(proxy) - ) - ); - console.log( - string.concat( - "Linked to implementation at address ", - Strings.toHexString(sLyxTokenImplementation) - ) - ); + console.log(string.concat("SLYXToken proxy deployed at ", Strings.toHexString(proxy))); + console.log(string.concat("Linked to implementation at address ", Strings.toHexString(sLyxTokenImplementation))); } } @@ -81,9 +58,7 @@ contract UpgradeSLYXToken is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - address newSLYXTokenImplementation = vm.envAddress( - "NEW_SLYX_TOKEN_IMPLEMENTATION_ADDRESS" - ); + address newSLYXTokenImplementation = vm.envAddress("NEW_SLYX_TOKEN_IMPLEMENTATION_ADDRESS"); vm.startBroadcast(deployerPrivateKey); IProxy(SLYX_TOKEN_PROXY_TESTNET).upgradeTo( @@ -94,8 +69,7 @@ contract UpgradeSLYXToken is Script { console.log( string.concat( - "SLYXToken proxy upgraded to implementation at ", - Strings.toHexString(newSLYXTokenImplementation) + "SLYXToken proxy upgraded to implementation at ", Strings.toHexString(newSLYXTokenImplementation) ) ); } @@ -119,11 +93,6 @@ contract ChangeAdmin is Script { vm.broadcast(signerPrivateKey); IProxy(SLYX_TOKEN_PROXY_TESTNET).changeAdmin(newProxyAdmin); - console.log( - string.concat( - "SLYXToken proxy admin changed to ", - Strings.toHexString(newProxyAdmin) - ) - ); + console.log(string.concat("SLYXToken proxy admin changed to ", Strings.toHexString(newProxyAdmin))); } } diff --git a/script/StakingverseVaultScriptMainnet.s.sol b/script/StakingverseVaultScriptMainnet.s.sol index 250c3e8..884635b 100644 --- a/script/StakingverseVaultScriptMainnet.s.sol +++ b/script/StakingverseVaultScriptMainnet.s.sol @@ -22,12 +22,7 @@ contract DeployVaultImplementation is Script { StakingverseVault vault = new StakingverseVault(); vm.stopBroadcast(); - console.log( - string.concat( - "StakingverseVault implementation deployed at ", - Strings.toHexString(address(vault)) - ) - ); + console.log(string.concat("StakingverseVault implementation deployed at ", Strings.toHexString(address(vault)))); } } @@ -35,9 +30,7 @@ contract DeployVaultProxy is Script { function run() external { // Proxy deployment parameters for the vault address admin = vm.envAddress("VAULT_PROXY_ADMIN_ADDRESS"); - address vaultImplementation = vm.envAddress( - "VAULT_IMPLEMENTATION_ADDRESS" - ); + address vaultImplementation = vm.envAddress("VAULT_IMPLEMENTATION_ADDRESS"); // Parameters to initialize the vault address owner = vm.envAddress("VAULT_OWNER_ADDRESS"); @@ -51,26 +44,13 @@ contract DeployVaultProxy is Script { // proxy admin admin, // `initialize(address,address,IDepositContract)` calldata - abi.encodeCall( - StakingverseVault.initialize, - (owner, operator, DepositContract) - ) + abi.encodeCall(StakingverseVault.initialize, (owner, operator, DepositContract)) ) ); vm.stopBroadcast(); - console.log( - string.concat( - "StakingverseVault proxy deployed at ", - Strings.toHexString(proxy) - ) - ); - console.log( - string.concat( - "Linked to implementation at address ", - Strings.toHexString(vaultImplementation) - ) - ); + console.log(string.concat("StakingverseVault proxy deployed at ", Strings.toHexString(proxy))); + console.log(string.concat("Linked to implementation at address ", Strings.toHexString(vaultImplementation))); } } @@ -82,20 +62,13 @@ contract ChangeAdmin is Script { IProxy(PROXY_ADMIN_MAINNET).changeAdmin(newProxyAdmin); vm.stopBroadcast(); - console.log( - string.concat( - "Vault proxy admin changed to ", - Strings.toHexString(newProxyAdmin) - ) - ); + console.log(string.concat("Vault proxy admin changed to ", Strings.toHexString(newProxyAdmin))); } } contract UpgradeVault is Script { function run() external { - address newVaultImplementationAddress = vm.envAddress( - "NEW_VAULT_IMPLEMENTATION_ADDRESS" - ); + address newVaultImplementationAddress = vm.envAddress("NEW_VAULT_IMPLEMENTATION_ADDRESS"); vm.startBroadcast(); IProxy(VAULT_PROXY_MAINNET).upgradeTo(newVaultImplementationAddress); diff --git a/script/StakingverseVaultScriptTestnet.s.sol b/script/StakingverseVaultScriptTestnet.s.sol index cd82995..547dfe2 100644 --- a/script/StakingverseVaultScriptTestnet.s.sol +++ b/script/StakingverseVaultScriptTestnet.s.sol @@ -23,12 +23,7 @@ contract DeployVaultImplementation is Script { vm.broadcast(deployerPrivateKey); StakingverseVault vault = new StakingverseVault(); - console.log( - string.concat( - "StakingverseVault implementation deployed at ", - Strings.toHexString(address(vault)) - ) - ); + console.log(string.concat("StakingverseVault implementation deployed at ", Strings.toHexString(address(vault)))); } } @@ -38,9 +33,7 @@ contract DeployVaultProxy is Script { // Proxy deployment parameters for the vault address proxyAdmin = vm.envAddress("VAULT_PROXY_ADMIN_ADDRESS"); - address vaultImplementation = vm.envAddress( - "VAULT_IMPLEMENTATION_ADDRESS" - ); + address vaultImplementation = vm.envAddress("VAULT_IMPLEMENTATION_ADDRESS"); // Parameters to initialize the vault address owner = vm.envAddress("VAULT_OWNER_ADDRESS"); @@ -54,25 +47,12 @@ contract DeployVaultProxy is Script { // proxy admin proxyAdmin, // `initialize(address,address,IDepositContract)` calldata - abi.encodeCall( - StakingverseVault.initialize, - (owner, operator, DepositContract) - ) + abi.encodeCall(StakingverseVault.initialize, (owner, operator, DepositContract)) ) ); - console.log( - string.concat( - "StakingverseVault proxy deployed at ", - Strings.toHexString(proxy) - ) - ); - console.log( - string.concat( - "Linked to implementation at address ", - Strings.toHexString(vaultImplementation) - ) - ); + console.log(string.concat("StakingverseVault proxy deployed at ", Strings.toHexString(proxy))); + console.log(string.concat("Linked to implementation at address ", Strings.toHexString(vaultImplementation))); } } @@ -94,12 +74,7 @@ contract ChangeAdmin is Script { vm.broadcast(signerAddress); IProxy(VAULT_PROXY_TESTNET).changeAdmin(newProxyAdmin); - console.log( - string.concat( - "Vault proxy admin changed to ", - Strings.toHexString(newProxyAdmin) - ) - ); + console.log(string.concat("Vault proxy admin changed to ", Strings.toHexString(newProxyAdmin))); } } @@ -116,9 +91,7 @@ contract UpgradeVault is Script { ) ); - address newVaultImplementationAddress = vm.envAddress( - "NEW_VAULT_IMPLEMENTATION_ADDRESS" - ); + address newVaultImplementationAddress = vm.envAddress("NEW_VAULT_IMPLEMENTATION_ADDRESS"); vm.broadcast(signerPrivateKey); IProxy(VAULT_PROXY_TESTNET).upgradeTo(newVaultImplementationAddress); diff --git a/src/Vault.sol b/src/Vault.sol new file mode 100644 index 0000000..28dafe4 --- /dev/null +++ b/src/Vault.sol @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.22; + +import {OwnableUnset} from "@erc725/smart-contracts/contracts/custom/OwnableUnset.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IDepositContract, DEPOSIT_AMOUNT} from "../../src/IDepositContract.sol"; + +contract Vault is OwnableUnset, ReentrancyGuardUpgradeable, PausableUpgradeable { + uint32 private constant _FEE_BASIS = 100_000; + uint32 private constant _MIN_FEE = 0; // 0% + uint32 private constant _MAX_FEE = 15_000; // 15% + uint256 private constant _MAX_VALIDATORS_SUPPORTED = 1_000_000; + uint256 private constant _MINIMUM_REQUIRED_SHARES = 1e3; + + error InvalidAmount(uint256 amount); + error WithdrawalFailed(address account, address beneficiary, uint256 amount); + error ClaimFailed(address account, address beneficiary, uint256 amount); + error DepositLimitExceeded(uint256 totalValue, uint256 depositLimit); + error CallerNotOracle(address account); + error InsufficientBalance(uint256 availableAmount, uint256 requestedAmount); + error CallerNotFeeRecipient(address account); + error FeeClaimFailed(address account, address beneficiary, uint256 amount); + error InvalidAddress(address account); + error ValidatorAlreadyRegistered(bytes pubkey); + error CallerNotOperator(address account); + + event Deposited(address indexed account, address indexed beneficiary, uint256 amount); + event Withdrawn(address indexed account, address indexed beneficiary, uint256 amount); + event WithdrawalRequested(address indexed account, address indexed beneficiary, uint256 amount); + event Claimed(address indexed account, address indexed beneficiary, uint256 amount); + event DepositLimitChanged(uint256 previousLimit, uint256 newLimit); + event FeeChanged(uint32 previousFee, uint32 newFee); + event FeeRecipientChanged(address previousFeeRecipient, address newFeeRecipient); + event FeeClaimed(address indexed account, address indexed beneficiary, uint256 amount); + event RewardsDistributed(uint256 balance, uint256 rewards, uint256 fee); + event OracleEnabled(address indexed oracle, bool enabled); + event Rebalanced( + uint256 previousTotalStaked, uint256 previousTotalUnstaked, uint256 totalStaked, uint256 totalUnstaked + ); + + // limit of total deposits in wei. + // This limits the total number of validators that can be registered. + uint256 public depositLimit; + // total number of shares in the vault + uint256 public totalShares; + // total amount of active stake in wei on beacon chain + uint256 public totalStaked; + // total amount of inactive stake in wei on execution layer + uint256 public totalUnstaked; + // total amount of pending withdrawals in wei. + // This is the amount that is taken from staked balance and may not be immidiately available for withdrawal + uint256 public totalPendingWithdrawal; + // Total number of ever registered validators + uint256 public totalValidatorsRegistered; + // Vault fee in parts per 100,000 + uint32 public fee; + // Recipient of the vault fee + address public feeRecipient; + // Total amount of fees available for withdrawal + uint256 public totalFees; + // Whether only allowlisted accounts can deposit + bool public restricted; + IDepositContract private _depositContract; + mapping(address => uint256) private _shares; + mapping(address => bool) private _oracles; + mapping(address => uint256) private _pendingWithdrawals; + mapping(address => bool) private _allowlisted; + mapping(bytes => bool) private _registeredKeys; + // Total amount of pending withdrawals that can be claimed immidiately + uint256 public totalClaimable; + address public operator; + + modifier onlyOracle() { + _checkOracle(); + _; + } + + modifier onlyOperator() { + _checkOperator(); + _; + } + + constructor() { + _disableInitializers(); + } + + function initialize(address owner_, address operator_, IDepositContract depositContract_) external initializer { + if (address(depositContract_) == address(0)) { + revert InvalidAddress(address(depositContract_)); + } + __ReentrancyGuard_init(); + __Pausable_init(); + _setOwner(owner_); + _setOperator(operator_); + _depositContract = depositContract_; + } + + receive() external payable { + deposit(msg.sender); + } + + function _checkOperator() private view { + if (msg.sender != operator && msg.sender != owner()) { + revert CallerNotOperator(msg.sender); + } + } + + function setOperator(address newOperator) external onlyOperator { + _setOperator(newOperator); + } + + function _setOperator(address newOperator) private { + if (newOperator == address(0)) { + revert InvalidAddress(newOperator); + } + operator = newOperator; + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function setFee(uint32 newFee) external onlyOperator { + if (newFee > _FEE_BASIS) { + revert InvalidAmount(newFee); + } + if (newFee < _MIN_FEE || newFee > _MAX_FEE) { + revert InvalidAmount(newFee); + } + uint32 previousFee = fee; + fee = newFee; + emit FeeChanged(previousFee, newFee); + } + + function setFeeRecipient(address newFeeRecipient) external onlyOperator { + if (newFeeRecipient == address(0)) { + revert InvalidAddress(newFeeRecipient); + } + address previousFeeRecipient = feeRecipient; + feeRecipient = newFeeRecipient; + emit FeeRecipientChanged(previousFeeRecipient, newFeeRecipient); + } + + function setDepositLimit(uint256 newDepositLimit) external onlyOperator { + if ( + newDepositLimit < totalValidatorsRegistered * DEPOSIT_AMOUNT + || newDepositLimit > _MAX_VALIDATORS_SUPPORTED * DEPOSIT_AMOUNT + ) { + revert InvalidAmount(newDepositLimit); + } + uint256 previousDepositLimit = depositLimit; + depositLimit = newDepositLimit; + emit DepositLimitChanged(previousDepositLimit, newDepositLimit); + } + + function enableOracle(address oracle, bool enabled) external onlyOperator { + _oracles[oracle] = enabled; + emit OracleEnabled(oracle, enabled); + } + + function isOracle(address oracle) public view returns (bool) { + return _oracles[oracle]; + } + + function allowlist(address account, bool enabled) external onlyOperator { + _allowlisted[account] = enabled; + } + + function isAllowlisted(address account) public view returns (bool) { + return _allowlisted[account]; + } + + function setRestricted(bool enabled) external onlyOperator { + restricted = enabled; + } + + function _checkOracle() private view { + address oracle = msg.sender; + if (!isOracle(oracle)) { + revert CallerNotOracle(oracle); + } + } + + function sharesOf(address account) external view returns (uint256) { + return _shares[account]; + } + + function balanceOf(address account) public view returns (uint256) { + return _toBalance(_shares[account]); + } + + function pendingBalanceOf(address account) external view returns (uint256) { + return _pendingWithdrawals[account]; + } + + function claimableBalanceOf(address account) external view returns (uint256) { + uint256 pendingWithdrawal = _pendingWithdrawals[account]; + return pendingWithdrawal > totalClaimable ? totalClaimable : pendingWithdrawal; + } + + function claim(uint256 amount, address beneficiary) external nonReentrant whenNotPaused { + if (beneficiary == address(0)) { + revert InvalidAddress(beneficiary); + } + address account = msg.sender; + if (amount == 0) { + revert InvalidAmount(amount); + } + if (amount > _pendingWithdrawals[account]) { + revert InsufficientBalance(_pendingWithdrawals[account], amount); + } + if (amount > totalClaimable) { + revert InsufficientBalance(totalClaimable, amount); + } + _pendingWithdrawals[account] -= amount; + totalPendingWithdrawal -= amount; + totalClaimable -= amount; + (bool success,) = beneficiary.call{value: amount}(""); + if (!success) { + revert ClaimFailed(account, beneficiary, amount); + } + emit Claimed(account, beneficiary, amount); + } + + function totalAssets() public view returns (uint256) { + return totalStaked + totalUnstaked + totalClaimable - totalPendingWithdrawal; + } + + function _toBalance(uint256 shares) private view returns (uint256) { + if (totalShares == 0) { + return 0; + } + // In some cases, totalShares may be slightly less than totalStaked + totalUnstaked due to rounding errors. + // The error is 1 wei considered insignificant and can be ignored. + return Math.mulDiv(shares, totalAssets(), totalShares); + } + + function _toShares(uint256 amount) private view returns (uint256) { + if (totalShares == 0) { + return amount; + } + return Math.mulDiv(amount, totalShares, totalAssets()); + } + + function deposit(address beneficiary) public payable whenNotPaused { + if (beneficiary == address(0)) { + revert InvalidAddress(beneficiary); + } + address account = msg.sender; + if (restricted && !isAllowlisted(account)) { + revert InvalidAddress(account); + } + uint256 amount = msg.value; + if (amount == 0) { + revert InvalidAmount(amount); + } + uint256 newTotalDeposits = + Math.max(totalValidatorsRegistered * DEPOSIT_AMOUNT, totalStaked + totalUnstaked) + amount; + if (newTotalDeposits > depositLimit) { + revert DepositLimitExceeded(newTotalDeposits, depositLimit); + } + uint256 shares = _toShares(amount); + if (shares == 0) { + revert InvalidAmount(amount); + } + // burn minimum shares of first depositor to prevent share inflation and dust shares attacks. + if (totalShares == 0) { + if (shares < _MINIMUM_REQUIRED_SHARES) { + revert InvalidAmount(amount); + } + _shares[address(0)] = _MINIMUM_REQUIRED_SHARES; + totalShares += _MINIMUM_REQUIRED_SHARES; + shares -= _MINIMUM_REQUIRED_SHARES; + } + _shares[beneficiary] += shares; + totalShares += shares; + totalUnstaked += amount; + emit Deposited(account, beneficiary, amount); + } + + function withdraw(uint256 amount, address beneficiary) external nonReentrant whenNotPaused { + if (beneficiary == address(0)) { + revert InvalidAddress(beneficiary); + } + address account = msg.sender; + if (amount == 0) { + revert InvalidAmount(amount); + } + if (amount > balanceOf(account)) { + revert InsufficientBalance(balanceOf(account), amount); + } + uint256 shares = _toShares(amount); + if (shares == 0) { + revert InvalidAmount(amount); + } + if (shares > _shares[account]) { + revert InsufficientBalance(_shares[account], shares); + } + _shares[account] -= shares; + totalShares -= shares; + + uint256 immediateAmount = amount > totalUnstaked ? totalUnstaked : amount; + uint256 delayedAmount = amount - immediateAmount; + + totalUnstaked -= immediateAmount; + totalPendingWithdrawal += delayedAmount; + _pendingWithdrawals[beneficiary] += delayedAmount; + + if (immediateAmount > 0) { + (bool success,) = beneficiary.call{value: immediateAmount}(""); + if (!success) { + revert WithdrawalFailed(account, beneficiary, immediateAmount); + } + emit Withdrawn(account, beneficiary, immediateAmount); + } + + if (delayedAmount > 0) { + emit WithdrawalRequested(account, beneficiary, delayedAmount); + } + } + + function claimFees(uint256 amount, address beneficiary) external nonReentrant whenNotPaused { + if (beneficiary == address(0)) { + revert InvalidAddress(beneficiary); + } + address account = msg.sender; + if (account != feeRecipient) { + revert CallerNotFeeRecipient(account); + } + if (amount == 0) { + revert InvalidAmount(amount); + } + if (amount > totalFees) { + revert InsufficientBalance(totalFees, amount); + } + totalFees -= amount; + (bool success,) = beneficiary.call{value: amount}(""); + if (!success) { + revert FeeClaimFailed(account, beneficiary, amount); + } + emit FeeClaimed(account, beneficiary, amount); + } + + // Rebalance the vault by accounting balance of the vault. + // This is called periodically by the oracle. + // The balance of the vault is the sum of: + // - totalStaked + totalUnstaked: the total amount of stake on beacon chain and execution layer + // - totalPendingWithdrawal: the total amount of pending withdrawals + // - totalClaimable: the total amount of pending withdrawals that can be claimed immidiately + // - totalFees: the total amount of fees available for withdrawal + // + // Rebalancing is not accounting for potential validator penalties. It is assumed that the penalties + // shall not occur or shall be negligible. + function rebalance() external onlyOracle whenNotPaused { + uint256 balance = address(this).balance; + + // account for completed withdrawals + uint256 pendingWithdrawal = totalPendingWithdrawal - totalClaimable; + uint256 completedWithdrawal = Math.min( + (balance - totalFees - totalUnstaked - totalClaimable) / DEPOSIT_AMOUNT, // actual completed withdrawals + pendingWithdrawal / DEPOSIT_AMOUNT // pending withdrawals + + (pendingWithdrawal % DEPOSIT_AMOUNT == 0 ? 0 : 1) // partial withdrawals + ) * DEPOSIT_AMOUNT; + + // adjust staked balance for completed withdrawals + uint256 staked = totalStaked - completedWithdrawal; + + // take out any claimable balances from unstaked balance prior to calculating rewards. + uint256 unstaked = balance - totalFees - totalClaimable - completedWithdrawal; + + // account for withdrawals to claim later + uint256 claimable = totalClaimable + completedWithdrawal; + + // account for partial completeted withdrawals + uint256 partialWithdrawal = 0; + if (claimable > totalPendingWithdrawal) { + partialWithdrawal = claimable - totalPendingWithdrawal; + unstaked += partialWithdrawal; + claimable = totalPendingWithdrawal; + } + + // at this point the difference represents the rewards. + // If the difference is positive, it means that the rewards are available for distribution. + // Fees are subsidized in attempt to prevent validator exits. + if (unstaked - partialWithdrawal > totalUnstaked) { + uint256 rewards = unstaked - partialWithdrawal - totalUnstaked; + uint256 feeAmount = Math.mulDiv(rewards, fee, _FEE_BASIS); + emit RewardsDistributed(totalStaked + totalUnstaked, rewards, feeAmount); + totalFees += feeAmount; + unstaked -= feeAmount; + } + + emit Rebalanced(totalStaked, totalUnstaked, staked, unstaked); + totalClaimable = claimable; + totalUnstaked = unstaked; + totalStaked = staked; + } + + function isValidatorRegistered(bytes calldata pubkey) external view returns (bool) { + return _registeredKeys[pubkey]; + } + + function registerValidator(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) + public + onlyOracle + nonReentrant + whenNotPaused + { + if (totalUnstaked < DEPOSIT_AMOUNT) { + revert InsufficientBalance(totalUnstaked, DEPOSIT_AMOUNT); + } + if (_registeredKeys[pubkey]) { + revert ValidatorAlreadyRegistered(pubkey); + } + _registeredKeys[pubkey] = true; + totalValidatorsRegistered += 1; + totalStaked += DEPOSIT_AMOUNT; + totalUnstaked -= DEPOSIT_AMOUNT; + bytes memory withdrawalCredentials = abi.encodePacked(hex"010000000000000000000000", address(this)); + _depositContract.deposit{value: DEPOSIT_AMOUNT}(pubkey, withdrawalCredentials, signature, depositDataRoot); + } + + function registerValidators( + bytes[] calldata pubkeys, + bytes[] calldata signatures, + bytes32[] calldata depositDataRoots + ) external { + uint256 length = pubkeys.length; + if (length != signatures.length || length != depositDataRoots.length) { + revert InvalidAmount(length); + } + for (uint256 i = 0; i < length; i++) { + registerValidator(pubkeys[i], signatures[i], depositDataRoots[i]); + } + } +} diff --git a/test/Deployment.t.sol b/test/Deployment.t.sol index 9731c88..9b06910 100644 --- a/test/Deployment.t.sol +++ b/test/Deployment.t.sol @@ -43,6 +43,15 @@ contract Deployment is SLYXTokenBaseTest { assertEq(sLyxToken.totalSupply(), 0); } + // This test ensures two things. Calling `getExchangeRate()` initially: + // 1. will not revert + // 2. will return 1:1 ratio + function test_callingGetExhangeRateWhenNoSLYXTokensHaveBeenMintedInitially() public view { + assertEq(sLyxToken.getExchangeRate(), 1 ether); + assertEq(sLyxToken.getNativeTokenValue(12345 ether), 12345 ether); + assertEq(sLyxToken.getSLYXTokenValue(12345 ether), 12345 ether); + } + function test_shouldHaveSetCorrectLinkedStakingVault() public view { assertEq(address(sLyxToken.stakingVault()), address(vault)); } diff --git a/test/OZUpgradeTest.t.sol b/test/OZUpgradeTest.t.sol new file mode 100644 index 0000000..3a99805 --- /dev/null +++ b/test/OZUpgradeTest.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Upgrades, Options} from "openzeppelin-foundry-upgrades/LegacyUpgrades.sol"; +import {IDepositContract} from "../src/IDepositContract.sol"; + +// Contracts to test +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy as IProxy +} from "@openzeppelin/contracts-v4.9.0/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +import {Vault as VaultV1} from "../src/Vault.sol"; +import {StakingverseVault} from "../src/StakingverseVault.sol"; + +// Constants +import {PROXY_ADMIN_MAINNET, VAULT_PROXY_MAINNET} from "../script/MainnetConstants.sol"; + +/// @dev Test to ensure that the upgrade is safe and the new implementation contract is valid, using OpenZeppelin Foundry upgrades library +/// This ensures that the new implementation contract is safe to upgrade to and that the storage layout is compatible with the proxy linked to the current implementation +/// Tested with local anvil chain +contract LocalUpgradeTest is Test { + address constant CURRENT_VAULT_OWNER = 0x983aBC616f2442bAB7a917E6bb8660Df8b01F3bF; + address constant CURRENT_VAULT_OPERATOR = 0x983aBC616f2442bAB7a917E6bb8660Df8b01F3bF; + + address proxyAdmin; + + // Necessary as OZ upgrade plugin uses a contract to manage the proxy and perform upgrade through operations through it. + address proxyAdminContract; + + // current vault contract implementation to upgrade from + VaultV1 currentVaultImplementation = new VaultV1(); + + IProxy vaultProxy; + + function setUp() public { + proxyAdmin = address(11); + + vm.prank(proxyAdmin); + proxyAdminContract = address(new ProxyAdmin()); + + // 1. Deploy the existing vault implementation + currentVaultImplementation = new VaultV1(); + + // 2. Deploy the proxy and initialize it + bytes memory initializeCalldata = abi.encodeCall( + currentVaultImplementation.initialize, + (CURRENT_VAULT_OWNER, CURRENT_VAULT_OPERATOR, IDepositContract(0xCAfe00000000000000000000000000000000CAfe)) + ); + + vaultProxy = IProxy( + address( + new TransparentUpgradeableProxy( + address(currentVaultImplementation), proxyAdminContract, initializeCalldata + ) + ) + ); + } + + // 3. Deploy the new vault implementation contract + upgrade the proxy to this new implementation. + // The function `Upgrades.upgradeProxy` deploy the new implementation contract for us. + function test_tryDeployingNewImplementationAndUpgrade() public { + Options memory opts; + + // reference current Vault implementation to compare to for storage layout and upgrade checks + opts.referenceContract = "Vault.sol"; + + // SKIP this check as new implementation contains a constructor that includes a `_disableInitializer` + // (to prevent implementation contract from being initialized) + // but also an actual `initialize(...)` function that is called by the proxy. + opts.unsafeAllow = "constructor"; + + Upgrades.upgradeProxy(address(vaultProxy), "StakingverseVault.sol", "", opts, proxyAdmin); + } +} diff --git a/test/base/SLYXTokenBaseTest.t.sol b/test/base/SLYXTokenBaseTest.t.sol index 9a2a7b7..4bd4e44 100644 --- a/test/base/SLYXTokenBaseTest.t.sol +++ b/test/base/SLYXTokenBaseTest.t.sol @@ -98,20 +98,23 @@ abstract contract SLYXTokenBaseTest is Test /*, FoundryRandom */ { // but we cannot mock these addresses in Foundry tests. // The cheatcode vm.etch(...) set the bytecode, but does not initialize the state variables of the vault proxy, // (logic contract, admin, etc...) so any call to the proxy will fail. - // Therefore, we need to deploy the new Vault contracts in the test suite. vaultImplementation = new StakingverseVault(); bytes memory initializeCalldata = abi.encodeCall(StakingverseVault.initialize, (vaultOwner, vaultOperator, depositContract)); + // Therefore, we need to deploy the new Vault contracts in the test suite... vault = StakingverseVault( payable(new TransparentUpgradeableProxy(address(vaultImplementation), proxyAdmin, initializeCalldata)) ); + // ...and mock the `implementation()` function to force returning the address of the implementation this proxy is linked to. vm.mockCall( address(vault), abi.encodeWithSelector(IProxy.implementation.selector), abi.encode(vaultImplementation) ); + // configure the vault + vm.startPrank(vaultOperator); vault.enableOracle(vaultOracle, true); vault.setFee(10_000); diff --git a/test/base/UniversalProfileTestHelpers.t.sol b/test/base/UniversalProfileTestHelpers.t.sol index 1bfb799..f966ec6 100644 --- a/test/base/UniversalProfileTestHelpers.t.sol +++ b/test/base/UniversalProfileTestHelpers.t.sol @@ -6,7 +6,8 @@ import {Test} from "forge-std/Test.sol"; // modules import {UniversalProfile} from "@lukso/universalprofile-contracts/contracts/UniversalProfile.sol"; import {LSP6KeyManager} from "@lukso/lsp6-contracts/contracts/LSP6KeyManager.sol"; -import {LSP1UniversalReceiverDelegateUP as LSP1Delegate} from "@lukso/lsp1delegate-contracts/contracts/LSP1UniversalReceiverDelegateUP.sol"; +import {LSP1UniversalReceiverDelegateUP as LSP1Delegate} from + "@lukso/lsp1delegate-contracts/contracts/LSP1UniversalReceiverDelegateUP.sol"; // libraries import {LSP2Utils} from "@lukso/lsp2-contracts/contracts/LSP2Utils.sol"; @@ -14,7 +15,12 @@ import {LSP6Utils} from "@lukso/lsp6-contracts/contracts/LSP6Utils.sol"; // constants import {_LSP1_UNIVERSAL_RECEIVER_DELEGATE_KEY} from "@lukso/lsp1-contracts/contracts/LSP1Constants.sol"; -import {_LSP6KEY_ADDRESSPERMISSIONS_PERMISSIONS_PREFIX, _PERMISSION_REENTRANCY, _PERMISSION_SUPER_SETDATA, ALL_REGULAR_PERMISSIONS} from "@lukso/lsp6-contracts/contracts/LSP6Constants.sol"; +import { + _LSP6KEY_ADDRESSPERMISSIONS_PERMISSIONS_PREFIX, + _PERMISSION_REENTRANCY, + _PERMISSION_SUPER_SETDATA, + ALL_REGULAR_PERMISSIONS +} from "@lukso/lsp6-contracts/contracts/LSP6Constants.sol"; contract UniversalProfileTestHelpers is Test { LSP1Delegate lsp1DelegateImplementation; @@ -23,62 +29,38 @@ contract UniversalProfileTestHelpers is Test { lsp1DelegateImplementation = new LSP1Delegate(); } - function _setUpUniversalProfileLikeBrowserExtension( - address mainController - ) internal returns (UniversalProfile) { - UniversalProfile universalProfile = new UniversalProfile( - mainController - ); + function _setUpUniversalProfileLikeBrowserExtension(address mainController) internal returns (UniversalProfile) { + UniversalProfile universalProfile = new UniversalProfile(mainController); - LSP6KeyManager keyManager = new LSP6KeyManager( - address(universalProfile) - ); + LSP6KeyManager keyManager = new LSP6KeyManager(address(universalProfile)); _setupMainControllerPermissions(universalProfile, mainController); _setUPLSP1DelegateWithPermissions(universalProfile, mainController); - _transferOwnershipToKeyManager( - universalProfile, - mainController, - keyManager - ); + _transferOwnershipToKeyManager(universalProfile, mainController, keyManager); return universalProfile; } - function _setUPLSP1DelegateWithPermissions( - UniversalProfile universalProfile, - address mainController - ) internal { + function _setUPLSP1DelegateWithPermissions(UniversalProfile universalProfile, address mainController) internal { vm.prank(mainController); - universalProfile.setData( - _LSP1_UNIVERSAL_RECEIVER_DELEGATE_KEY, - abi.encodePacked(lsp1DelegateImplementation) - ); + universalProfile.setData(_LSP1_UNIVERSAL_RECEIVER_DELEGATE_KEY, abi.encodePacked(lsp1DelegateImplementation)); // give SUPER_SETDATA permission to universalReceiverDelegate bytes32 dataKeyURD = LSP2Utils.generateMappingWithGroupingKey( - _LSP6KEY_ADDRESSPERMISSIONS_PERMISSIONS_PREFIX, - bytes20(abi.encodePacked(lsp1DelegateImplementation)) + _LSP6KEY_ADDRESSPERMISSIONS_PERMISSIONS_PREFIX, bytes20(abi.encodePacked(lsp1DelegateImplementation)) ); // use Bitwise OR to set each permission bit individually // (just for simplicity here and avoid creating a `bytes32[] memory` array). // However, it is recommended to use the LSP6Utils.combinePermissions(...) function. vm.prank(mainController); - universalProfile.setData( - dataKeyURD, - abi.encodePacked(_PERMISSION_REENTRANCY | _PERMISSION_SUPER_SETDATA) - ); + universalProfile.setData(dataKeyURD, abi.encodePacked(_PERMISSION_REENTRANCY | _PERMISSION_SUPER_SETDATA)); } - function _setupMainControllerPermissions( - UniversalProfile universalProfile, - address mainController - ) internal { + function _setupMainControllerPermissions(UniversalProfile universalProfile, address mainController) internal { bytes32 dataKey = LSP2Utils.generateMappingWithGroupingKey( - _LSP6KEY_ADDRESSPERMISSIONS_PERMISSIONS_PREFIX, - bytes20(mainController) + _LSP6KEY_ADDRESSPERMISSIONS_PERMISSIONS_PREFIX, bytes20(mainController) ); bytes memory dataValue = abi.encodePacked(ALL_REGULAR_PERMISSIONS); diff --git a/test/fork/ForkOZUpgradeTest.t.sol b/test/fork/ForkOZUpgradeTest.t.sol new file mode 100644 index 0000000..c09b16f --- /dev/null +++ b/test/fork/ForkOZUpgradeTest.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Upgrades, Options} from "openzeppelin-foundry-upgrades/LegacyUpgrades.sol"; + +// Contracts to test +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy as IProxy +} from "@openzeppelin/contracts-v4.9.0/proxy/transparent/TransparentUpgradeableProxy.sol"; + +// Constants +import {PROXY_ADMIN_MAINNET, VAULT_PROXY_MAINNET} from "../../script/MainnetConstants.sol"; + +/// @dev Test to ensure that the upgrade is safe and the new implementation contract is valid, using OpenZeppelin Foundry upgrades library +/// This ensures that the new implementation contract is safe to upgrade to and that the storage layout is compatible with the proxy linked to the current implementation +contract ForkMainnetUpgradeTest is Test { + address constant CURRENT_VAULT_IMPLEMENTATION = 0x2Cb02ef26aDDAB15686ed634d70699ab64F195f4; + + function test_shouldReturnCorrectProxyAdminAddress() public view { + // CHECK current proxy admin address before upgrading + assertEq(Upgrades.getAdminAddress(VAULT_PROXY_MAINNET), PROXY_ADMIN_MAINNET); + } + + function test_shouldReturnCorrectVaultImplementationAddress() public view { + // CHECK current implementation address before upgrading + assertEq(Upgrades.getImplementationAddress(VAULT_PROXY_MAINNET), CURRENT_VAULT_IMPLEMENTATION); + } + + function test_validateUpgradeIsSafe() public { + /** + * @dev Validates a new implementation contract in comparison with a reference contract, but does not deploy it. + * + * Requires that either the `referenceContract` option is set, or the contract has a `@custom:oz-upgrades-from ` annotation. + * + * @param contractName Name of the contract to validate, e.g. "MyContract.sol" or "MyContract.sol:MyContract" or artifact path relative to the project root directory + * @param opts Common options + */ + // function validateUpgrade(string memory contractName, Options memory opts) internal { + // Core.validateUpgrade(contractName, opts); + // } + Options memory opts; + opts.referenceContract = "Vault.sol"; + opts.unsafeAllow = "constructor"; + + Upgrades.validateUpgrade("StakingverseVault.sol", opts); + } + + function test_preparingUpgrade() public { + /** + * @dev Validates a new implementation contract in comparison with a reference contract, deploys the new implementation contract, + * and returns its address. + * + * Requires that either the `referenceContract` option is set, or the contract has a `@custom:oz-upgrades-from ` annotation. + * + * Use this method to prepare an upgrade to be run from an admin address you do not control directly or cannot use from your deployment environment. + * + * @param contractName Name of the contract to deploy, e.g. "MyContract.sol" or "MyContract.sol:MyContract" or artifact path relative to the project root directory + * @param opts Common options + * @return Address of the new implementation contract + */ + // function prepareUpgrade(string memory contractName, Options memory opts) internal returns (address) { + // return Core.prepareUpgrade(contractName, opts); + // } + Options memory opts; + opts.referenceContract = "Vault.sol"; + opts.unsafeAllow = "constructor"; + + address newStakingverseVaultAddress = Upgrades.prepareUpgrade("StakingverseVault.sol", opts); + console.log(unicode"๐Ÿงช Test deploying new StakingverseVault implementation!"); + console.log(unicode"๐Ÿš€ New StakingverseVault deployed at address: %s", newStakingverseVaultAddress); + } + + function test_tryDeployingNewImplementationAndUpgrade() public { + vm.skip(true); + console.log(unicode"๐Ÿ‘€ Testing if upgrade is safe with OZ upgrade"); + + Options memory opts; + + // reference current Vault implementation to compare to for storage layout and upgrade checks + opts.referenceContract = "Vault.sol"; + + // SKIP this check as new implementation contains a constructor that includes a `_disableInitializer` + // (to prevent implementation contract from being initialized) + // but also an actual `initialize(...)` function that is called by the proxy. + opts.unsafeAllow = "constructor"; + + Upgrades.upgradeProxy(VAULT_PROXY_MAINNET, "StakingverseVault.sol", "", opts, PROXY_ADMIN_MAINNET); + } +} diff --git a/test/ForkTest.t.sol b/test/fork/ForkTest.t.sol similarity index 88% rename from test/ForkTest.t.sol rename to test/fork/ForkTest.t.sol index 9947a02..d8078bc 100644 --- a/test/ForkTest.t.sol +++ b/test/fork/ForkTest.t.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; // Testing + Setups -import {StakingverseVault} from "../src/StakingverseVault.sol"; -import {IDepositContract} from "../src/IDepositContract.sol"; +import {StakingverseVault} from "../../src/StakingverseVault.sol"; +import {IDepositContract} from "../../src/IDepositContract.sol"; import { TransparentUpgradeableProxy, ITransparentUpgradeableProxy as IProxy @@ -15,17 +15,22 @@ import { // Contracts to test import {SLYXToken} from "../../src/SLYXToken.sol"; -address payable constant VAULT_IMPLEMENTATION = payable(0x2Cb02ef26aDDAB15686ed634d70699ab64F195f4); - -address payable constant VAULT_PROXY = payable(0x9F49a95b0c3c9e2A6c77a16C177928294c0F6F04); +// Constants +import {PROXY_ADMIN_MAINNET, VAULT_PROXY_MAINNET, SLYX_TOKEN_PROXY_MAINNET} from "../../script/MainnetConstants.sol"; -address payable constant SLYX_PROXY = payable(0x8A3982f0A7d154D11a5f43EEc7F50E52eBBc8F7D); +// Contract addresses deployed on mainnet +address payable constant VAULT_IMPLEMENTATION = payable(0x2Cb02ef26aDDAB15686ed634d70699ab64F195f4); +address payable constant VAULT_PROXY = payable(VAULT_PROXY_MAINNET); +address payable constant SLYX_PROXY = payable(SLYX_TOKEN_PROXY_MAINNET); -// Existing addresses setup on mainnet -address constant VAULT_AND_SLYX_PROXY_ADMIN = 0xe460f0cB1227399B129715895157E8cB443b269E; -address constant VAULT_ADMIN = 0x8909ce174B12Be1311bA80797d2f3A8BEdD913Bf; +// Config addresses setup on mainnet +address constant VAULT_AND_SLYX_PROXY_ADMIN = PROXY_ADMIN_MAINNET; +address constant VAULT_CONTRACT_OWNER_AND_OPERATOR = 0x8909ce174B12Be1311bA80797d2f3A8BEdD913Bf; address constant VAULT_ORACLE = 0x6a44823e20CD97250AfA3c73e45aBef4Ff79c51F; +// Upcoming SLYX Token Contract owner +address constant SLYX_CONTRACT_OWNER = 0x49d32954698344592407C2C1f76c431F0032167c; + bytes constant SLYX_PROXY_BYTECODE = hex"60806040523661001357610011610017565b005b6100115b61001f610168565b6001600160a01b0316330361015e5760606001600160e01b03195f35166364d3180d60e11b81016100595761005261019a565b9150610156565b63587086bd60e11b6001600160e01b0319821601610079576100526101ed565b63070d7c6960e41b6001600160e01b031982160161009957610052610231565b621eb96f60e61b6001600160e01b03198216016100b857610052610261565b63a39f25e560e01b6001600160e01b03198216016100d8576100526102a0565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b6101666102b3565b565b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101a46102c3565b5f6101b23660048184610668565b8101906101bf91906106aa565b90506101da8160405180602001604052805f8152505f6102cd565b505060408051602081019091525f815290565b60605f806101fe3660048184610668565b81019061020b91906106d7565b9150915061021b828260016102cd565b60405180602001604052805f8152509250505090565b606061023b6102c3565b5f6102493660048184610668565b81019061025691906106aa565b90506101da816102f8565b606061026b6102c3565b5f610274610168565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b60606102aa6102c3565b5f61027461034f565b6101666102be61034f565b61035d565b3415610166575f80fd5b6102d68361037b565b5f825111806102e25750805b156102f3576102f183836103ba565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610321610168565b604080516001600160a01b03928316815291841660208301520160405180910390a161034c816103e6565b50565b5f61035861048f565b905090565b365f80375f80365f845af43d5f803e808015610377573d5ff35b3d5ffd5b610384816104b6565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a250565b60606103df83836040518060600160405280602781526020016108036027913961054a565b9392505050565b6001600160a01b03811661044b5760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161014d565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61018b565b6001600160a01b0381163b6105235760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161014d565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61046e565b60605f80856001600160a01b03168560405161056691906107b5565b5f60405180830381855af49150503d805f811461059e576040519150601f19603f3d011682016040523d82523d5f602084013e6105a3565b606091505b50915091506105b4868383876105be565b9695505050505050565b6060831561062c5782515f03610625576001600160a01b0385163b6106255760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161014d565b5081610636565b610636838361063e565b949350505050565b81511561064e5781518083602001fd5b8060405162461bcd60e51b815260040161014d91906107d0565b5f8085851115610676575f80fd5b83861115610682575f80fd5b5050820193919092039150565b80356001600160a01b03811681146106a5575f80fd5b919050565b5f602082840312156106ba575f80fd5b6103df8261068f565b634e487b7160e01b5f52604160045260245ffd5b5f80604083850312156106e8575f80fd5b6106f18361068f565b9150602083013567ffffffffffffffff8082111561070d575f80fd5b818501915085601f830112610720575f80fd5b813581811115610732576107326106c3565b604051601f8201601f19908116603f0116810190838211818310171561075a5761075a6106c3565b81604052828152886020848701011115610772575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f5b838110156107ad578181015183820152602001610795565b50505f910152565b5f82516107c6818460208701610793565b9190910192915050565b602081525f82518060208401526107ee816040850160208701610793565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a264697066735822122015b4231dda86f0402c82dbd8505d83543efd1acf89fcd12f9a9ddb52aab9449364736f6c63430008160033"; bytes constant VAULT_PROXY_BYTECODE = @@ -37,7 +42,6 @@ contract ForkTest is Test { uint256 internal constant VAULT_ROUNDING_ERROR_LOSS = 1 wei; address sLyxProxyAdmin; - address sLyxContractOwner; // existing vault contract address (proxy) StakingverseVault vault; @@ -50,9 +54,6 @@ contract ForkTest is Test { uint256 initialDepositLimit; function setUp() public { - sLyxProxyAdmin = VAULT_AND_SLYX_PROXY_ADMIN; - sLyxContractOwner = VAULT_ADMIN; - console.log(unicode"๐Ÿ›ž Setup Fork testing. Block number: ", block.number); vault = StakingverseVault(VAULT_PROXY); @@ -66,8 +67,8 @@ contract ForkTest is Test { // Note that this value might change and is increased regularly. assertGt(initialDepositLimit, 1_476_000 ether); - assertEq(vault.owner(), VAULT_ADMIN); - assertEq(vault.operator(), VAULT_ADMIN); + assertEq(vault.owner(), VAULT_CONTRACT_OWNER_AND_OPERATOR); + assertEq(vault.operator(), VAULT_CONTRACT_OWNER_AND_OPERATOR); assertTrue(vault.isOracle(VAULT_ORACLE)); vm.startPrank(VAULT_AND_SLYX_PROXY_ADMIN); @@ -76,7 +77,7 @@ contract ForkTest is Test { vm.stopPrank(); // For simplicity, we set the deposit limit to very high value to not reach quickly the error `DepositLimitExceeded`.. - vm.prank(VAULT_ADMIN); + vm.prank(VAULT_CONTRACT_OWNER_AND_OPERATOR); vault.setDepositLimit(3_000_000 ether); // CHECK the bytecode of the Vault proxy is correct @@ -102,18 +103,23 @@ contract ForkTest is Test { // CHECK the bytecode of the Vault proxy is correct assertEq(address(sLyxToken).code, SLYX_PROXY_BYTECODE); + // CHECK SLYX Proxy admin is correctly set + vm.prank(VAULT_AND_SLYX_PROXY_ADMIN); + assertEq(IProxy(SLYX_PROXY).admin(), VAULT_AND_SLYX_PROXY_ADMIN); + // Deploy the SLYX Token contract implementation + // + upgrade the SLYX Proxy contract to the new implementation sLyxTokenImplementation = new SLYXToken(); - // upgrade SLYX Proxy to new implementation - bytes memory slyxInitializeCalldata = abi.encodeCall(SLYXToken.initialize, (sLyxContractOwner, vault)); - - vm.prank(VAULT_AND_SLYX_PROXY_ADMIN); - assertEq(IProxy(SLYX_PROXY).admin(), VAULT_AND_SLYX_PROXY_ADMIN); + bytes memory slyxInitializeCalldata = abi.encodeCall(SLYXToken.initialize, (SLYX_CONTRACT_OWNER, vault)); vm.startPrank(VAULT_AND_SLYX_PROXY_ADMIN); IProxy(SLYX_PROXY).upgradeToAndCall(address(sLyxTokenImplementation), slyxInitializeCalldata); + + // CHECK implementation address has been upgraded successfully assertEq(IProxy(SLYX_PROXY).implementation(), address(sLyxTokenImplementation)); + + // CHECK admin remains the same after upgrade assertEq(IProxy(SLYX_PROXY).admin(), VAULT_AND_SLYX_PROXY_ADMIN); vm.stopPrank(); @@ -124,20 +130,49 @@ contract ForkTest is Test { function test_vaultStorageRemainSameAfterUpgrade() public view { assertGt(vault.depositLimit(), initialDepositLimit); // Note that this value might change - assertEq(vault.owner(), VAULT_ADMIN); - assertEq(vault.operator(), VAULT_ADMIN); + assertEq(vault.owner(), VAULT_CONTRACT_OWNER_AND_OPERATOR); + assertEq(vault.operator(), VAULT_CONTRACT_OWNER_AND_OPERATOR); assertTrue(vault.isOracle(VAULT_ORACLE)); } - function test_callingInitializeOnVaultAfterUpgradeNotPossible() public { + function test_cannotInitializeVaultProxyAfterUpgrade() public { vm.expectRevert("Initializable: contract is already initialized"); - vault.initialize(VAULT_ADMIN, VAULT_ADMIN, IDepositContract(0xCAfe00000000000000000000000000000000CAfe)); + vault.initialize( + VAULT_CONTRACT_OWNER_AND_OPERATOR, + VAULT_CONTRACT_OWNER_AND_OPERATOR, + IDepositContract(0xCAfe00000000000000000000000000000000CAfe) + ); + } + + function test_cannotInitializeNewVaultImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + newVaultImplementation.initialize( + VAULT_CONTRACT_OWNER_AND_OPERATOR, + VAULT_CONTRACT_OWNER_AND_OPERATOR, + IDepositContract(0xCAfe00000000000000000000000000000000CAfe) + ); + } + + function test_cannotInitializeSLYXTokenProxyAfterUpgrade() public { + vm.expectRevert("Initializable: contract is already initialized"); + sLyxToken.initialize(address(111), vault); + } + + function test_cannotInitializeSLYXTokenImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + sLyxTokenImplementation.initialize(address(111), vault); } function test_NoSLYXTokenMintedInitially() public view { assertEq(sLyxToken.totalSupply(), 0); } + function test_ExchangeRateForSLYXToLyxStartAt1To1SinceNoSLYXGotMinted() public view { + assertEq(sLyxToken.getExchangeRate(), 1 ether); + assertEq(sLyxToken.getNativeTokenValue(12345 ether), 12345 ether); + assertEq(sLyxToken.getSLYXTokenValue(12345 ether), 12345 ether); + } + function test_NewUserStakeAndMintSLYXLater() public { address alice = makeAddr("alice"); uint256 depositAmount = 320 ether; @@ -224,7 +259,7 @@ contract ForkTest is Test { address largeStaker = 0x01C2a6EB1568C1A3E0B8fE864056FaC8B4867f77; uint256 largeStakerStake = vault.balanceOf(largeStaker); uint256 largeStakerShares = vault.sharesOf(largeStaker); - console.log("largeStakerShares: ", largeStakerShares); + assertGt(vault.balanceOf(largeStaker), 6_400 ether); // Convert half of the stake to sLYX @@ -233,7 +268,7 @@ contract ForkTest is Test { vm.prank(largeStaker); vault.transferStake(address(sLyxToken), amountToConvertAsSLYX, ""); - assertEq(vault.balanceOf(largeStaker), largeStakerStake - amountToConvertAsSLYX); + assertEq(vault.balanceOf(largeStaker), largeStakerStake - amountToConvertAsSLYX + VAULT_ROUNDING_ERROR_LOSS); assertEq(vault.balanceOf(address(sLyxToken)), amountToConvertAsSLYX - VAULT_ROUNDING_ERROR_LOSS); assertEq(vault.sharesOf(largeStaker), largeStakerShares / 2 + VAULT_ROUNDING_ERROR_LOSS); @@ -260,7 +295,10 @@ contract ForkTest is Test { assertEq(sLyxToken.balanceOf(existingUser), userShares - VAULT_ROUNDING_ERROR_LOSS); } - function test_ExchangeRateForSLYXToLyxStartAbove1To1() public { + function test_ExchangeRateForSLYXToLyxStartIncreasesAfterMintingSLYX() public { + uint256 initialSLyxToLyxExchangeRate = sLyxToken.getExchangeRate(); + assertEq(initialSLyxToLyxExchangeRate, 1 ether); + address alice = makeAddr("alice"); uint256 depositAmount = 320 ether; @@ -276,6 +314,10 @@ contract ForkTest is Test { vault.transferStake(address(sLyxToken), aliceStake, ""); + // CHECK the exchange rate increased + uint256 newSLyxToLyxExchangeRate = sLyxToken.getExchangeRate(); + assertGt(newSLyxToLyxExchangeRate, initialSLyxToLyxExchangeRate); + assertEq(vault.balanceOf(address(sLyxToken)), aliceStake - VAULT_ROUNDING_ERROR_LOSS); assertEq(sLyxToken.totalSupply(), aliceShares - VAULT_ROUNDING_ERROR_LOSS); assertEq(sLyxToken.balanceOf(alice), aliceShares - VAULT_ROUNDING_ERROR_LOSS);