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);