diff --git a/.env.example b/.env.example index d5a5ada..4610aa2 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ ETHERSCAN_API_KEY= ## Params TESTNET_ADMIN_ADDRESS= TESTNET_MAINTAINER_ADDRESS= -TESTNET_TREASURE_MANAGER_ADDRESS= +TESTNET_PROTOCOL_MANAGER_ADDRESS= TESTNET_SECURITY_COUNCIL_ADDRESS= TESTNET_DONOR_ADDRESS= diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index a5e8081..e1b4b70 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -30,7 +30,7 @@ jobs: with: foundry: true - run: npm run compile - - run: npm run deploy:initial hardhat + - run: npm run deploy:init hardhat integration-test: needs: lint diff --git a/hardhat.config.ts b/hardhat.config.ts index 48be757..9b092c5 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -77,8 +77,11 @@ export default { networks: { hardhat: { params: { + admin: config?.parsed?.TESTNET_ADMIN_ADDRESS, + maintainer: config?.parsed?.TESTNET_MAINTAINER_ADDRESS, + manager: config?.parsed?.TESTNET_PROTOCOL_MANAGER_ADDRESS, + securityCouncil: config?.parsed?.TESTNET_SECURITY_COUNCIL_ADDRESS, dropMerkleRoot: loadDropRoot(".drop.json"), - donor: config?.parsed?.TESTNET_DONOR_ADDRESS, }, mining: { auto: true, @@ -90,7 +93,7 @@ export default { params: { admin: config?.parsed?.TESTNET_ADMIN_ADDRESS, maintainer: config?.parsed?.TESTNET_MAINTAINER_ADDRESS, - treasureManager: config?.parsed?.TESTNET_TREASURE_MANAGER_ADDRESS, + manager: config?.parsed?.TESTNET_PROTOCOL_MANAGER_ADDRESS, securityCouncil: config?.parsed?.TESTNET_SECURITY_COUNCIL_ADDRESS, dropMerkleRoot: loadDropRoot(".drop.json"), }, @@ -100,7 +103,7 @@ export default { params: { admin: config?.parsed?.TESTNET_ADMIN_ADDRESS, maintainer: config?.parsed?.TESTNET_MAINTAINER_ADDRESS, - treasureManager: config?.parsed?.TESTNET_TREASURE_MANAGER_ADDRESS, + manager: config?.parsed?.TESTNET_PROTOCOL_MANAGER_ADDRESS, securityCouncil: config?.parsed?.TESTNET_SECURITY_COUNCIL_ADDRESS, dropMerkleRoot: loadDropRoot(".drop.json"), token: AaveV3Base.ASSETS.USDC.UNDERLYING, @@ -114,7 +117,7 @@ export default { params: { admin: config?.parsed?.TESTNET_ADMIN_ADDRESS, maintainer: config?.parsed?.TESTNET_MAINTAINER_ADDRESS, - treasureManager: config?.parsed?.TESTNET_TREASURE_MANAGER_ADDRESS, + manager: config?.parsed?.TESTNET_PROTOCOL_MANAGER_ADDRESS, securityCouncil: config?.parsed?.TESTNET_SECURITY_COUNCIL_ADDRESS, dropMerkleRoot: loadDropRoot(".drop.json"), }, @@ -125,7 +128,7 @@ export default { params: { admin: config?.parsed?.TESTNET_ADMIN_ADDRESS, maintainer: config?.parsed?.TESTNET_MAINTAINER_ADDRESS, - treasureManager: config?.parsed?.TESTNET_TREASURE_MANAGER_ADDRESS, + manager: config?.parsed?.TESTNET_PROTOCOL_MANAGER_ADDRESS, securityCouncil: config?.parsed?.TESTNET_SECURITY_COUNCIL_ADDRESS, token: AaveV3Base.ASSETS.USDC.UNDERLYING, aavePool: AaveV3Base.POOL, @@ -136,7 +139,7 @@ export default { // params: { // admin: config?.parsed?.ADMIN_ADDRESS, // maintainer: config?.parsed?.MAINTAINER_ADDRESS, - // treasureManager: config?.parsed?.TREASURE_MANAGER_ADDRESS, + // manager: config?.parsed?.TREASURE_MANAGER_ADDRESS, // securityCouncil: config?.parsed?.SECURITY_COUNCIL_ADDRESS, // token: AaveV3Base.ASSETS.USDC.UNDERLYING, // aavePool: AaveV3Base.POOL, diff --git a/ignition/deployments/chain-84532/deployed_addresses.json b/ignition/deployments/chain-84532/deployed_addresses.json index 212ae87..cc3732d 100644 --- a/ignition/deployments/chain-84532/deployed_addresses.json +++ b/ignition/deployments/chain-84532/deployed_addresses.json @@ -1,42 +1,34 @@ { - "Mocks#MockERC20": "0xD08a11a3C4c505E7Cb43903d8b7baD14920322bA", - "Mocks#MockAavePool": "0x9D39E06BBCA0980ec1E347670e6ee370a4938a9c", - "Administrator#Administrator_0": "0xFaADa5A99963Cb7f52224B2FAD0a4422450F5B33", - "Forwarder#Forwarder_0": "0x3d19CFB53AbF526068B128cd0fd66D15939a51E0", - "InvoiceFactory#InvoiceFactory_0": "0xaB1Dad5f51b19fcF4305f7E584568D734161Ff93", - "Vault#TokenDistributorDeployLib_0": "0x029b4CEe371a7b2f1969905251C05D2A2844b01a", - "Verifiers#DepositVerifier": "0x43ef743B6dbf4B83016899Ee57376B09F47D7C76", - "Vault#InterestLib_0": "0xC4170be7d4b47596C67d9B837DF6dF57e2c3AC7f", - "Verifiers#Spend11Verifier": "0xdc33DfD3977D619Ba0366828e2C0C8e272cB79D9", - "Vault#VaultMathLib_0": "0x2F5F2A16e066B6c95583Ae7ae35C45285D801Ef9", - "Verifiers#Spend12Verifier": "0x12A9090182442FF399F5E17A85476232DCd93454", - "Vault#InputsLib_0": "0x106cF9585353B694C6b42F48671e448Cf46D2D4d", - "Verifiers#Spend13Verifier": "0xB4D2E831322660d60150410Cf38F30AC76A7B604", - "Vault#InvestmentVaultLib_0": "0xaDd7464a5eD3468D39071B38274baBAC8619F47a", - "Verifiers#Spend21Verifier": "0x5c39BB4Fc96dCe11ea849074C9a12829b9eBD075", - "Vault#AaveVaultDeployerLib_0": "0xde9F845Af031E7e3E90F3717097Bb2C041caB5Ae", - "Verifiers#Spend22Verifier": "0x7577EA451156b4ad0B5bb3D177bC78f643cBc20C", - "Vault#InputModifierLib_0": "0x08EaA55f2Ea37d06ad005A21E6fAf186C9822f19", - "Verifiers#Spend23Verifier": "0xFfceC36Bbd7d5726855E90c4B4F0183022d8F56b", - "Vault#VaultInitLib_0": "0xA87d49eF5c6505f602F936F42dBb09D3d425D37E", - "Verifiers#Spend31Verifier": "0xdBd83BE0F07711882a7ec88BFC263296baEcf21D", - "Vault#VaultDepositLib_0": "0x9080f5F0dC5aa42852AC3Cd8b995870A0Aa52940", - "Verifiers#Spend32Verifier": "0xA64783643669600AeCCDC0f4bA5834deD6986dFC", - "Vault#VaultSpendLib_0": "0xf6Eae53A578cf09920387C2228340DF3Ff2C81C9", - "Verifiers#Spend33Verifier": "0x16289558FF1311267119660C38E125F8f6510eC9", - "Vault#Vault_0": "0xDb49697357c950cCcdCF544e68c71028Fd2746C3", - "Verifiers#Spend61Verifier": "0x0ED70125d4253F94502BA1e3159179E8989E7931", - "Verifiers#Spend121Verifier": "0x8A9a1db2530D7440997e4304345F11829764EcD3", - "Verifiers#Verifiers": "0x763d4D82B030ad03e368F46F401151549cc0884c", - "Main#VaultProxy": "0x651556bDE06F0c4749ff88d56Cd025050Ec0F194", - "Main#Vault": "0x651556bDE06F0c4749ff88d56Cd025050Ec0F194", - "Main#ProtocolManager": "0x080E7168a396d4e658fa0c1DCCbec134825200AC", - "VaultUpgrade#InterestLib_0": "0x3aFAAA9fc66874023A61981C18671a264493E9B3", - "VaultUpgrade#VaultMathLib_0": "0x22f38E86798F217CC79Ded05D021C092A9B96d33", - "VaultUpgrade#InputsLib_0": "0x4Ec56F008F9f1C626720A8426F9A8eB59d3e57fC", - "VaultUpgrade#InvestmentVaultLib_0": "0xc651b7C70E85d4fB0790B1b12A1E6948F7DAdd8e", - "VaultUpgrade#InputModifierLib_0": "0x37C09a71056fa40C0bb73e11Be0A59d0c43C40E9", - "VaultUpgrade#VaultDepositLib_0": "0x1fA3FE37e906Fec15b28fbc3750F93E53D5e0815", - "VaultUpgrade#VaultSpendLib_0": "0x6Ecf38A16F90F6694971FF7383A52DDc34B7706C", - "VaultUpgrade#Vault_0": "0x25f47B87880649Edf067D655e21CB0E2Ac82995B" + "Mocks#MockERC20": "0x86b427D282967a4E6bE8aAc881fC8f5332cc07c4", + "Mocks#MockAavePool": "0xf040D84549427524b2126e62119D06686990a9cE", + "Main#DepositVerifier": "0x278f8A147B17043A2d4B5B03c10d7F03236A659d", + "Main#Spend11Verifier": "0x150c21a06944c9654C0c9C8268ea8D79DBD6089F", + "Main#Spend12Verifier": "0x645D5E73F06DA5C99EF5f25C2080cf03934333C9", + "Main#Spend13Verifier": "0xe07b87b94541B2f7B8C73018093ba92b50603dC7", + "Main#Spend21Verifier": "0x57EBbd87D73c6C55750b933Af5aF7BA97d513E89", + "Main#Spend22Verifier": "0xd01310A02b2089631FCCb7E6E67566B674e5ea46", + "Main#Spend23Verifier": "0xc0fCc97165043144058E327Ab453b8AB75d95d27", + "Main#Spend31Verifier": "0x34AbB27f2caD00a43ae0E26871c3b856866022F8", + "Main#Spend32Verifier": "0x033DbEA9784cF725d52A1320EF6223776262f560", + "Main#Spend33Verifier": "0x6433Bd3Fd0F13420FA811B94692Cf81cdC6A4ac4", + "Main#Spend61Verifier": "0xc42bDdCbD709c477e89edbf431CFF1b7E21bb4d8", + "Main#Spend121Verifier": "0xC12bbe244848177dd20F7636c0Fb73E9B464D029", + "Main#Verifiers": "0xf0274a464587d51162e2813199edc0c907580F5C", + "Main#Administrator_0": "0x0F4E9efAe56Aea54E69E7710674E630B8a81ba30", + "Main#Forwarder_0": "0x9C8FD2734D125871d7cE97b607Ba1c3d64C670CA", + "Main#InvoiceFactory_0": "0x4D59822db7C5fFD7fd108dB8d05eEA3e7C59f7e6", + "Main#TokenDistributorDeployLib_0": "0xb2BB51656F497ffaF4B0996d9aA09241BC1679Cc", + "Main#InterestLib_0": "0x3b4CA0789E9975723F62668A0D870f18b355299F", + "Main#VaultMathLib_0": "0xC925e889FBC32D18F6ac878A5DAf2Aa47Cf79d48", + "Main#InputsLib_0": "0xe745971305d281B58F0224fF39EC72E91cB04Db5", + "Main#InvestmentVaultLib_0": "0x9020ee6A78884B30916fA8b94CC10676984dc550", + "Main#AaveVaultDeployerLib_0": "0x2e782cD1276E6a5370c6F4216edCe6aCCd63Ab9b", + "Main#InputModifierLib_0": "0xf9785323B45D75f642b5F43c22DA00e04572D3Ad", + "Main#VaultInitLib_0": "0x4E98229EA16aDEA38A58938aa1900D45360D03ee", + "Main#VaultDepositLib_0": "0xCabD9354E1ff4Be8d6466c2f54AC39179E15Ee12", + "Main#VaultSpendLib_0": "0xe4d76fF306C36b192af11e9c33c435d8256468fF", + "Main#Vault_0": "0x5f8fB1D8249F3d648f4c513abaA838cAaDadf572", + "Main#VaultProxy": "0xA9084f27569694a98b096d21A0537d179795Cc86", + "Main#Vault": "0xA9084f27569694a98b096d21A0537d179795Cc86", + "Main#ProtocolManager": "0xFeFEdAc1eD5D5611689c9974CbE8545d11aF3e92" } diff --git a/ignition/modules/Administrator.module.ts b/ignition/modules/Administrator.module.ts deleted file mode 100644 index dc312fc..0000000 --- a/ignition/modules/Administrator.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("Administrator", (m) => { - const admin = m.getParameter("admin"); - const maintainer = m.getParameter("maintainer"); - const treasureManager = m.getParameter("treasureManager"); - const securityCouncil = m.getParameter("securityCouncil"); - const defaultUpgradeDelay = m.getParameter("defaultUpgradeDelay"); - const administrator = m.contract( - "Administrator", - [admin, maintainer, securityCouncil, treasureManager, defaultUpgradeDelay], - { id: `Administrator_${process.env.VERSION_TAG ?? 0}` }, - ); - - return { - administrator, - }; -}); diff --git a/ignition/modules/Forwarder.module.ts b/ignition/modules/Forwarder.module.ts deleted file mode 100644 index f4cedd6..0000000 --- a/ignition/modules/Forwarder.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("Forwarder", (m) => { - const forwarder = m.contract("Forwarder", [], { - id: `Forwarder_${process.env.VERSION_TAG ?? 0}`, - }); - - return { - forwarder, - }; -}); diff --git a/ignition/modules/InvoiceFactory.module.ts b/ignition/modules/InvoiceFactory.module.ts deleted file mode 100644 index 5403ebf..0000000 --- a/ignition/modules/InvoiceFactory.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -/** - * Ignition module for deploying InvoiceFactory - * - * The InvoiceFactory deploys its own Invoice implementation contract - * in the constructor using EIP-1167 minimal proxy pattern. - * - * Usage: - * npx hardhat ignition deploy ignition/modules/InvoiceFactory.module.ts --network - * After deployment: - * - InvoiceFactory address will be available at the returned address - * - Invoice implementation address can be retrieved by calling invoiceFactory.invoiceImplementation() - * - Use invoiceFactory.deployInvoice(paramsHash) to create new invoice clones - */ -export default buildModule("InvoiceFactory", (m) => { - // Deploy InvoiceFactory (which deploys Invoice implementation internally) - const invoiceFactory = m.contract("InvoiceFactory", [], { - id: `InvoiceFactory_${process.env.VERSION_TAG ?? 0}`, - }); - - return { - invoiceFactory, - }; -}); diff --git a/ignition/modules/Main.module.ts b/ignition/modules/Main.module.ts index 7d822f8..35d03a5 100644 --- a/ignition/modules/Main.module.ts +++ b/ignition/modules/Main.module.ts @@ -1,25 +1,125 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; -import VerifiersModule from "./Verifiers.module"; -import VaultModule from "./Vault.module"; -import ForwarderModule from "./Forwarder.module"; -import AdministratorModule from "./Administrator.module"; -import InvoiceFactoryModule from "./InvoiceFactory.module"; export default buildModule("Main", (m) => { - const { verifiers, ...circuitVerifiers } = m.useModule(VerifiersModule); - const { administrator } = m.useModule(AdministratorModule); + const depositVerifier = m.contract("DepositVerifier"); + const spend11Verifier = m.contract("Spend11Verifier", [], { after: [depositVerifier] }); + const spend12Verifier = m.contract("Spend12Verifier", [], { after: [spend11Verifier] }); + const spend13Verifier = m.contract("Spend13Verifier", [], { after: [spend12Verifier] }); + const spend21Verifier = m.contract("Spend21Verifier", [], { after: [spend13Verifier] }); + const spend22Verifier = m.contract("Spend22Verifier", [], { after: [spend21Verifier] }); + const spend23Verifier = m.contract("Spend23Verifier", [], { after: [spend22Verifier] }); + const spend31Verifier = m.contract("Spend31Verifier", [], { after: [spend23Verifier] }); + const spend32Verifier = m.contract("Spend32Verifier", [], { after: [spend31Verifier] }); + const spend33Verifier = m.contract("Spend33Verifier", [], { after: [spend32Verifier] }); + const spend61Verifier = m.contract("Spend61Verifier", [], { after: [spend33Verifier] }); + const spend121Verifier = m.contract("Spend121Verifier", [], { after: [spend61Verifier] }); - const { - vault: vaultImplementation, - inputsLib, - tokenDistributorDeployLib, - interestLib, - vaultMathLib, - investmentVaultLib, - aaveVaultDeployerLib, - } = m.useModule(VaultModule); - const { forwarder } = m.useModule(ForwarderModule); - const { invoiceFactory } = m.useModule(InvoiceFactoryModule); + // Deploy Verifiers contract with the deployed verifier addresses + const verifiers = m.contract("Verifiers", [ + depositVerifier, + spend11Verifier, + spend12Verifier, + spend13Verifier, + spend21Verifier, + spend22Verifier, + spend23Verifier, + spend31Verifier, + spend32Verifier, + spend33Verifier, + spend61Verifier, + spend121Verifier, + ]); + + const administrator = m.contract("Administrator", [m.getParameter("initialAdmin")], { + id: `Administrator_${process.env.VERSION_TAG ?? 0}`, + after: [verifiers], + }); + + const forwarder = m.contract("Forwarder", [], { + id: `Forwarder_${process.env.VERSION_TAG ?? 0}`, + after: [administrator], + }); + const invoiceFactory = m.contract("InvoiceFactory", [], { + id: `InvoiceFactory_${process.env.VERSION_TAG ?? 0}`, + after: [forwarder], + }); + + const tokenDistributorDeployLib = m.library("TokenDistributorDeployLib", { + id: `TokenDistributorDeployLib_${process.env.VERSION_TAG ?? 0}`, + after: [invoiceFactory], + }); + const interestLib = m.library("InterestLib", { + id: `InterestLib_${process.env.VERSION_TAG ?? 0}`, + after: [tokenDistributorDeployLib], + }); + const vaultMathLib = m.library("VaultMathLib", { + id: `VaultMathLib_${process.env.VERSION_TAG ?? 0}`, + after: [interestLib], + }); + + const inputsLib = m.library("InputsLib", { + id: `InputsLib_${process.env.VERSION_TAG ?? 0}`, + after: [vaultMathLib], + }); + + const investmentVaultLib = m.library("InvestmentVaultLib", { + id: `InvestmentVaultLib_${process.env.VERSION_TAG ?? 0}`, + after: [inputsLib], + }); + const aaveVaultDeployerLib = m.library("AaveVaultDeployerLib", { + id: `AaveVaultDeployerLib_${process.env.VERSION_TAG ?? 0}`, + after: [investmentVaultLib], + }); + + const inputModifierLib = m.library("InputModifierLib", { + id: `InputModifierLib_${process.env.VERSION_TAG ?? 0}`, + libraries: { VaultMathLib: vaultMathLib, InterestLib: interestLib, InvestmentVaultLib: investmentVaultLib }, + after: [aaveVaultDeployerLib], + }); + + const vaultInitLib = m.library("VaultInitLib", { + id: `VaultInitLib_${process.env.VERSION_TAG ?? 0}`, + libraries: { + TokenDistributorDeployLib: tokenDistributorDeployLib, + AaveVaultDeployerLib: aaveVaultDeployerLib, + InvestmentVaultLib: investmentVaultLib, + }, + after: [inputModifierLib], + }); + + const vaultDepositLib = m.library("VaultDepositLib", { + id: `VaultDepositLib_${process.env.VERSION_TAG ?? 0}`, + libraries: { + VaultMathLib: vaultMathLib, + InterestLib: interestLib, + InvestmentVaultLib: investmentVaultLib, + InputsLib: inputsLib, + }, + after: [vaultInitLib], + }); + + const vaultSpendLib = m.library("VaultSpendLib", { + id: `VaultSpendLib_${process.env.VERSION_TAG ?? 0}`, + libraries: { + VaultMathLib: vaultMathLib, + InterestLib: interestLib, + InvestmentVaultLib: investmentVaultLib, + InputsLib: inputsLib, + }, + after: [vaultDepositLib], + }); + const vaultImplementation = m.contract("Vault", [], { + libraries: { + InvestmentVaultLib: investmentVaultLib, + InputModifierLib: inputModifierLib, + VaultInitLib: vaultInitLib, + VaultDepositLib: vaultDepositLib, + VaultSpendLib: vaultSpendLib, + InterestLib: interestLib, + }, + id: `Vault_${process.env.VERSION_TAG ?? 0}`, + after: [vaultSpendLib], + }); const token = m.getParameter("token"); const aavePool = m.getParameter("aavePool"); @@ -50,16 +150,25 @@ export default buildModule("Main", (m) => { ); const vault = m.contractAt("Vault", vaultProxy); - m.call(vault, "initialize", [ - verifiers, - forwarder, - protocolManager, + const contractInitialize = m.call( + vault, + "initialize", + [verifiers, forwarder, protocolManager, administrator, dropMerkleRoot, token, aavePool, aToken], + { after: [protocolManager] }, + ); + + const admin = m.getParameter("admin"); + const maintainer = m.getParameter("maintainer"); + const manager = m.getParameter("manager"); + const securityCouncil = m.getParameter("securityCouncil"); + const upgradeDelay = m.getParameter("upgradeDelay"); + const adminDelay = m.getParameter("adminDelay"); + m.call( administrator, - dropMerkleRoot, - token, - aavePool, - aToken, - ]); + "setupGovernance", + [admin, vault, maintainer, securityCouncil, manager, upgradeDelay, adminDelay], + { after: [contractInitialize] }, + ); return { verifiers, @@ -75,6 +184,5 @@ export default buildModule("Main", (m) => { investmentVaultLib, aaveVaultDeployerLib, invoiceFactory, - ...circuitVerifiers, }; }); diff --git a/ignition/modules/ProtocolManager.module.ts b/ignition/modules/ProtocolManager.module.ts deleted file mode 100644 index 57109df..0000000 --- a/ignition/modules/ProtocolManager.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("ProtocolManager", (m) => { - const token = m.getParameter("token"); - const authority = m.getParameter("administrator"); - const maxTVL = m.getParameter("maxTVL"); - const spendFee = m.getParameter("spendFee"); - const depositFee = m.getParameter("depositFee"); - const withdrawFee = m.getParameter("withdrawFee"); - const curatorPerformanceFee = m.getParameter("curatorPerformanceFee", 0n); - const investmentStrategyAssetsOffset = m.getParameter("investmentStrategyAssetsOffset", 0n); - - const protocolManager = m.contract( - "ProtocolManager", - [ - authority, - token, - maxTVL, - { spend: spendFee, deposit: depositFee, withdraw: withdrawFee }, - curatorPerformanceFee, - investmentStrategyAssetsOffset, - ], - { id: `ProtocolManager_${process.env.VERSION_TAG ?? 0}` }, - ); - - return { - protocolManager, - }; -}); diff --git a/ignition/modules/ShareMathLib.module.ts b/ignition/modules/ShareMathLib.module.ts deleted file mode 100644 index 043bc3b..0000000 --- a/ignition/modules/ShareMathLib.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -/** - * Ignition module for deploying ShareMathLib - * - * The ShareMathLib is a library contract that provides a function to calculate the share of a value based on a percentage. - */ -export default buildModule("ShareMathLib", (m) => { - const shareMathLib = m.library("ShareMathLib", { id: `ShareMathLib_${process.env.VERSION_TAG ?? 0}` }); - - return { - shareMathLib, - }; -}); diff --git a/ignition/modules/Vault.module.ts b/ignition/modules/Vault.module.ts deleted file mode 100644 index b150dba..0000000 --- a/ignition/modules/Vault.module.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("Vault", (m) => { - const tokenDistributorDeployLib = m.library("TokenDistributorDeployLib", { - id: `TokenDistributorDeployLib_${process.env.VERSION_TAG ?? 0}`, - }); - const interestLib = m.library("InterestLib", { - id: `InterestLib_${process.env.VERSION_TAG ?? 0}`, - after: [tokenDistributorDeployLib], - }); - const vaultMathLib = m.library("VaultMathLib", { - id: `VaultMathLib_${process.env.VERSION_TAG ?? 0}`, - after: [interestLib], - }); - - const inputsLib = m.library("InputsLib", { - id: `InputsLib_${process.env.VERSION_TAG ?? 0}`, - after: [vaultMathLib], - }); - - const investmentVaultLib = m.library("InvestmentVaultLib", { - id: `InvestmentVaultLib_${process.env.VERSION_TAG ?? 0}`, - after: [inputsLib], - }); - const aaveVaultDeployerLib = m.library("AaveVaultDeployerLib", { - id: `AaveVaultDeployerLib_${process.env.VERSION_TAG ?? 0}`, - after: [investmentVaultLib], - }); - - const inputModifierLib = m.library("InputModifierLib", { - id: `InputModifierLib_${process.env.VERSION_TAG ?? 0}`, - libraries: { VaultMathLib: vaultMathLib, InterestLib: interestLib, InvestmentVaultLib: investmentVaultLib }, - after: [aaveVaultDeployerLib], - }); - - const vaultInitLib = m.library("VaultInitLib", { - id: `VaultInitLib_${process.env.VERSION_TAG ?? 0}`, - libraries: { - TokenDistributorDeployLib: tokenDistributorDeployLib, - AaveVaultDeployerLib: aaveVaultDeployerLib, - InvestmentVaultLib: investmentVaultLib, - }, - after: [inputModifierLib], - }); - - const vaultDepositLib = m.library("VaultDepositLib", { - id: `VaultDepositLib_${process.env.VERSION_TAG ?? 0}`, - libraries: { - VaultMathLib: vaultMathLib, - InterestLib: interestLib, - InvestmentVaultLib: investmentVaultLib, - InputsLib: inputsLib, - }, - after: [vaultInitLib], - }); - - const vaultSpendLib = m.library("VaultSpendLib", { - id: `VaultSpendLib_${process.env.VERSION_TAG ?? 0}`, - libraries: { - VaultMathLib: vaultMathLib, - InterestLib: interestLib, - InvestmentVaultLib: investmentVaultLib, - InputsLib: inputsLib, - }, - after: [vaultDepositLib], - }); - const vault = m.contract("Vault", [], { - libraries: { - InvestmentVaultLib: investmentVaultLib, - InputModifierLib: inputModifierLib, - VaultInitLib: vaultInitLib, - VaultDepositLib: vaultDepositLib, - VaultSpendLib: vaultSpendLib, - InterestLib: interestLib, - }, - id: `Vault_${process.env.VERSION_TAG ?? 0}`, - after: [vaultSpendLib], - }); - - return { - vault, - tokenDistributorDeployLib, - interestLib, - inputsLib, - vaultMathLib, - investmentVaultLib, - aaveVaultDeployerLib, - }; -}); diff --git a/ignition/modules/Verifiers.module.ts b/ignition/modules/Verifiers.module.ts deleted file mode 100644 index bdc6793..0000000 --- a/ignition/modules/Verifiers.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("Verifiers", (m) => { - // Deploy individual verifiers first - const depositVerifier = m.contract("DepositVerifier"); - const spend11Verifier = m.contract("Spend11Verifier", [], { after: [depositVerifier] }); - const spend12Verifier = m.contract("Spend12Verifier", [], { after: [spend11Verifier] }); - const spend13Verifier = m.contract("Spend13Verifier", [], { after: [spend12Verifier] }); - const spend21Verifier = m.contract("Spend21Verifier", [], { after: [spend13Verifier] }); - const spend22Verifier = m.contract("Spend22Verifier", [], { after: [spend21Verifier] }); - const spend23Verifier = m.contract("Spend23Verifier", [], { after: [spend22Verifier] }); - const spend31Verifier = m.contract("Spend31Verifier", [], { after: [spend23Verifier] }); - const spend32Verifier = m.contract("Spend32Verifier", [], { after: [spend31Verifier] }); - const spend33Verifier = m.contract("Spend33Verifier", [], { after: [spend32Verifier] }); - const spend61Verifier = m.contract("Spend61Verifier", [], { after: [spend33Verifier] }); - const spend121Verifier = m.contract("Spend121Verifier", [], { after: [spend61Verifier] }); - - // Deploy Verifiers contract with the deployed verifier addresses - const verifiers = m.contract("Verifiers", [ - depositVerifier, - spend11Verifier, - spend12Verifier, - spend13Verifier, - spend21Verifier, - spend22Verifier, - spend23Verifier, - spend31Verifier, - spend32Verifier, - spend33Verifier, - spend61Verifier, - spend121Verifier, - ]); - - return { - verifiers, - depositVerifier, - spend11Verifier, - spend12Verifier, - spend13Verifier, - spend21Verifier, - spend22Verifier, - spend23Verifier, - spend31Verifier, - spend32Verifier, - spend33Verifier, - spend61Verifier, - spend121Verifier, - }; -}); diff --git a/ignition/modules/VirtualSharesVaultFactory.module.ts b/ignition/modules/VirtualSharesVaultFactory.module.ts deleted file mode 100644 index 9ddc71c..0000000 --- a/ignition/modules/VirtualSharesVaultFactory.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("VirtualSharesVaultFactory", (m) => { - const virtualSharesVaultFactory = m.contract("VirtualSharesVaultFactory", [], { - id: `VirtualSharesVaultFactory_${process.env.VERSION_TAG ?? 0}`, - }); - - return { - virtualSharesVaultFactory, - }; -}); diff --git a/integration/vault/vault.test.utils.ts b/integration/vault/vault.test.utils.ts index 14ef47f..37da86d 100644 --- a/integration/vault/vault.test.utils.ts +++ b/integration/vault/vault.test.utils.ts @@ -3,11 +3,7 @@ import { ethers, ignition } from "hardhat"; import { randomBytes, type BigNumberish } from "ethers"; import { poseidon2 } from "poseidon-lite"; import { exportSolidityCallData, prove } from "../prove.helper"; -import { - DepositParamsStruct, - DepositCommitmentParamsStruct, - Vault, -} from "../../typechain-types/src/vault/Vault"; +import { DepositParamsStruct, DepositCommitmentParamsStruct, Vault } from "../../typechain-types/src/vault/Vault"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { Administrator, @@ -119,18 +115,18 @@ export async function deployVaultFixture(): Promise { MainModule, { parameters: { - Administrator: { - admin: owner.address, - maintainer: owner.address, - treasureManager: owner.address, - securityCouncil: owner.address, - defaultUpgradeDelay: 12 * 60 * 60, - }, Main: { + initialAdmin: owner.address, maxTVL: ethers.parseEther("10000"), spendFee: ethers.parseEther("5"), // 5 tokens absolute depositFee: 50_000_000, // 5% (50M / 1e9) withdrawFee: 50_000_000, // 5% (50M / 1e9) + admin: owner.address, + maintainer: owner.address, + manager: owner.address, + securityCouncil: owner.address, + upgradeDelay: 12 * 60 * 60, + adminDelay: 6 * 60 * 60, curatorPerformanceFee: 0n, investmentStrategyAssetsOffset: 0n, token: tokenAddress, diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 840c0b3..f8b71af 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -9,7 +9,7 @@ import { deploymentChange } from "./deploymentChange"; type Params = { admin: string; maintainer: string; - treasureManager: string; + manager?: string; securityCouncil: string; token?: string; aavePool?: string; @@ -29,20 +29,22 @@ async function main() { console.log(`Deployer: ${deployer.address}`); const isHardhat = hre.network.name === "hardhat"; + const rawParams = ( + hre.network.config as NetworkConfig & { + params?: Params; + } + ).params; + const params = { admin: deployer.address, maintainer: deployer.address, - treasureManager: deployer.address, securityCouncil: deployer.address, donor: deployer.address, - ...( - hre.network.config as NetworkConfig & { - params?: Params; - } - ).params, + ...rawParams, }; - const { admin, maintainer, treasureManager, securityCouncil } = params; + const manager = params.manager ?? deployer.address; + const { admin, maintainer, securityCouncil } = params; const salt = mkCreateXSalt(deployer.address); let token = params.token; @@ -55,7 +57,7 @@ async function main() { strategy: isHardhat ? "basic" : "create2", parameters: { Mocks: { - admin: admin, + admin, }, }, strategyConfig: { @@ -72,7 +74,7 @@ async function main() { mockAavePool.aToken(), ]); - await new Promise((resolve) => setTimeout(() => resolve(), 2_500)); + await new Promise((resolve) => setTimeout(() => resolve(), 2_000)); token = mockERC20Address; aavePool = mockAavePoolAddress; @@ -84,7 +86,7 @@ async function main() { console.log(`Deploying with params: admin: ${admin} maintainer: ${maintainer} - treasureManager: ${treasureManager} + manager: ${manager} securityCouncil: ${securityCouncil} token: ${token} aavePool: ${aavePool} @@ -106,14 +108,8 @@ async function main() { MainModule, { parameters: { - Administrator: { - admin: admin, - maintainer: maintainer, - treasureManager: treasureManager, - securityCouncil: securityCouncil, - defaultUpgradeDelay: 6 * 60 * 60, // 6 hours - }, Main: { + initialAdmin: deployer.address, maxTVL, spendFee, depositFee, @@ -124,6 +120,12 @@ async function main() { aavePool: aavePool!, aToken: aToken!, dropMerkleRoot, + admin, + maintainer, + manager, + securityCouncil, + upgradeDelay: 48 * 60 * 60, // 48 hours + adminDelay: 12 * 60 * 60, // 12 hours }, }, strategy: isHardhat ? "basic" : "create2", @@ -169,7 +171,7 @@ async function main() { trueOrThrow(await administrator.hasRole(0, admin), "Admin role not correctly set"); trueOrThrow(await administrator.hasRole(1, maintainer), "Maintainer role not correctly set"); trueOrThrow(await administrator.hasRole(2, securityCouncil), "Security council role not correctly set"); - trueOrThrow(await administrator.hasRole(3, treasureManager), "Treasure manager role not correctly set"); + trueOrThrow(await administrator.hasRole(3, manager), "Protocol manager role not correctly set"); trueOrThrow((await vault.authority()) === administratorAddress, "Vault administrator is not correctly set"); trueOrThrow((await vault.getManager()) === protocolManagerAddress, "Vault manager is not correctly set"); trueOrThrow((await vault.getVerifiers()) === verifiersAddress, "Vault Verifiers is not correctly set"); diff --git a/scripts/mkCreateXSalt.ts b/scripts/mkCreateXSalt.ts index bb3a9fd..be6dbb8 100644 --- a/scripts/mkCreateXSalt.ts +++ b/scripts/mkCreateXSalt.ts @@ -7,7 +7,7 @@ import { getBytes, id, concat } from "ethers"; export function mkCreateXSalt(deployer: string) { const addr20 = getBytes(deployer); // 20 bytes const flag1 = getBytes("0x01"); // 1 byte - const rnd11 = getBytes(id("zeroledger_deploy_salt_2")); + const rnd11 = getBytes(id("_ZeroLedger_Deploy_Salt_")); const salt = concat([addr20, flag1, rnd11]).slice(0, 66); // 32 bytes hex return salt; } diff --git a/src/Administrator.sol b/src/Administrator.sol index 80c2d3a..49cdb3e 100644 --- a/src/Administrator.sol +++ b/src/Administrator.sol @@ -2,41 +2,117 @@ pragma solidity >=0.8.21; import {AccessManager} from "@openzeppelin/contracts/access/manager/AccessManager.sol"; +import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; import {RolesLib} from "src/Roles.lib.sol"; +import {IVault} from "src/vault/Vault.types.sol"; +import {IProtocolManager} from "src/ProtocolManager.types.sol"; /** * @title Administrator - * @notice Contract to admin roles for the Forwarder, Vault, ProtocolManager contract administration] + * @notice Contract for atomic execution of initial governance configuration * @dev Roles should be assigned for each contract separately * Roles Description with config example: - * [ADMIN] - multisig 3/5 wallet, grand / suspend roles, freeze / unfreeze roles + * [ADMIN] - multisig 3/5 wallet, `root` role * [PROTOCOL_MANAGER] - multisig 2/3 wallet to manage protocol tokenomics - * [SECURITY_COUNCIL] - multisig 2/3 wallet to pause/unpause vault operations + * [SECURITY_COUNCIL] - multisig 1/2 wallet to pause/unpause vault operations * [MAINTAINER] - multisig 2/3 wallet to approve dependent contract upgrades * @author ZeroLedger */ contract Administrator is AccessManager { + error GovernanceNotAuthorized(); + error VaultAuthorityMismatch(); + error ProtocolManagerAuthorityMismatch(); + /** * @notice Constructor to initialize the Administrator contract - * @param admin The address of the admin multisig wallet - * @param maintainer The address of the maintainer multisig wallet. - * This role is responsible for contract upgrades approvals. - * @param securityCouncil The address of the security council multisig wallet. - * This role is responsible for emergency pauses and security functions. - * @param protocolManager The address of the protocol params manager multisig wallet. - * This role is responsible for managing protocol parameters. - * @param defaultGrantDelay The default grant delay for the roles. - * This is the time after which the role can be revoked. + * @param initialAdmin The address of the initial admin */ - constructor( + constructor(address initialAdmin) AccessManager(initialAdmin) {} + + /** + * @notice Constructor to initialize the Governance contract + * @param admin The address of the new admin + * @param vault The address of the vault + * @param maintainer The address of the maintainer + * @param securityCouncil The address of the security council + * @param manager The address of the manager that manages the protocolManager contract + * @param upgradeDelay The upgrade delay + * @param adminDelay The future admin actions delay + */ + function setupGovernance( address admin, + address vault, address maintainer, address securityCouncil, - address protocolManager, - uint32 defaultGrantDelay - ) AccessManager(admin) { - _grantRole(RolesLib.MAINTAINER, maintainer, 0, defaultGrantDelay); + address manager, + uint32 upgradeDelay, + uint32 adminDelay + ) external onlyAuthorized { + (bool isAdmin,) = hasRole(ADMIN_ROLE, msg.sender); + if (!isAdmin) { + revert GovernanceNotAuthorized(); + } + if (AccessManaged(vault).authority() != address(this)) { + revert VaultAuthorityMismatch(); + } + address protocolManagerContract = IVault(vault).getManager(); + if (AccessManaged(protocolManagerContract).authority() != address(this)) { + revert ProtocolManagerAuthorityMismatch(); + } + setupMaintainerRole(vault, maintainer, upgradeDelay); + setupSecurityCouncilRole(vault, securityCouncil); + setupProtocolManagerRole(vault, manager); + setupAdminRole(admin, adminDelay); + } + + /** + * @notice Setup the maintainer role + * @param vault The address of the vault + * @param maintainer The address of the maintainer + * @param executionDelay The execution delay + */ + function setupMaintainerRole(address vault, address maintainer, uint32 executionDelay) internal { + _grantRole(RolesLib.MAINTAINER, maintainer, 0, executionDelay); + _setTargetFunctionRole(vault, IVault.approveUpgrade.selector, RolesLib.MAINTAINER); + } + + /** + * @notice Setup the security council role + * @param vault The address of the vault + * @param securityCouncil The address of the security council + */ + function setupSecurityCouncilRole(address vault, address securityCouncil) internal { _grantRole(RolesLib.SECURITY_COUNCIL, securityCouncil, 0, 0); - _grantRole(RolesLib.PROTOCOL_MANAGER, protocolManager, 0, 0); + _setTargetFunctionRole(vault, IVault.pause.selector, RolesLib.SECURITY_COUNCIL); + _setTargetFunctionRole(vault, IVault.unpause.selector, RolesLib.SECURITY_COUNCIL); + } + + /** + * @notice Setup the protocol manager role + * @param vault The address of the vault + * @param manager The address of the admin for the protocol manager contract + */ + function setupProtocolManagerRole(address vault, address manager) internal { + _grantRole(RolesLib.PROTOCOL_MANAGER, manager, 0, 0); + _setTargetFunctionRole(vault, IVault.collectCuratorPerformanceFee.selector, RolesLib.PROTOCOL_MANAGER); + address protocolManager = IVault(vault).getManager(); + _setTargetFunctionRole( + protocolManager, IProtocolManager.setCuratorPerformanceFee.selector, RolesLib.PROTOCOL_MANAGER + ); + _setTargetFunctionRole( + protocolManager, IProtocolManager.setInvestmentStrategyAssetsOffset.selector, RolesLib.PROTOCOL_MANAGER + ); + _setTargetFunctionRole(protocolManager, IProtocolManager.setFees.selector, RolesLib.PROTOCOL_MANAGER); + _setTargetFunctionRole(protocolManager, IProtocolManager.setMaxTVL.selector, RolesLib.PROTOCOL_MANAGER); + } + + /** + * @notice Setup the admin role + * @param admin The address of the admin + * @param delay The future admin actions delay + */ + function setupAdminRole(address admin, uint32 delay) internal { + _grantRole(ADMIN_ROLE, admin, 0, 0); + _setGrantDelay(ADMIN_ROLE, delay); } } diff --git a/src/helpers/MockAavePool.sol b/src/helpers/MockAavePool.sol index a04d86a..efaba00 100644 --- a/src/helpers/MockAavePool.sol +++ b/src/helpers/MockAavePool.sol @@ -31,7 +31,8 @@ contract MockAavePool { return amount; } - /// @notice Simulates yield: mints aTokens to the strategy and matching underlying to the pool so withdrawals stay liquid. + /// @notice Simulates yield: mints aTokens to the strategy and matching underlying to the pool so withdrawals stay + /// liquid. function __fakeIncome(address strategy, uint256 amount) external { aToken.mint(strategy, amount); MockERC20(address(asset)).mint(address(this), amount); diff --git a/src/invoice/Invoice.sol b/src/invoice/Invoice.sol index 7a7eeb6..dc56a29 100644 --- a/src/invoice/Invoice.sol +++ b/src/invoice/Invoice.sol @@ -16,6 +16,7 @@ contract Invoice is Initializable { using SafeERC20 for IERC20; /// @notice Address of the factory that deployed the implementation. /// Clones use delegatecall into this code, so the immutable is still readable. + address public immutable factory; /// @notice Locks the implementation against direct initialization. diff --git a/src/invoice/InvoiceFactory.sol b/src/invoice/InvoiceFactory.sol index cb72907..7ffbed3 100644 --- a/src/invoice/InvoiceFactory.sol +++ b/src/invoice/InvoiceFactory.sol @@ -6,7 +6,7 @@ import {Invoice, InvoiceLib, DepositCommitmentParams} from "./Invoice.sol"; /** * @title InvoiceFactory - * @notice A contract that deploys Invoice clones using CREATE2 + * @notice A contract that deploys Invoice clones using CREATE2 * @author ZeroLedger */ contract InvoiceFactory { diff --git a/src/vault/InvestmentVault.lib.sol b/src/vault/InvestmentVault.lib.sol index 8494d47..07221e7 100644 --- a/src/vault/InvestmentVault.lib.sol +++ b/src/vault/InvestmentVault.lib.sol @@ -94,7 +94,10 @@ library InvestmentVaultLib { /** * @notice Withdraws all strategy shares back to idle underlying in this contract. - * @dev This function must be only called during atomic migration to the new strategy vault + * @dev This function must be only called: + * - during atomic migration to the new strategy vault + * - during the migration of the vault to a new implementation + * - during emergency withdrawal * @param state The state of the vault. * @param token The address of the token. */ @@ -110,11 +113,14 @@ library InvestmentVaultLib { /** * @notice Withdraws assets to `receiver` in a single transaction. - * @dev (1) If one third of idle covers `amount`, transfer from idle only. - * (2) Else if the strategy can cover the full `amount` (`totalAssets`), withdraw from the - * strategy only and leave idle untouched. - * (3) Otherwise use all idle and withdraw `amount - idle` from the strategy. - * Reverts if `amount > idle + strategy.totalAssets()`.\n + * @dev Evaluated in order. `idle` is this contract token balance; `strategyLiquidity` is + * `IERC4626Strategy.totalAssets()` on the strategy vault. + * 1. Buffer preference: if `amount <= idle / 3`, satisfy from idle only (no strategy call). + * 2. Else if `amount <= strategyLiquidity`, withdraw entirely from the strategy; idle unchanged. + * 3. Else if `amount <= idle`, satisfy from idle only (withdrawal exceeds one third of idle but + * still fits in idle; strategy alone is insufficient because `amount > strategyLiquidity`). + * 4. Else send all `idle` to `receiver`, then `strategyVault.withdraw(amount - idle, ...)`. + * Reverts with `InsufficientWithdrawLiquidity` when `amount > idle + strategyLiquidity`. * @param state The state of the vault. * @param token The address of the token. * @param amount The amount of assets to withdraw. diff --git a/src/vault/Vault.sol b/src/vault/Vault.sol index 51ac431..c76a2c9 100644 --- a/src/vault/Vault.sol +++ b/src/vault/Vault.sol @@ -74,24 +74,29 @@ contract Vault is __ReentrancyGuard_init(); __Pausable_init(); __AccessManaged_init(authority); + if (AccessManagedUpgradeable(protocolManager).authority() != authority) { + revert ProtocolManagerAuthorityMismatch(); + } VaultInitLib.init_unchained( _getStorage(), verifiers, trustedForwarder, protocolManager, dropMerkleRoot, token, aavePool, aToken ); } + /// @inheritdoc IVault + function approveUpgrade(address newImplementation) external restricted { + _getStorage().approvedImplementation = newImplementation; + } + /** * @notice Authorizes the upgrade * @dev This function is called to authorize the upgrade of Vault proxy with particular implementation. - * Only autorized via Administrator contract address can whitelist upgrade. * @param newImplementation The address of the new implementation. */ - function _authorizeUpgrade(address newImplementation) internal override restricted {} + function _authorizeUpgrade(address newImplementation) internal view override { + require(_getStorage().approvedImplementation == newImplementation, "Implementation not approved"); + } - /** - * @notice Helper function to check if the forwarder is trusted - * @param forwarder The address of the forwarder to check. - * @return isTrustedForwarder Whether the forwarder is trusted. - */ + /// @inheritdoc IVault function isTrustedForwarder(address forwarder) public view virtual returns (bool) { return _getStorage().trustedForwarder == forwarder; } @@ -140,20 +145,12 @@ contract Vault is return 20; } - /** - * @notice Pauses the vault - * @dev This function is called to pause the vault main functions. - * Only the whitelisted by Administrator contract address can pause the vault. - */ + /// @inheritdoc IVault function pause() external restricted { _pause(); } - /** - * @notice Unpauses the vault - * @dev This function is called to unpause the vault main functions. - * Only the whitelisted by Administrator contract address can unpause the vault. - */ + /// @inheritdoc IVault function unpause() external restricted { _unpause(); } diff --git a/src/vault/Vault.types.sol b/src/vault/Vault.types.sol index b55443a..e943627 100644 --- a/src/vault/Vault.types.sol +++ b/src/vault/Vault.types.sol @@ -138,6 +138,13 @@ interface IVault is IVaultEvents { */ function unpause() external; + /** + * @notice Approves the upgrade of the vault + * @dev This function is called to approve the upgrade of the vault with particular implementation. + * @param newImplementation The address of the new implementation. + */ + function approveUpgrade(address newImplementation) external; + /** * @notice Deposit tokens with commitments and ZK proof validation * @param depositParams The deposit parameters including token, amount, and commitments @@ -282,4 +289,5 @@ interface IVaultErrors { error FeeIsLessThanMinimumRequiredFees(uint208 expectedFee); error InvalidInterestForSharedInput(); error InvalidInterestForInput(uint256 poseidonHash); + error ProtocolManagerAuthorityMismatch(); } diff --git a/src/vault/VaultStorage.sol b/src/vault/VaultStorage.sol index 26eacb0..8936254 100644 --- a/src/vault/VaultStorage.sol +++ b/src/vault/VaultStorage.sol @@ -15,6 +15,7 @@ struct State { address trustedForwarder; IProtocolManager manager; ITokenDistributor tokenDistributor; + address approvedImplementation; /// @dev Gap for future upgrades uint256[50] __gap; } diff --git a/src/vault/VaultV2.sol b/src/vault/VaultV2.sol index 3013b8e..2a43a64 100644 --- a/src/vault/VaultV2.sol +++ b/src/vault/VaultV2.sol @@ -53,21 +53,23 @@ contract VaultV2 is * @dev This function is called by the upgradeable contract after the upgrade. * This can be used for new initializations and for tracking the upgrade version. */ - function upgradeCallBack() external reinitializer(3) {} + function upgradeCallBack() external reinitializer(2) {} + + /// @inheritdoc IVault + function approveUpgrade(address newImplementation) external restricted { + _getStorage().approvedImplementation = newImplementation; + } /** * @notice Authorizes the upgrade * @dev This function is called to authorize the upgrade of Vault proxy with particular implementation. - * Only autorized via Administrator contract address can whitelist upgrade. * @param newImplementation The address of the new implementation. */ - function _authorizeUpgrade(address newImplementation) internal override restricted {} + function _authorizeUpgrade(address newImplementation) internal view override { + require(_getStorage().approvedImplementation == newImplementation, "Implementation not approved"); + } - /** - * @notice Helper function to check if the forwarder is trusted - * @param forwarder The address of the forwarder to check. - * @return isTrustedForwarder Whether the forwarder is trusted. - */ + /// @inheritdoc IVault function isTrustedForwarder(address forwarder) public view virtual returns (bool) { return _getStorage().trustedForwarder == forwarder; } @@ -116,20 +118,12 @@ contract VaultV2 is return 20; } - /** - * @notice Pauses the vault - * @dev This function is called to pause the vault main functions. - * Only the whitelisted by Administrator contract address can pause the vault. - */ + /// @inheritdoc IVault function pause() external restricted { _pause(); } - /** - * @notice Unpauses the vault - * @dev This function is called to unpause the vault main functions. - * Only the whitelisted by Administrator contract address can unpause the vault. - */ + /// @inheritdoc IVault function unpause() external restricted { _unpause(); } diff --git a/test/Administrator.t.sol b/test/Administrator.t.sol new file mode 100644 index 0000000..8a4696e --- /dev/null +++ b/test/Administrator.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import "@std/Test.sol"; + +import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; +import {IAccessManager} from "@openzeppelin/contracts/access/manager/IAccessManager.sol"; + +import {Administrator} from "src/Administrator.sol"; +import {ProtocolManager} from "src/ProtocolManager.sol"; +import {Fees} from "src/ProtocolManager.types.sol"; +import {MockERC20} from "src/helpers/MockERC20.sol"; +import {RolesLib} from "src/Roles.lib.sol"; + +/// @dev Minimal vault stand-in: `AccessManaged` wiring plus `getManager()` (the `ProtocolManager` contract). +contract VaultStub is AccessManaged { + address private immutable _managerContract; + + constructor(address authority, address managerContract) AccessManaged(authority) { + _managerContract = managerContract; + } + + function getManager() external view returns (address) { + return _managerContract; + } +} + +contract AdministratorTest is Test { + Administrator internal administrator; + MockERC20 internal token; + VaultStub internal vault; + ProtocolManager internal protocolManagerContract; + + address internal newAdmin = address(0xA11CE); + address internal maintainer = address(0xB0B); + address internal securityCouncil = address(0xC0C); + address internal manager = address(0xE11E); + address internal stranger = address(0xDEAD); + + uint32 internal constant UPGRADE_DELAY = 3600; + uint32 internal constant ADMIN_DELAY = 86400; + + function setUp() public { + administrator = new Administrator(address(this)); + token = new MockERC20("T", "T"); + protocolManagerContract = new ProtocolManager( + address(administrator), address(token), type(uint208).max, Fees({spend: 0, deposit: 0, withdraw: 0}), 0, 0 + ); + vault = new VaultStub(address(administrator), address(protocolManagerContract)); + } + + function test_setupGovernance_grants_expected_roles() public { + administrator.setupGovernance( + newAdmin, address(vault), maintainer, securityCouncil, manager, UPGRADE_DELAY, ADMIN_DELAY + ); + + (bool adminMember,) = administrator.hasRole(administrator.ADMIN_ROLE(), newAdmin); + assertTrue(adminMember); + + (bool maintainerMember,) = administrator.hasRole(RolesLib.MAINTAINER, maintainer); + assertTrue(maintainerMember); + + (bool scMember,) = administrator.hasRole(RolesLib.SECURITY_COUNCIL, securityCouncil); + assertTrue(scMember); + + (bool pmMember,) = administrator.hasRole(RolesLib.PROTOCOL_MANAGER, manager); + assertTrue(pmMember); + } + + function test_setupGovernance_reverts_when_caller_not_admin() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessManager.AccessManagerUnauthorizedAccount.selector, stranger, administrator.ADMIN_ROLE() + ) + ); + vm.prank(stranger); + administrator.setupGovernance( + newAdmin, address(vault), maintainer, securityCouncil, manager, UPGRADE_DELAY, ADMIN_DELAY + ); + } + + function test_setupGovernance_reverts_when_vault_authority_mismatch() public { + VaultStub wrongVault = new VaultStub(stranger, address(protocolManagerContract)); + vm.expectRevert(Administrator.VaultAuthorityMismatch.selector); + administrator.setupGovernance( + newAdmin, address(wrongVault), maintainer, securityCouncil, manager, UPGRADE_DELAY, ADMIN_DELAY + ); + } + + function test_setupGovernance_reverts_when_protocol_manager_authority_mismatch() public { + ProtocolManager wrongPm = + new ProtocolManager(stranger, address(token), type(uint208).max, Fees({spend: 0, deposit: 0, withdraw: 0}), 0, 0); + VaultStub badVault = new VaultStub(address(administrator), address(wrongPm)); + vm.expectRevert(Administrator.ProtocolManagerAuthorityMismatch.selector); + administrator.setupGovernance( + newAdmin, address(badVault), maintainer, securityCouncil, manager, UPGRADE_DELAY, ADMIN_DELAY + ); + } +} diff --git a/test/Interest.lib.t.sol b/test/Interest.lib.t.sol index c0a9cdc..c16ac1b 100644 --- a/test/Interest.lib.t.sol +++ b/test/Interest.lib.t.sol @@ -127,10 +127,7 @@ contract InterestLibTest is Test { afterAps, uint208( Math.mulDiv( - (uint256(beforeAssets) + uint256(bonusAssets)), - InterestLib.scale(), - beforeShares, - Math.Rounding.Floor + (uint256(beforeAssets) + uint256(bonusAssets)), InterestLib.scale(), beforeShares, Math.Rounding.Floor ) ) ); diff --git a/test/Invoice.t.sol b/test/Invoice.t.sol index afb0550..bd8b706 100644 --- a/test/Invoice.t.sol +++ b/test/Invoice.t.sol @@ -15,7 +15,8 @@ contract InvoiceTest is VaultTest, IVaultEvents { function setUp() public { baseSetup(); - protocolManager.setFees(address(mockToken), Fees({deposit: 50_000_000, spend: 0, withdraw: 0})); // 5% deposit fee (50M / 1e9) + protocolManager.setFees(address(mockToken), Fees({deposit: 50_000_000, spend: 0, withdraw: 0})); // 5% deposit fee + // (50M / 1e9) // Deploy InvoiceFactory (which deploys the implementation) invoiceFactory = new InvoiceFactory(); diff --git a/test/ProtocolManager.t.sol b/test/ProtocolManager.t.sol index f2646cf..9a78f68 100644 --- a/test/ProtocolManager.t.sol +++ b/test/ProtocolManager.t.sol @@ -49,9 +49,7 @@ contract ProtocolManagerTest is Test, IManagerErrors, IManagerEvents { function test_setFees_reverts_when_deposit_above_max() public { uint256 maxFee = 5 * 10 ** (9 - 2); vm.expectRevert( - abi.encodeWithSelector( - InvalidDepositFeeConfig.selector, uint256(MAX_DEPOSIT_WITHDRAW_FEE + 1), maxFee - ) + abi.encodeWithSelector(InvalidDepositFeeConfig.selector, uint256(MAX_DEPOSIT_WITHDRAW_FEE + 1), maxFee) ); manager.setFees(address(token), Fees({spend: 0, deposit: MAX_DEPOSIT_WITHDRAW_FEE + 1, withdraw: 0})); } @@ -59,9 +57,7 @@ contract ProtocolManagerTest is Test, IManagerErrors, IManagerEvents { function test_setFees_reverts_when_withdraw_above_max() public { uint256 maxFee = 5 * 10 ** (9 - 2); vm.expectRevert( - abi.encodeWithSelector( - InvalidWithdrawFeeConfig.selector, uint256(MAX_DEPOSIT_WITHDRAW_FEE + 1), maxFee - ) + abi.encodeWithSelector(InvalidWithdrawFeeConfig.selector, uint256(MAX_DEPOSIT_WITHDRAW_FEE + 1), maxFee) ); manager.setFees(address(token), Fees({spend: 0, deposit: 0, withdraw: MAX_DEPOSIT_WITHDRAW_FEE + 1})); } @@ -83,9 +79,7 @@ contract ProtocolManagerTest is Test, IManagerErrors, IManagerEvents { token.mint(address(this), 1000e18); uint256 maxSpendAllowed = token.totalSupply() / 100_000; - vm.expectRevert( - abi.encodeWithSelector(InvalidSpendFeeConfig.selector, maxSpendAllowed + 1, maxSpendAllowed) - ); + vm.expectRevert(abi.encodeWithSelector(InvalidSpendFeeConfig.selector, maxSpendAllowed + 1, maxSpendAllowed)); manager.setFees(address(token), Fees({spend: uint208(maxSpendAllowed + 1), deposit: 0, withdraw: 0})); } diff --git a/test/TokenDistributor.t.sol b/test/TokenDistributor.t.sol index c7d7433..b99442e 100644 --- a/test/TokenDistributor.t.sol +++ b/test/TokenDistributor.t.sol @@ -127,11 +127,7 @@ contract TokenDistributorTest is Test { /// @dev Deposits anchor equal to final cumulative TDV cap (fills every tier); liquidity ZRL minted must be 27.5M. function test_full_liquidity_schedule_mints_27_5_million_zrl() public { uint256 expectedTotal = _totalZrlMintedIfFullLiquidityScheduleFilled(); - assertEq( - expectedTotal, - LIQUIDITY_FARMING_ZRL_WEI, - "tier floor-mulDiv sum must match tokenomics 27.5M ZRL" - ); + assertEq(expectedTotal, LIQUIDITY_FARMING_ZRL_WEI, "tier floor-mulDiv sum must match tokenomics 27.5M ZRL"); uint208 fullAnchorToReachFinalCap = _scaledCap(9); assertEq(distributor.previewDistribute(fullAnchorToReachFinalCap), expectedTotal); diff --git a/test/Vault.deposit.t.sol b/test/Vault.deposit.t.sol index 995bab9..a5686f2 100644 --- a/test/Vault.deposit.t.sol +++ b/test/Vault.deposit.t.sol @@ -22,9 +22,7 @@ contract VaultDepositTest is VaultTest, IVaultEvents { function setUp() public { baseSetup(); - protocolManager.setFees( - address(mockToken), Fees({deposit: defaultDepositFeeBasisPoints, spend: 0, withdraw: 0}) - ); + protocolManager.setFees(address(mockToken), Fees({deposit: defaultDepositFeeBasisPoints, spend: 0, withdraw: 0})); } // Fuzzy test for various deposit amounts and fee scenarios @@ -49,9 +47,7 @@ contract VaultDepositTest is VaultTest, IVaultEvents { uint256 totalAmount = (numerator + denominator - 1) / denominator; // Ceiling division vm.assume(totalAmount <= type(uint208).max); - protocolManager.setFees( - address(mockToken), Fees({deposit: depositFeeBasisPoints, spend: 0, withdraw: 0}) - ); + protocolManager.setFees(address(mockToken), Fees({deposit: depositFeeBasisPoints, spend: 0, withdraw: 0})); // Ensure Alice has enough tokens for this test if (mockToken.balanceOf(alice) < totalAmount) { @@ -81,9 +77,7 @@ contract VaultDepositTest is VaultTest, IVaultEvents { "Vault should receive net deposit and protocol fee" ); assertEq( - mockToken.balanceOf(address(protocolManager)), - 0, - "ProtocolManager should not directly receive fee tokens" + mockToken.balanceOf(address(protocolManager)), 0, "ProtocolManager should not directly receive fee tokens" ); assertEq( mockToken.balanceOf(address(zeroLedgerForwarder)), @@ -544,7 +538,7 @@ contract VaultDepositTest is VaultTest, IVaultEvents { emit CommitmentCreated(alice, address(mockToken), alice, 123456789, "metadata1"); vm.expectEmit(true, true, true, true); - emit CommitmentCreated(bob, address(mockToken), alice,987654321, "metadata2"); + emit CommitmentCreated(bob, address(mockToken), alice, 987654321, "metadata2"); vm.expectEmit(true, true, true, true); emit CommitmentCreated(charlie, address(mockToken), alice, 555666777, "metadata3"); @@ -747,9 +741,7 @@ contract VaultDepositTest is VaultTest, IVaultEvents { uint256 totalAmount = (numerator + denominator - 1) / denominator; // Ceiling division vm.assume(totalAmount <= type(uint208).max); - protocolManager.setFees( - address(mockToken), Fees({deposit: depositFeeBasisPoints, spend: 0, withdraw: 0}) - ); + protocolManager.setFees(address(mockToken), Fees({deposit: depositFeeBasisPoints, spend: 0, withdraw: 0})); // Ensure Alice has enough tokens for this test if (mockToken.balanceOf(alice) < totalAmount) { @@ -779,9 +771,7 @@ contract VaultDepositTest is VaultTest, IVaultEvents { "Vault should receive net deposit and protocol fee" ); assertEq( - mockToken.balanceOf(address(protocolManager)), - 0, - "ProtocolManager should not directly receive fee tokens" + mockToken.balanceOf(address(protocolManager)), 0, "ProtocolManager should not directly receive fee tokens" ); assertEq( mockToken.balanceOf(address(zeroLedgerForwarder)), diff --git a/test/VaultTest.util.sol b/test/VaultTest.util.sol index fc9974b..00ab453 100644 --- a/test/VaultTest.util.sol +++ b/test/VaultTest.util.sol @@ -77,17 +77,10 @@ contract VaultTest is Test, IVaultErrors { mockToken = new MockERC20("Test Token", "TEST"); permitUtils = new PermitUtils(mockToken.DOMAIN_SEPARATOR()); - address runner = address(this); - - Administrator administrator = new Administrator(runner, runner, runner, runner, 5 days); + Administrator administrator = new Administrator(address(this)); protocolManager = new ProtocolManager( - address(administrator), - address(mockToken), - type(uint208).max, - Fees({spend: 0, deposit: 0, withdraw: 0}), - 0, - 0 + address(administrator), address(mockToken), type(uint208).max, Fees({spend: 0, deposit: 0, withdraw: 0}), 0, 0 ); zeroLedgerForwarder = new Forwarder();