diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..de8c0bf4 --- /dev/null +++ b/.env.sample @@ -0,0 +1,4 @@ +HOODI_RPC_URL= +MAINNET_RPC_URL= +ETHERSCAN_API_KEY= +DEPLOYER_PRIVATE_KEY= \ No newline at end of file diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..087a33bb --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "github>ssvlabs/shared-configs//renovate/renovate.json" + ] +} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8a86c44..d68b352a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '22' cache: 'npm' cache-dependency-path: package-lock.json diff --git a/.gitignore b/.gitignore index 2ab6307d..68a6866c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,6 @@ out/ /broadcast/**/dry-run/ /broadcast -# Docs -docs/ - # Dotenv file .env @@ -21,6 +18,4 @@ coverage/ report/ lcov.info -.gas-snapshot - -test.txt \ No newline at end of file +.gas-snapshot \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index c8b57798..b6c6aac5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -8,7 +8,7 @@ { "files": "*.sol", "options": { - "parser": "solidity-parse", + "parser": "slang", "printWidth": 80, "tabWidth": 4, "useTabs": false, diff --git a/README.md b/README.md index e1b8a1b9..dda8eab2 100644 --- a/README.md +++ b/README.md @@ -61,94 +61,58 @@ __`❍ forge test`__   -## 🔨 _Slashing Mechanism_ +## 🔨 _Slashing & Withdrawals Mechanisms_ -The `slash` function allows for the reduction of a strategy’s token balance under specific conditions, either as a penalty or to enforce protocol-defined behavior. Slashing can happen in two distinct modes, depending on whether: +[Slashing & Withdrawals](./guides/slashing-and-withdrawals.md) -**1)** The bApp is a compliant smart contract; +[Generation pattern](./guides/generations.md) -**2)** The bApp is a non-compliant smart contract or an EOA. +## :gear: _Feature Activation_ -### 🧠 Compliant BApp +[Feature Activation](./guides/feature-activation.md) -If the bApp is a compliant contract implementing the required interface `IBasedApp`, - -The slash function of the bApp is called: `(success, receiver, exit) = IBasedApp(bApp).slash(...)` - -* `data` parameter is forwarded and may act as a proof or auxiliary input. - -* The bApp decides: - - * Who receives the slashed funds by setting the `receiver` fund, it can burn by setting the receiver as `address(0)`; - - * Whether to exit the strategy or adjust obligations; - - * If `exit == true`, the strategy is exited and the obligation value is set to 0; - - * Otherwise, obligations are adjusted proportionally based on remaining balances, the new obligated amount is set to the previous one less the slashed amount; - - * Funds are credited to the receiver in the slashing fund. - -### 🔐 Non-compliant bApp (EOA or Non-compliant Contract) - -If the bApp is an EOA or does not comply with the required interface: - -* Only the bApp itself can invoke slashing; - -* The receiver of slashed funds is forcibly set to the bApp itself; - -* The strategy is always exited (no obligation adjustment); - -* Funds are added to the bApp’s slashing fund. - -### ⏳ Post Slashing +## :page_facing_up: _Whitepaper_ -⚠️ Important: After an obligation has been exited, it can be updated again to a value greater than 0, but only after a 14-day obligation timelock. +[Whitepaper](https://ssv.network/wp-content/uploads/2025/01/SSV2.0-Based-Applications-Protocol-1.pdf) -This acts as a safeguard to prevent immediate re-entry and encourages more deliberate strategy participation. -### 💸 Slashing Fund +## :books: _More Resources_ -Slashed tokens are not immediately transferred. They are deposited into an internal slashing fund. +[Based Apps Onboarding Guide](./guides/bApp-onboarding.md) -The `receiver` (set during slashing) can later withdraw them using: -``` -function withdrawSlashingFund(address token, uint256 amount) external -function withdrawETHSlashingFund(uint256 amount) external -``` +## :rocket: _Deployments_ -These functions verify balances and authorize the caller to retrieve their accumulated slashed tokens. +### How to Deploy -  +**2)** Set the environment variables in the `.env` file. -## :page_facing_up: _Whitepaper_ +**1)** Run the deployment script `DeployAllHoodi.s.sol` defined in `script/`: -[Whitepaper](https://ssv.network/wp-content/uploads/2025/01/SSV2.0-Based-Applications-Protocol-1.pdf) +__`❍ npm run deploy:hoodi-stage`__: verification is done automatically. -  +### How to Update Module Contracts -## :books: _More Resources_ +It is possible to update each one of the three modules: `StrategyManager`, `BasedAppsManager` and `ProtocolManager`. -[Based Apps Onboarding Guide](./doc/bAppOnBoarding.md) +It is possible to update multiple modules at the same time. -  +**1)** Go on the Proxy Contract on Etherscan, under "Write as Proxy" call the function: -## :rocket: _Deployments_ +__`❍ updateModules`__: specifying the correct module id and the new module address. -### How to Deploy +### How to Upgrade the Implementation Contract -**1)** Run the deployment script defined in `scripts/`: +**1)** Go on the Proxy Contract on Etherscan, under "Write as Proxy" call the function: -__`❍ npm run deploy:holesky`__: verification is done automatically. +__`❍ upgradeToAndCall`__: specifying the new implementation address. The data field can be left empty in this case. -__`❍ npm run deploy:hoodi`__: verification needs to be done manually for now. -### Public Testnet +### Public Testnet Hoodi | Name | Proxy | Implementation | Notes | | -------- | -------- | -------- | -------- | -| [`BasedApplications`](https://github.com/ssvlabs/based-applications/blob/main/src/BasedAppManager.sol) | [`0x1Bd6ceB98Daf7FfEB590236b720F81b65213836A`](https://holesky.etherscan.io/address/0x1Bd6ceB98Daf7FfEB590236b720F81b65213836A) | [`0x9a09A49870353867b0ce9901B44E84C32B2A47AC`](https://holesky.etherscan.io/address/0x9a09A49870353867b0ce9901B44E84C32B2A47AC) | Proxy: [`UUPS@5.1.0`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.1.0/contracts/proxy/utils/UUPSUpgradeable.sol) | +| [`SSVBasedApps`](https://github.com/ssvlabs/based-applications/blob/main/src/BasedAppManager.sol) | [``](https://holesky.etherscan.io/address/0x1Bd6ceB98Daf7FfEB590236b720F81b65213836A) | [``](https://holesky.etherscan.io/address/0x9a09A49870353867b0ce9901B44E84C32B2A47AC) | Proxy: [`UUPS@5.1.0`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.1.0/contracts/proxy/utils/UUPSUpgradeable.sol) |   diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 00000000..0cd755bb --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,27 @@ +# Release Notes + +## [v0.1.1] 2024-06-xx + +### Functions + +#### Modified +- `function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external` +- `function updateDisabledFeatures(uint32 value) external onlyOwner` + +### Errors + +#### New +- `error InvalidDisabledFeatures();` +- `error InvalidFeeExpireTime();` +- `error InvalidFeeTimelockPeriod();` +- `error InvalidMaxShares();` +- `error InvalidObligationExpireTime();` +- `error InvalidObligationTimelockPeriod();` +- `error InvalidTokenUpdateTimelockPeriod();` +- `error InvalidWithdrawalExpireTime();` +- `error InvalidWithdrawalTimelockPeriod();` + +### Events + +#### Modified +- `event BAppRegistered(address indexed bApp, ICore.TokenConfig[] tokenConfigs, string metadataURI);` diff --git a/artifacts/deploy-hoodi-prod.json b/artifacts/deploy-hoodi-prod.json new file mode 100644 index 00000000..d9a64146 --- /dev/null +++ b/artifacts/deploy-hoodi-prod.json @@ -0,0 +1,25 @@ +{ + "addresses": { + "BAppsModule": "0xCf9894B93C34E9Af8E912EcE7089DCE879740141", + "ProtocolModule": "0xA11022BB79cE25F9bAdfDB426746A4a47a1FD768", + "SSVBasedAppsImpl": "0xe5a75122b158D405518C6752aEE5D40b8FB1C364", + "SSVBasedAppsProxy": "0xc7fCFeEc5FB9962bDC2234A7a25dCec739e27f9f", + "StrategyModule": "0x0B81DcaF34f4455221A7d446eA41A70B8Baf91FF" + }, + "chainInfo": { + "chainId": 560048, + "deploymentBlock": 260728 + }, + "parameters": { + "disabledFeatures": 0, + "feeExpireTime": 3600, + "feeTimelockPeriod": 86400, + "maxFeeIncrement": 500, + "maxShares": 100000000000000000000000000000000000000000000000000, + "obligationExpireTime": 86400, + "obligationTimelockPeriod": 172800, + "tokenUpdateTimelockPeriod": 3600, + "withdrawalExpireTime": 86400, + "withdrawalTimelockPeriod": 172800 + } +} \ No newline at end of file diff --git a/artifacts/deploy-hoodi-stage.json b/artifacts/deploy-hoodi-stage.json new file mode 100644 index 00000000..3e35ccf0 --- /dev/null +++ b/artifacts/deploy-hoodi-stage.json @@ -0,0 +1,25 @@ +{ + "addresses": { + "BAppsModule": "0xdC97f7935Ed6FDcA407dDdE95EB26f7807E52Dc9", + "ProtocolModule": "0x7AadaE0f159eECCe6a4Faad78FFF94e1eab4C540", + "SSVBasedAppsImpl": "0xfCD7243D8fBb441497fEd67e19B7681C27BB20aD", + "SSVBasedAppsProxy": "0x40d959B95e7c56962D6d388d87921c03734b9C2C", + "StrategyModule": "0x7cF08Af55c6939BB748D4c4D2d219791EDe9dBC1" + }, + "chainInfo": { + "chainId": 560048, + "deploymentBlock": 259312 + }, + "parameters": { + "disabledFeatures": 0, + "feeExpireTime": 3600, + "feeTimelockPeriod": 300, + "maxFeeIncrement": 500, + "maxShares": 100000000000000000000000000000000000000000000000000, + "obligationExpireTime": 3600, + "obligationTimelockPeriod": 300, + "tokenUpdateTimelockPeriod": 300, + "withdrawalExpireTime": 3600, + "withdrawalTimelockPeriod": 300 + } +} \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..4e42a1bc --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book/ \ No newline at end of file diff --git a/docs/book.css b/docs/book.css new file mode 100644 index 00000000..b5ce903f --- /dev/null +++ b/docs/book.css @@ -0,0 +1,13 @@ +table { + margin: 0 auto; + border-collapse: collapse; + width: 100%; +} + +table td:first-child { + width: 15%; +} + +table td:nth-child(2) { + width: 25%; +} \ No newline at end of file diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 00000000..a2e2683a --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,13 @@ +[book] +src = "src" +title = "" + +[output.html] +no-section-label = true +additional-js = ["solidity.min.js"] +additional-css = ["book.css"] +mathjax-support = true +git-repository-url = "https://github.com/ssvlabs/based-applications" + +[output.html.fold] +enable = true diff --git a/docs/solidity.min.js b/docs/solidity.min.js new file mode 100644 index 00000000..19249329 --- /dev/null +++ b/docs/solidity.min.js @@ -0,0 +1,74 @@ +hljs.registerLanguage("solidity",(()=>{"use strict";function e(){try{return!0 +}catch(e){return!1}} +var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/ +;e()&&(a=a.source.replace(/\\b/g,"(?{ +var a=r(e),o=l(e),c=/[A-Za-z_$][A-Za-z_$0-9.]*/,d=e.inherit(e.TITLE_MODE,{ +begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:c,keywords:n}),u={className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:c,keywords:n, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,o,s]},_={ +className:"operator",begin:/:=|->/};return{keywords:n,lexemes:c, +contains:[a,o,i,t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,_,{ +className:"function",lexemes:c,beginKeywords:"function",end:"{",excludeEnd:!0, +contains:[d,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,_]}]}}, +solAposStringMode:r,solQuoteStringMode:l,HEX_APOS_STRING_MODE:i, +HEX_QUOTE_STRING_MODE:t,SOL_NUMBER:s,isNegativeLookbehindAvailable:e} +;const{baseAssembly:c,solAposStringMode:d,solQuoteStringMode:u,HEX_APOS_STRING_MODE:_,HEX_QUOTE_STRING_MODE:m,SOL_NUMBER:b,isNegativeLookbehindAvailable:E}=o +;return e=>{for(var a=d(e),s=u(e),n=[],i=0;i<32;i++)n[i]=i+1 +;var t=n.map((e=>8*e)),r=[];for(i=0;i<=80;i++)r[i]=i +;var l=n.map((e=>"bytes"+e)).join(" ")+" ",o=t.map((e=>"uint"+e)).join(" ")+" ",g=t.map((e=>"int"+e)).join(" ")+" ",M=[].concat.apply([],t.map((e=>r.map((a=>e+"x"+a))))),p={ +keyword:"var bool string int uint "+g+o+"byte bytes "+l+"fixed ufixed "+M.map((e=>"fixed"+e)).join(" ")+" "+M.map((e=>"ufixed"+e)).join(" ")+" enum struct mapping address new delete if else for while continue break return throw emit try catch revert unchecked _ function modifier event constructor fallback receive error virtual override constant immutable anonymous indexed storage memory calldata external public internal payable pure view private returns import from as using pragma contract interface library is abstract type assembly", +literal:"true false wei gwei szabo finney ether seconds minutes hours days weeks years", +built_in:"self this super selfdestruct suicide now msg block tx abi blockhash gasleft assert require Error Panic sha3 sha256 keccak256 ripemd160 ecrecover addmod mulmod log0 log1 log2 log3 log4" +},O={className:"operator",begin:/[+\-!~*\/%<>&^|=]/ +},C=/[A-Za-z_$][A-Za-z_$0-9]*/,N={className:"params",begin:/\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,lexemes:C,keywords:p, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,b,"self"]},f={ +begin:/\.\s*/,end:/[^A-Za-z0-9$_\.]/,excludeBegin:!0,excludeEnd:!0,keywords:{ +built_in:"gas value selector address length push pop send transfer call callcode delegatecall staticcall balance code codehash wrap unwrap name creationCode runtimeCode interfaceId min max" +},relevance:2},y=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/, +lexemes:C,keywords:p}),w={className:"built_in", +begin:(E()?"(?`](https://holesky.etherscan.io/address/0x1Bd6ceB98Daf7FfEB590236b720F81b65213836A) | [``](https://holesky.etherscan.io/address/0x9a09A49870353867b0ce9901B44E84C32B2A47AC) | Proxy: [`UUPS@5.1.0`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.1.0/contracts/proxy/utils/UUPSUpgradeable.sol) | + +  + +## :scroll: _License_ + +2025 SSV Network + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License, or any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the [GNU General Public License](LICENSE) +along with this program. If not, see . \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 00000000..2a23b608 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,35 @@ +# Summary +- [Home](README.md) +# src + - [❱ core](src/core/README.md) + - [❱ interfaces](src/core/interfaces/README.md) + - [IBasedAppManager](src/core/interfaces/IBasedAppManager.sol/interface.IBasedAppManager.md) + - [ICore](src/core/interfaces/ICore.sol/interface.ICore.md) + - [IProtocolManager](src/core/interfaces/IProtocolManager.sol/interface.IProtocolManager.md) + - [ISSVBasedApps](src/core/interfaces/ISSVBasedApps.sol/interface.ISSVBasedApps.md) + - [IStrategyManager](src/core/interfaces/IStrategyManager.sol/interface.IStrategyManager.md) + - [IViews](src/core/interfaces/IViews.sol/interface.IViews.md) + - [❱ libraries](src/core/libraries/README.md) + - [SSVCoreModules](src/core/libraries/CoreStorageLib.sol/enum.SSVCoreModules.md) + - [CoreStorageLib](src/core/libraries/CoreStorageLib.sol/library.CoreStorageLib.md) + - [ProtocolStorageLib](src/core/libraries/ProtocolStorageLib.sol/library.ProtocolStorageLib.md) + - [ValidationLib](src/core/libraries/ValidationLib.sol/library.ValidationLib.md) + - [ValidationLib constants](src/core/libraries/ValidationLib.sol/constants.ValidationLib.md) + - [❱ modules](src/core/modules/README.md) + - [BasedAppsManager](src/core/modules/BasedAppsManager.sol/contract.BasedAppsManager.md) + - [ProtocolManager](src/core/modules/ProtocolManager.sol/contract.ProtocolManager.md) + - [StrategyManager](src/core/modules/StrategyManager.sol/contract.StrategyManager.md) + - [SSVBasedApps](src/core/SSVBasedApps.sol/contract.SSVBasedApps.md) + - [❱ middleware](src/middleware/README.md) + - [❱ examples](src/middleware/examples/README.md) + - [WhitelistExample](src/middleware/examples/WhitelistExample.sol/contract.WhitelistExample.md) + - [❱ interfaces](src/middleware/interfaces/README.md) + - [IBasedApp](src/middleware/interfaces/IBasedApp.sol/interface.IBasedApp.md) + - [IBasedAppWhitelisted](src/middleware/interfaces/IBasedAppWhitelisted.sol/interface.IBasedAppWhitelisted.md) + - [❱ modules](src/middleware/modules/README.md) + - [❱ core](src/middleware/modules/core/README.md) + - [BasedAppCore](src/middleware/modules/core/BasedAppCore.sol/abstract.BasedAppCore.md) + - [❱ core+roles](src/middleware/modules/core+roles/README.md) + - [AccessControlBasedApp](src/middleware/modules/core+roles/AccessControlBasedApp.sol/abstract.AccessControlBasedApp.md) + - [OwnableBasedApp](src/middleware/modules/core+roles/OwnableBasedApp.sol/abstract.OwnableBasedApp.md) + - [BasedAppWhitelisted](src/middleware/modules/BasedAppWhitelisted.sol/abstract.BasedAppWhitelisted.md) diff --git a/docs/src/src/README.md b/docs/src/src/README.md new file mode 100644 index 00000000..2c17bcbb --- /dev/null +++ b/docs/src/src/README.md @@ -0,0 +1,5 @@ + + +# Contents +- [core](/src/core) +- [middleware](/src/middleware) diff --git a/docs/src/src/core/README.md b/docs/src/src/core/README.md new file mode 100644 index 00000000..3d574836 --- /dev/null +++ b/docs/src/src/core/README.md @@ -0,0 +1,7 @@ + + +# Contents +- [interfaces](/src/core/interfaces) +- [libraries](/src/core/libraries) +- [modules](/src/core/modules) +- [SSVBasedApps](SSVBasedApps.sol/contract.SSVBasedApps.md) diff --git a/docs/src/src/core/SSVBasedApps.sol/contract.SSVBasedApps.md b/docs/src/src/core/SSVBasedApps.sol/contract.SSVBasedApps.md new file mode 100644 index 00000000..57b5d4f6 --- /dev/null +++ b/docs/src/src/core/SSVBasedApps.sol/contract.SSVBasedApps.md @@ -0,0 +1,572 @@ +# SSVBasedApps +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/SSVBasedApps.sol) + +**Inherits:** +[ISSVBasedApps](/src/core/interfaces/ISSVBasedApps.sol/interface.ISSVBasedApps.md), UUPSUpgradeable, Ownable2StepUpgradeable + +**Author:** + +Marco Tabasco +Riccardo Persiani + +The Core Contract to manage Based Applications, Validator Balance Delegations & Strategies for SSV Based Applications Platform. +GLOSSARY ** + +*The following terms are used throughout the contract: +- **Account**: An Ethereum address that can: +1. Delegate its balance to another address. +2. Create and manage a strategy. +3. Create and manage or be a bApp. +- **Based Application**: or bApp. +The entity that requests validation services from operators. On-chain is represented by an Ethereum address. +A bApp can be created by registering to this Core Contract, specifying the risk level. +The bApp can also specify one or many tokens as slashable capital to be provided by strategies. +During the bApp registration, the bApp owner can set the shared risk level and optionally a metadata URI, to be used in the SSV bApp marketplace. +- **Delegator**: An Ethereum address that has Ethereum Validator Balance of Staked ETH within the SSV platform. This capital delegated is non-slashable. +The delegator can decide to delegate its balance to itself or/and to a single or many receiver accounts. +The delegator has to set its address as the receiver account, when the delegator wants to delegate its balance to itself. +The delegated balance goes to an account and not to a strategy. This receiver account can manage only a single strategy. +- **Strategy**: The entity that manages the slashable assets bounded to based apps. +The strategy has its own balance, accounted in this core contract. +The strategy can be created by an account that becomes its owner. +The assets can be ERC20 tokens or Native ETH tokens, that can be deposited or withdrawn by the participants. +The strategy can manage its assets via s.obligations to one or many bApps. +- **Obligation**: A percentage of the strategy's balance of ERC20 (or Native ETH), that is reserved for securing a bApp. +The obligation is set exclusively by the strategy owner and can be updated by the strategy owner. +The tokens specified in an obligation needs to match the tokens specified in the bApp. +AUTHORS *** + + +## Functions +### initialize + + +```solidity +function initialize( + address owner_, + IBasedAppManager ssvBasedAppManger_, + IStrategyManager ssvStrategyManager_, + IProtocolManager protocolManager_, + ProtocolStorageLib.Data calldata config +) external override initializer onlyProxy; +``` + +### __SSVBasedApplications_init_unchained + + +```solidity +function __SSVBasedApplications_init_unchained( + IBasedAppManager ssvBasedAppManger_, + IStrategyManager ssvStrategyManager_, + IProtocolManager protocolManager_, + ProtocolStorageLib.Data calldata config +) internal onlyInitializing; +``` + +### constructor + +**Note:** +oz-upgrades-unsafe-allow: constructor + + +```solidity +constructor(); +``` + +### _authorizeUpgrade + + +```solidity +function _authorizeUpgrade(address) internal override onlyOwner; +``` + +### updateBAppMetadataURI + + +```solidity +function updateBAppMetadataURI(string calldata metadataURI) external; +``` + +### registerBApp + + +```solidity +function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external; +``` + +### updateBAppsTokens + + +```solidity +function updateBAppsTokens(ICore.TokenConfig[] calldata tokenConfigs) external; +``` + +### createObligation + + +```solidity +function createObligation(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) external; +``` + +### createStrategy + + +```solidity +function createStrategy(uint32 fee, string calldata metadataURI) external returns (uint32 strategyId); +``` + +### delegateBalance + + +```solidity +function delegateBalance(address receiver, uint32 percentage) external; +``` + +### depositERC20 + + +```solidity +function depositERC20(uint32 strategyId, IERC20 token, uint256 amount) external; +``` + +### depositETH + + +```solidity +function depositETH(uint32 strategyId) external payable; +``` + +### finalizeFeeUpdate + + +```solidity +function finalizeFeeUpdate(uint32 strategyId) external; +``` + +### finalizeUpdateObligation + + +```solidity +function finalizeUpdateObligation(uint32 strategyId, address bApp, address token) external; +``` + +### finalizeWithdrawal + + +```solidity +function finalizeWithdrawal(uint32 strategyId, IERC20 token) external; +``` + +### finalizeWithdrawalETH + + +```solidity +function finalizeWithdrawalETH(uint32 strategyId) external; +``` + +### getSlashableBalance + + +```solidity +function getSlashableBalance(uint32 strategyId, address bApp, address token) public view returns (uint256 slashableBalance); +``` + +### proposeFeeUpdate + + +```solidity +function proposeFeeUpdate(uint32 strategyId, uint32 proposedFee) external; +``` + +### proposeUpdateObligation + + +```solidity +function proposeUpdateObligation(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) external; +``` + +### proposeWithdrawal + + +```solidity +function proposeWithdrawal(uint32 strategyId, address token, uint256 amount) external; +``` + +### proposeWithdrawalETH + + +```solidity +function proposeWithdrawalETH(uint32 strategyId, uint256 amount) external; +``` + +### reduceFee + + +```solidity +function reduceFee(uint32 strategyId, uint32 proposedFee) external; +``` + +### removeDelegatedBalance + + +```solidity +function removeDelegatedBalance(address receiver) external; +``` + +### updateDelegatedBalance + + +```solidity +function updateDelegatedBalance(address receiver, uint32 percentage) external; +``` + +### updateStrategyMetadataURI + + +```solidity +function updateStrategyMetadataURI(uint32 strategyId, string calldata metadataURI) external; +``` + +### updateAccountMetadataURI + + +```solidity +function updateAccountMetadataURI(string calldata metadataURI) external; +``` + +### slash + + +```solidity +function slash(uint32 strategyId, address bApp, address token, uint32 percentage, bytes calldata data) external; +``` + +### withdrawSlashingFund + + +```solidity +function withdrawSlashingFund(address token, uint256 amount) external; +``` + +### withdrawETHSlashingFund + + +```solidity +function withdrawETHSlashingFund(uint256 amount) external; +``` + +### optInToBApp + + +```solidity +function optInToBApp(uint32 strategyId, address bApp, address[] calldata tokens, uint32[] calldata obligationPercentages, bytes calldata data) external; +``` + +### updateFeeTimelockPeriod + + +```solidity +function updateFeeTimelockPeriod(uint32 value) external onlyOwner; +``` + +### updateFeeExpireTime + + +```solidity +function updateFeeExpireTime(uint32 value) external onlyOwner; +``` + +### updateWithdrawalTimelockPeriod + + +```solidity +function updateWithdrawalTimelockPeriod(uint32 value) external onlyOwner; +``` + +### updateWithdrawalExpireTime + + +```solidity +function updateWithdrawalExpireTime(uint32 value) external onlyOwner; +``` + +### updateObligationTimelockPeriod + + +```solidity +function updateObligationTimelockPeriod(uint32 value) external onlyOwner; +``` + +### updateObligationExpireTime + + +```solidity +function updateObligationExpireTime(uint32 value) external onlyOwner; +``` + +### updateTokenUpdateTimelockPeriod + + +```solidity +function updateTokenUpdateTimelockPeriod(uint32 value) external onlyOwner; +``` + +### updateMaxShares + + +```solidity +function updateMaxShares(uint256 value) external onlyOwner; +``` + +### updateMaxFeeIncrement + + +```solidity +function updateMaxFeeIncrement(uint32 value) external onlyOwner; +``` + +### updateDisabledFeatures + + +```solidity +function updateDisabledFeatures(uint32 value) external onlyOwner; +``` + +### delegations + + +```solidity +function delegations(address account, address receiver) external view returns (uint32); +``` + +### totalDelegatedPercentage + + +```solidity +function totalDelegatedPercentage(address delegator) external view returns (uint32); +``` + +### registeredBApps + + +```solidity +function registeredBApps(address bApp) external view returns (bool isRegistered); +``` + +### strategies + + +```solidity +function strategies(uint32 strategyId) external view returns (address strategyOwner, uint32 fee); +``` + +### ownedStrategies + + +```solidity +function ownedStrategies(address owner) external view returns (uint32[] memory strategyIds); +``` + +### strategyAccountShares + + +```solidity +function strategyAccountShares(uint32 strategyId, address account, address token) external view returns (uint256); +``` + +### strategyTotalBalance + + +```solidity +function strategyTotalBalance(uint32 strategyId, address token) external view returns (uint256); +``` + +### strategyTotalShares + + +```solidity +function strategyTotalShares(uint32 strategyId, address token) external view returns (uint256); +``` + +### strategyGeneration + + +```solidity +function strategyGeneration(uint32 strategyId, address token) external view returns (uint256); +``` + +### obligations + + +```solidity +function obligations(uint32 strategyId, address bApp, address token) external view returns (uint32 percentage, bool isSet); +``` + +### bAppTokens + + +```solidity +function bAppTokens(address bApp, address token) external view returns (uint32 currentValue, bool isSet, uint32 pendingValue, uint32 effectTime); +``` + +### accountBAppStrategy + + +```solidity +function accountBAppStrategy(address account, address bApp) external view returns (uint32); +``` + +### feeUpdateRequests + + +```solidity +function feeUpdateRequests(uint32 strategyId) external view returns (uint32 percentage, uint32 requestTime); +``` + +### withdrawalRequests + + +```solidity +function withdrawalRequests(uint32 strategyId, address account, address token) external view returns (uint256 shares, uint32 requestTime); +``` + +### obligationRequests + + +```solidity +function obligationRequests(uint32 strategyId, address token, address bApp) external view returns (uint32 percentage, uint32 requestTime); +``` + +### slashingFund + + +```solidity +function slashingFund(address account, address token) external view returns (uint256); +``` + +### maxPercentage + + +```solidity +function maxPercentage() external pure returns (uint32); +``` + +### ethAddress + + +```solidity +function ethAddress() external pure returns (address); +``` + +### maxShares + + +```solidity +function maxShares() external view returns (uint256); +``` + +### maxFeeIncrement + + +```solidity +function maxFeeIncrement() external view returns (uint32); +``` + +### feeTimelockPeriod + + +```solidity +function feeTimelockPeriod() external view returns (uint32); +``` + +### feeExpireTime + + +```solidity +function feeExpireTime() external view returns (uint32); +``` + +### withdrawalTimelockPeriod + + +```solidity +function withdrawalTimelockPeriod() external view returns (uint32); +``` + +### withdrawalExpireTime + + +```solidity +function withdrawalExpireTime() external view returns (uint32); +``` + +### obligationTimelockPeriod + + +```solidity +function obligationTimelockPeriod() external view returns (uint32); +``` + +### obligationExpireTime + + +```solidity +function obligationExpireTime() external view returns (uint32); +``` + +### disabledFeatures + + +```solidity +function disabledFeatures() external view returns (uint32); +``` + +### tokenUpdateTimelockPeriod + + +```solidity +function tokenUpdateTimelockPeriod() external view returns (uint32); +``` + +### getVersion + + +```solidity +function getVersion() external pure returns (string memory); +``` + +### getModuleAddress + +Retrieves the currently configured Module contract address. + + +```solidity +function getModuleAddress(SSVCoreModules moduleId) external view returns (address); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`moduleId`|`SSVCoreModules`|The ID of the SSV Module.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address`|The address of the SSV Module.| + + +### updateModule + + +```solidity +function updateModule(SSVCoreModules[] calldata moduleIds, address[] calldata moduleAddresses) external onlyOwner; +``` + +### _delegateTo + + +```solidity +function _delegateTo(SSVCoreModules moduleId) internal; +``` + diff --git a/docs/src/src/core/interfaces/IBasedAppManager.sol/interface.IBasedAppManager.md b/docs/src/src/core/interfaces/IBasedAppManager.sol/interface.IBasedAppManager.md new file mode 100644 index 00000000..4fb15889 --- /dev/null +++ b/docs/src/src/core/interfaces/IBasedAppManager.sol/interface.IBasedAppManager.md @@ -0,0 +1,76 @@ +# IBasedAppManager +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/interfaces/IBasedAppManager.sol) + + +## Functions +### registerBApp + + +```solidity +function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external; +``` + +### updateBAppMetadataURI + + +```solidity +function updateBAppMetadataURI(string calldata metadataURI) external; +``` + +### updateBAppsTokens + + +```solidity +function updateBAppsTokens(ICore.TokenConfig[] calldata tokenConfigs) external; +``` + +## Events +### BAppMetadataURIUpdated + +```solidity +event BAppMetadataURIUpdated(address indexed bApp, string metadataURI); +``` + +### BAppRegistered + +```solidity +event BAppRegistered(address indexed bApp, ICore.TokenConfig[] tokenConfigs, string metadataURI); +``` + +### BAppTokensUpdated + +```solidity +event BAppTokensUpdated(address indexed bApp, ICore.TokenConfig[] tokenConfigs); +``` + +## Errors +### BAppAlreadyRegistered + +```solidity +error BAppAlreadyRegistered(); +``` + +### BAppDoesNotSupportInterface + +```solidity +error BAppDoesNotSupportInterface(); +``` + +### BAppNotRegistered + +```solidity +error BAppNotRegistered(); +``` + +### TokenAlreadyAddedToBApp + +```solidity +error TokenAlreadyAddedToBApp(address token); +``` + +### ZeroAddressNotAllowed + +```solidity +error ZeroAddressNotAllowed(); +``` + diff --git a/docs/src/src/core/interfaces/ICore.sol/interface.ICore.md b/docs/src/src/core/interfaces/ICore.sol/interface.ICore.md new file mode 100644 index 00000000..24ae6f6e --- /dev/null +++ b/docs/src/src/core/interfaces/ICore.sol/interface.ICore.md @@ -0,0 +1,105 @@ +# ICore +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/interfaces/ICore.sol) + + +## Structs +### SharedRiskLevel +Represents a SharedRiskLevel + + +```solidity +struct SharedRiskLevel { + uint32 currentValue; + bool isSet; + uint32 pendingValue; + uint32 effectTime; +} +``` + +### Obligation +Represents an Obligation + + +```solidity +struct Obligation { + uint32 percentage; + bool isSet; +} +``` + +### Strategy +Represents a Strategy + + +```solidity +struct Strategy { + address owner; + uint32 fee; +} +``` + +### FeeUpdateRequest +Represents a FeeUpdateRequest + + +```solidity +struct FeeUpdateRequest { + uint32 percentage; + uint32 requestTime; +} +``` + +### WithdrawalRequest +Represents a request for a withdrawal from a participant of a strategy + + +```solidity +struct WithdrawalRequest { + uint256 shares; + uint32 requestTime; +} +``` + +### ObligationRequest +Represents a change in the obligation in a strategy. Only the owner can submit one. + + +```solidity +struct ObligationRequest { + uint32 percentage; + uint32 requestTime; +} +``` + +### Shares +Represents the shares system of a strategy + + +```solidity +struct Shares { + uint256 totalTokenBalance; + uint256 totalShareBalance; + uint256 currentGeneration; + mapping(address => uint256) accountShareBalance; + mapping(address => uint256) accountGeneration; +} +``` + +### TokenUpdateRequest + +```solidity +struct TokenUpdateRequest { + TokenConfig[] tokens; + uint32 requestTime; +} +``` + +### TokenConfig + +```solidity +struct TokenConfig { + address token; + uint32 sharedRiskLevel; +} +``` + diff --git a/docs/src/src/core/interfaces/IProtocolManager.sol/interface.IProtocolManager.md b/docs/src/src/core/interfaces/IProtocolManager.sol/interface.IProtocolManager.md new file mode 100644 index 00000000..bb7fa5b1 --- /dev/null +++ b/docs/src/src/core/interfaces/IProtocolManager.sol/interface.IProtocolManager.md @@ -0,0 +1,129 @@ +# IProtocolManager +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/interfaces/IProtocolManager.sol) + + +## Functions +### updateFeeExpireTime + + +```solidity +function updateFeeExpireTime(uint32 value) external; +``` + +### updateFeeTimelockPeriod + + +```solidity +function updateFeeTimelockPeriod(uint32 value) external; +``` + +### updateMaxFeeIncrement + + +```solidity +function updateMaxFeeIncrement(uint32 value) external; +``` + +### updateMaxShares + + +```solidity +function updateMaxShares(uint256 value) external; +``` + +### updateObligationExpireTime + + +```solidity +function updateObligationExpireTime(uint32 value) external; +``` + +### updateObligationTimelockPeriod + + +```solidity +function updateObligationTimelockPeriod(uint32 value) external; +``` + +### updateTokenUpdateTimelockPeriod + + +```solidity +function updateTokenUpdateTimelockPeriod(uint32 value) external; +``` + +### updateWithdrawalExpireTime + + +```solidity +function updateWithdrawalExpireTime(uint32 value) external; +``` + +### updateWithdrawalTimelockPeriod + + +```solidity +function updateWithdrawalTimelockPeriod(uint32 value) external; +``` + +## Events +### FeeExpireTimeUpdated + +```solidity +event FeeExpireTimeUpdated(uint32 feeExpireTime); +``` + +### FeeTimelockPeriodUpdated + +```solidity +event FeeTimelockPeriodUpdated(uint32 feeTimelockPeriod); +``` + +### ObligationExpireTimeUpdated + +```solidity +event ObligationExpireTimeUpdated(uint32 obligationExpireTime); +``` + +### ObligationTimelockPeriodUpdated + +```solidity +event ObligationTimelockPeriodUpdated(uint32 obligationTimelockPeriod); +``` + +### TokenUpdateTimelockPeriodUpdated + +```solidity +event TokenUpdateTimelockPeriodUpdated(uint32 tokenUpdateTimelockPeriod); +``` + +### StrategyMaxFeeIncrementUpdated + +```solidity +event StrategyMaxFeeIncrementUpdated(uint32 maxFeeIncrement); +``` + +### StrategyMaxSharesUpdated + +```solidity +event StrategyMaxSharesUpdated(uint256 maxShares); +``` + +### WithdrawalExpireTimeUpdated + +```solidity +event WithdrawalExpireTimeUpdated(uint32 withdrawalExpireTime); +``` + +### WithdrawalTimelockPeriodUpdated + +```solidity +event WithdrawalTimelockPeriodUpdated(uint32 withdrawalTimelockPeriod); +``` + +### DisabledFeaturesUpdated + +```solidity +event DisabledFeaturesUpdated(uint32 disabledFeatures); +``` + diff --git a/docs/src/src/core/interfaces/ISSVBasedApps.sol/interface.ISSVBasedApps.md b/docs/src/src/core/interfaces/ISSVBasedApps.sol/interface.ISSVBasedApps.md new file mode 100644 index 00000000..ca7e186e --- /dev/null +++ b/docs/src/src/core/interfaces/ISSVBasedApps.sol/interface.ISSVBasedApps.md @@ -0,0 +1,109 @@ +# ISSVBasedApps +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/interfaces/ISSVBasedApps.sol) + +**Inherits:** +[IStrategyManager](/src/core/interfaces/IStrategyManager.sol/interface.IStrategyManager.md), [IBasedAppManager](/src/core/interfaces/IBasedAppManager.sol/interface.IBasedAppManager.md), [IProtocolManager](/src/core/interfaces/IProtocolManager.sol/interface.IProtocolManager.md), [IViews](/src/core/interfaces/IViews.sol/interface.IViews.md) + + +## Functions +### getModuleAddress + + +```solidity +function getModuleAddress(SSVCoreModules moduleId) external view returns (address); +``` + +### initialize + + +```solidity +function initialize( + address owner_, + IBasedAppManager ssvBasedAppManger_, + IStrategyManager ssvStrategyManager_, + IProtocolManager protocolManager_, + ProtocolStorageLib.Data memory config +) external; +``` + +### updateModule + + +```solidity +function updateModule(SSVCoreModules[] calldata moduleIds, address[] calldata moduleAddresses) external; +``` + +## Events +### ModuleUpdated + +```solidity +event ModuleUpdated(SSVCoreModules indexed moduleId, address moduleAddress); +``` + +## Errors +### InvalidMaxFeeIncrement + +```solidity +error InvalidMaxFeeIncrement(); +``` + +### InvalidMaxShares + +```solidity +error InvalidMaxShares(); +``` + +### InvalidFeeTimelockPeriod + +```solidity +error InvalidFeeTimelockPeriod(); +``` + +### InvalidFeeExpireTime + +```solidity +error InvalidFeeExpireTime(); +``` + +### InvalidWithdrawalTimelockPeriod + +```solidity +error InvalidWithdrawalTimelockPeriod(); +``` + +### InvalidWithdrawalExpireTime + +```solidity +error InvalidWithdrawalExpireTime(); +``` + +### InvalidObligationTimelockPeriod + +```solidity +error InvalidObligationTimelockPeriod(); +``` + +### InvalidObligationExpireTime + +```solidity +error InvalidObligationExpireTime(); +``` + +### InvalidTokenUpdateTimelockPeriod + +```solidity +error InvalidTokenUpdateTimelockPeriod(); +``` + +### InvalidDisabledFeatures + +```solidity +error InvalidDisabledFeatures(); +``` + +### TargetModuleDoesNotExist + +```solidity +error TargetModuleDoesNotExist(uint8 moduleId); +``` + diff --git a/docs/src/src/core/interfaces/IStrategyManager.sol/interface.IStrategyManager.md b/docs/src/src/core/interfaces/IStrategyManager.sol/interface.IStrategyManager.md new file mode 100644 index 00000000..e77b31e8 --- /dev/null +++ b/docs/src/src/core/interfaces/IStrategyManager.sol/interface.IStrategyManager.md @@ -0,0 +1,449 @@ +# IStrategyManager +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/interfaces/IStrategyManager.sol) + + +## Functions +### createObligation + + +```solidity +function createObligation(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) external; +``` + +### createStrategy + + +```solidity +function createStrategy(uint32 fee, string calldata metadataURI) external returns (uint32 strategyId); +``` + +### delegateBalance + + +```solidity +function delegateBalance(address receiver, uint32 percentage) external; +``` + +### depositERC20 + + +```solidity +function depositERC20(uint32 strategyId, IERC20 token, uint256 amount) external; +``` + +### depositETH + + +```solidity +function depositETH(uint32 strategyId) external payable; +``` + +### finalizeFeeUpdate + + +```solidity +function finalizeFeeUpdate(uint32 strategyId) external; +``` + +### finalizeUpdateObligation + + +```solidity +function finalizeUpdateObligation(uint32 strategyId, address bApp, address token) external; +``` + +### finalizeWithdrawal + + +```solidity +function finalizeWithdrawal(uint32 strategyId, IERC20 token) external; +``` + +### finalizeWithdrawalETH + + +```solidity +function finalizeWithdrawalETH(uint32 strategyId) external; +``` + +### optInToBApp + + +```solidity +function optInToBApp(uint32 strategyId, address bApp, address[] calldata tokens, uint32[] calldata obligationPercentages, bytes calldata data) external; +``` + +### proposeFeeUpdate + + +```solidity +function proposeFeeUpdate(uint32 strategyId, uint32 proposedFee) external; +``` + +### proposeUpdateObligation + + +```solidity +function proposeUpdateObligation(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) external; +``` + +### proposeWithdrawal + + +```solidity +function proposeWithdrawal(uint32 strategyId, address token, uint256 amount) external; +``` + +### proposeWithdrawalETH + + +```solidity +function proposeWithdrawalETH(uint32 strategyId, uint256 amount) external; +``` + +### reduceFee + + +```solidity +function reduceFee(uint32 strategyId, uint32 proposedFee) external; +``` + +### removeDelegatedBalance + + +```solidity +function removeDelegatedBalance(address receiver) external; +``` + +### slash + + +```solidity +function slash(uint32 strategyId, address bApp, address token, uint32 percentage, bytes calldata data) external; +``` + +### updateAccountMetadataURI + + +```solidity +function updateAccountMetadataURI(string calldata metadataURI) external; +``` + +### updateDelegatedBalance + + +```solidity +function updateDelegatedBalance(address receiver, uint32 percentage) external; +``` + +### updateStrategyMetadataURI + + +```solidity +function updateStrategyMetadataURI(uint32 strategyId, string calldata metadataURI) external; +``` + +### withdrawETHSlashingFund + + +```solidity +function withdrawETHSlashingFund(uint256 amount) external; +``` + +### withdrawSlashingFund + + +```solidity +function withdrawSlashingFund(address token, uint256 amount) external; +``` + +## Events +### AccountMetadataURIUpdated + +```solidity +event AccountMetadataURIUpdated(address indexed account, string metadataURI); +``` + +### BAppOptedInByStrategy + +```solidity +event BAppOptedInByStrategy(uint32 indexed strategyId, address indexed bApp, bytes data, address[] tokens, uint32[] obligationPercentages); +``` + +### DelegationCreated + +```solidity +event DelegationCreated(address indexed delegator, address indexed receiver, uint32 percentage); +``` + +### DelegationRemoved + +```solidity +event DelegationRemoved(address indexed delegator, address indexed receiver); +``` + +### DelegationUpdated + +```solidity +event DelegationUpdated(address indexed delegator, address indexed receiver, uint32 percentage); +``` + +### MaxFeeIncrementSet + +```solidity +event MaxFeeIncrementSet(uint32 newMaxFeeIncrement); +``` + +### ObligationCreated + +```solidity +event ObligationCreated(uint32 indexed strategyId, address indexed bApp, address token, uint32 percentage); +``` + +### ObligationUpdated + +```solidity +event ObligationUpdated(uint32 indexed strategyId, address indexed bApp, address token, uint32 percentage); +``` + +### ObligationUpdateProposed + +```solidity +event ObligationUpdateProposed(uint32 indexed strategyId, address indexed bApp, address token, uint32 percentage); +``` + +### StrategyCreated + +```solidity +event StrategyCreated(uint32 indexed strategyId, address indexed owner, uint32 fee, string metadataURI); +``` + +### StrategyDeposit + +```solidity +event StrategyDeposit(uint32 indexed strategyId, address indexed account, address token, uint256 amount); +``` + +### StrategyFeeUpdated + +```solidity +event StrategyFeeUpdated(uint32 indexed strategyId, address owner, uint32 newFee, bool isFast); +``` + +### StrategyFeeUpdateProposed + +```solidity +event StrategyFeeUpdateProposed(uint32 indexed strategyId, address owner, uint32 proposedFee); +``` + +### StrategyMetadataURIUpdated + +```solidity +event StrategyMetadataURIUpdated(uint32 indexed strategyId, string metadataURI); +``` + +### StrategyWithdrawal + +```solidity +event StrategyWithdrawal(uint32 indexed strategyId, address indexed account, address token, uint256 amount, bool isFast); +``` + +### StrategyWithdrawalProposed + +```solidity +event StrategyWithdrawalProposed(uint32 indexed strategyId, address indexed account, address token, uint256 amount); +``` + +### SlashingFundWithdrawn + +```solidity +event SlashingFundWithdrawn(address token, uint256 amount); +``` + +### StrategySlashed + +```solidity +event StrategySlashed(uint32 indexed strategyId, address indexed bApp, address token, uint32 percentage, address receiver); +``` + +## Errors +### BAppAlreadyOptedIn + +```solidity +error BAppAlreadyOptedIn(); +``` + +### BAppNotOptedIn + +```solidity +error BAppNotOptedIn(); +``` + +### BAppOptInFailed + +```solidity +error BAppOptInFailed(); +``` + +### BAppSlashingFailed + +```solidity +error BAppSlashingFailed(); +``` + +### DelegationAlreadyExists + +```solidity +error DelegationAlreadyExists(); +``` + +### DelegationDoesNotExist + +```solidity +error DelegationDoesNotExist(); +``` + +### DelegationExistsWithSameValue + +```solidity +error DelegationExistsWithSameValue(); +``` + +### ExceedingMaxShares + +```solidity +error ExceedingMaxShares(); +``` + +### ExceedingPercentageUpdate + +```solidity +error ExceedingPercentageUpdate(); +``` + +### FeeAlreadySet + +```solidity +error FeeAlreadySet(); +``` + +### InsufficientBalance + +```solidity +error InsufficientBalance(); +``` + +### InsufficientLiquidity + +```solidity +error InsufficientLiquidity(); +``` + +### InvalidAccountGeneration + +```solidity +error InvalidAccountGeneration(); +``` + +### InvalidAmount + +```solidity +error InvalidAmount(); +``` + +### InvalidBAppOwner + +```solidity +error InvalidBAppOwner(address caller, address expectedOwner); +``` + +### InvalidPercentageIncrement + +```solidity +error InvalidPercentageIncrement(); +``` + +### InvalidStrategyFee + +```solidity +error InvalidStrategyFee(); +``` + +### InvalidStrategyOwner + +```solidity +error InvalidStrategyOwner(address caller, address expectedOwner); +``` + +### InvalidToken + +```solidity +error InvalidToken(); +``` + +### NoPendingFeeUpdate + +```solidity +error NoPendingFeeUpdate(); +``` + +### NoPendingObligationUpdate + +```solidity +error NoPendingObligationUpdate(); +``` + +### NoPendingWithdrawal + +```solidity +error NoPendingWithdrawal(); +``` + +### ObligationAlreadySet + +```solidity +error ObligationAlreadySet(); +``` + +### ObligationHasNotBeenCreated + +```solidity +error ObligationHasNotBeenCreated(); +``` + +### RequestTimeExpired + +```solidity +error RequestTimeExpired(); +``` + +### SlashingDisabled + +```solidity +error SlashingDisabled(); +``` + +### TimelockNotElapsed + +```solidity +error TimelockNotElapsed(); +``` + +### TokenNotSupportedByBApp + +```solidity +error TokenNotSupportedByBApp(address token); +``` + +### WithdrawTransferFailed + +```solidity +error WithdrawTransferFailed(); +``` + +### WithdrawalsDisabled + +```solidity +error WithdrawalsDisabled(); +``` + diff --git a/docs/src/src/core/interfaces/IViews.sol/interface.IViews.md b/docs/src/src/core/interfaces/IViews.sol/interface.IViews.md new file mode 100644 index 00000000..a50a20aa --- /dev/null +++ b/docs/src/src/core/interfaces/IViews.sol/interface.IViews.md @@ -0,0 +1,208 @@ +# IViews +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/interfaces/IViews.sol) + + +## Functions +### delegations + + +```solidity +function delegations(address account, address receiver) external view returns (uint32); +``` + +### totalDelegatedPercentage + + +```solidity +function totalDelegatedPercentage(address delegator) external view returns (uint32); +``` + +### registeredBApps + + +```solidity +function registeredBApps(address bApp) external view returns (bool isRegistered); +``` + +### strategies + + +```solidity +function strategies(uint32 strategyId) external view returns (address strategyOwner, uint32 fee); +``` + +### ownedStrategies + + +```solidity +function ownedStrategies(address owner) external view returns (uint32[] memory strategyIds); +``` + +### strategyAccountShares + + +```solidity +function strategyAccountShares(uint32 strategyId, address account, address token) external view returns (uint256); +``` + +### strategyTotalBalance + + +```solidity +function strategyTotalBalance(uint32 strategyId, address token) external view returns (uint256); +``` + +### strategyTotalShares + + +```solidity +function strategyTotalShares(uint32 strategyId, address token) external view returns (uint256); +``` + +### strategyGeneration + + +```solidity +function strategyGeneration(uint32 strategyId, address token) external view returns (uint256); +``` + +### obligations + + +```solidity +function obligations(uint32 strategyId, address bApp, address token) external view returns (uint32 percentage, bool isSet); +``` + +### bAppTokens + + +```solidity +function bAppTokens(address bApp, address token) external view returns (uint32 currentValue, bool isSet, uint32 pendingValue, uint32 effectTime); +``` + +### accountBAppStrategy + + +```solidity +function accountBAppStrategy(address account, address bApp) external view returns (uint32); +``` + +### feeUpdateRequests + + +```solidity +function feeUpdateRequests(uint32 strategyId) external view returns (uint32 percentage, uint32 requestTime); +``` + +### withdrawalRequests + + +```solidity +function withdrawalRequests(uint32 strategyId, address account, address token) external view returns (uint256 shares, uint32 requestTime); +``` + +### obligationRequests + + +```solidity +function obligationRequests(uint32 strategyId, address token, address bApp) external view returns (uint32 percentage, uint32 requestTime); +``` + +### slashingFund + + +```solidity +function slashingFund(address account, address token) external view returns (uint256); +``` + +### maxPercentage + + +```solidity +function maxPercentage() external pure returns (uint32); +``` + +### ethAddress + + +```solidity +function ethAddress() external pure returns (address); +``` + +### maxShares + + +```solidity +function maxShares() external view returns (uint256); +``` + +### maxFeeIncrement + + +```solidity +function maxFeeIncrement() external view returns (uint32); +``` + +### feeTimelockPeriod + + +```solidity +function feeTimelockPeriod() external view returns (uint32); +``` + +### feeExpireTime + + +```solidity +function feeExpireTime() external view returns (uint32); +``` + +### withdrawalTimelockPeriod + + +```solidity +function withdrawalTimelockPeriod() external view returns (uint32); +``` + +### withdrawalExpireTime + + +```solidity +function withdrawalExpireTime() external view returns (uint32); +``` + +### obligationTimelockPeriod + + +```solidity +function obligationTimelockPeriod() external view returns (uint32); +``` + +### obligationExpireTime + + +```solidity +function obligationExpireTime() external view returns (uint32); +``` + +### disabledFeatures + + +```solidity +function disabledFeatures() external view returns (uint32); +``` + +### tokenUpdateTimelockPeriod + + +```solidity +function tokenUpdateTimelockPeriod() external view returns (uint32); +``` + +### getVersion + + +```solidity +function getVersion() external pure returns (string memory); +``` + diff --git a/docs/src/src/core/interfaces/README.md b/docs/src/src/core/interfaces/README.md new file mode 100644 index 00000000..0593724a --- /dev/null +++ b/docs/src/src/core/interfaces/README.md @@ -0,0 +1,9 @@ + + +# Contents +- [IBasedAppManager](IBasedAppManager.sol/interface.IBasedAppManager.md) +- [ICore](ICore.sol/interface.ICore.md) +- [IProtocolManager](IProtocolManager.sol/interface.IProtocolManager.md) +- [ISSVBasedApps](ISSVBasedApps.sol/interface.ISSVBasedApps.md) +- [IStrategyManager](IStrategyManager.sol/interface.IStrategyManager.md) +- [IViews](IViews.sol/interface.IViews.md) diff --git a/docs/src/src/core/libraries/CoreStorageLib.sol/enum.SSVCoreModules.md b/docs/src/src/core/libraries/CoreStorageLib.sol/enum.SSVCoreModules.md new file mode 100644 index 00000000..68a026db --- /dev/null +++ b/docs/src/src/core/libraries/CoreStorageLib.sol/enum.SSVCoreModules.md @@ -0,0 +1,12 @@ +# SSVCoreModules +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/libraries/CoreStorageLib.sol) + + +```solidity +enum SSVCoreModules { + SSV_PROTOCOL_MANAGER, + SSV_BAPPS_MANAGER, + SSV_STRATEGY_MANAGER +} +``` + diff --git a/docs/src/src/core/libraries/CoreStorageLib.sol/library.CoreStorageLib.md b/docs/src/src/core/libraries/CoreStorageLib.sol/library.CoreStorageLib.md new file mode 100644 index 00000000..cfb1ab50 --- /dev/null +++ b/docs/src/src/core/libraries/CoreStorageLib.sol/library.CoreStorageLib.md @@ -0,0 +1,46 @@ +# CoreStorageLib +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/libraries/CoreStorageLib.sol) + + +## State Variables +### SSV_BASED_APPS_STORAGE_POSITION + +```solidity +uint256 private constant SSV_BASED_APPS_STORAGE_POSITION = uint256(keccak256("ssv.based-apps.storage.main")) - 1; +``` + + +## Functions +### load + + +```solidity +function load() internal pure returns (Data storage sd); +``` + +## Structs +### Data +Represents all operational state required by the SSV Based Application platform. + + +```solidity +struct Data { + uint32 _strategyCounter; + mapping(SSVCoreModules => address) ssvContracts; + mapping(uint32 strategyId => ICore.Strategy) strategies; + mapping(address owner => uint32[] strategyId) strategyOwners; + mapping(address account => mapping(address bApp => uint32 strategyId)) accountBAppStrategy; + mapping(address delegator => mapping(address account => uint32 percentage)) delegations; + mapping(address delegator => uint32 totalPercentage) totalDelegatedPercentage; + mapping(uint32 strategyId => mapping(address token => ICore.Shares shares)) strategyTokenShares; + mapping(uint32 strategyId => mapping(address bApp => mapping(address token => ICore.Obligation))) obligations; + mapping(uint32 strategyId => mapping(address account => mapping(address token => ICore.WithdrawalRequest))) withdrawalRequests; + mapping(uint32 strategyId => mapping(address token => mapping(address bApp => ICore.ObligationRequest))) obligationRequests; + mapping(uint32 strategyId => ICore.FeeUpdateRequest) feeUpdateRequests; + mapping(address account => mapping(address token => uint256 amount)) slashingFund; + mapping(address bApp => bool isRegistered) registeredBApps; + mapping(address bApp => mapping(address token => ICore.SharedRiskLevel)) bAppTokens; + mapping(address bApp => ICore.TokenUpdateRequest) tokenUpdateRequests; +} +``` + diff --git a/docs/src/src/core/libraries/ProtocolStorageLib.sol/library.ProtocolStorageLib.md b/docs/src/src/core/libraries/ProtocolStorageLib.sol/library.ProtocolStorageLib.md new file mode 100644 index 00000000..bce8a21b --- /dev/null +++ b/docs/src/src/core/libraries/ProtocolStorageLib.sol/library.ProtocolStorageLib.md @@ -0,0 +1,40 @@ +# ProtocolStorageLib +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/libraries/ProtocolStorageLib.sol) + + +## State Variables +### SSV_STORAGE_POSITION + +```solidity +uint256 private constant SSV_STORAGE_POSITION = uint256(keccak256("ssv.based-apps.storage.protocol")) - 1; +``` + + +## Functions +### load + + +```solidity +function load() internal pure returns (Data storage sd); +``` + +## Structs +### Data +Represents the operational settings and parameters required by the SSV Based Application Platform + + +```solidity +struct Data { + uint256 maxShares; + uint32 feeTimelockPeriod; + uint32 feeExpireTime; + uint32 withdrawalTimelockPeriod; + uint32 withdrawalExpireTime; + uint32 obligationTimelockPeriod; + uint32 obligationExpireTime; + uint32 tokenUpdateTimelockPeriod; + uint32 maxFeeIncrement; + uint32 disabledFeatures; +} +``` + diff --git a/docs/src/src/core/libraries/README.md b/docs/src/src/core/libraries/README.md new file mode 100644 index 00000000..3eba618a --- /dev/null +++ b/docs/src/src/core/libraries/README.md @@ -0,0 +1,8 @@ + + +# Contents +- [SSVCoreModules](CoreStorageLib.sol/enum.SSVCoreModules.md) +- [CoreStorageLib](CoreStorageLib.sol/library.CoreStorageLib.md) +- [ProtocolStorageLib](ProtocolStorageLib.sol/library.ProtocolStorageLib.md) +- [ValidationLib](ValidationLib.sol/library.ValidationLib.md) +- [ValidationLib constants](ValidationLib.sol/constants.ValidationLib.md) diff --git a/docs/src/src/core/libraries/ValidationLib.sol/constants.ValidationLib.md b/docs/src/src/core/libraries/ValidationLib.sol/constants.ValidationLib.md new file mode 100644 index 00000000..a39eaf59 --- /dev/null +++ b/docs/src/src/core/libraries/ValidationLib.sol/constants.ValidationLib.md @@ -0,0 +1,27 @@ +# Constants +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/libraries/ValidationLib.sol) + +### MIN_TIME_LOCK_PERIOD + +```solidity +uint32 constant MIN_TIME_LOCK_PERIOD = 1 days; +``` + +### MIN_EXPIRE_TIME + +```solidity +uint32 constant MIN_EXPIRE_TIME = 1 hours; +``` + +### MAX_PERCENTAGE + +```solidity +uint32 constant MAX_PERCENTAGE = 1e4; +``` + +### ETH_ADDRESS + +```solidity +address constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; +``` + diff --git a/docs/src/src/core/libraries/ValidationLib.sol/library.ValidationLib.md b/docs/src/src/core/libraries/ValidationLib.sol/library.ValidationLib.md new file mode 100644 index 00000000..0440a706 --- /dev/null +++ b/docs/src/src/core/libraries/ValidationLib.sol/library.ValidationLib.md @@ -0,0 +1,52 @@ +# ValidationLib +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/libraries/ValidationLib.sol) + + +## Functions +### validatePercentage + + +```solidity +function validatePercentage(uint32 percentage) internal pure; +``` + +### validatePercentageAndNonZero + + +```solidity +function validatePercentageAndNonZero(uint32 percentage) internal pure; +``` + +### validateArrayLengths + + +```solidity +function validateArrayLengths(address[] calldata tokens, uint32[] memory values) internal pure; +``` + +### validateNonZeroAddress + + +```solidity +function validateNonZeroAddress(address addr) internal pure; +``` + +## Errors +### InvalidPercentage + +```solidity +error InvalidPercentage(); +``` + +### LengthsNotMatching + +```solidity +error LengthsNotMatching(); +``` + +### ZeroAddressNotAllowed + +```solidity +error ZeroAddressNotAllowed(); +``` + diff --git a/docs/src/src/core/modules/BasedAppsManager.sol/contract.BasedAppsManager.md b/docs/src/src/core/modules/BasedAppsManager.sol/contract.BasedAppsManager.md new file mode 100644 index 00000000..e31ce82a --- /dev/null +++ b/docs/src/src/core/modules/BasedAppsManager.sol/contract.BasedAppsManager.md @@ -0,0 +1,73 @@ +# BasedAppsManager +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/modules/BasedAppsManager.sol) + +**Inherits:** +[IBasedAppManager](/src/core/interfaces/IBasedAppManager.sol/interface.IBasedAppManager.md) + + +## Functions +### _onlyRegisteredBApp + +Allow the function to be called only by a registered bApp + + +```solidity +function _onlyRegisteredBApp(CoreStorageLib.Data storage s) private view; +``` + +### registerBApp + +Registers a bApp. + +*Allows creating a bApp even with an empty token list.* + + +```solidity +function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenConfigs`|`ICore.TokenConfig[]`|The list of tokens configs the bApp accepts; can be empty.| +|`metadataURI`|`string`|The metadata URI of the bApp, which is a link (e.g., http://example.com) to a JSON file containing metadata such as the name, description, logo, etc.| + + +### updateBAppMetadataURI + +Function to update the metadata URI of the Based Application + + +```solidity +function updateBAppMetadataURI(string calldata metadataURI) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`metadataURI`|`string`|The new metadata URI| + + +### updateBAppsTokens + + +```solidity +function updateBAppsTokens(ICore.TokenConfig[] calldata tokenConfigs) external; +``` + +### _addNewTokens + +Function to add tokens to a bApp + + +```solidity +function _addNewTokens(address bApp, ICore.TokenConfig[] calldata tokenConfigs) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`bApp`|`address`|The address of the bApp| +|`tokenConfigs`|`ICore.TokenConfig[]`|The list of tokens to add| + + diff --git a/docs/src/src/core/modules/ProtocolManager.sol/contract.ProtocolManager.md b/docs/src/src/core/modules/ProtocolManager.sol/contract.ProtocolManager.md new file mode 100644 index 00000000..8d080770 --- /dev/null +++ b/docs/src/src/core/modules/ProtocolManager.sol/contract.ProtocolManager.md @@ -0,0 +1,93 @@ +# ProtocolManager +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/modules/ProtocolManager.sol) + +**Inherits:** +[IProtocolManager](/src/core/interfaces/IProtocolManager.sol/interface.IProtocolManager.md) + + +## State Variables +### SLASHING_DISABLED + +```solidity +uint32 private constant SLASHING_DISABLED = 1 << 0; +``` + + +### WITHDRAWALS_DISABLED + +```solidity +uint32 private constant WITHDRAWALS_DISABLED = 1 << 1; +``` + + +## Functions +### updateFeeTimelockPeriod + + +```solidity +function updateFeeTimelockPeriod(uint32 feeTimelockPeriod) external; +``` + +### updateFeeExpireTime + + +```solidity +function updateFeeExpireTime(uint32 feeExpireTime) external; +``` + +### updateWithdrawalTimelockPeriod + + +```solidity +function updateWithdrawalTimelockPeriod(uint32 withdrawalTimelockPeriod) external; +``` + +### updateWithdrawalExpireTime + + +```solidity +function updateWithdrawalExpireTime(uint32 withdrawalExpireTime) external; +``` + +### updateObligationTimelockPeriod + + +```solidity +function updateObligationTimelockPeriod(uint32 obligationTimelockPeriod) external; +``` + +### updateObligationExpireTime + + +```solidity +function updateObligationExpireTime(uint32 obligationExpireTime) external; +``` + +### updateTokenUpdateTimelockPeriod + + +```solidity +function updateTokenUpdateTimelockPeriod(uint32 tokenUpdateTimelockPeriod) external; +``` + +### updateMaxShares + + +```solidity +function updateMaxShares(uint256 maxShares) external; +``` + +### updateMaxFeeIncrement + + +```solidity +function updateMaxFeeIncrement(uint32 maxFeeIncrement) external; +``` + +### updateDisabledFeatures + + +```solidity +function updateDisabledFeatures(uint32 disabledFeatures) external; +``` + diff --git a/docs/src/src/core/modules/README.md b/docs/src/src/core/modules/README.md new file mode 100644 index 00000000..532896ac --- /dev/null +++ b/docs/src/src/core/modules/README.md @@ -0,0 +1,6 @@ + + +# Contents +- [BasedAppsManager](BasedAppsManager.sol/contract.BasedAppsManager.md) +- [ProtocolManager](ProtocolManager.sol/contract.ProtocolManager.md) +- [StrategyManager](StrategyManager.sol/contract.StrategyManager.md) diff --git a/docs/src/src/core/modules/StrategyManager.sol/contract.StrategyManager.md b/docs/src/src/core/modules/StrategyManager.sol/contract.StrategyManager.md new file mode 100644 index 00000000..15c37bd1 --- /dev/null +++ b/docs/src/src/core/modules/StrategyManager.sol/contract.StrategyManager.md @@ -0,0 +1,611 @@ +# StrategyManager +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/core/modules/StrategyManager.sol) + +**Inherits:** +ReentrancyGuardTransient, [IStrategyManager](/src/core/interfaces/IStrategyManager.sol/interface.IStrategyManager.md) + + +## State Variables +### SLASHING_DISABLED + +```solidity +uint32 private constant SLASHING_DISABLED = 1 << 0; +``` + + +### WITHDRAWALS_DISABLED + +```solidity +uint32 private constant WITHDRAWALS_DISABLED = 1 << 1; +``` + + +## Functions +### _onlyStrategyOwner + +Checks if the caller is the strategy owner + + +```solidity +function _onlyStrategyOwner(uint32 strategyId, CoreStorageLib.Data storage s) private view; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`s`|`CoreStorageLib.Data`|The CoreStorageLib data| + + +### updateAccountMetadataURI + +Function to update the metadata URI of the Account + + +```solidity +function updateAccountMetadataURI(string calldata metadataURI) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`metadataURI`|`string`|The new metadata URI| + + +### delegateBalance + +Function to delegate a percentage of the account's balance to another account + +*The percentage is scaled by 1e4 so the minimum unit is 0.01%* + + +```solidity +function delegateBalance(address account, uint32 percentage) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`account`|`address`|The address of the account to delegate to| +|`percentage`|`uint32`|The percentage of the account's balance to delegate| + + +### updateDelegatedBalance + +Function to update the delegated validator balance percentage to another account + +*The percentage is scaled by 1e4 so the minimum unit is 0.01%* + + +```solidity +function updateDelegatedBalance(address account, uint32 percentage) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`account`|`address`|The address of the account to delegate to| +|`percentage`|`uint32`|The updated percentage of the account's balance to delegate| + + +### removeDelegatedBalance + +Removes delegation from an account. + + +```solidity +function removeDelegatedBalance(address account) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`account`|`address`|The address of the account whose delegation is being removed.| + + +### createStrategy + +Function to create a new Strategy + + +```solidity +function createStrategy(uint32 fee, string calldata metadataURI) external returns (uint32 strategyId); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`fee`|`uint32`|| +|`metadataURI`|`string`|The metadata URI of the strategy| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the new Strategy| + + +### updateStrategyMetadataURI + +Function to update the metadata URI of the Strategy + + +```solidity +function updateStrategyMetadataURI(uint32 strategyId, string calldata metadataURI) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The id of the strategy| +|`metadataURI`|`string`|The new metadata URI| + + +### optInToBApp + +Opt-in to a bApp with a list of tokens and obligation percentages + +*checks that each token is supported by the bApp, but not that the obligation is > 0* + + +```solidity +function optInToBApp(uint32 strategyId, address bApp, address[] calldata tokens, uint32[] calldata obligationPercentages, bytes calldata data) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`bApp`|`address`|The address of the bApp| +|`tokens`|`address[]`|The list of tokens to opt-in with| +|`obligationPercentages`|`uint32[]`|The list of obligation percentages for each token| +|`data`|`bytes`|Optional parameter that could be required by the service| + + +### _isContract + +Function to check if an address is a contract + + +```solidity +function _isContract(address bApp) private view returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`bApp`|`address`|The address of the bApp| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the address is a contract| + + +### depositERC20 + +Deposit ERC20 tokens into the strategy + + +```solidity +function depositERC20(uint32 strategyId, IERC20 token, uint256 amount) external nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`token`|`IERC20`|The ERC20 token address| +|`amount`|`uint256`|The amount to deposit| + + +### depositETH + +Deposit ETH into the strategy + + +```solidity +function depositETH(uint32 strategyId) external payable nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| + + +### proposeWithdrawal + +Propose a withdrawal of ERC20 tokens from the strategy. + + +```solidity +function proposeWithdrawal(uint32 strategyId, address token, uint256 amount) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy.| +|`token`|`address`|The ERC20 token address.| +|`amount`|`uint256`|The amount to withdraw.| + + +### finalizeWithdrawal + +Finalize the ERC20 withdrawal after the timelock period has passed. + + +```solidity +function finalizeWithdrawal(uint32 strategyId, IERC20 token) external nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy.| +|`token`|`IERC20`|The ERC20 token address.| + + +### proposeWithdrawalETH + +Propose an ETH withdrawal from the strategy. + + +```solidity +function proposeWithdrawalETH(uint32 strategyId, uint256 amount) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy.| +|`amount`|`uint256`|The amount of ETH to withdraw.| + + +### finalizeWithdrawalETH + +Finalize the ETH withdrawal after the timelock period has passed. + + +```solidity +function finalizeWithdrawalETH(uint32 strategyId) external nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy.| + + +### createObligation + +Add a new obligation for a bApp + + +```solidity +function createObligation(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`bApp`|`address`|The address of the bApp| +|`token`|`address`|The address of the token| +|`obligationPercentage`|`uint32`|The obligation percentage| + + +### proposeUpdateObligation + +Propose a withdrawal of ERC20 tokens from the strategy. + + +```solidity +function proposeUpdateObligation(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy.| +|`bApp`|`address`|| +|`token`|`address`|The ERC20 token address.| +|`obligationPercentage`|`uint32`|The new percentage of the obligation| + + +### finalizeUpdateObligation + +Finalize the withdrawal after the timelock period has passed. + + +```solidity +function finalizeUpdateObligation(uint32 strategyId, address bApp, address token) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy.| +|`bApp`|`address`|The address of the bApp.| +|`token`|`address`|The ERC20 token address.| + + +### reduceFee + +Instantly lowers the fee for a strategy + + +```solidity +function reduceFee(uint32 strategyId, uint32 proposedFee) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`proposedFee`|`uint32`|The proposed fee| + + +### proposeFeeUpdate + +Propose a new fee for a strategy + + +```solidity +function proposeFeeUpdate(uint32 strategyId, uint32 proposedFee) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`proposedFee`|`uint32`|The proposed fee| + + +### finalizeFeeUpdate + +Finalize the fee update for a strategy + + +```solidity +function finalizeFeeUpdate(uint32 strategyId) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| + + +### _createOptInObligations + +Set the obligation percentages for a strategy + + +```solidity +function _createOptInObligations(uint32 strategyId, address bApp, address[] calldata tokens, uint32[] calldata obligationPercentages) private; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`bApp`|`address`|The address of the bApp| +|`tokens`|`address[]`|The list of tokens to set s.obligations for| +|`obligationPercentages`|`uint32[]`|The list of obligation percentages for each token| + + +### _createSingleObligation + +Set a single obligation for a strategy + + +```solidity +function _createSingleObligation(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) private; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`bApp`|`address`|The address of the bApp| +|`token`|`address`|The address of the token| +|`obligationPercentage`|`uint32`|The obligation percentage| + + +### _validateObligationUpdateInput + +Validate the input for the obligation creation or update + + +```solidity +function _validateObligationUpdateInput(uint32 strategyId, address bApp, address token, uint32 obligationPercentage) private view; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`bApp`|`address`|The address of the bApp| +|`token`|`address`|The address of the token| +|`obligationPercentage`|`uint32`|The obligation percentage| + + +### _checkTimelocks + +Check the timelocks + + +```solidity +function _checkTimelocks(uint256 requestTime, uint256 timelockPeriod, uint256 expireTime) internal view; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`requestTime`|`uint256`|The time of the request| +|`timelockPeriod`|`uint256`|The timelock period| +|`expireTime`|`uint256`|The expire time| + + +### _beforeDeposit + + +```solidity +function _beforeDeposit(uint32 strategyId, address token, uint256 amount) internal; +``` + +### _proposeWithdrawal + +*override the previous share balance* + + +```solidity +function _proposeWithdrawal(uint32 strategyId, address token, uint256 amount) internal; +``` + +### _finalizeWithdrawal + + +```solidity +function _finalizeWithdrawal(uint32 strategyId, address token) private returns (uint256 amount); +``` + +### getSlashableBalance + +Get the slashable balance for a strategy + + +```solidity +function getSlashableBalance(CoreStorageLib.Data storage s, uint32 strategyId, address bApp, address token, ICore.Shares storage strategyTokenShares) + internal + view + returns (uint256 slashableBalance); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`s`|`CoreStorageLib.Data`|| +|`strategyId`|`uint32`|The ID of the strategy| +|`bApp`|`address`|The address of the bApp| +|`token`|`address`|The address of the token| +|`strategyTokenShares`|`ICore.Shares`|| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`slashableBalance`|`uint256`|The slashable balance| + + +### _checkStrategyOptedIn + + +```solidity +function _checkStrategyOptedIn(CoreStorageLib.Data storage s, uint32 strategyId, address bApp) internal view; +``` + +### slash + +Slash a strategy + + +```solidity +function slash(uint32 strategyId, address bApp, address token, uint32 percentage, bytes calldata data) external nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`strategyId`|`uint32`|The ID of the strategy| +|`bApp`|`address`|The address of the bApp| +|`token`|`address`|The address of the token| +|`percentage`|`uint32`|The amount to slash| +|`data`|`bytes`|Optional parameter that could be required by the service| + + +### _exitStrategy + + +```solidity +function _exitStrategy(CoreStorageLib.Data storage s, uint32 strategyId, address bApp, address token) private; +``` + +### _adjustObligation + + +```solidity +function _adjustObligation( + CoreStorageLib.Data storage s, + uint32 strategyId, + address bApp, + address token, + uint256 amount, + ICore.Shares storage strategyTokenShares +) internal returns (uint32 obligationPercentage); +``` + +### withdrawSlashingFund + +Withdraw the slashing fund for a token + + +```solidity +function withdrawSlashingFund(address token, uint256 amount) external nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The address of the token| +|`amount`|`uint256`|The amount to withdraw| + + +### withdrawETHSlashingFund + +Withdraw the slashing fund for ETH + + +```solidity +function withdrawETHSlashingFund(uint256 amount) external nonReentrant; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`amount`|`uint256`|The amount to withdraw| + + +### _withdrawSlashingFund + +General withdraw code the slashing fund + + +```solidity +function _withdrawSlashingFund(address token, uint256 amount) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The address of the token| +|`amount`|`uint256`|The amount to withdraw| + + +### _checkSlashingAllowed + + +```solidity +function _checkSlashingAllowed() internal view; +``` + +### _checkWithdrawalsAllowed + + +```solidity +function _checkWithdrawalsAllowed() internal view; +``` + diff --git a/docs/src/src/middleware/README.md b/docs/src/src/middleware/README.md new file mode 100644 index 00000000..5f178fe0 --- /dev/null +++ b/docs/src/src/middleware/README.md @@ -0,0 +1,6 @@ + + +# Contents +- [examples](/src/middleware/examples) +- [interfaces](/src/middleware/interfaces) +- [modules](/src/middleware/modules) diff --git a/docs/src/src/middleware/examples/README.md b/docs/src/src/middleware/examples/README.md new file mode 100644 index 00000000..0d309593 --- /dev/null +++ b/docs/src/src/middleware/examples/README.md @@ -0,0 +1,4 @@ + + +# Contents +- [WhitelistExample](WhitelistExample.sol/contract.WhitelistExample.md) diff --git a/docs/src/src/middleware/examples/WhitelistExample.sol/contract.WhitelistExample.md b/docs/src/src/middleware/examples/WhitelistExample.sol/contract.WhitelistExample.md new file mode 100644 index 00000000..e1a56051 --- /dev/null +++ b/docs/src/src/middleware/examples/WhitelistExample.sol/contract.WhitelistExample.md @@ -0,0 +1,27 @@ +# WhitelistExample +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/middleware/examples/WhitelistExample.sol) + +**Inherits:** +[OwnableBasedApp](/src/middleware/modules/core+roles/OwnableBasedApp.sol/abstract.OwnableBasedApp.md), [BasedAppWhitelisted](/src/middleware/modules/BasedAppWhitelisted.sol/abstract.BasedAppWhitelisted.md) + + +## Functions +### constructor + + +```solidity +constructor(address _basedAppManager, address _initOwner) OwnableBasedApp(_basedAppManager, _initOwner); +``` + +### optInToBApp + + +```solidity +function optInToBApp(uint32 strategyId, address[] calldata, uint32[] calldata, bytes calldata) + external + view + override + onlySSVBasedAppManager + returns (bool success); +``` + diff --git a/docs/src/src/middleware/interfaces/IBasedApp.sol/interface.IBasedApp.md b/docs/src/src/middleware/interfaces/IBasedApp.sol/interface.IBasedApp.md new file mode 100644 index 00000000..8ad4607b --- /dev/null +++ b/docs/src/src/middleware/interfaces/IBasedApp.sol/interface.IBasedApp.md @@ -0,0 +1,49 @@ +# IBasedApp +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/middleware/interfaces/IBasedApp.sol) + + +## Functions +### optInToBApp + + +```solidity +function optInToBApp(uint32 strategyId, address[] calldata tokens, uint32[] calldata obligationPercentages, bytes calldata data) external returns (bool); +``` + +### registerBApp + + +```solidity +function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external; +``` + +### slash + + +```solidity +function slash(uint32 strategyId, address token, uint32 percentage, address sender, bytes calldata data) + external + returns (bool success, address receiver, bool exit); +``` + +### updateBAppMetadataURI + + +```solidity +function updateBAppMetadataURI(string calldata metadataURI) external; +``` + +### updateBAppTokens + + +```solidity +function updateBAppTokens(ICore.TokenConfig[] calldata tokenConfigs) external; +``` + +## Errors +### UnauthorizedCaller + +```solidity +error UnauthorizedCaller(); +``` + diff --git a/docs/src/src/middleware/interfaces/IBasedAppWhitelisted.sol/interface.IBasedAppWhitelisted.md b/docs/src/src/middleware/interfaces/IBasedAppWhitelisted.sol/interface.IBasedAppWhitelisted.md new file mode 100644 index 00000000..5db5cac9 --- /dev/null +++ b/docs/src/src/middleware/interfaces/IBasedAppWhitelisted.sol/interface.IBasedAppWhitelisted.md @@ -0,0 +1,44 @@ +# IBasedAppWhitelisted +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/middleware/interfaces/IBasedAppWhitelisted.sol) + + +## Functions +### addWhitelisted + + +```solidity +function addWhitelisted(uint32 strategyId) external; +``` + +### removeWhitelisted + + +```solidity +function removeWhitelisted(uint32 strategyId) external; +``` + +## Errors +### AlreadyWhitelisted + +```solidity +error AlreadyWhitelisted(); +``` + +### NonWhitelistedCaller + +```solidity +error NonWhitelistedCaller(); +``` + +### NotWhitelisted + +```solidity +error NotWhitelisted(); +``` + +### ZeroID + +```solidity +error ZeroID(); +``` + diff --git a/docs/src/src/middleware/interfaces/README.md b/docs/src/src/middleware/interfaces/README.md new file mode 100644 index 00000000..ae5a297b --- /dev/null +++ b/docs/src/src/middleware/interfaces/README.md @@ -0,0 +1,5 @@ + + +# Contents +- [IBasedApp](IBasedApp.sol/interface.IBasedApp.md) +- [IBasedAppWhitelisted](IBasedAppWhitelisted.sol/interface.IBasedAppWhitelisted.md) diff --git a/docs/src/src/middleware/modules/BasedAppWhitelisted.sol/abstract.BasedAppWhitelisted.md b/docs/src/src/middleware/modules/BasedAppWhitelisted.sol/abstract.BasedAppWhitelisted.md new file mode 100644 index 00000000..4a1798b8 --- /dev/null +++ b/docs/src/src/middleware/modules/BasedAppWhitelisted.sol/abstract.BasedAppWhitelisted.md @@ -0,0 +1,30 @@ +# BasedAppWhitelisted +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/middleware/modules/BasedAppWhitelisted.sol) + +**Inherits:** +[IBasedAppWhitelisted](/src/middleware/interfaces/IBasedAppWhitelisted.sol/interface.IBasedAppWhitelisted.md) + + +## State Variables +### isWhitelisted + +```solidity +mapping(uint32 => bool) public isWhitelisted; +``` + + +## Functions +### addWhitelisted + + +```solidity +function addWhitelisted(uint32 strategyId) external virtual; +``` + +### removeWhitelisted + + +```solidity +function removeWhitelisted(uint32 strategyId) external virtual; +``` + diff --git a/docs/src/src/middleware/modules/README.md b/docs/src/src/middleware/modules/README.md new file mode 100644 index 00000000..a80ba19f --- /dev/null +++ b/docs/src/src/middleware/modules/README.md @@ -0,0 +1,6 @@ + + +# Contents +- [core](/src/middleware/modules/core) +- [core+roles](/src/middleware/modules/core+roles) +- [BasedAppWhitelisted](BasedAppWhitelisted.sol/abstract.BasedAppWhitelisted.md) diff --git a/docs/src/src/middleware/modules/core+roles/AccessControlBasedApp.sol/abstract.AccessControlBasedApp.md b/docs/src/src/middleware/modules/core+roles/AccessControlBasedApp.sol/abstract.AccessControlBasedApp.md new file mode 100644 index 00000000..9103c220 --- /dev/null +++ b/docs/src/src/middleware/modules/core+roles/AccessControlBasedApp.sol/abstract.AccessControlBasedApp.md @@ -0,0 +1,84 @@ +# AccessControlBasedApp +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/middleware/modules/core+roles/AccessControlBasedApp.sol) + +**Inherits:** +[BasedAppCore](/src/middleware/modules/core/BasedAppCore.sol/abstract.BasedAppCore.md), AccessControl + + +## State Variables +### MANAGER_ROLE + +```solidity +bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); +``` + + +### OWNER_ROLE + +```solidity +bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); +``` + + +## Functions +### constructor + + +```solidity +constructor(address _basedAppManager, address owner) AccessControl() BasedAppCore(_basedAppManager); +``` + +### grantManagerRole + + +```solidity +function grantManagerRole(address manager) external onlyRole(DEFAULT_ADMIN_ROLE); +``` + +### revokeManagerRole + + +```solidity +function revokeManagerRole(address manager) external onlyRole(DEFAULT_ADMIN_ROLE); +``` + +### registerBApp + +Registers a BApp calling the SSV SSVBasedApps + +*metadata should point to a json that respect template: +{ +"name": "SSV Based App", +"website": "https://www.ssvlabs.io/", +"description": "SSV Based App Core", +"logo": "https://link-to-your-logo.png", +"social": "https://x.com/ssv_network" +}* + + +```solidity +function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external override onlyRole(MANAGER_ROLE); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenConfigs`|`ICore.TokenConfig[]`|array of token addresses and shared risk levels| +|`metadataURI`|`string`|URI of the metadata| + + +### updateBAppMetadataURI + +Updates the metadata URI of a BApp + + +```solidity +function updateBAppMetadataURI(string calldata metadataURI) external override onlyRole(MANAGER_ROLE); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`metadataURI`|`string`|new metadata URI| + + diff --git a/docs/src/src/middleware/modules/core+roles/OwnableBasedApp.sol/abstract.OwnableBasedApp.md b/docs/src/src/middleware/modules/core+roles/OwnableBasedApp.sol/abstract.OwnableBasedApp.md new file mode 100644 index 00000000..fa6eec5b --- /dev/null +++ b/docs/src/src/middleware/modules/core+roles/OwnableBasedApp.sol/abstract.OwnableBasedApp.md @@ -0,0 +1,55 @@ +# OwnableBasedApp +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/middleware/modules/core+roles/OwnableBasedApp.sol) + +**Inherits:** +Ownable, [BasedAppCore](/src/middleware/modules/core/BasedAppCore.sol/abstract.BasedAppCore.md) + + +## Functions +### constructor + + +```solidity +constructor(address _basedAppManager, address _initOwner) BasedAppCore(_basedAppManager) Ownable(_initOwner); +``` + +### registerBApp + +Registers a BApp calling the SSV SSVBasedApps + +*metadata should point to a json that respect template: +{ +"name": "SSV Based App", +"website": "https://www.ssvlabs.io/", +"description": "SSV Based App Core", +"logo": "https://link-to-your-logo.png", +"social": "https://x.com/ssv_network" +}* + + +```solidity +function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external override onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenConfigs`|`ICore.TokenConfig[]`|array of token addresses and shared risk levels| +|`metadataURI`|`string`|URI of the metadata| + + +### updateBAppMetadataURI + +Updates the metadata URI of a BApp + + +```solidity +function updateBAppMetadataURI(string calldata metadataURI) external override onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`metadataURI`|`string`|new metadata URI| + + diff --git a/docs/src/src/middleware/modules/core+roles/README.md b/docs/src/src/middleware/modules/core+roles/README.md new file mode 100644 index 00000000..1fbff46f --- /dev/null +++ b/docs/src/src/middleware/modules/core+roles/README.md @@ -0,0 +1,5 @@ + + +# Contents +- [AccessControlBasedApp](AccessControlBasedApp.sol/abstract.AccessControlBasedApp.md) +- [OwnableBasedApp](OwnableBasedApp.sol/abstract.OwnableBasedApp.md) diff --git a/docs/src/src/middleware/modules/core/BasedAppCore.sol/abstract.BasedAppCore.md b/docs/src/src/middleware/modules/core/BasedAppCore.sol/abstract.BasedAppCore.md new file mode 100644 index 00000000..17d90255 --- /dev/null +++ b/docs/src/src/middleware/modules/core/BasedAppCore.sol/abstract.BasedAppCore.md @@ -0,0 +1,145 @@ +# BasedAppCore +[Git Source](https://github.com/ssvlabs/based-applications/blob/3ee95af731e4fce61ac2b03f418aa4e9fb5f64bd/src/middleware/modules/core/BasedAppCore.sol) + +**Inherits:** +[IBasedApp](/src/middleware/interfaces/IBasedApp.sol/interface.IBasedApp.md) + + +## State Variables +### SSV_BASED_APPS_NETWORK +Address of the SSV Based App Manager contract + + +```solidity +address public immutable SSV_BASED_APPS_NETWORK; +``` + + +## Functions +### onlySSVBasedAppManager + +*Allows only the SSV Based App Manager to call the function* + + +```solidity +modifier onlySSVBasedAppManager(); +``` + +### constructor + +constructor for the BasedAppCore contract, +initializes the contract with the SSVBasedApps address and the owner and disables the initializers. + + +```solidity +constructor(address _ssvBasedAppsNetwork); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_ssvBasedAppsNetwork`|`address`|address of the SSVBasedApps contract| + + +### registerBApp + +Registers a BApp calling the SSVBasedApps + +*metadata should point to a json that respect template: +{ +"name": "SSV Based App", +"website": "https://www.ssvlabs.io/", +"description": "SSV Based App Core", +"logo": "https://link-to-your-logo.png", +"social": "https://x.com/ssv_network" +}* + + +```solidity +function registerBApp(ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI) external virtual; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenConfigs`|`ICore.TokenConfig[]`|array of token configs (address, shared risk level)| +|`metadataURI`|`string`|URI of the metadata| + + +### updateBAppMetadataURI + +Updates the metadata URI of a BApp + + +```solidity +function updateBAppMetadataURI(string calldata metadataURI) external virtual; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`metadataURI`|`string`|new metadata URI| + + +### updateBAppTokens + +Updates the tokens of a BApp + + +```solidity +function updateBAppTokens(ICore.TokenConfig[] calldata tokenConfigs) external virtual; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenConfigs`|`ICore.TokenConfig[]`|new list of tokens and their shared risk levels| + + +### withdrawSlashingFund + + +```solidity +function withdrawSlashingFund(address token, uint256 amount) external virtual; +``` + +### withdrawETHSlashingFund + + +```solidity +function withdrawETHSlashingFund(uint256 amount) external virtual; +``` + +### optInToBApp + +Allows a Strategy to Opt-in to a BApp, it can be called only by the SSV Based App Manager + + +```solidity +function optInToBApp(uint32, address[] calldata, uint32[] calldata, bytes calldata) external virtual onlySSVBasedAppManager returns (bool success); +``` + +### slash + +*--- CORE LOGIC (TO BE IMPLEMENTED) ---* + +*--- RETURN TRUE IF SUCCESS, FALSE OTHERWISE ---* + + +```solidity +function slash(uint32, address, uint32, address, bytes calldata) external virtual onlySSVBasedAppManager returns (bool, address, bool); +``` + +### receive + +*--- CORE LOGIC (TO BE IMPLEMENTED) ---* + +*--- RETURN TRUE IF SUCCESS, FALSE OTHERWISE ---* + +*--- RETURN RECEIVER ADDRESS FOR THE SLASHED FUNDS ---* + + +```solidity +receive() external payable virtual; +``` + diff --git a/docs/src/src/middleware/modules/core/README.md b/docs/src/src/middleware/modules/core/README.md new file mode 100644 index 00000000..4ac46fa6 --- /dev/null +++ b/docs/src/src/middleware/modules/core/README.md @@ -0,0 +1,4 @@ + + +# Contents +- [BasedAppCore](BasedAppCore.sol/abstract.BasedAppCore.md) diff --git a/foundry.toml b/foundry.toml index f7122ba6..b2904e3f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,15 @@ optimizer = true optimizer_runs = 10_000 gas_reports = ["*"] gas_reports_ignore = ["ERC20Mock", "BasedAppMock", "BasedAppMock2", "BasedAppMock3", "ERC1967Proxy"] +fs_permissions = [{ access = "read", path = "./script/config/"}, { access = "write", path = "./artifacts/"}] + +[rpc_endpoints] +hoodi = "${HOODI_RPC_URL}" +mainnet = "${MAINNET_RPC_URL}" + +[etherscan] +hoodi = { key = "${ETHERSCAN_API_KEY}" } +mainnet = { key = "${ETHERSCAN_API_KEY}" } [fmt] bracket_spacing = false diff --git a/doc/bAppOnBoarding.md b/guides/bApp-onboarding.md similarity index 100% rename from doc/bAppOnBoarding.md rename to guides/bApp-onboarding.md diff --git a/guides/feature-activation.md b/guides/feature-activation.md new file mode 100644 index 00000000..ffcd0d01 --- /dev/null +++ b/guides/feature-activation.md @@ -0,0 +1,100 @@ +# Feature Flags in StrategyManager + +## Purpose + +Introduce a compact, on‑chain mechanism to enable or disable selected features of the `StrategyManager` contract without a full redeploy. By packing multiple boolean toggles into a single `uint32`, the design: + +- Minimizes storage footprint and gas cost. +- Provides an upgrade‑friendly switchboard for safety (e.g., emergency pause). +- Centralizes feature management under DAO control. + +## Technical Explanation + +- **Storage**: A `uint32 disabledFeatures` field in `ProtocolStorageLib.Data`. +- **Bitmask Layout**: + - Bit 0 (LSB) → Slashing Disabled (`SLASHING_DISABLED = 1 << 0`) + - Bit 1 → Withdrawals Disabled (`WITHDRAWALS_DISABLED = 1 << 1`) + - Further bits reserved for future toggles. + +- **Checks**: Two internal functions in `StrategyManager`: + ```solidity + function _checkSlashingAllowed() internal view { + if (ProtocolStorageLib.load().disabledFeatures & SLASHING_DISABLED != 0) + revert SlashingDisabled(); + } + + function _checkWithdrawalsAllowed() internal view { + if (ProtocolStorageLib.load().disabledFeatures & WITHDRAWALS_DISABLED != 0) + revert WithdrawalsDisabled(); + } + ``` + - Called at the entry points of: + - `slash(...)` + - `proposeWithdrawal(...)` + - `finalizeWithdrawal(...)` + - `proposeWithdrawalETH(...)` + - `finalizeWithdrawalETH(...)` + +## Authorized Accounts + +- Only the **DAO (owner)** can update the entire `disabledFeatures` bitmask via: + ```solidity + function updateDisabledFeatures(uint32 disabledFeatures) external onlyOwner; + ``` +- No per-feature granularity: toggles are applied in bulk. + +## Current Features That Can Be Enabled/Disabled + +| Bit Position | Feature | Constant | Description | +|:------------:|:-------------------|:----------------------|:------------------------------------------| +| 0 | Slashing | `SLASHING_DISABLED` | Stops all calls to `slash(...)` | +| 1 | Withdrawals | `WITHDRAWALS_DISABLED`| Stops all withdrawal proposals and finalizations | + +## Usage & Examples + +1. **Disable slashing only**: + ```js + // binary: 0b...01 → 1 + SSVBasedApps.updateDisabledFeatures(1); + // `slash(...)` now reverts with `SlashingDisabled()`. + ``` +2. **Re-enable slashing, disable withdrawals**: + ```js + // binary: 0b...10 → 2 + SSVBasedApps.updateDisabledFeatures(2); + // `slash(...)` resumes; `proposeWithdrawal(...)` and `finalizeWithdrawal(...)` revert. + ``` +3. **Disable both**: + ```js + SSVBasedApps.updateDisabledFeatures(3); + ``` +4. **Full example:** + ```js + // bit-definitions + const SLASHING_DISABLED = 1 << 0; // 0b0001 + const WITHDRAWALS_DISABLED = 1 << 1; // 0b0010 + + // Suppose you want to disable only withdrawals: + let flags = 0; + flags |= WITHDRAWALS_DISABLED; // flags === 0b0010 + + // Later you decide to also disable slashing: + flags |= SLASHING_DISABLED; // flags === 0b0011 + + // To re-enable withdrawals but keep slashing disabled: + flags &= ~WITHDRAWALS_DISABLED; // flags === 0b0001 + + // Finally, send the update on-chain: + await SSVBasedApps.updateDisabledFeatures(flags); + ``` + +## Future Extensions + +- Reserve bits 2–31 for other purposes: + - Feature disabling + - Emergency pause (global) + + +--- +*This document outlines the bitmask‑driven feature gating mechanism for `StrategyManager`. It ensures rapid reaction to on‑chain emergencies and fine‑grained control over critical operations.* + diff --git a/guides/generations.md b/guides/generations.md new file mode 100644 index 00000000..0f16bc12 --- /dev/null +++ b/guides/generations.md @@ -0,0 +1,115 @@ +# Generation Versioning in Strategy Shares + +## Purpose + +When a slashing event fully drains a strategy’s token balance, we need to ensure that any _old_ share‑balances and pending withdrawals become invalidated. The “generation” mechanism provides a simple versioning layer: bumping the `currentGeneration` counter makes all previous per‑account shares stale, preventing them from being redeemed after a full slash. + +## Rationale + +Without generation/versioning: + +1. A user deposits → receives `N` shares. +2. A slash drains the strategy → token balance is zero. +3. If we didn’t invalidate old shares, a user could still call `withdraw`, burn “old” shares, and withdraw zero tokens—potentially confusing or even exploitable in edge flows. + +By grouping every deposit/withdraw epoch under a “generation” number: + +- **Deposits** record both a share count _and_ the current generation at that time. +- On a full‑balance slash, we bump `currentGeneration`. +- Any subsequent withdrawal attempts carry an out‑of‑date generation tag and revert. + +This cleanly tears down all outstanding share balances in one on‑chain operation. + +## High-Level Flow + +1. **Deposit** + - If `totalShares == 0` (fresh strategy), `shares = amount`; + - Store `accountGeneration[msg.sender] = currentGeneration`; + - Add to `accountShareBalance[msg.sender]`. + +2. **Slash & Generation bump** + - If `strategyTokenShares.totalTokenBalance` goes to zero after slashing: + ```solidity + delete strategyTokenShares.totalTokenBalance; + delete strategyTokenShares.totalShareBalance; + strategyTokenShares.currentGeneration += 1; + ``` + - Now `currentGeneration` > any `accountGeneration[...]` recorded so far. + +3. **Withdraw** + - On `proposeWithdrawal` and `finalizeWithdrawal`, we check + ```solidity + if (strategyTokenShares.currentGeneration != + strategyTokenShares.accountGeneration[msg.sender] + ) revert InvalidAccountGeneration(); + ``` + - Out‑of‑date generations cannot proceed. + +## Technical Explanation + +### Storage Layout (excerpt) + +```solidity +struct Shares { + uint256 totalTokenBalance; + uint256 totalShareBalance; + uint256 currentGeneration; + mapping(address => uint256) accountShareBalance; + mapping(address => uint256) accountGeneration; +} +mapping(uint32 strategyId => mapping(address token => Shares)) public strategyTokenShares; +``` + +- `currentGeneration` is a global counter per `(strategyId,token)` pair. +- `accountGeneration[addr]` records which generation that account last deposited in. +- When generations mismatch, the user’s local `accountShareBalance` is considered invalid. + +### Deposit (`_beforeDeposit`) + +```solidity +if (strategyTokenShares.currentGeneration + != strategyTokenShares.accountGeneration[msg.sender] +) { + // brand-new generation for this account + strategyTokenShares.accountGeneration[msg.sender] + = strategyTokenShares.currentGeneration; + strategyTokenShares.accountShareBalance[msg.sender] + = computedShares; +} else { + // same generation: accumulate shares + strategyTokenShares.accountShareBalance[msg.sender] += computedShares; +} +``` + +### Full Slash & Bump + +```solidity +if (strategyTokenShares.totalTokenBalance == 0) { + // clear balances + delete strategyTokenShares.totalTokenBalance; + delete strategyTokenShares.totalShareBalance; + // bump to invalidate everyone else + strategyTokenShares.currentGeneration += 1; +} +``` + +### Withdrawal Check + +```solidity +// in _proposeWithdrawal or finalize: +if (strategyTokenShares.currentGeneration + != strategyTokenShares.accountGeneration[msg.sender] +) revert InvalidAccountGeneration(); +``` + +## References + +- **CoreStorageLib** + Defines `strategyTokenShares` and where `Shares` lives: + [`src/core/libraries/CoreStorageLib.sol`](src/core/libraries/CoreStorageLib.sol) +- **ERC-20 share math + generation logic** + In `StrategyManager._beforeDeposit`, `StrategyManager._proposeWithdrawal`, and the slash handlers: + [`src/core/modules/StrategyManager.sol`](src/core/modules/StrategyManager.sol) +- **IStrategyManager.Shares** + Describes the struct and intent: + [`src/core/interfaces/ICore.sol`](src/core/interfaces/ICore.sol) diff --git a/guides/img/sw_correlated.png b/guides/img/sw_correlated.png new file mode 100644 index 00000000..93ba38ae Binary files /dev/null and b/guides/img/sw_correlated.png differ diff --git a/guides/img/sw_early_attack.png b/guides/img/sw_early_attack.png new file mode 100644 index 00000000..4ed86edb Binary files /dev/null and b/guides/img/sw_early_attack.png differ diff --git a/guides/img/sw_example.png b/guides/img/sw_example.png new file mode 100644 index 00000000..a65e0e04 Binary files /dev/null and b/guides/img/sw_example.png differ diff --git a/guides/img/sw_late_attack.png b/guides/img/sw_late_attack.png new file mode 100644 index 00000000..ca1574b4 Binary files /dev/null and b/guides/img/sw_late_attack.png differ diff --git a/guides/slashing-and-withdrawals.md b/guides/slashing-and-withdrawals.md new file mode 100644 index 00000000..89fa1b48 --- /dev/null +++ b/guides/slashing-and-withdrawals.md @@ -0,0 +1,221 @@ +# Slashing & Withdrawals + +> _This document describes the rationale and technical implementation of slashing and withdrawal mechanisms in the SSV Based Applications platform._ + +--- + +## 1. Rationale & Security Considerations + +Slashing is particularly sensitive to withdrawal operations as they are the gateway for malicious strategies to attempt risk-free attacks — when a strategy commits slashable offenses and then withdraws its funds before suffering any penalty. + +As a security mechanism, withdrawals are implemented as a two-step process: + +1. **Withdrawal Request** (`proposeWithdrawal`) — strategy submits a request and records a share-based amount and timestamp. +2. **Execution** (`finalizeWithdrawal`) — after a configurable time window, the requested amount is released. + +However, to fully prevent risk‑free attacks, bApps should **track pending withdrawals** and **immediately adjust the strategy’s voting weight** to reflect its _future_ obligated balance upon the request. + +--- + +## 2. Illustrative Example + +![alt text](./img/sw_example.png "Example") + +For simplicity, assume **X** obligated tokens correspond to **X%** of voting weight. + +- At **t0**, the strategy’s voting weight is aligned with its obligated balance: 10 tokens ⇒ **10%** voting weight. +- At **t1**, the strategy calls `proposeWithdrawal(6)`. The bApp should immediately align the voting weight to reflect the _future_ obligated balance (10 − 6 = 4 tokens), reducing weight to **4%** even though the tokens remain until finalization. +- At **t1 + Δwindow**, executing the withdrawal releases the 6 tokens, leaving 4 tokens and **4%** weight. + +| Time | Obligated Balance | Voting Weight | +|----------------------------|-------------------|---------------| +| **t0** | 10 tokens | 10% | +| **t1** (_after request_) | 10 tokens | **4%** | +| **t1 + Δwindow** (finalize)| 4 tokens | 4% | + +--- + +## 3. Late‑Attack Scenario + +![alt text](./img/sw_late_attack.png "Late attack") + +Therefore, if the strategy attacks **just before** `finalizeWithdrawal`, its attack power is already capped to **4%**. The bApp can still call `slash(...)` on the remaining obligated tokens, seizing those 4 tokens. + +--- + +## 4. Early‑Attack Scenario + +![alt text](./img/sw_early_attack.png "Early attack") + +A strategy may attack at full **10%** voting power **before** the bApp detects its withdrawal request. In this case, although more powerful, the bApp can still fully slash up to **100%** of the obligation (10 tokens) at any point _during_ the withdrawal waiting period, preventing a risk‑free exit. + +--- + +## 5. Correlated Slashing & Withdrawal Timing + +![alt text](./img/sw_correlated.png "Correlated attack") + +When using correlated slashing, bApps often wait a delay Δ_slash after detecting a slashable offense before executing the penalty. To prevent a strategy from escaping in the gap: + +```text +Δ_slashing < Δ_withdrawal +``` + +That is, the correlated slashing window must be **shorter** than the withdrawal timelock window, ensuring the slash occurs _before_ funds can be withdrawn. + +--- + +## 6. Core On‑Chain Data Structures + +Refer to `libraries/CoreStorageLib.sol` and `libraries/ProtocolStorageLib.sol` for complete definitions. + +```solidity +// CoreStorageLib.Data +mapping(uint32 => mapping(address => mapping(address => ICore.WithdrawalRequest))) withdrawalRequests; +mapping(uint32 => mapping(address => mapping(address => ICore.Obligation))) obligations; +mapping(address => mapping(address => uint256)) slashingFund; + +// ProtocolStorageLib.Data +uint32 withdrawalTimelockPeriod; +uint32 withdrawalExpireTime; +uint32 disabledFeatures; // bitmask: slashingDisabled, withdrawalsDisabled, ... +``` + +--- + +## 7. Key Contract Snippets + +### Propose Withdrawal + +```solidity +function proposeWithdrawal( + uint32 strategyId, + address token, + uint256 amount +) external { + _checkWithdrawalsAllowed(); + _proposeWithdrawal(strategyId, token, amount); +} +``` + +### Finalize Withdrawal + +```solidity +function finalizeWithdrawal( + uint32 strategyId, + IERC20 token +) external nonReentrant { + _checkWithdrawalsAllowed(); + uint256 amount = _finalizeWithdrawal(strategyId, address(token)); + token.safeTransfer(msg.sender, amount); +} +``` + +### Slash Strategy + +```solidity +function slash( + uint32 strategyId, + address bApp, + address token, + uint32 percentage, + bytes calldata data +) external nonReentrant { + _checkSlashingAllowed(); + uint256 slashable = getSlashableBalance(strategyId, bApp, token); + uint256 amount = (slashable * percentage) / MAX_PERCENTAGE; + // delegate to bApp callback, adjust obligations or exit + s.slashingFund[receiver][token] += amount; +} +``` + + +## 8. Consequences for bApps + +When a slashing event or withdrawal occurs, bApps face several technical and economic consequences: + +### 8.1 Reduced Security Collateral +- **Obligated capital depletion** + Slashing permanently removes tokens from the strategy’s collateral. If the strategy was the only provider for that token, the bApp’s total secured stake decreases, potentially exposing it to higher risk. +- **Voting-weight impact** + Reduced obligations also lower the strategy’s voting weight, which may affect governance quorum and the bApp’s ability to influence protocol parameters. + +### 8.2 Financial Flows +- **Slashing fund accrual** + Upon a slash, the bApp (or designated receiver) accumulates funds in its `slashingFund` balance. These can be withdrawn via `withdrawSlashingFund(...)`: + ```solidity + function withdrawSlashingFund(address token, uint256 amount) external nonReentrant; + ``` +- **Capital rebalancing** + The bApp must decide whether to: + 1. **Replenish** obligations by incentivizing strategies to deposit more. + 2. **Adjust risk levels** or reduce service capacity to match the new collateral. + +### 8.3 Governance and Reputation +- **Risk-level adjustment** + Repeated slashing may trigger bApp marketplace logic (off-chain or on-chain) to flag higher shared risk levels for delegators and integrators. +- **Strategy exit** + If a strategy’s obligations drop to zero (full exit), the bApp loses that collateral entirely and must opt-in a new strategy or await new deposits. + +## 9. Slashing Mechanism Details + +This section consolidates the detailed behavior of the `slash(...)` function for both compliant and non‑compliant bApps, as well as post‑slashing rules and the slashing fund lifecycle. + +### 9.1 Modes of Invocation + +**1) Compliant bApp (Smart Contract implementing `IBasedApp`)** + +- The platform calls: + ```solidity + (success, receiver, exit) = IBasedApp(bApp).slash( + strategyId, + token, + percentage, + msg.sender, + data + ); + ``` +- `data` is forwarded, often containing proofs or auxiliary input. +- The bApp contract decides: + - **Receiver** of slashed funds (e.g. burn with `address(0)` or send to a treasury). + - **Exit or Adjust**: + - If `exit == true`, the strategy’s obligation percentage is set to 0 (full exit). + - Otherwise, obligations are reduced proportionally: + ```solidity + newObligated = oldObligated - amount; + newTotal = oldTotal - amount; + obligation.pct = newObligated * MAX_PERCENTAGE / newTotal; + ``` +- Finally, the slashed `amount` is credited to `slashingFund[receiver][token]`. + +**2) Non‑Compliant bApp (EOA or contract not supporting `IBasedApp`)** + +- Only the bApp address itself can invoke `slash(...)`. +- Receiver is forcibly `bApp` address. +- Strategy always **exits**: obligation percentage → 0. +- Slashed tokens accumulate in `slashingFund[bApp][token]`. + +### 9.2 Post‑Slashing Rules + +- **Re‑opt‑in Timelock** + After a strategy obligation is exited (percentage == 0), a new obligation can only be created again after the protocol’s **obligation timelock** (default 14 days). This prevents immediate re‑entry and enforces deliberate participation. + +### 9.3 Slashing Fund Lifecycle + +- **Accrual** + Slashed tokens are held in an internal fund (`slashingFund` mapping) until withdrawal. + +- **Withdrawal** + The designated receiver calls: + ```solidity + function withdrawSlashingFund(address token, uint256 amount) external nonReentrant; + function withdrawETHSlashingFund(uint256 amount) external nonReentrant; + ``` + under non‑zero and balance‑checked preconditions. + +- **Economic Impact** + Withdrawn funds are immediately available to the receiver, while the strategy’s capital and voting power remain permanently reduced. + + + + diff --git a/lib/forge-std b/lib/forge-std index bf909b22..9530d9ec 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit bf909b22fa55e244796dfa920c9639fdffa1c545 +Subproject commit 9530d9ec702df1b27b7f8f50c0a63a11b1b5fba9 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 9586aaf3..da32fb3b 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 9586aaf35241daf4b17e4858bf7c86edbb4b7247 +Subproject commit da32fb3bd89be5ad3c70dd329a34c9f150fa552f diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 773e11a8..50740658 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 773e11a869622361ac63dc1da992d1156f440464 +Subproject commit 50740658e6f1798de061de16155ee196a68e4956 diff --git a/package-lock.json b/package-lock.json index c28090d3..9c49699e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,47 +1,64 @@ { "name": "based-applications", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "based-applications", - "version": "0.0.0", + "version": "0.1.0", "license": "GPL-3.0", "devDependencies": { "@openzeppelin/contracts-upgradeable": "^5.3.0", "husky": "^9.1.7", - "lint-staged": "^15.5.1", + "lint-staged": "^16.0.0", "prettier": "^3.5.3", - "prettier-plugin-solidity": "^1.4.2", - "solhint": "^5.0.5" + "prettier-plugin-solidity": "^2.0.0", + "solhint": "^5.1.0" } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.2.tgz", + "integrity": "sha512-mNm/lblgES8UkVle8rGImXOz4TtL3eU3inHay/7TVchkKrb/lgcVvTK0+VAw8p5zQ0rgQsXm1j5dOlAAd+MeoA==", + "dev": true, + "license": "(Apache-2.0 WITH LLVM-exception)" + }, + "node_modules/@nomicfoundation/slang": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-1.1.0.tgz", + "integrity": "sha512-g2BofMUq1qCP22L/ksOftScrCxjdHTxgg8ch5PYon2zfSSKGCMwE4TgIC64CuorMcSsvCmqNNFEWR/fwFcMeTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bytecodealliance/preview2-shim": "0.17.2" + } + }, "node_modules/@openzeppelin/contracts": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.3.0.tgz", @@ -112,9 +129,9 @@ } }, "node_modules/@solidity-parser/parser": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.19.0.tgz", - "integrity": "sha512-RV16k/qIxW/wWc+mLzV3ARyKUaMUTBy9tOLMzFhtNSKYeTAanQ3a5MudJKf/8arIFnA2L27SNjarQKmFg0w/jA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.1.tgz", + "integrity": "sha512-58I2sRpzaQUN+jJmWbHfbWf9AKfzqCI8JAdFB0vbyY+u8tBRcuTt9LxzasvR0LGQpcRv97eyV7l61FQ3Ib7zVw==", "dev": true, "license": "MIT" }, @@ -172,26 +189,26 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -301,17 +318,13 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -350,103 +363,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -475,13 +391,13 @@ "license": "MIT" }, "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/config-chain": { @@ -522,25 +438,10 @@ } } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -605,9 +506,9 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -641,43 +542,6 @@ "dev": true, "license": "MIT" }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -837,9 +701,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, "license": "BSD-2-Clause" }, @@ -857,16 +721,6 @@ "node": ">=10.19.0" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -944,13 +798,16 @@ "license": "MIT" }, "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-number": { @@ -963,26 +820,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1071,60 +908,37 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz", - "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.0.tgz", + "integrity": "sha512-HkpQh69XHxgCjObjejBT3s2ILwNjFx8M3nw+tJ/ssBauDlIpkx2RpqWSi1fBgkXLSSXnbR3iEq1NkVtpvV+FLQ==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", + "commander": "^14.0.0", + "debug": "^4.4.1", "lilconfig": "^3.1.3", - "listr2": "^8.2.5", + "listr2": "^8.3.3", "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", - "yaml": "^2.7.0" + "yaml": "^2.8.0" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=18.12.0" + "node": ">=20.17" }, "funding": { "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/listr2": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.2.tgz", - "integrity": "sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1173,32 +987,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", @@ -1232,42 +1020,19 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { - "node": ">=12" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -1282,19 +1047,6 @@ "node": ">=8.6" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1351,43 +1103,27 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-url": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", - "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=20.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1404,16 +1140,16 @@ } }, "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1480,16 +1216,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -1560,20 +1286,21 @@ } }, "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==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-2.0.0.tgz", + "integrity": "sha512-tis3SwLSrYKDzzRFle48fjPM4GQKBtkVBUajAkt4b75/cc6zojFP7qjz6fDxKfup+34q0jKeSM3QeP9flJFXWw==", "dev": true, "license": "MIT", "dependencies": { - "@solidity-parser/parser": "^0.19.0", - "semver": "^7.6.3" + "@nomicfoundation/slang": "1.1.0", + "@solidity-parser/parser": "^0.20.1", + "semver": "^7.7.1" }, "engines": { "node": ">=18" }, "peerDependencies": { - "prettier": ">=2.3.0" + "prettier": ">=3.0.0" } }, "node_modules/proto-list": { @@ -1711,22 +1438,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -1735,9 +1446,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -1747,29 +1458,6 @@ "node": ">=10" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -1784,31 +1472,30 @@ } }, "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/solhint": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-5.0.5.tgz", - "integrity": "sha512-WrnG6T+/UduuzSWsSOAbfq1ywLUDwNea3Gd5hg6PS+pLUm8lz2ECNr0beX609clBxmDeZ3676AiA9nPDljmbJQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-5.1.0.tgz", + "integrity": "sha512-KWg4gnOnznxHXzH0fUvnhnxnk+1R50GiPChcPeQgA7SKQTSF1LLIEh8R1qbkCEn/fFzz4CfJs+Gh7Rl9uhHy+g==", "dev": true, "license": "MIT", "dependencies": { - "@solidity-parser/parser": "^0.19.0", + "@solidity-parser/parser": "^0.20.0", "ajv": "^6.12.6", "antlr4": "^4.13.1-patch-1", "ast-parents": "^0.0.1", @@ -1834,6 +1521,59 @@ "prettier": "^2.8.3" } }, + "node_modules/solhint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/solhint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/solhint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/solhint/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/solhint/node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -1851,6 +1591,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/solhint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -1862,44 +1615,37 @@ } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -1959,6 +1705,49 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/table/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -1966,6 +1755,52 @@ "dev": true, "license": "MIT" }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -1996,22 +1831,6 @@ "punycode": "^2.1.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -2030,73 +1849,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2105,16 +1857,16 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } } } diff --git a/package.json b/package.json index 480a1c62..445ef9d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "based-applications", - "version": "0.0.0", + "version": "0.1.0", "description": "SSV Based Applications", "author": "SSV.Network", "repository": { @@ -20,9 +20,9 @@ "scripts": { "compile": "forge compile", "build": "npm run lint:fix && forge clean && forge build", - "deploy:holesky": "npm run build && forge script scripts/DeployProxy.s.sol --rpc-url $HOLESKY_RPC_URL --private-key $PRIVATE_KEY --verify -vvv --broadcast", - "deploy:hoodi": "npm run build && forge script scripts/DeployProxy.s.sol --rpc-url $HOODI_RPC_URL --private-key $PRIVATE_KEY --etherscan-api-key $ETHERSCAN_API_KEY --verify -vvv --broadcast", - "verify:hoodi": "forge verify-contract --rpc-url https://hoodi.cloud.blockscout.com/api/eth-rpc --verifier blockscout --verifier-url 'https://hoodi.cloud.blockscout.com/api/' $IMPLEMENTATION_ADDRESS src/SSVBasedApps.sol:SSVBasedApps", + "deploy:hoodi-stage": "source .env && forge script script/DeployAllHoodi.s.sol false --sig 'run(bool)' --rpc-url $HOODI_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --verify -vvv --broadcast", + "deploy:hoodi-prod": "source .env && forge script script/DeployAllHoodi.s.sol true --sig 'run(bool)' --rpc-url $HOODI_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --verify -vvv --broadcast", + "deploy:mainnet": "source .env && forge script script/DeployAllMainnet.s.sol --rpc-url $MAINNET_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --verify -vvv --broadcast", "gas-report": "forge test --gas-report", "generate-docs": "solc --include-path node_modules --base-path . --combined-json userdoc,devdoc src/SSVBasedApps.sol --output-dir ./docs --overwrite", "prepare": "husky", @@ -30,16 +30,16 @@ "lint:check": "prettier --check **.sol", "lint:fix": "prettier --write **.sol", "test": "forge test", - "test-coverage": "forge coverage --no-match-coverage \"(scripts|test)\"", - "test-coverage-report": "forge coverage --no-match-coverage \"(scripts|test)\" --report debug > test.txt" + "test-coverage": "forge coverage --no-match-coverage \"(script|test)\"", + "test-coverage-report": "forge coverage --no-match-coverage \"(script|test)\" --report debug > test.txt" }, "devDependencies": { "@openzeppelin/contracts-upgradeable": "^5.3.0", "husky": "^9.1.7", - "lint-staged": "^15.5.1", + "lint-staged": "^16.0.0", "prettier": "^3.5.3", - "prettier-plugin-solidity": "^1.4.2", - "solhint": "^5.0.5" + "prettier-plugin-solidity": "^2.0.0", + "solhint": "^5.1.0" }, "lint-staged": { "src/**/*.sol": "solhint" diff --git a/remappings.txt b/remappings.txt index d79616a2..6d581d02 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,10 +3,8 @@ @ssv/src/middleware=src/middleware @ssv/lib/=lib/ @ssv/test/=test/ -@ssv/scripts/=scripts/ -@ssv/forge-std/=lib/forge-std/src/ +@ssv/script/=script/ @ssv/src/core=src/core @ssv/src/core/interfaces=src/core/interfaces @ssv/src/core/libraries=src/core/libraries -@ssv/src/core/modules=src/core/modules - +@ssv/src/core/modules=src/core/modules \ No newline at end of file diff --git a/script/DeployAll.sol b/script/DeployAll.sol new file mode 100644 index 00000000..4856923a --- /dev/null +++ b/script/DeployAll.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.29; + +import { + ERC1967Proxy +} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Script, console } from "forge-std/Script.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +import { StrategyManager } from "src/core/modules/StrategyManager.sol"; +import { BasedAppsManager } from "src/core/modules/BasedAppsManager.sol"; +import { ProtocolManager } from "src/core/modules/ProtocolManager.sol"; +import { SSVBasedApps } from "src/core/SSVBasedApps.sol"; +import { ProtocolStorageLib } from "src/core/libraries/ProtocolStorageLib.sol"; + +contract DeployAll is Script { + using stdJson for string; + + function _deployAll(string memory raw) internal returns (string memory) { + vm.startBroadcast(); + + SSVBasedApps impl = new SSVBasedApps(); + StrategyManager strategyMod = new StrategyManager(); + BasedAppsManager bAppsMod = new BasedAppsManager(); + ProtocolManager protocolMod = new ProtocolManager(); + + ERC1967Proxy proxy = deployProxy( + impl, + strategyMod, + bAppsMod, + protocolMod, + raw + ); + + vm.stopBroadcast(); + + console.log("SSVBasedApps Impl: ", address(impl)); + console.log("StrategyModule: ", address(strategyMod)); + console.log("BAppsModule: ", address(bAppsMod)); + console.log("ProtocolModule: ", address(protocolMod)); + console.log("SSVBasedApps Proxy: ", address(proxy)); + + return saveToJson(impl, proxy, strategyMod, bAppsMod, protocolMod, raw); + } + + function saveToJson( + SSVBasedApps impl, + ERC1967Proxy proxy, + StrategyManager strategyMod, + BasedAppsManager bAppsMod, + ProtocolManager protocolMod, + string memory raw + ) internal returns (string memory) { + string memory parent = "parent"; + + string memory deployed_addresses = "addresses"; + vm.serializeAddress( + deployed_addresses, + "SSVBasedAppsProxy", + address(proxy) + ); + vm.serializeAddress( + deployed_addresses, + "SSVBasedAppsImpl", + address(impl) + ); + vm.serializeAddress( + deployed_addresses, + "StrategyModule", + address(strategyMod) + ); + vm.serializeAddress( + deployed_addresses, + "BAppsModule", + address(bAppsMod) + ); + + string memory deployed_addresses_output = vm.serializeAddress( + deployed_addresses, + "ProtocolModule", + address(protocolMod) + ); + + string memory parameters = "parameters"; + vm.serializeUint( + parameters, + "feeTimelockPeriod", + raw.readUint(".feeTimelockPeriod") + ); + vm.serializeUint( + parameters, + "feeExpireTime", + raw.readUint(".feeExpireTime") + ); + vm.serializeUint( + parameters, + "withdrawalTimelockPeriod", + raw.readUint(".withdrawalTimelockPeriod") + ); + vm.serializeUint( + parameters, + "withdrawalExpireTime", + raw.readUint(".withdrawalExpireTime") + ); + vm.serializeUint( + parameters, + "obligationTimelockPeriod", + raw.readUint(".obligationTimelockPeriod") + ); + vm.serializeUint( + parameters, + "obligationExpireTime", + raw.readUint(".obligationExpireTime") + ); + vm.serializeUint( + parameters, + "tokenUpdateTimelockPeriod", + raw.readUint(".tokenUpdateTimelockPeriod") + ); + vm.serializeUint(parameters, "maxShares", raw.readUint(".maxShares")); + vm.serializeUint( + parameters, + "maxFeeIncrement", + raw.readUint(".maxFeeIncrement") + ); + string memory parameters_output = vm.serializeUint( + parameters, + "disabledFeatures", + raw.readUint(".disabledFeatures") + ); + + string memory chain_info = "chainInfo"; + vm.serializeUint(chain_info, "deploymentBlock", block.number); + string memory chain_info_output = vm.serializeUint( + chain_info, + "chainId", + block.chainid + ); + + // serialize all the data + vm.serializeString( + parent, + deployed_addresses, + deployed_addresses_output + ); + vm.serializeString(parent, chain_info, chain_info_output); + return vm.serializeString(parent, parameters, parameters_output); + } + + function deployProxy( + SSVBasedApps impl, + StrategyManager strategyMod, + BasedAppsManager bAppsMod, + ProtocolManager protocolMod, + string memory raw + ) internal returns (ERC1967Proxy proxy) { + return + new ERC1967Proxy( + address(impl), + abi.encodeWithSelector( + impl.initialize.selector, + msg.sender, + address(bAppsMod), + address(strategyMod), + address(protocolMod), + ProtocolStorageLib.Data({ + feeTimelockPeriod: uint32( + raw.readUint(".feeTimelockPeriod") + ), + feeExpireTime: uint32(raw.readUint(".feeExpireTime")), + withdrawalTimelockPeriod: uint32( + raw.readUint(".withdrawalTimelockPeriod") + ), + withdrawalExpireTime: uint32( + raw.readUint(".withdrawalExpireTime") + ), + obligationTimelockPeriod: uint32( + raw.readUint(".obligationTimelockPeriod") + ), + obligationExpireTime: uint32( + raw.readUint(".obligationExpireTime") + ), + tokenUpdateTimelockPeriod: uint32( + raw.readUint(".tokenUpdateTimelockPeriod") + ), + maxShares: raw.readUint(".maxShares"), + maxFeeIncrement: uint32( + raw.readUint(".maxFeeIncrement") + ), + disabledFeatures: uint32( + raw.readUint(".disabledFeatures") + ) + }) + ) + ); + } +} diff --git a/script/DeployAllHoodi.s.sol b/script/DeployAllHoodi.s.sol new file mode 100644 index 00000000..95432c9a --- /dev/null +++ b/script/DeployAllHoodi.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.29; + +import { Script } from "forge-std/Script.sol"; +import { DeployAll } from "./DeployAll.sol"; + +contract DeployAllHoodi is Script, DeployAll { + function run(bool isProd) external { + if (block.chainid != 560_048) { + revert("This script is only for the Hoodi prod"); + } + + string memory cfgPath; + string memory outPath; + if (isProd) { + cfgPath = "script/config/hoodi-prod.json"; + outPath = "artifacts/deploy-hoodi-prod.json"; + } else { + cfgPath = "script/config/hoodi-stage.json"; + outPath = "artifacts/deploy-hoodi-stage.json"; + } + + string memory finalJson = _deployAll(vm.readFile(cfgPath)); + + vm.writeJson(finalJson, outPath); + } +} diff --git a/script/DeployAllMainnet.s.sol b/script/DeployAllMainnet.s.sol new file mode 100644 index 00000000..4da956cc --- /dev/null +++ b/script/DeployAllMainnet.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.29; + +import { Script, console } from "forge-std/Script.sol"; +import { DeployAll } from "./DeployAll.sol"; + +contract DeployAllHoodi is Script, DeployAll { + function run() external { + if (block.chainid != 1) { + revert("This script is only for Mainnet"); + } + + string memory finalJson = _deployAll( + vm.readFile("script/config/mainnet.json") + ); + + vm.writeJson(finalJson, "artifacts/deploy-mainnet.json"); + } +} diff --git a/script/DeployAllSepolia.s.sol b/script/DeployAllSepolia.s.sol new file mode 100644 index 00000000..f66ce64f --- /dev/null +++ b/script/DeployAllSepolia.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.29; + +import { Script, console } from "forge-std/Script.sol"; +import { DeployAll } from "./DeployAll.sol"; + +contract DeployAllHoodi is Script, DeployAll { + function run() external { + if (block.chainid != 11155111) { + revert("This script is only for Sepolia"); + } + + string memory finalJson = _deployAll( + vm.readFile("script/config/sepolia.json") + ); + + vm.writeJson(finalJson, "artifacts/deploy-sepolia.json"); + } +} diff --git a/script/config/hoodi-prod.json b/script/config/hoodi-prod.json new file mode 100644 index 00000000..9b0f9b60 --- /dev/null +++ b/script/config/hoodi-prod.json @@ -0,0 +1,12 @@ +{ + "feeTimelockPeriod": 86400, + "feeExpireTime": 3600, + "withdrawalTimelockPeriod": 172800, + "withdrawalExpireTime": 86400, + "obligationTimelockPeriod": 172800, + "obligationExpireTime": 86400, + "tokenUpdateTimelockPeriod": 3600, + "maxShares": "1e50", + "maxFeeIncrement": 500, + "disabledFeatures": 0 +} diff --git a/script/config/hoodi-stage.json b/script/config/hoodi-stage.json new file mode 100644 index 00000000..b7dc49ca --- /dev/null +++ b/script/config/hoodi-stage.json @@ -0,0 +1,12 @@ +{ + "feeTimelockPeriod": 300, + "feeExpireTime": 3600, + "withdrawalTimelockPeriod": 300, + "withdrawalExpireTime": 3600, + "obligationTimelockPeriod": 300, + "obligationExpireTime": 3600, + "tokenUpdateTimelockPeriod": 300, + "maxShares": "1e50", + "maxFeeIncrement": 500, + "disabledFeatures": 0 +} diff --git a/script/config/mainnet.json b/script/config/mainnet.json new file mode 100644 index 00000000..60fe7331 --- /dev/null +++ b/script/config/mainnet.json @@ -0,0 +1,12 @@ +{ + "feeTimelockPeriod": 604800, + "feeExpireTime": 86400, + "withdrawalTimelockPeriod": 1209600, + "withdrawalExpireTime": 259200, + "obligationTimelockPeriod": 1209600, + "obligationExpireTime": 259200, + "tokenUpdateTimelockPeriod": 1209600, + "maxShares": "1e50", + "maxFeeIncrement": 500, + "disabledFeatures": 3 +} diff --git a/scripts/DeployProxy.s.sol b/scripts/DeployProxy.s.sol deleted file mode 100644 index 8ef0810d..00000000 --- a/scripts/DeployProxy.s.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.29; - -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -import { Script, console } from "@ssv/forge-std/Script.sol"; -import { StrategyManager } from "@ssv/src/core/modules/StrategyManager.sol"; -import { BasedAppsManager } from "@ssv/src/core/modules/BasedAppsManager.sol"; -import { ProtocolManager } from "@ssv/src/core/modules/ProtocolManager.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; -import { IProtocolManager } from "@ssv/src/core/interfaces/IProtocolManager.sol"; -import { SSVBasedApps } from "src/core/SSVBasedApps.sol"; -import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; - -// solhint-disable no-console -contract DeployProxy is Script { - function run() external { - vm.startBroadcast(); - - SSVBasedApps implementation = new SSVBasedApps(); - - StrategyManager strategyManagerMod = new StrategyManager(); - BasedAppsManager basedAppsManagerMod = new BasedAppsManager(); - ProtocolManager protocolManagerMod = new ProtocolManager(); - - ProtocolStorageLib.Data memory config = ProtocolStorageLib.Data({ - feeTimelockPeriod: 7 days, - feeExpireTime: 1 days, - withdrawalTimelockPeriod: 14 days, - maxShares: 1e50, - withdrawalExpireTime: 3 days, - obligationTimelockPeriod: 14 days, - obligationExpireTime: 3 days, - maxFeeIncrement: 500 - }); - - bytes memory initData = abi.encodeWithSelector( - implementation.initialize.selector, - msg.sender, - IBasedAppManager(basedAppsManagerMod), - IStrategyManager(strategyManagerMod), - IProtocolManager(protocolManagerMod), - config - ); - - ERC1967Proxy proxy = new ERC1967Proxy( - address(implementation), - initData - ); - - console.log("Implementation deployed at:", address(implementation)); - console.log( - "Module StrategyManager deployed at:", - address(strategyManagerMod) - ); - console.log( - "Module BasedAppsManager deployed at:", - address(basedAppsManagerMod) - ); - console.log( - "Module ProtocolManager deployed at:", - address(protocolManagerMod) - ); - console.log("Proxy deployed at:", address(proxy)); - - console.log("Fee Timelock Period:", config.feeTimelockPeriod); - console.log("Fee Expire Time:", config.feeExpireTime); - console.log( - "Withdrawal Timelock Period:", - config.withdrawalTimelockPeriod - ); - console.log("Withdrawal Expire Time:", config.withdrawalExpireTime); - console.log( - "Obligation Timelock Period:", - config.obligationTimelockPeriod - ); - console.log("Obligation Expire Time:", config.obligationExpireTime); - console.log("Max Shares:", config.maxShares); - console.log("Max Fee Increment:", config.maxFeeIncrement); - vm.stopBroadcast(); - } -} diff --git a/scripts/UpgradeProxy.s.sol b/scripts/UpgradeProxy.s.sol deleted file mode 100644 index 2ac113e5..00000000 --- a/scripts/UpgradeProxy.s.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.29; - -import { Script, console } from "@ssv/forge-std/Script.sol"; - -interface IUUPSUpgradeable { - function upgradeToAndCall( - address newImplementation, - bytes calldata data - ) external; -} - -// solhint-disable no-console -contract UpgradeScript is Script { - function run() external { - vm.startBroadcast(); - - address proxyAddress = vm.envAddress("PROXY_ADDRESS"); - address newImplementation = vm.envAddress("IMPLEMENTATION_ADDRESS"); - - IUUPSUpgradeable(proxyAddress).upgradeToAndCall(newImplementation, ""); - - console.log( - "Proxy ", - proxyAddress, - " upgraded to new implementation: ", - newImplementation - ); - - vm.stopBroadcast(); - } -} diff --git a/src/core/SSVBasedApps.sol b/src/core/SSVBasedApps.sol index cc2001fd..6fdd958d 100644 --- a/src/core/SSVBasedApps.sol +++ b/src/core/SSVBasedApps.sol @@ -2,17 +2,37 @@ pragma solidity 0.8.29; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -import { MAX_PERCENTAGE, ETH_ADDRESS } from "@ssv/src/core/libraries/ValidationLib.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + Ownable2StepUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { + UUPSUpgradeable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import { + MIN_EXPIRE_TIME, + MIN_TIME_LOCK_PERIOD, + MAX_PERCENTAGE, + ETH_ADDRESS +} from "@ssv/src/core/libraries/ValidationLib.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; import { ISSVBasedApps } from "@ssv/src/core/interfaces/ISSVBasedApps.sol"; -import { IProtocolManager } from "@ssv/src/core/interfaces/IProtocolManager.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; -import { CoreStorageLib, SSVCoreModules } from "@ssv/src/core/libraries/CoreStorageLib.sol"; -import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; +import { + IProtocolManager +} from "@ssv/src/core/interfaces/IProtocolManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + CoreStorageLib, + SSVCoreModules +} from "@ssv/src/core/libraries/CoreStorageLib.sol"; +import { + ProtocolStorageLib +} from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; /** * @title SSVBasedApps @@ -59,10 +79,7 @@ import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.s contract SSVBasedApps is ISSVBasedApps, UUPSUpgradeable, - Ownable2StepUpgradeable, - IBasedAppManager, - IStrategyManager, - IProtocolManager + Ownable2StepUpgradeable { // *************************** // ** Section: Initializers ** @@ -72,7 +89,7 @@ contract SSVBasedApps is IBasedAppManager ssvBasedAppManger_, IStrategyManager ssvStrategyManager_, IProtocolManager protocolManager_, - ProtocolStorageLib.Data memory config + ProtocolStorageLib.Data calldata config ) external override initializer onlyProxy { __UUPSUpgradeable_init(); __Ownable_init_unchained(owner_); @@ -89,7 +106,7 @@ contract SSVBasedApps is IBasedAppManager ssvBasedAppManger_, IStrategyManager ssvStrategyManager_, IProtocolManager protocolManager_, - ProtocolStorageLib.Data memory config + ProtocolStorageLib.Data calldata config ) internal onlyInitializing { CoreStorageLib.Data storage s = CoreStorageLib.load(); ProtocolStorageLib.Data storage sp = ProtocolStorageLib.load(); @@ -103,10 +120,49 @@ contract SSVBasedApps is protocolManager_ ); - if (config.maxFeeIncrement == 0 || config.maxFeeIncrement > 10_000) { + if ( + config.maxFeeIncrement == 0 || + config.maxFeeIncrement > MAX_PERCENTAGE + ) { revert InvalidMaxFeeIncrement(); } + if (config.maxShares == 0 || config.maxShares < 1e50) { + revert InvalidMaxShares(); + } + + if (config.feeTimelockPeriod < MIN_TIME_LOCK_PERIOD) { + revert InvalidFeeTimelockPeriod(); + } + + if (config.feeExpireTime < MIN_EXPIRE_TIME) { + revert InvalidFeeExpireTime(); + } + + if (config.withdrawalTimelockPeriod < MIN_TIME_LOCK_PERIOD) { + revert InvalidWithdrawalTimelockPeriod(); + } + + if (config.withdrawalExpireTime < MIN_EXPIRE_TIME) { + revert InvalidWithdrawalExpireTime(); + } + + if (config.obligationTimelockPeriod < MIN_TIME_LOCK_PERIOD) { + revert InvalidObligationTimelockPeriod(); + } + + if (config.obligationExpireTime < MIN_EXPIRE_TIME) { + revert InvalidObligationExpireTime(); + } + + if (config.tokenUpdateTimelockPeriod < MIN_EXPIRE_TIME) { + revert InvalidTokenUpdateTimelockPeriod(); + } + + if (config.disabledFeatures > 3) { + revert InvalidDisabledFeatures(); + } + sp.maxFeeIncrement = config.maxFeeIncrement; sp.feeTimelockPeriod = config.feeTimelockPeriod; sp.feeExpireTime = config.feeExpireTime; @@ -114,7 +170,9 @@ contract SSVBasedApps is sp.withdrawalExpireTime = config.withdrawalExpireTime; sp.obligationTimelockPeriod = config.obligationTimelockPeriod; sp.obligationExpireTime = config.obligationExpireTime; + sp.tokenUpdateTimelockPeriod = config.tokenUpdateTimelockPeriod; sp.maxShares = config.maxShares; + sp.disabledFeatures = config.disabledFeatures; emit MaxFeeIncrementSet(sp.maxFeeIncrement); } @@ -140,13 +198,18 @@ contract SSVBasedApps is } function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external { _delegateTo(SSVCoreModules.SSV_BAPPS_MANAGER); } + function updateBAppsTokens( + ICore.TokenConfig[] calldata tokenConfigs + ) external { + _delegateTo(SSVCoreModules.SSV_BAPPS_MANAGER); + } + function createObligation( uint32 strategyId, address bApp, @@ -271,7 +334,7 @@ contract SSVBasedApps is uint32 strategyId, address bApp, address token, - uint256 amount, + uint32 percentage, bytes calldata data ) external { _delegateTo(SSVCoreModules.SSV_STRATEGY_MANAGER); @@ -323,6 +386,10 @@ contract SSVBasedApps is _delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER); } + function updateTokenUpdateTimelockPeriod(uint32 value) external onlyOwner { + _delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER); + } + function updateMaxShares(uint256 value) external onlyOwner { _delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER); } @@ -331,6 +398,10 @@ contract SSVBasedApps is _delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER); } + function updateDisabledFeatures(uint32 value) external onlyOwner { + _delegateTo(SSVCoreModules.SSV_PROTOCOL_MANAGER); + } + // ***************************** // ** Section: External Views ** // ***************************** @@ -364,6 +435,13 @@ contract SSVBasedApps is return (s.strategies[strategyId].owner, s.strategies[strategyId].fee); } + function ownedStrategies( + address owner + ) external view returns (uint32[] memory strategyIds) { + CoreStorageLib.Data storage s = CoreStorageLib.load(); + return s.strategyOwners[owner]; + } + function strategyAccountShares( uint32 strategyId, address account, @@ -420,22 +498,25 @@ contract SSVBasedApps is ); } - function usedTokens( - uint32 strategyId, - address token - ) external view returns (uint32) { - CoreStorageLib.Data storage s = CoreStorageLib.load(); - return s.usedTokens[strategyId][token]; - } - function bAppTokens( address bApp, address token - ) external view returns (uint32 value, bool isSet) { + ) + external + view + returns ( + uint32 currentValue, + bool isSet, + uint32 pendingValue, + uint32 effectTime + ) + { CoreStorageLib.Data storage s = CoreStorageLib.load(); return ( - s.bAppTokens[bApp][token].value, - s.bAppTokens[bApp][token].isSet + s.bAppTokens[bApp][token].currentValue, + s.bAppTokens[bApp][token].isSet, + s.bAppTokens[bApp][token].pendingValue, + s.bAppTokens[bApp][token].effectTime ); } @@ -444,7 +525,7 @@ contract SSVBasedApps is address bApp ) external view returns (uint32) { CoreStorageLib.Data storage s = CoreStorageLib.load(); - return s.accountBAppStrategy[account][bApp]; + return s.accountBAppStrategy[account][bApp].strategyId; } function feeUpdateRequests( @@ -533,8 +614,16 @@ contract SSVBasedApps is return ProtocolStorageLib.load().obligationExpireTime; } + function disabledFeatures() external view returns (uint32) { + return ProtocolStorageLib.load().disabledFeatures; + } + + function tokenUpdateTimelockPeriod() external view returns (uint32) { + return ProtocolStorageLib.load().tokenUpdateTimelockPeriod; + } + function getVersion() external pure returns (string memory) { - return "0.0.1"; + return "0.1.0"; } // ********************************* diff --git a/src/core/interfaces/IBasedAppManager.sol b/src/core/interfaces/IBasedAppManager.sol index 6c30eccc..55441b7d 100644 --- a/src/core/interfaces/IBasedAppManager.sol +++ b/src/core/interfaces/IBasedAppManager.sol @@ -1,24 +1,28 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; + interface IBasedAppManager { - event BAppMetadataURIUpdated( - address indexed bAppAddress, - string metadataURI - ); + event BAppMetadataURIUpdated(address indexed bApp, string metadataURI); event BAppRegistered( - address indexed bAppAddress, - address[] tokens, - uint32[] sharedRiskLevel, + address indexed bApp, + ICore.TokenConfig[] tokenConfigs, string metadataURI ); + event BAppTokensUpdated( + address indexed bApp, + ICore.TokenConfig[] tokenConfigs + ); function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external; function updateBAppMetadataURI(string calldata metadataURI) external; + function updateBAppsTokens( + ICore.TokenConfig[] calldata tokenConfigs + ) external; error BAppAlreadyRegistered(); error BAppDoesNotSupportInterface(); diff --git a/src/core/interfaces/ICore.sol b/src/core/interfaces/ICore.sol index 86f9df0c..2949a275 100644 --- a/src/core/interfaces/ICore.sol +++ b/src/core/interfaces/ICore.sol @@ -7,9 +7,13 @@ interface ICore { /// @dev The shared risk level /// Encoding: The value is stored as a uint32. However, it represents a real (float) value. /// To get the actual real value (decode), divide by 10^6. - uint32 value; + uint32 currentValue; /// @dev if the shared risk level is set bool isSet; + /// @dev The value to be updated + uint32 pendingValue; + /// @dev The block time when the update request was sent + uint32 effectTime; } /// @notice Represents an Obligation @@ -28,6 +32,15 @@ interface ICore { uint32 fee; } + struct StrategyOptIn { + /// @dev The strategy ID + uint32 strategyId; + /// @dev The block time when the opt-in was sent + uint32 requestTime; + /// @dev The opt-out time + uint32 optOutEffectiveTime; + } + /// @notice Represents a FeeUpdateRequest struct FeeUpdateRequest { /// @dev The new fee percentage @@ -67,4 +80,14 @@ interface ICore { /// @dev The account latest generation mapping(address => uint256) accountGeneration; } + + struct TokenUpdateRequest { + TokenConfig[] tokens; + uint32 requestTime; + } + + struct TokenConfig { + address token; + uint32 sharedRiskLevel; + } } diff --git a/src/core/interfaces/IProtocolManager.sol b/src/core/interfaces/IProtocolManager.sol index 7d22b049..65801c25 100644 --- a/src/core/interfaces/IProtocolManager.sol +++ b/src/core/interfaces/IProtocolManager.sol @@ -6,10 +6,12 @@ interface IProtocolManager { event FeeTimelockPeriodUpdated(uint32 feeTimelockPeriod); event ObligationExpireTimeUpdated(uint32 obligationExpireTime); event ObligationTimelockPeriodUpdated(uint32 obligationTimelockPeriod); + event TokenUpdateTimelockPeriodUpdated(uint32 tokenUpdateTimelockPeriod); event StrategyMaxFeeIncrementUpdated(uint32 maxFeeIncrement); event StrategyMaxSharesUpdated(uint256 maxShares); event WithdrawalExpireTimeUpdated(uint32 withdrawalExpireTime); event WithdrawalTimelockPeriodUpdated(uint32 withdrawalTimelockPeriod); + event DisabledFeaturesUpdated(uint32 disabledFeatures); function updateFeeExpireTime(uint32 value) external; function updateFeeTimelockPeriod(uint32 value) external; @@ -17,6 +19,7 @@ interface IProtocolManager { function updateMaxShares(uint256 value) external; function updateObligationExpireTime(uint32 value) external; function updateObligationTimelockPeriod(uint32 value) external; + function updateTokenUpdateTimelockPeriod(uint32 value) external; function updateWithdrawalExpireTime(uint32 value) external; function updateWithdrawalTimelockPeriod(uint32 value) external; } diff --git a/src/core/interfaces/ISSVBasedApps.sol b/src/core/interfaces/ISSVBasedApps.sol index 93950c25..791f1b22 100644 --- a/src/core/interfaces/ISSVBasedApps.sol +++ b/src/core/interfaces/ISSVBasedApps.sol @@ -1,19 +1,32 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; -import { IProtocolManager } from "@ssv/src/core/interfaces/IProtocolManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IProtocolManager +} from "@ssv/src/core/interfaces/IProtocolManager.sol"; +import { IViews } from "@ssv/src/core/interfaces/IViews.sol"; import { SSVCoreModules } from "@ssv/src/core/libraries/CoreStorageLib.sol"; -import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; +import { + ProtocolStorageLib +} from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; -interface ISSVBasedApps { +interface ISSVBasedApps is + IStrategyManager, + IBasedAppManager, + IProtocolManager, + IViews +{ event ModuleUpdated(SSVCoreModules indexed moduleId, address moduleAddress); function getModuleAddress( SSVCoreModules moduleId ) external view returns (address); - function getVersion() external pure returns (string memory version); function initialize( address owner_, IBasedAppManager ssvBasedAppManger_, @@ -27,5 +40,14 @@ interface ISSVBasedApps { ) external; error InvalidMaxFeeIncrement(); + error InvalidMaxShares(); + error InvalidFeeTimelockPeriod(); + error InvalidFeeExpireTime(); + error InvalidWithdrawalTimelockPeriod(); + error InvalidWithdrawalExpireTime(); + error InvalidObligationTimelockPeriod(); + error InvalidObligationExpireTime(); + error InvalidTokenUpdateTimelockPeriod(); + error InvalidDisabledFeatures(); error TargetModuleDoesNotExist(uint8 moduleId); } diff --git a/src/core/interfaces/IStrategyManager.sol b/src/core/interfaces/IStrategyManager.sol index fe4d770c..649be827 100644 --- a/src/core/interfaces/IStrategyManager.sol +++ b/src/core/interfaces/IStrategyManager.sol @@ -15,6 +15,10 @@ interface IStrategyManager { address[] tokens, uint32[] obligationPercentages ); + event BAppOptedOutByStrategy( + uint32 indexed strategyId, + address indexed bApp + ); event DelegationCreated( address indexed delegator, address indexed receiver, @@ -93,7 +97,7 @@ interface IStrategyManager { uint32 indexed strategyId, address indexed bApp, address token, - uint256 amount, + uint32 percentage, address receiver ); @@ -148,7 +152,7 @@ interface IStrategyManager { uint32 strategyId, address bApp, address token, - uint256 amount, + uint32 percentage, bytes calldata data ) external; function updateAccountMetadataURI(string calldata metadataURI) external; @@ -165,6 +169,7 @@ interface IStrategyManager { error BAppAlreadyOptedIn(); error BAppNotOptedIn(); + error BAppAlreadyOptedOut(); error BAppOptInFailed(); error BAppSlashingFailed(); error DelegationAlreadyExists(); @@ -188,7 +193,9 @@ interface IStrategyManager { error ObligationAlreadySet(); error ObligationHasNotBeenCreated(); error RequestTimeExpired(); + error SlashingDisabled(); error TimelockNotElapsed(); error TokenNotSupportedByBApp(address token); error WithdrawTransferFailed(); + error WithdrawalsDisabled(); } diff --git a/src/core/interfaces/IViews.sol b/src/core/interfaces/IViews.sol new file mode 100644 index 00000000..b1d30776 --- /dev/null +++ b/src/core/interfaces/IViews.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.29; + +interface IViews { + function delegations( + address account, + address receiver + ) external view returns (uint32); + function totalDelegatedPercentage( + address delegator + ) external view returns (uint32); + function registeredBApps( + address bApp + ) external view returns (bool isRegistered); + function strategies( + uint32 strategyId + ) external view returns (address strategyOwner, uint32 fee); + function ownedStrategies( + address owner + ) external view returns (uint32[] memory strategyIds); + function strategyAccountShares( + uint32 strategyId, + address account, + address token + ) external view returns (uint256); + function strategyTotalBalance( + uint32 strategyId, + address token + ) external view returns (uint256); + function strategyTotalShares( + uint32 strategyId, + address token + ) external view returns (uint256); + function strategyGeneration( + uint32 strategyId, + address token + ) external view returns (uint256); + function obligations( + uint32 strategyId, + address bApp, + address token + ) external view returns (uint32 percentage, bool isSet); + function bAppTokens( + address bApp, + address token + ) + external + view + returns ( + uint32 currentValue, + bool isSet, + uint32 pendingValue, + uint32 effectTime + ); + function accountBAppStrategy( + address account, + address bApp + ) external view returns (uint32); + function feeUpdateRequests( + uint32 strategyId + ) external view returns (uint32 percentage, uint32 requestTime); + function withdrawalRequests( + uint32 strategyId, + address account, + address token + ) external view returns (uint256 shares, uint32 requestTime); + function obligationRequests( + uint32 strategyId, + address token, + address bApp + ) external view returns (uint32 percentage, uint32 requestTime); + function slashingFund( + address account, + address token + ) external view returns (uint256); + + // External Protocol Views + function maxPercentage() external pure returns (uint32); + function ethAddress() external pure returns (address); + function maxShares() external view returns (uint256); + function maxFeeIncrement() external view returns (uint32); + function feeTimelockPeriod() external view returns (uint32); + function feeExpireTime() external view returns (uint32); + function withdrawalTimelockPeriod() external view returns (uint32); + function withdrawalExpireTime() external view returns (uint32); + function obligationTimelockPeriod() external view returns (uint32); + function obligationExpireTime() external view returns (uint32); + function disabledFeatures() external view returns (uint32); + function tokenUpdateTimelockPeriod() external view returns (uint32); + function getVersion() external pure returns (string memory); +} diff --git a/src/core/libraries/CoreStorageLib.sol b/src/core/libraries/CoreStorageLib.sol index 41d09964..2e4160b0 100644 --- a/src/core/libraries/CoreStorageLib.sol +++ b/src/core/libraries/CoreStorageLib.sol @@ -21,11 +21,16 @@ library CoreStorageLib { * @dev The strategy ID is incremental and unique */ mapping(uint32 strategyId => ICore.Strategy) strategies; + /** + * @notice Tracks the owners of the strategies + * @dev The strategy ID is incremental and unique + */ + mapping(address owner => uint32[] strategyId) strategyOwners; /** * @notice Links an account to a single strategy for a specific bApp * @dev Guarantees that an account cannot have more than one strategy for a given bApp */ - mapping(address account => mapping(address bApp => uint32 strategyId)) accountBAppStrategy; + mapping(address account => mapping(address bApp => ICore.StrategyOptIn strategyOptIn)) accountBAppStrategy; /** * @notice Tracks the percentage of validator balance a delegator has delegated to a specific receiver account * @dev Each delegator can allocate a portion of their validator balance to multiple accounts including itself @@ -47,12 +52,6 @@ library CoreStorageLib { * @dev Uses a hash of the bApp and token to map the obligation percentage for the strategy. */ mapping(uint32 strategyId => mapping(address bApp => mapping(address token => ICore.Obligation))) obligations; - /** - * @notice Tracks unallocated tokens in a strategy. - * @dev Count the number of bApps that have one obligation set for the token. - * If the counter is 0, the token is unused and we can allow fast withdrawal. - */ - mapping(uint32 strategyId => mapping(address token => uint32 bAppsCounter)) usedTokens; /** * @notice Tracks all the withdrawal requests divided by token per strategy. * @dev User can have only one pending withdrawal request per token. @@ -87,6 +86,12 @@ library CoreStorageLib { * @dev The bApp is identified with its address */ mapping(address bApp => mapping(address token => ICore.SharedRiskLevel)) bAppTokens; + /** + * @notice Tracks the token update requests for a bApp + * @dev Only the bApp owner can submit one. + * Submitting a new request will overwrite the previous one and reset the timer. + */ + mapping(address bApp => ICore.TokenUpdateRequest) tokenUpdateRequests; } uint256 private constant SSV_BASED_APPS_STORAGE_POSITION = diff --git a/src/core/libraries/ProtocolStorageLib.sol b/src/core/libraries/ProtocolStorageLib.sol index 8fba0bd6..2f277342 100644 --- a/src/core/libraries/ProtocolStorageLib.sol +++ b/src/core/libraries/ProtocolStorageLib.sol @@ -5,14 +5,20 @@ library ProtocolStorageLib { /// @title SSV Based Apps Storage Protocol /// @notice Represents the operational settings and parameters required by the SSV Based Application Platform struct Data { + uint256 maxShares; uint32 feeTimelockPeriod; uint32 feeExpireTime; uint32 withdrawalTimelockPeriod; uint32 withdrawalExpireTime; uint32 obligationTimelockPeriod; uint32 obligationExpireTime; + uint32 tokenUpdateTimelockPeriod; uint32 maxFeeIncrement; - uint256 maxShares; + // each bit, starting from the LSB, represents a DISABLED feature + // bit 0 = slashingDisabled + // bit 1 = withdrawalsDisabled + // ... + uint32 disabledFeatures; } uint256 private constant SSV_STORAGE_POSITION = diff --git a/src/core/libraries/ValidationLib.sol b/src/core/libraries/ValidationLib.sol index 7dd4ebec..3dd1ece0 100644 --- a/src/core/libraries/ValidationLib.sol +++ b/src/core/libraries/ValidationLib.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.29; +uint32 constant MIN_TIME_LOCK_PERIOD = 1 days; +uint32 constant MIN_EXPIRE_TIME = 1 hours; + uint32 constant MAX_PERCENTAGE = 1e4; // 100% in basis points address constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; diff --git a/src/core/modules/BasedAppsManager.sol b/src/core/modules/BasedAppsManager.sol index 2a9e17c5..f2b3ed9d 100644 --- a/src/core/modules/BasedAppsManager.sol +++ b/src/core/modules/BasedAppsManager.sol @@ -2,29 +2,30 @@ pragma solidity 0.8.29; import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; import { CoreStorageLib } from "@ssv/src/core/libraries/CoreStorageLib.sol"; +import { + ProtocolStorageLib +} from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; import { ValidationLib } from "@ssv/src/core/libraries/ValidationLib.sol"; contract BasedAppsManager is IBasedAppManager { /// @notice Allow the function to be called only by a registered bApp - modifier onlyRegisteredBApp() { - CoreStorageLib.Data storage s = CoreStorageLib.load(); + function _onlyRegisteredBApp(CoreStorageLib.Data storage s) private view { if (!s.registeredBApps[msg.sender]) { revert IBasedAppManager.BAppNotRegistered(); } - _; } /// @notice Registers a bApp. - /// @param tokens The list of tokens the bApp accepts; can be empty. - /// @param sharedRiskLevels The shared risk level of the bApp. + /// @param tokenConfigs The list of tokens configs the bApp accepts; can be empty. /// @param metadataURI The metadata URI of the bApp, which is a link (e.g., http://example.com) /// to a JSON file containing metadata such as the name, description, logo, etc. /// @dev Allows creating a bApp even with an empty token list. function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external { CoreStorageLib.Data storage s = CoreStorageLib.load(); @@ -35,58 +36,71 @@ contract BasedAppsManager is IBasedAppManager { s.registeredBApps[msg.sender] = true; - _addNewTokens(msg.sender, tokens, sharedRiskLevels); + _addNewTokens(msg.sender, tokenConfigs); - emit BAppRegistered(msg.sender, tokens, sharedRiskLevels, metadataURI); + emit BAppRegistered(msg.sender, tokenConfigs, metadataURI); } /// @notice Function to update the metadata URI of the Based Application /// @param metadataURI The new metadata URI - function updateBAppMetadataURI( - string calldata metadataURI - ) external onlyRegisteredBApp { + function updateBAppMetadataURI(string calldata metadataURI) external { + _onlyRegisteredBApp(CoreStorageLib.load()); emit BAppMetadataURIUpdated(msg.sender, metadataURI); } + function updateBAppsTokens( + ICore.TokenConfig[] calldata tokenConfigs + ) external { + CoreStorageLib.Data storage s = CoreStorageLib.load(); + + _onlyRegisteredBApp(s); + + uint32 requestTime = uint32(block.timestamp); + + address token; + ICore.SharedRiskLevel storage tokenData; + ProtocolStorageLib.Data storage sp = ProtocolStorageLib.load(); + + for (uint256 i = 0; i < tokenConfigs.length; ) { + token = tokenConfigs[i].token; + tokenData = s.bAppTokens[msg.sender][token]; + // Update current value if the previous effect time has passed + if (requestTime > tokenData.effectTime) { + tokenData.currentValue = tokenData.pendingValue; + } + tokenData.pendingValue = tokenConfigs[i].sharedRiskLevel; + tokenData.effectTime = requestTime + sp.tokenUpdateTimelockPeriod; + tokenData.isSet = true; + unchecked { + i++; + } + } + + emit BAppTokensUpdated(msg.sender, tokenConfigs); + } + /// @notice Function to add tokens to a bApp /// @param bApp The address of the bApp - /// @param tokens The list of tokens to add - /// @param sharedRiskLevels The shared risk levels of the tokens + /// @param tokenConfigs The list of tokens to add function _addNewTokens( address bApp, - address[] calldata tokens, - uint32[] calldata sharedRiskLevels + ICore.TokenConfig[] calldata tokenConfigs ) internal { - ValidationLib.validateArrayLengths(tokens, sharedRiskLevels); - uint256 length = tokens.length; + uint256 length = tokenConfigs.length; address token; CoreStorageLib.Data storage s = CoreStorageLib.load(); for (uint256 i = 0; i < length; ) { - token = tokens[i]; + token = tokenConfigs[i].token; ValidationLib.validateNonZeroAddress(token); if (s.bAppTokens[bApp][token].isSet) { revert IBasedAppManager.TokenAlreadyAddedToBApp(token); } - _setTokenRiskLevel(bApp, token, sharedRiskLevels[i]); + ICore.SharedRiskLevel storage tokenData = s.bAppTokens[bApp][token]; + tokenData.currentValue = tokenConfigs[i].sharedRiskLevel; + tokenData.isSet = true; unchecked { i++; } } } - - /// @notice Internal function to set the shared risk level for a token - /// @param bApp The address of the bApp - /// @param token The address of the token - /// @param sharedRiskLevel The shared risk level - function _setTokenRiskLevel( - address bApp, - address token, - uint32 sharedRiskLevel - ) internal { - CoreStorageLib.Data storage s = CoreStorageLib.load(); - ICore.SharedRiskLevel storage tokenData = s.bAppTokens[bApp][token]; - - tokenData.value = sharedRiskLevel; - tokenData.isSet = true; - } } diff --git a/src/core/modules/ProtocolManager.sol b/src/core/modules/ProtocolManager.sol index db1a8146..a2f5b623 100644 --- a/src/core/modules/ProtocolManager.sol +++ b/src/core/modules/ProtocolManager.sol @@ -1,16 +1,32 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.29; -import { IProtocolManager } from "@ssv/src/core/interfaces/IProtocolManager.sol"; -import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; +import { + MIN_EXPIRE_TIME, + MIN_TIME_LOCK_PERIOD +} from "@ssv/src/core/libraries/ValidationLib.sol"; +import { + IProtocolManager +} from "@ssv/src/core/interfaces/IProtocolManager.sol"; +import { + ProtocolStorageLib +} from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; +import { ISSVBasedApps } from "@ssv/src/core/interfaces/ISSVBasedApps.sol"; contract ProtocolManager is IProtocolManager { + uint32 private constant SLASHING_DISABLED = 1 << 0; + uint32 private constant WITHDRAWALS_DISABLED = 1 << 1; + function updateFeeTimelockPeriod(uint32 feeTimelockPeriod) external { + if (feeTimelockPeriod < MIN_TIME_LOCK_PERIOD) + revert ISSVBasedApps.InvalidFeeTimelockPeriod(); ProtocolStorageLib.load().feeTimelockPeriod = feeTimelockPeriod; emit FeeTimelockPeriodUpdated(feeTimelockPeriod); } function updateFeeExpireTime(uint32 feeExpireTime) external { + if (feeExpireTime < MIN_EXPIRE_TIME) + revert ISSVBasedApps.InvalidFeeExpireTime(); ProtocolStorageLib.load().feeExpireTime = feeExpireTime; emit FeeExpireTimeUpdated(feeExpireTime); } @@ -18,6 +34,8 @@ contract ProtocolManager is IProtocolManager { function updateWithdrawalTimelockPeriod( uint32 withdrawalTimelockPeriod ) external { + if (withdrawalTimelockPeriod < MIN_TIME_LOCK_PERIOD) + revert ISSVBasedApps.InvalidWithdrawalTimelockPeriod(); ProtocolStorageLib .load() .withdrawalTimelockPeriod = withdrawalTimelockPeriod; @@ -25,6 +43,8 @@ contract ProtocolManager is IProtocolManager { } function updateWithdrawalExpireTime(uint32 withdrawalExpireTime) external { + if (withdrawalExpireTime < MIN_EXPIRE_TIME) + revert ISSVBasedApps.InvalidWithdrawalExpireTime(); ProtocolStorageLib.load().withdrawalExpireTime = withdrawalExpireTime; emit WithdrawalExpireTimeUpdated(withdrawalExpireTime); } @@ -32,6 +52,8 @@ contract ProtocolManager is IProtocolManager { function updateObligationTimelockPeriod( uint32 obligationTimelockPeriod ) external { + if (obligationTimelockPeriod < MIN_TIME_LOCK_PERIOD) + revert ISSVBasedApps.InvalidObligationTimelockPeriod(); ProtocolStorageLib .load() .obligationTimelockPeriod = obligationTimelockPeriod; @@ -39,17 +61,39 @@ contract ProtocolManager is IProtocolManager { } function updateObligationExpireTime(uint32 obligationExpireTime) external { + if (obligationExpireTime < MIN_EXPIRE_TIME) + revert ISSVBasedApps.InvalidObligationExpireTime(); ProtocolStorageLib.load().obligationExpireTime = obligationExpireTime; emit ObligationExpireTimeUpdated(obligationExpireTime); } + function updateTokenUpdateTimelockPeriod( + uint32 tokenUpdateTimelockPeriod + ) external { + if (tokenUpdateTimelockPeriod < MIN_TIME_LOCK_PERIOD) + revert ISSVBasedApps.InvalidTokenUpdateTimelockPeriod(); + ProtocolStorageLib + .load() + .tokenUpdateTimelockPeriod = tokenUpdateTimelockPeriod; + emit TokenUpdateTimelockPeriodUpdated(tokenUpdateTimelockPeriod); + } + function updateMaxShares(uint256 maxShares) external { + if (maxShares < 1e38) revert ISSVBasedApps.InvalidMaxShares(); ProtocolStorageLib.load().maxShares = maxShares; emit StrategyMaxSharesUpdated(maxShares); } function updateMaxFeeIncrement(uint32 maxFeeIncrement) external { + if (maxFeeIncrement < 50) + // 0.5% increment + revert ISSVBasedApps.InvalidMaxFeeIncrement(); ProtocolStorageLib.load().maxFeeIncrement = maxFeeIncrement; emit StrategyMaxFeeIncrementUpdated(maxFeeIncrement); } + + function updateDisabledFeatures(uint32 disabledFeatures) external { + ProtocolStorageLib.load().disabledFeatures = disabledFeatures; + emit DisabledFeaturesUpdated(disabledFeatures); + } } diff --git a/src/core/modules/StrategyManager.sol b/src/core/modules/StrategyManager.sol index a5685030..995942a5 100644 --- a/src/core/modules/StrategyManager.sol +++ b/src/core/modules/StrategyManager.sol @@ -2,22 +2,38 @@ pragma solidity 0.8.29; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; - -import { ValidationLib, MAX_PERCENTAGE, ETH_ADDRESS } from "@ssv/src/core/libraries/ValidationLib.sol"; +import { + ReentrancyGuardTransient +} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { + ValidationLib, + MAX_PERCENTAGE, + ETH_ADDRESS +} from "@ssv/src/core/libraries/ValidationLib.sol"; import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; import { CoreStorageLib } from "@ssv/src/core/libraries/CoreStorageLib.sol"; -import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + ProtocolStorageLib +} from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; import { IBasedApp } from "@ssv/src/middleware/interfaces/IBasedApp.sol"; contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { using SafeERC20 for IERC20; + uint32 private constant SLASHING_DISABLED = 1 << 0; + uint32 private constant WITHDRAWALS_DISABLED = 1 << 1; + /// @notice Checks if the caller is the strategy owner /// @param strategyId The ID of the strategy /// @param s The CoreStorageLib data @@ -144,6 +160,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { newStrategy.owner = msg.sender; newStrategy.fee = fee; + s.strategyOwners[msg.sender].push(strategyId); + emit StrategyCreated(strategyId, msg.sender, fee, metadataURI); } @@ -178,9 +196,18 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { ValidationLib.validateArrayLengths(tokens, obligationPercentages); + if (!s.registeredBApps[bApp]) { + revert IBasedAppManager.BAppNotRegistered(); + } + // Check if a strategy exists for the given bApp. // It is not possible opt-in to the same bApp twice with the same strategy owner. - if (s.accountBAppStrategy[msg.sender][bApp] != 0) { + // use the effectiveTime to check if the strategy is not opted-in and opted-out + if ( + (s.accountBAppStrategy[msg.sender][bApp].strategyId != 0 && + s.accountBAppStrategy[msg.sender][bApp].optOutEffectiveTime == + 0) + ) { revert BAppAlreadyOptedIn(); } @@ -191,9 +218,14 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { obligationPercentages ); - s.accountBAppStrategy[msg.sender][bApp] = strategyId; + // todo, save in one struct to save gas + s.accountBAppStrategy[msg.sender][bApp].strategyId = strategyId; + s.accountBAppStrategy[msg.sender][bApp].requestTime = uint32( + block.timestamp + ); + s.accountBAppStrategy[msg.sender][bApp].optOutEffectiveTime = 0; - if (_isBApp(bApp)) { + if (_isContract(bApp)) { bool success = IBasedApp(bApp).optInToBApp( strategyId, tokens, @@ -212,12 +244,34 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { ); } + /// @notice Function to check if an address is a contract + function optOutFromBApp(uint32 strategyId, address bApp) external { + CoreStorageLib.Data storage s = CoreStorageLib.load(); + _onlyStrategyOwner(strategyId, s); + + if (s.accountBAppStrategy[msg.sender][bApp].strategyId != strategyId) { + revert BAppNotOptedIn(); + } + + if ( + s.accountBAppStrategy[msg.sender][bApp].optOutEffectiveTime < + block.timestamp + ) { + revert BAppAlreadyOptedOut(); + } + + s.accountBAppStrategy[msg.sender][bApp].optOutEffectiveTime = + uint32(block.timestamp) + + 1; //ProtocolStorageLib.load().bAppOptOutDelay; + + emit IStrategyManager.BAppOptedOutByStrategy(strategyId, bApp); + } + /// @notice Function to check if an address uses the correct bApp interface /// @param bApp The address of the bApp - /// @return True if the address uses the correct bApp interface - function _isBApp(address bApp) private view returns (bool) { - return - ERC165Checker.supportsInterface(bApp, type(IBasedApp).interfaceId); + /// @return True if the address is a contract + function _isContract(address bApp) private view returns (bool) { + return bApp.code.length > 0; } /// @notice Deposit ERC20 tokens into the strategy @@ -253,6 +307,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { address token, uint256 amount ) external { + _checkWithdrawalsAllowed(); + if (token == ETH_ADDRESS) revert InvalidToken(); _proposeWithdrawal(strategyId, token, amount); } @@ -264,6 +320,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { uint32 strategyId, IERC20 token ) external nonReentrant { + _checkWithdrawalsAllowed(); + uint256 amount = _finalizeWithdrawal(strategyId, address(token)); token.safeTransfer(msg.sender, amount); @@ -281,12 +339,15 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { /// @param strategyId The ID of the strategy. /// @param amount The amount of ETH to withdraw. function proposeWithdrawalETH(uint32 strategyId, uint256 amount) external { + _checkWithdrawalsAllowed(); _proposeWithdrawal(strategyId, ETH_ADDRESS, amount); } /// @notice Finalize the ETH withdrawal after the timelock period has passed. /// @param strategyId The ID of the strategy. function finalizeWithdrawalETH(uint32 strategyId) external nonReentrant { + _checkWithdrawalsAllowed(); + uint256 amount = _finalizeWithdrawal(strategyId, ETH_ADDRESS); payable(msg.sender).transfer(amount); @@ -314,7 +375,7 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { CoreStorageLib.Data storage s = CoreStorageLib.load(); _onlyStrategyOwner(strategyId, s); - if (s.accountBAppStrategy[msg.sender][bApp] != strategyId) { + if (s.accountBAppStrategy[msg.sender][bApp].strategyId != strategyId) { revert BAppNotOptedIn(); } @@ -387,14 +448,7 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { sp.obligationExpireTime ); - if ( - percentage == 0 && - s.obligations[strategyId][bApp][address(token)].percentage > 0 - ) { - s.usedTokens[strategyId][address(token)] -= 1; - } - - _updateObligation(strategyId, bApp, address(token), percentage); + s.obligations[strategyId][bApp][address(token)].percentage = percentage; emit ObligationUpdated(strategyId, bApp, address(token), percentage); @@ -526,9 +580,8 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { } if (obligationPercentage != 0) { - s.usedTokens[strategyId][token] += 1; s - .obligations[strategyId][bApp][token] + .obligations[strategyId][bApp][token] .percentage = obligationPercentage; } @@ -548,11 +601,10 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { ) private view { CoreStorageLib.Data storage s = CoreStorageLib.load(); - if (s.accountBAppStrategy[msg.sender][bApp] != strategyId) { + if (s.accountBAppStrategy[msg.sender][bApp].strategyId != strategyId) { revert BAppNotOptedIn(); } - //if (obligationPercentage > MAX_PERCENTAGE) revert ICore.InvalidPercentage(); ValidationLib.validatePercentage(obligationPercentage); if ( @@ -566,28 +618,6 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { } } - /// @notice Update a single obligation for a bApp - /// @param strategyId The ID of the strategy - /// @param bApp The address of the bApp - /// @param token The address of the token - function _updateObligation( - uint32 strategyId, - address bApp, - address token, - uint32 obligationPercentage - ) private { - CoreStorageLib.Data storage s = CoreStorageLib.load(); - - if ( - s.obligations[strategyId][bApp][token].percentage == 0 && - obligationPercentage > 0 - ) { - s.usedTokens[strategyId][token] += 1; - } - s - .obligations[strategyId][bApp][token].percentage = obligationPercentage; - } - /// @notice Check the timelocks /// @param requestTime The time of the request /// @param timelockPeriod The timelock period @@ -741,133 +771,165 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { /// @param token The address of the token /// @return slashableBalance The slashable balance function getSlashableBalance( + CoreStorageLib.Data storage s, uint32 strategyId, address bApp, - address token + address token, + ICore.Shares storage strategyTokenShares ) internal view returns (uint256 slashableBalance) { - CoreStorageLib.Data storage s = CoreStorageLib.load(); - - ICore.Shares storage strategyTokenShares = s.strategyTokenShares[ - strategyId - ][token]; - uint32 percentage = s.obligations[strategyId][bApp][token].percentage; uint256 balance = strategyTokenShares.totalTokenBalance; return (balance * percentage) / MAX_PERCENTAGE; } + function _checkStrategyOptedIn( + CoreStorageLib.Data storage s, + uint32 strategyId, + address bApp + ) internal view { + // It is possible to slash only if the strategy owner has opted-in to the bApp + address strategyOwner = s.strategies[strategyId].owner; // Load the strategy to check if it exists + if ( + s.accountBAppStrategy[strategyOwner][bApp].strategyId != strategyId + ) { + revert BAppNotOptedIn(); + } + } + /// @notice Slash a strategy /// @param strategyId The ID of the strategy /// @param bApp The address of the bApp /// @param token The address of the token - /// @param amount The amount to slash + /// @param percentage The amount to slash /// @param data Optional parameter that could be required by the service function slash( uint32 strategyId, address bApp, address token, - uint256 amount, + uint32 percentage, bytes calldata data ) external nonReentrant { - if (amount == 0) revert InvalidAmount(); + _checkSlashingAllowed(); + + ValidationLib.validatePercentageAndNonZero(percentage); + CoreStorageLib.Data storage s = CoreStorageLib.load(); if (!s.registeredBApps[bApp]) { revert IBasedAppManager.BAppNotRegistered(); } - uint256 slashableBalance = getSlashableBalance(strategyId, bApp, token); - if (slashableBalance < amount) revert InsufficientBalance(); + _checkStrategyOptedIn(s, strategyId, bApp); + + // TODO _checkIfDeregistred(); use delay? - address receiver; - bool exit; - bool success; ICore.Shares storage strategyTokenShares = s.strategyTokenShares[ strategyId ][token]; - if (_isBApp(bApp)) { + uint256 slashableBalance = getSlashableBalance( + s, + strategyId, + bApp, + token, + strategyTokenShares + ); + if (slashableBalance == 0) revert InsufficientBalance(); + uint256 amount = (slashableBalance * percentage) / MAX_PERCENTAGE; + + address receiver; + bool exit; + bool success; + uint32 obligationPercentage; + if (_isContract(bApp)) { (success, receiver, exit) = IBasedApp(bApp).slash( strategyId, token, - amount, + percentage, + msg.sender, data ); if (!success) revert IStrategyManager.BAppSlashingFailed(); - if (exit) _exitStrategy(strategyId, bApp, token); - else _adjustObligation(strategyId, bApp, token, amount); + if (exit) { + _exitStrategy(s, strategyId, bApp, token); + obligationPercentage = 0; + } else + obligationPercentage = _adjustObligation( + s, + strategyId, + bApp, + token, + amount, + strategyTokenShares + ); } else { - // Only the bApp EOA or non-compliant bapp owner can slash + // Only the bApp EOA can slash if (msg.sender != bApp) revert InvalidBAppOwner(msg.sender, bApp); receiver = bApp; - _exitStrategy(strategyId, bApp, token); + _exitStrategy(s, strategyId, bApp, token); } strategyTokenShares.totalTokenBalance -= amount; s.slashingFund[receiver][token] += amount; if (strategyTokenShares.totalTokenBalance == 0) { - delete s.strategyTokenShares[strategyId][token].totalTokenBalance; - delete s.strategyTokenShares[strategyId][token].totalShareBalance; - s.strategyTokenShares[strategyId][token].currentGeneration += 1; + delete strategyTokenShares.totalTokenBalance; + delete strategyTokenShares.totalShareBalance; + strategyTokenShares.currentGeneration += 1; } emit IStrategyManager.StrategySlashed( strategyId, bApp, token, - amount, + percentage, receiver ); + + emit IStrategyManager.ObligationUpdated( + strategyId, + bApp, + token, + obligationPercentage + ); } function _exitStrategy( + CoreStorageLib.Data storage s, uint32 strategyId, address bApp, address token ) private { - CoreStorageLib.Data storage s = CoreStorageLib.load(); s.obligations[strategyId][bApp][token].percentage = 0; - - emit IStrategyManager.ObligationUpdated(strategyId, bApp, token, 0); } function _adjustObligation( + CoreStorageLib.Data storage s, uint32 strategyId, address bApp, address token, - uint256 amount - ) internal { - CoreStorageLib.Data storage s = CoreStorageLib.load(); + uint256 amount, + ICore.Shares storage strategyTokenShares + ) internal returns (uint32 obligationPercentage) { ICore.Obligation storage obligation = s.obligations[strategyId][bApp][ token ]; - ICore.Shares storage strategyTokenShares = s.strategyTokenShares[ - strategyId - ][token]; uint256 currentStrategyBalance = strategyTokenShares.totalTokenBalance; uint256 currentObligatedBalance = (obligation.percentage * currentStrategyBalance) / MAX_PERCENTAGE; uint256 postSlashStrategyBalance = currentStrategyBalance - amount; uint256 postSlashObligatedBalance = currentObligatedBalance - amount; if (postSlashStrategyBalance == 0) { - s.obligations[strategyId][bApp][token].percentage = 0; - emit IStrategyManager.ObligationUpdated(strategyId, bApp, token, 0); + obligation.percentage = 0; + return 0; } else { - uint32 postSlashPercentage = uint32( - (postSlashObligatedBalance * MAX_PERCENTAGE) / - currentStrategyBalance - ); - s - .obligations[strategyId][bApp][token] - .percentage = postSlashPercentage; - emit IStrategyManager.ObligationUpdated( - strategyId, - bApp, - token, - postSlashPercentage + uint32 postSlashObligationPercentage = uint32( + (postSlashObligatedBalance / postSlashStrategyBalance) * + MAX_PERCENTAGE ); + obligation.percentage = postSlashObligationPercentage; + return postSlashObligationPercentage; } } @@ -911,4 +973,21 @@ contract StrategyManager is ReentrancyGuardTransient, IStrategyManager { s.slashingFund[msg.sender][token] -= amount; } + + function _checkSlashingAllowed() internal view { + if ( + ProtocolStorageLib.load().disabledFeatures & SLASHING_DISABLED != 0 + ) { + revert SlashingDisabled(); + } + } + + function _checkWithdrawalsAllowed() internal view { + if ( + ProtocolStorageLib.load().disabledFeatures & WITHDRAWALS_DISABLED != + 0 + ) { + revert WithdrawalsDisabled(); + } + } } diff --git a/src/middleware/README.md b/src/middleware/README.md index 66bbed2a..785a6526 100644 --- a/src/middleware/README.md +++ b/src/middleware/README.md @@ -1,4 +1,4 @@ -# :construction_worker: :closed_lock_with_key: __Middleware Contracts__ +# :construction_worker: :closed_lock_with_key: **Middleware Contracts** ## Modules & Examples for BApp Development @@ -78,11 +78,10 @@ Ideal for BApps that want to avoid excessive reward costs. ## :page_facing_up: To Have: Examples -* **BLS Strategy Example**: Demonstrates how to implement and use the BLS verification module securely. +- **BLS Strategy Example**: Demonstrates how to implement and use the BLS verification module securely. -* **ECDSA Strategy Example**: Showcases a lighter alternative using ECDSA verification. +- **ECDSA Strategy Example**: Showcases a lighter alternative using ECDSA verification. -* **Capped Strategy Example**: Implements a BApp that limits deposits to 100k SSV. - -* **Whitelist Manager Example**: Uses whitelist-based permissions instead of owner-based control. +- **Capped Strategy Example**: Implements a BApp that limits deposits to 100k SSV. +- **Whitelist Manager Example**: Uses whitelist-based permissions instead of owner-based control. diff --git a/src/middleware/examples/WhitelistExample.sol b/src/middleware/examples/WhitelistExample.sol index ec9e344e..45ed2b7e 100644 --- a/src/middleware/examples/WhitelistExample.sol +++ b/src/middleware/examples/WhitelistExample.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { BasedAppWhitelisted } from "@ssv/src/middleware/modules/BasedAppWhitelisted.sol"; -import { OwnableBasedApp } from "@ssv/src/middleware/modules/core+roles/OwnableBasedApp.sol"; +import { + BasedAppWhitelisted +} from "@ssv/src/middleware/modules/BasedAppWhitelisted.sol"; +import { + OwnableBasedApp +} from "@ssv/src/middleware/modules/core+roles/OwnableBasedApp.sol"; contract WhitelistExample is OwnableBasedApp, BasedAppWhitelisted { constructor( diff --git a/src/middleware/interfaces/IBasedApp.sol b/src/middleware/interfaces/IBasedApp.sol index ddbe2863..c7892876 100644 --- a/src/middleware/interfaces/IBasedApp.sol +++ b/src/middleware/interfaces/IBasedApp.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; -interface IBasedApp is IERC165 { +interface IBasedApp { function optInToBApp( uint32 strategyId, address[] calldata tokens, @@ -11,18 +11,20 @@ interface IBasedApp is IERC165 { bytes calldata data ) external returns (bool); function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external; function slash( uint32 strategyId, address token, - uint256 amount, + uint32 percentage, + address sender, bytes calldata data ) external returns (bool success, address receiver, bool exit); - function supportsInterface(bytes4 interfaceId) external view returns (bool); function updateBAppMetadataURI(string calldata metadataURI) external; + function updateBAppTokens( + ICore.TokenConfig[] calldata tokenConfigs + ) external; error UnauthorizedCaller(); } diff --git a/src/middleware/modules/BasedAppWhitelisted.sol b/src/middleware/modules/BasedAppWhitelisted.sol index 2bdab384..aebb90ef 100644 --- a/src/middleware/modules/BasedAppWhitelisted.sol +++ b/src/middleware/modules/BasedAppWhitelisted.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { IBasedAppWhitelisted } from "@ssv/src/middleware/interfaces/IBasedAppWhitelisted.sol"; +import { + IBasedAppWhitelisted +} from "@ssv/src/middleware/interfaces/IBasedAppWhitelisted.sol"; abstract contract BasedAppWhitelisted is IBasedAppWhitelisted { mapping(uint32 => bool) public isWhitelisted; diff --git a/src/middleware/modules/core+roles/AccessControlBasedApp.sol b/src/middleware/modules/core+roles/AccessControlBasedApp.sol index 04b4a1ac..2a894551 100644 --- a/src/middleware/modules/core+roles/AccessControlBasedApp.sol +++ b/src/middleware/modules/core+roles/AccessControlBasedApp.sol @@ -1,13 +1,19 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { + AccessControl +} from "@openzeppelin/contracts/access/AccessControl.sol"; import { IBasedApp } from "@ssv/src/middleware/interfaces/IBasedApp.sol"; -import { BasedAppCore } from "@ssv/src/middleware/modules/core/BasedAppCore.sol"; +import { + BasedAppCore +} from "@ssv/src/middleware/modules/core/BasedAppCore.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; abstract contract AccessControlBasedApp is BasedAppCore, AccessControl { bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); @@ -34,8 +40,7 @@ abstract contract AccessControlBasedApp is BasedAppCore, AccessControl { } /// @notice Registers a BApp calling the SSV SSVBasedApps - /// @param tokens array of token addresses - /// @param sharedRiskLevels array of shared risk levels + /// @param tokenConfigs array of token addresses and shared risk levels /// @param metadataURI URI of the metadata /// @dev metadata should point to a json that respect template: /// { @@ -46,13 +51,11 @@ abstract contract AccessControlBasedApp is BasedAppCore, AccessControl { /// "social": "https://x.com/ssv_network" /// } function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external override onlyRole(MANAGER_ROLE) { IBasedAppManager(SSV_BASED_APPS_NETWORK).registerBApp( - tokens, - sharedRiskLevels, + tokenConfigs, metadataURI ); } @@ -66,20 +69,4 @@ abstract contract AccessControlBasedApp is BasedAppCore, AccessControl { metadataURI ); } - - /// @notice Checks if the contract supports the interface - /// @param interfaceId interface id - /// @return isSupported if the contract supports the interface - function supportsInterface( - bytes4 interfaceId - ) - public - pure - override(AccessControl, BasedAppCore) - returns (bool isSupported) - { - return - interfaceId == type(IBasedApp).interfaceId || - interfaceId == type(IERC165).interfaceId; - } } diff --git a/src/middleware/modules/core+roles/OwnableBasedApp.sol b/src/middleware/modules/core+roles/OwnableBasedApp.sol index eec60448..27450933 100644 --- a/src/middleware/modules/core+roles/OwnableBasedApp.sol +++ b/src/middleware/modules/core+roles/OwnableBasedApp.sol @@ -2,12 +2,16 @@ pragma solidity 0.8.29; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { IBasedApp } from "@ssv/src/middleware/interfaces/IBasedApp.sol"; -import { BasedAppCore } from "@ssv/src/middleware/modules/core/BasedAppCore.sol"; +import { + BasedAppCore +} from "@ssv/src/middleware/modules/core/BasedAppCore.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; abstract contract OwnableBasedApp is Ownable, BasedAppCore { constructor( @@ -16,8 +20,7 @@ abstract contract OwnableBasedApp is Ownable, BasedAppCore { ) BasedAppCore(_basedAppManager) Ownable(_initOwner) {} /// @notice Registers a BApp calling the SSV SSVBasedApps - /// @param tokens array of token addresses - /// @param sharedRiskLevels array of shared risk levels + /// @param tokenConfigs array of token addresses and shared risk levels /// @param metadataURI URI of the metadata /// @dev metadata should point to a json that respect template: /// { @@ -28,13 +31,11 @@ abstract contract OwnableBasedApp is Ownable, BasedAppCore { /// "social": "https://x.com/ssv_network" /// } function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external override onlyOwner { IBasedAppManager(SSV_BASED_APPS_NETWORK).registerBApp( - tokens, - sharedRiskLevels, + tokenConfigs, metadataURI ); } @@ -48,15 +49,4 @@ abstract contract OwnableBasedApp is Ownable, BasedAppCore { metadataURI ); } - - /// @notice Checks if the contract supports the interface - /// @param interfaceId interface id - /// @return isSupported if the contract supports the interface - function supportsInterface( - bytes4 interfaceId - ) public pure override(BasedAppCore) returns (bool isSupported) { - return - interfaceId == type(IBasedApp).interfaceId || - interfaceId == type(IERC165).interfaceId; - } } diff --git a/src/middleware/modules/core/BasedAppCore.sol b/src/middleware/modules/core/BasedAppCore.sol index 21de53cd..8647b447 100644 --- a/src/middleware/modules/core/BasedAppCore.sol +++ b/src/middleware/modules/core/BasedAppCore.sol @@ -1,13 +1,17 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - import { IBasedApp } from "@ssv/src/middleware/interfaces/IBasedApp.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; + +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; // ===================================================================================== // ⚠️ WARNING: IMPLEMENT OWNER OR ACCESS ROLES ⚠️ @@ -35,9 +39,8 @@ abstract contract BasedAppCore is IBasedApp { SSV_BASED_APPS_NETWORK = _ssvBasedAppsNetwork; } - /// @notice Registers a BApp calling the SSV SSVBasedApps - /// @param tokens array of token addresses - /// @param sharedRiskLevels array of shared risk levels + /// @notice Registers a BApp calling the SSVBasedApps + /// @param tokenConfigs array of token configs (address, shared risk level) /// @param metadataURI URI of the metadata /// @dev metadata should point to a json that respect template: /// { @@ -48,13 +51,11 @@ abstract contract BasedAppCore is IBasedApp { /// "social": "https://x.com/ssv_network" /// } function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external virtual { IBasedAppManager(SSV_BASED_APPS_NETWORK).registerBApp( - tokens, - sharedRiskLevels, + tokenConfigs, metadataURI ); } @@ -69,6 +70,16 @@ abstract contract BasedAppCore is IBasedApp { ); } + /// @notice Updates the tokens of a BApp + /// @param tokenConfigs new list of tokens and their shared risk levels + function updateBAppTokens( + ICore.TokenConfig[] calldata tokenConfigs + ) external virtual { + IBasedAppManager(SSV_BASED_APPS_NETWORK).updateBAppsTokens( + tokenConfigs + ); + } + function withdrawSlashingFund( address token, uint256 amount @@ -105,27 +116,24 @@ abstract contract BasedAppCore is IBasedApp { /*strategyId*/ address, /*token*/ - uint256, - /*amount*/ + uint32, + /*percentage*/ + address, + /*sender*/ bytes calldata - ) external virtual onlySSVBasedAppManager returns (bool, address, bool) { + ) + external + virtual + /*data*/ + onlySSVBasedAppManager + returns (bool, address, bool) + { ///@dev --- CORE LOGIC (TO BE IMPLEMENTED) --- ///@dev --- RETURN TRUE IF SUCCESS, FALSE OTHERWISE --- ///@dev --- RETURN RECEIVER ADDRESS FOR THE SLASHED FUNDS --- return (true, address(this), true); } - /// @notice Checks if the contract supports the interface - /// @param interfaceId interface id - /// @return true if the contract supports the interface - function supportsInterface( - bytes4 interfaceId - ) public pure virtual returns (bool) { - return - interfaceId == type(IBasedApp).interfaceId || - interfaceId == type(IERC165).interfaceId; - } - // Receive function to accept plain Ether transfers receive() external payable virtual {} } diff --git a/test/Config.t.sol b/test/Config.t.sol new file mode 100644 index 00000000..feb36aa4 --- /dev/null +++ b/test/Config.t.sol @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.29; + +import { Test } from "forge-std/Test.sol"; + +import { + ERC1967Proxy +} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { BasedAppsManager } from "@ssv/src/core/modules/BasedAppsManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IProtocolManager +} from "@ssv/src/core/interfaces/IProtocolManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { SSVBasedApps } from "@ssv/src/core/SSVBasedApps.sol"; +import { ProtocolManager } from "@ssv/src/core/modules/ProtocolManager.sol"; +import { StrategyManager } from "@ssv/src/core/modules/StrategyManager.sol"; +import { + ProtocolStorageLib +} from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; +import { ISSVBasedApps } from "@ssv/src/core/interfaces/ISSVBasedApps.sol"; + +contract Config is Test { + // Main Contract + SSVBasedApps public implementation; + // Modules + StrategyManager public strategyManagerMod; + BasedAppsManager public basedAppsManagerMod; + ProtocolManager public protocolManagerMod; + + // Proxies + ERC1967Proxy public proxy; // UUPS Proxy contract + SSVBasedApps public proxiedManager; // Proxy interface for interaction + + // EOAs + address public immutable OWNER = makeAddr("Owner"); + + // Constants + address public constant ETH_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint32 public constant MAX_FEE_INCREMENT = 500; // 5% + // Array containing all the BApps created + ProtocolStorageLib.Data public config; + + function setUp() public virtual { + vm.label(OWNER, "Owner"); + vm.startPrank(OWNER); + basedAppsManagerMod = new BasedAppsManager(); + strategyManagerMod = new StrategyManager(); + protocolManagerMod = new ProtocolManager(); + implementation = new SSVBasedApps(); + vm.stopPrank(); + } + + function testRevertInvalidMaxFeeIncrementWithZeroFee() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: 0, + feeTimelockPeriod: 0 days, + feeExpireTime: 1 days, + withdrawalTimelockPeriod: 14 days, + withdrawalExpireTime: 3 days, + obligationTimelockPeriod: 14 days, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidMaxFeeIncrement.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidMaxFeeIncrementWithExcessiveFee() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: 10001, + feeTimelockPeriod: 0 days, + feeExpireTime: 1 days, + withdrawalTimelockPeriod: 14 days, + withdrawalExpireTime: 3 days, + obligationTimelockPeriod: 14 days, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidMaxFeeIncrement.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidFeeTimelockPeriod() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 0 days, + feeExpireTime: 1 days, + withdrawalTimelockPeriod: 14 days, + withdrawalExpireTime: 3 days, + obligationTimelockPeriod: 14 days, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidFeeTimelockPeriod.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidFeeExpireTime() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 59 minutes, + withdrawalTimelockPeriod: 14 days, + withdrawalExpireTime: 3 days, + obligationTimelockPeriod: 14 days, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector(ISSVBasedApps.InvalidFeeExpireTime.selector) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidWithdrawalTimelockPeriod() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 1 hours, + withdrawalTimelockPeriod: 0 days, + withdrawalExpireTime: 3 days, + obligationTimelockPeriod: 14 days, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidWithdrawalTimelockPeriod.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidWithdrawalExpireTime() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 1 hours, + withdrawalTimelockPeriod: 1 days, + withdrawalExpireTime: 0 hours, + obligationTimelockPeriod: 14 days, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidWithdrawalExpireTime.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidObligationTimelockPeriod() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 1 hours, + withdrawalTimelockPeriod: 1 days, + withdrawalExpireTime: 1 hours, + obligationTimelockPeriod: 23 hours, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidObligationTimelockPeriod.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidObligationExpireTime() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 1 hours, + withdrawalTimelockPeriod: 1 days, + withdrawalExpireTime: 1 hours, + obligationTimelockPeriod: 1 days, + obligationExpireTime: 0 hours, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidObligationExpireTime.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidTokenUpdateTimelockPeriod() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 1 hours, + withdrawalTimelockPeriod: 1 days, + withdrawalExpireTime: 1 hours, + obligationTimelockPeriod: 1 days, + obligationExpireTime: 1 hours, + tokenUpdateTimelockPeriod: 0 days, + maxShares: 1e50, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidTokenUpdateTimelockPeriod.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + + function testRevertInvalidMaxShares() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 1 hours, + withdrawalTimelockPeriod: 1 days, + withdrawalExpireTime: 1 hours, + obligationTimelockPeriod: 1 days, + obligationExpireTime: 1 hours, + tokenUpdateTimelockPeriod: 1 days, + maxShares: 1e49, + disabledFeatures: 0 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector(ISSVBasedApps.InvalidMaxShares.selector) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } + function testRevertInvalidDisabledFeatures() public virtual { + config = ProtocolStorageLib.Data({ + maxFeeIncrement: MAX_FEE_INCREMENT, + feeTimelockPeriod: 1 days, + feeExpireTime: 1 hours, + withdrawalTimelockPeriod: 1 days, + withdrawalExpireTime: 3 days, + obligationTimelockPeriod: 14 days, + obligationExpireTime: 3 days, + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 4 + }); + + bytes memory data = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidDisabledFeatures.selector + ) + ); + proxy = new ERC1967Proxy(address(implementation), data); + } +} diff --git a/test/SSVBasedApps.t.sol b/test/SSVBasedApps.t.sol index 31439204..d44735a8 100644 --- a/test/SSVBasedApps.t.sol +++ b/test/SSVBasedApps.t.sol @@ -1,12 +1,18 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -import { Setup, IStrategyManager, IBasedAppManager, IProtocolManager, SSVBasedApps, ERC1967Proxy } from "@ssv/test/helpers/Setup.t.sol"; +import { + Ownable2StepUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; + +import { + Setup, + IStrategyManager, + IBasedAppManager, + IProtocolManager, + SSVBasedApps +} from "@ssv/test/helpers/Setup.t.sol"; import { ISSVBasedApps } from "@ssv/src/core/interfaces/ISSVBasedApps.sol"; -import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; -import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; import { SSVCoreModules } from "@ssv/src/core/libraries/CoreStorageLib.sol"; contract SSVBasedAppsTest is Setup, Ownable2StepUpgradeable { @@ -154,62 +160,6 @@ contract SSVBasedAppsTest is Setup, Ownable2StepUpgradeable { ); } - function testRevertInitializeWithZeroFee() public { - ProtocolStorageLib.Data memory configZeroFee = ProtocolStorageLib.Data({ - maxFeeIncrement: 0, - feeTimelockPeriod: 7 days, - feeExpireTime: 1 days, - withdrawalTimelockPeriod: 14 days, - withdrawalExpireTime: 3 days, - obligationTimelockPeriod: 14 days, - obligationExpireTime: 3 days, - maxShares: 1e50 - }); - vm.expectRevert( - abi.encodeWithSelector( - ISSVBasedApps.InvalidMaxFeeIncrement.selector - ) - ); - - bytes memory initData = abi.encodeWithSelector( - implementation.initialize.selector, - address(OWNER), - address(basedAppsManagerMod), - address(strategyManagerMod), - address(protocolManagerMod), - configZeroFee - ); - proxy = new ERC1967Proxy(address(implementation), initData); - } - - function testRevertInitializeWithExcessiveFee() public { - ProtocolStorageLib.Data memory configExcessiveFee = ProtocolStorageLib - .Data({ - feeTimelockPeriod: 7 days, - feeExpireTime: 1 days, - withdrawalTimelockPeriod: 14 days, - maxShares: 1e50, - withdrawalExpireTime: 3 days, - obligationTimelockPeriod: 14 days, - obligationExpireTime: 3 days, - maxFeeIncrement: 10_001 - }); - vm.expectRevert( - abi.encodeWithSelector( - ISSVBasedApps.InvalidMaxFeeIncrement.selector - ) - ); - bytes memory initData = abi.encodeWithSelector( - implementation.initialize.selector, - address(OWNER), - address(basedAppsManagerMod), - address(strategyManagerMod), - address(protocolManagerMod), - configExcessiveFee - ); - proxy = new ERC1967Proxy(address(implementation), initData); - } - function testUpdateStrategyModule() public { SSVCoreModules[] memory moduleIds = new SSVCoreModules[](1); address[] memory moduleAddresses = new address[](1); diff --git a/test/helpers/Setup.t.sol b/test/helpers/Setup.t.sol index 1ee4e99b..1a3ebece 100644 --- a/test/helpers/Setup.t.sol +++ b/test/helpers/Setup.t.sol @@ -3,24 +3,36 @@ pragma solidity 0.8.29; import { Test } from "forge-std/Test.sol"; -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { + ERC1967Proxy +} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { BasedAppMock } from "@ssv/test/mocks/MockBApp.sol"; import { BasedAppMock2 } from "@ssv/test/mocks/MockBApp2.sol"; import { BasedAppMock3 } from "@ssv/test/mocks/MockBAppAccessControl.sol"; import { BasedAppMock4 } from "@ssv/test/mocks/MockBApp4RejectEth.sol"; import { BasedAppsManager } from "@ssv/src/core/modules/BasedAppsManager.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; import { IERC20, ERC20Mock } from "@ssv/test/mocks/MockERC20.sol"; -import { IProtocolManager } from "@ssv/src/core/interfaces/IProtocolManager.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + IProtocolManager +} from "@ssv/src/core/interfaces/IProtocolManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; import { NonCompliantBApp } from "@ssv/test/mocks/MockNonCompliantBApp.sol"; import { SSVBasedApps } from "@ssv/src/core/SSVBasedApps.sol"; import { ProtocolManager } from "@ssv/src/core/modules/ProtocolManager.sol"; import { StrategyManager } from "@ssv/src/core/modules/StrategyManager.sol"; -import { ProtocolStorageLib } from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; +import { + ProtocolStorageLib +} from "@ssv/src/core/libraries/ProtocolStorageLib.sol"; -import { WhitelistExample } from "@ssv/src/middleware/examples/WhitelistExample.sol"; +import { + WhitelistExample +} from "@ssv/src/middleware/examples/WhitelistExample.sol"; import { IBasedApp } from "@ssv/src/middleware/interfaces/IBasedApp.sol"; contract Setup is Test { @@ -62,10 +74,10 @@ contract Setup is Test { uint32 public constant STRATEGY3 = 3; uint32 public constant STRATEGY4 = 4; // Fees - uint32 public constant STRATEGY1_INITIAL_FEE = 5; - uint32 public constant STRATEGY2_INITIAL_FEE = 0; - uint32 public constant STRATEGY3_INITIAL_FEE = 1000; - uint32 public constant STRATEGY4_INITIAL_FEE = 900; + uint32 public constant STRATEGY1_INITIAL_FEE = 5; // %0.05 + uint32 public constant STRATEGY2_INITIAL_FEE = 0; // %0.00 + uint32 public constant STRATEGY3_INITIAL_FEE = 1000; // %10.00 + uint32 public constant STRATEGY4_INITIAL_FEE = 900; // %9.00 uint32 public constant STRATEGY1_UPDATE_FEE = 10; // Initial Balances uint256 public constant INITIAL_USER1_BALANCE_ERC20 = 1000 * 10 ** 18; @@ -82,7 +94,7 @@ contract Setup is Test { // Constants address public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - uint32 public constant MAX_FEE_INCREMENT = 500; + uint32 public constant MAX_FEE_INCREMENT = 500; // 5% // Array containing all the BApps created IBasedApp[] public bApps; ProtocolStorageLib.Data public config; @@ -109,7 +121,9 @@ contract Setup is Test { withdrawalExpireTime: 3 days, obligationTimelockPeriod: 14 days, obligationExpireTime: 3 days, - maxShares: 1e50 + tokenUpdateTimelockPeriod: 14 days, + maxShares: 1e50, + disabledFeatures: 0 }); bytes memory data = abi.encodeWithSelector( @@ -122,7 +136,7 @@ contract Setup is Test { ); proxy = new ERC1967Proxy(address(implementation), data); proxiedManager = SSVBasedApps(payable(address(proxy))); - assertEq(proxiedManager.getVersion(), "0.0.1", "Version mismatch"); + assertEq(proxiedManager.getVersion(), "0.1.0", "Version mismatch"); assertEq( proxiedManager.maxFeeIncrement(), 500, diff --git a/test/helpers/Utils.t.sol b/test/helpers/Utils.t.sol index dc1f11a1..97e52ad0 100644 --- a/test/helpers/Utils.t.sol +++ b/test/helpers/Utils.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.29; import { SSVBasedApps } from "@ssv/src/core/SSVBasedApps.sol"; import { Setup } from "@ssv/test/helpers/Setup.t.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; contract UtilsTest is Setup { function createSingleTokenAndSingleRiskLevel( @@ -21,6 +22,16 @@ contract UtilsTest is Setup { sharedRiskLevelInput = new uint32[](1); sharedRiskLevelInput[0] = sharedRiskLevel; } + function createSingleTokenConfig( + address token, + uint32 sharedRiskLevel + ) internal pure returns (ICore.TokenConfig[] memory tokenConfigs) { + tokenConfigs = new ICore.TokenConfig[](1); + tokenConfigs[0] = ICore.TokenConfig({ + token: token, + sharedRiskLevel: sharedRiskLevel + }); + } function createSingleTokenAndSingleObligationPercentage( address token, @@ -40,25 +51,17 @@ contract UtilsTest is Setup { } function checkBAppInfo( - address[] memory tokensInput, - uint32[] memory riskLevelInput, + ICore.TokenConfig[] memory tokenConfigsInput, address bApp, SSVBasedApps proxiedManager ) internal view { - assertEq( - tokensInput.length, - riskLevelInput.length, - "BApp tokens and sharedRiskLevel length" - ); bool isRegistered = proxiedManager.registeredBApps(bApp); assertEq(isRegistered, true, "BApp registered"); - for (uint32 i = 0; i < tokensInput.length; i++) { - (uint32 sharedRiskLevel, bool isSet) = proxiedManager.bAppTokens( - bApp, - tokensInput[i] - ); + for (uint32 i = 0; i < tokenConfigsInput.length; i++) { + (uint32 sharedRiskLevel, bool isSet, , ) = proxiedManager + .bAppTokens(bApp, tokenConfigsInput[i].token); assertEq( - riskLevelInput[i], + tokenConfigsInput[i].sharedRiskLevel, sharedRiskLevel, "BApp risk level percentage" ); @@ -73,7 +76,6 @@ contract UtilsTest is Setup { address token, uint32 percentage, SSVBasedApps proxiedManager, - uint32 expectedTokens, bool expectedIsSet ) internal view { uint32 id = proxiedManager.accountBAppStrategy(owner, bApp); @@ -85,8 +87,6 @@ contract UtilsTest is Setup { ); assertEq(isSet, expectedIsSet, "Obligation is set"); assertEq(obligationPercentage, percentage, "Obligation percentage"); - uint256 usedTokens = proxiedManager.usedTokens(strategyId, token); - assertEq(usedTokens, expectedTokens, "Used tokens"); (address strategyOwner, ) = proxiedManager.strategies(strategyId); if (strategyOwner != address(0)) { assertEq(owner, strategyOwner, "Strategy owner"); @@ -98,7 +98,6 @@ contract UtilsTest is Setup { address bApp, address token, uint32 expectedPercentage, - uint32 expectedUsedTokens, bool expectedIsSet, SSVBasedApps proxiedManager ) internal view { @@ -109,8 +108,6 @@ contract UtilsTest is Setup { ); assertEq(percentage, expectedPercentage, "Obligation percentage"); assertEq(isSet, expectedIsSet, "Obligation is set"); - uint32 usedTokens = proxiedManager.usedTokens(strategyId, token); - assertEq(usedTokens, expectedUsedTokens, "Used tokens"); } function checkSlashableBalance( @@ -302,19 +299,6 @@ contract UtilsTest is Setup { ); } - function checkUsedTokens( - uint32 strategyId, - address token, - uint32 expectedUsedTokens - ) internal view { - uint32 usedTokens = proxiedManager.usedTokens(strategyId, token); - assertEq( - usedTokens, - expectedUsedTokens, - "Should have set the correct used tokens" - ); - } - function checkAdjustedPercentage( address token, uint256 previousBalance, @@ -324,9 +308,10 @@ contract UtilsTest is Setup { uint256 previousObligatedBalance = (previousPercentage * previousBalance) / proxiedManager.maxPercentage(); uint256 newObligatedBalance = previousObligatedBalance - slashAmount; + uint256 newTotalBalance = previousBalance - slashAmount; uint32 expectedAdjustedPercentage = uint32( (newObligatedBalance * proxiedManager.maxPercentage()) / - previousBalance + newTotalBalance ); (uint32 adjustedPercentage, ) = proxiedManager.obligations( STRATEGY1, @@ -340,4 +325,38 @@ contract UtilsTest is Setup { ); return adjustedPercentage; } + + function checkBAppUpdatedTokens( + ICore.TokenConfig[] memory tokenConfigs, + address bApp + ) internal view { + bool isRegistered = proxiedManager.registeredBApps(bApp); + assertEq(isRegistered, true, "BApp registered"); + for (uint32 i = 0; i < tokenConfigs.length; i++) { + ( + , + bool isSet, + uint32 pendingValue, + uint32 effectiveTime + ) = proxiedManager.bAppTokens(bApp, tokenConfigs[i].token); + assertEq( + tokenConfigs[i].sharedRiskLevel, + pendingValue, + "BApp risk level percentage" + ); + assertNotEq(effectiveTime, 0); + assertEq(isSet, true, "BApp token set"); + } + } + + function calculateSlashAmount( + uint256 depositAmount, + uint32 obligationPercentage, + uint32 slashingPercentage + ) internal view returns (uint256) { + uint256 obligatedAmount = (depositAmount * obligationPercentage) / + proxiedManager.maxPercentage(); + return ((obligatedAmount * slashingPercentage) / + proxiedManager.maxPercentage()); + } } diff --git a/test/middleware/examples/WhitelistExample.t.sol b/test/middleware/examples/WhitelistExample.t.sol index 1462be9b..6ffe0cba 100644 --- a/test/middleware/examples/WhitelistExample.t.sol +++ b/test/middleware/examples/WhitelistExample.t.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { IBasedAppWhitelisted } from "@ssv/src/middleware/interfaces/IBasedAppWhitelisted.sol"; +import { + IBasedAppWhitelisted +} from "@ssv/src/middleware/interfaces/IBasedAppWhitelisted.sol"; import { IBasedApp } from "@ssv/test/helpers/Setup.t.sol"; import { UtilsTest } from "@ssv/test/helpers/Utils.t.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; contract WhitelistExampleTest is UtilsTest { function testCreateStrategies() public { @@ -31,14 +34,13 @@ contract WhitelistExampleTest is UtilsTest { function testRegisterWhitelistExampleBApp() public { vm.startPrank(USER1); - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createSingleTokenAndSingleRiskLevel(address(erc20mock), 102); - whitelistExample.registerBApp(tokensInput, sharedRiskLevelInput, ""); + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + address(erc20mock), + 102 + ); + whitelistExample.registerBApp(tokenConfigsInput, ""); checkBAppInfo( - tokensInput, - sharedRiskLevelInput, + tokenConfigsInput, address(whitelistExample), proxiedManager ); diff --git a/test/mocks/MockBApp.sol b/test/mocks/MockBApp.sol index db494642..3e895316 100644 --- a/test/mocks/MockBApp.sol +++ b/test/mocks/MockBApp.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { OwnableBasedApp } from "@ssv/src/middleware/modules/core+roles/OwnableBasedApp.sol"; +import { + OwnableBasedApp +} from "@ssv/src/middleware/modules/core+roles/OwnableBasedApp.sol"; contract BasedAppMock is OwnableBasedApp { event OptInToBApp( diff --git a/test/mocks/MockBApp2.sol b/test/mocks/MockBApp2.sol index c65ba6c6..7df5ea44 100644 --- a/test/mocks/MockBApp2.sol +++ b/test/mocks/MockBApp2.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { BasedAppCore } from "@ssv/src/middleware/modules/core/BasedAppCore.sol"; +import { + BasedAppCore +} from "@ssv/src/middleware/modules/core/BasedAppCore.sol"; contract BasedAppMock2 is BasedAppCore { event OptInToBApp( @@ -18,12 +20,15 @@ contract BasedAppMock2 is BasedAppCore { } function slash( - uint32, /*strategyId*/ - address, + uint32, /*token*/ - uint256, - /*amount*/ + address, + /*percentage*/ + uint32, + /*sender*/ + address, + /*data*/ bytes calldata ) external diff --git a/test/mocks/MockBApp4RejectEth.sol b/test/mocks/MockBApp4RejectEth.sol index deb4fb92..ac790371 100644 --- a/test/mocks/MockBApp4RejectEth.sol +++ b/test/mocks/MockBApp4RejectEth.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { OwnableBasedApp } from "@ssv/src/middleware/modules/core+roles/OwnableBasedApp.sol"; +import { + OwnableBasedApp +} from "@ssv/src/middleware/modules/core+roles/OwnableBasedApp.sol"; contract BasedAppMock4 is OwnableBasedApp { event OptInToBApp( diff --git a/test/mocks/MockBAppAccessControl.sol b/test/mocks/MockBAppAccessControl.sol index a5248181..51ee107f 100644 --- a/test/mocks/MockBAppAccessControl.sol +++ b/test/mocks/MockBAppAccessControl.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { AccessControlBasedApp } from "@ssv/src/middleware/modules/core+roles/AccessControlBasedApp.sol"; +import { + AccessControlBasedApp +} from "@ssv/src/middleware/modules/core+roles/AccessControlBasedApp.sol"; contract BasedAppMock3 is AccessControlBasedApp { event OptInToBApp( @@ -25,13 +27,16 @@ contract BasedAppMock3 is AccessControlBasedApp { /*strategyId*/ address, /*token*/ - uint256, - /*amount*/ + uint32, + /*percentage*/ + address, + /*sender*/ bytes calldata ) external view override + /*data*/ onlySSVBasedAppManager returns (bool, address, bool) { diff --git a/test/mocks/MockNonCompliantBApp.sol b/test/mocks/MockNonCompliantBApp.sol index 9a76355c..5885c5a1 100644 --- a/test/mocks/MockNonCompliantBApp.sol +++ b/test/mocks/MockNonCompliantBApp.sol @@ -1,17 +1,18 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; + interface ICustomBasedAppManager { function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external; function slash( uint32 strategyId, address bApp, address token, - uint256 amount, + uint32 percentage, bytes calldata data ) external; } @@ -33,23 +34,25 @@ contract NonCompliantBApp { } function registerBApp( - address[] calldata tokens, - uint32[] calldata sharedRiskLevels, + ICore.TokenConfig[] calldata tokenConfigs, string calldata metadataURI ) external { ICustomBasedAppManager(BASED_APP_MANAGER).registerBApp( - tokens, - sharedRiskLevels, + tokenConfigs, metadataURI ); } - function slash(uint32 strategyId, address token, uint256 amount) external { + function slash( + uint32 strategyId, + address token, + uint32 percentage + ) external { ICustomBasedAppManager(BASED_APP_MANAGER).slash( strategyId, address(this), token, - amount, + percentage, "" ); } diff --git a/test/modules/BasedAppsManager.t.sol b/test/modules/BasedAppsManager.t.sol index 8059d2f6..f75f163d 100644 --- a/test/modules/BasedAppsManager.t.sol +++ b/test/modules/BasedAppsManager.t.sol @@ -2,14 +2,17 @@ pragma solidity 0.8.29; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { + IAccessControl +} from "@openzeppelin/contracts/access/IAccessControl.sol"; import { UtilsTest } from "@ssv/test/helpers/Utils.t.sol"; import { IBasedAppManager, IBasedApp } from "@ssv/test/helpers/Setup.t.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; import { ValidationLib } from "@ssv/src/core/libraries/ValidationLib.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; contract BasedAppsManagerTest is UtilsTest { string public metadataURI = "http://metadata.com"; @@ -26,77 +29,50 @@ contract BasedAppsManagerTest is UtilsTest { function createTwoTokenAndRiskInputs() private view - returns ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) + returns (ICore.TokenConfig[] memory tokenConfigsInput) { - tokensInput = new address[](2); - sharedRiskLevelInput = new uint32[](2); + address[] memory tokensInput = new address[](2); + uint32[] memory sharedRiskLevelInput = new uint32[](2); tokensInput[0] = address(erc20mock); tokensInput[1] = address(erc20mock2); sharedRiskLevelInput[0] = 102; sharedRiskLevelInput[1] = 103; + tokenConfigsInput = new ICore.TokenConfig[](2); + for (uint256 i = 0; i < tokensInput.length; i++) { + tokenConfigsInput[i] = ICore.TokenConfig({ + token: tokensInput[i], + sharedRiskLevel: sharedRiskLevelInput[i] + }); + } } function createTwoTokenAndRiskInputsWithTheSameToken() private view - returns ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) + returns (ICore.TokenConfig[] memory tokenConfigsInput) { - tokensInput = new address[](2); - sharedRiskLevelInput = new uint32[](2); + tokenConfigsInput = new ICore.TokenConfig[](2); + address[] memory tokensInput = new address[](2); + uint32[] memory sharedRiskLevelInput = new uint32[](2); tokensInput[0] = address(erc20mock); tokensInput[1] = address(erc20mock); sharedRiskLevelInput[0] = 102; sharedRiskLevelInput[1] = 103; - } - - function createTwoTokenAndMoreRiskInputs() - private - view - returns ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) - { - tokensInput = new address[](2); - sharedRiskLevelInput = new uint32[](3); - tokensInput[0] = address(erc20mock); - tokensInput[1] = address(erc20mock2); - sharedRiskLevelInput[0] = 102; - sharedRiskLevelInput[1] = 103; - sharedRiskLevelInput[1] = 104; - } - - function createTwoTokenAndLessRiskInputs() - private - view - returns ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) - { - tokensInput = new address[](2); - sharedRiskLevelInput = new uint32[](1); - tokensInput[0] = address(erc20mock); - tokensInput[1] = address(erc20mock2); - sharedRiskLevelInput[0] = 102; + for (uint256 i = 0; i < tokensInput.length; i++) { + tokenConfigsInput[i] = ICore.TokenConfig({ + token: tokensInput[i], + sharedRiskLevel: sharedRiskLevelInput[i] + }); + } } function createFiveTokenAndRiskInputs() private view - returns ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) + returns (ICore.TokenConfig[] memory tokenConfigsInput) { - tokensInput = new address[](5); - sharedRiskLevelInput = new uint32[](5); + address[] memory tokensInput = new address[](5); + uint32[] memory sharedRiskLevelInput = new uint32[](5); tokensInput[0] = address(erc20mock); tokensInput[1] = address(erc20mock2); tokensInput[2] = address(erc20mock3); @@ -107,127 +83,93 @@ contract BasedAppsManagerTest is UtilsTest { sharedRiskLevelInput[2] = 104; sharedRiskLevelInput[3] = 105; sharedRiskLevelInput[4] = 106; + tokenConfigsInput = new ICore.TokenConfig[](5); + for (uint256 i = 0; i < tokensInput.length; i++) { + tokenConfigsInput[i] = ICore.TokenConfig({ + token: tokensInput[i], + sharedRiskLevel: sharedRiskLevelInput[i] + }); + } } function testRegisterBApp() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createSingleTokenAndSingleRiskLevel(address(erc20mock), 102); + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + address(erc20mock), + 102 + ); for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); vm.expectEmit(true, true, true, true); emit IBasedAppManager.BAppRegistered( address(bApps[i]), - tokensInput, - sharedRiskLevelInput, - metadataURIs[i] - ); - bApps[i].registerBApp( - tokensInput, - sharedRiskLevelInput, + tokenConfigsInput, metadataURIs[i] ); - checkBAppInfo( - tokensInput, - sharedRiskLevelInput, - address(bApps[i]), - proxiedManager - ); + bApps[i].registerBApp(tokenConfigsInput, metadataURIs[i]); + checkBAppInfo(tokenConfigsInput, address(bApps[i]), proxiedManager); } } function testRegisterBAppWithEOA() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createSingleTokenAndSingleRiskLevel(address(erc20mock), 102); - vm.prank(USER1); - proxiedManager.registerBApp( - tokensInput, - sharedRiskLevelInput, - metadataURIs[0] + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + address(erc20mock), + 102 ); - checkBAppInfo(tokensInput, sharedRiskLevelInput, USER1, proxiedManager); + vm.prank(USER1); + proxiedManager.registerBApp(tokenConfigsInput, metadataURIs[0]); + checkBAppInfo(tokenConfigsInput, USER1, proxiedManager); } function testRegisterBAppWithEOAWithEth() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createSingleTokenAndSingleRiskLevel(ETH_ADDRESS, 102); - vm.prank(USER1); - proxiedManager.registerBApp( - tokensInput, - sharedRiskLevelInput, - metadataURIs[0] + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + ETH_ADDRESS, + 102 ); - checkBAppInfo(tokensInput, sharedRiskLevelInput, USER1, proxiedManager); + vm.prank(USER1); + proxiedManager.registerBApp(tokenConfigsInput, metadataURIs[0]); + checkBAppInfo(tokenConfigsInput, USER1, proxiedManager); } function testRegisterBAppWith2Tokens() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createTwoTokenAndRiskInputs(); + ICore.TokenConfig[] + memory tokenConfigsInput = createTwoTokenAndRiskInputs(); for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); - bApps[i].registerBApp(tokensInput, sharedRiskLevelInput, ""); - checkBAppInfo( - tokensInput, - sharedRiskLevelInput, - address(bApps[i]), - proxiedManager - ); + bApps[i].registerBApp(tokenConfigsInput, ""); + checkBAppInfo(tokenConfigsInput, address(bApps[i]), proxiedManager); } } function testRegisterBAppWithETH() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createSingleTokenAndSingleRiskLevel(ETH_ADDRESS, 100); + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + ETH_ADDRESS, + 100 + ); for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); - bApps[i].registerBApp(tokensInput, sharedRiskLevelInput, ""); - checkBAppInfo( - tokensInput, - sharedRiskLevelInput, - address(bApps[i]), - proxiedManager - ); + bApps[i].registerBApp(tokenConfigsInput, ""); + checkBAppInfo(tokenConfigsInput, address(bApps[i]), proxiedManager); } } function testRegisterBAppWithNoTokens() public { - address[] memory tokensInput = new address[](0); - uint32[] memory sharedRiskLevelInput = new uint32[](0); + ICore.TokenConfig[] memory tokenConfigsInput = new ICore.TokenConfig[]( + 0 + ); for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); - bApps[i].registerBApp(tokensInput, sharedRiskLevelInput, ""); - checkBAppInfo( - tokensInput, - sharedRiskLevelInput, - address(bApps[i]), - proxiedManager - ); + bApps[i].registerBApp(tokenConfigsInput, ""); + checkBAppInfo(tokenConfigsInput, address(bApps[i]), proxiedManager); } } function testRegisterBAppWithFiveTokens() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createFiveTokenAndRiskInputs(); + ICore.TokenConfig[] + memory tokenConfigsInput = createFiveTokenAndRiskInputs(); for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); - bApps[i].registerBApp(tokensInput, sharedRiskLevelInput, ""); - checkBAppInfo( - tokensInput, - sharedRiskLevelInput, - address(bApps[i]), - proxiedManager - ); + bApps[i].registerBApp(tokenConfigsInput, ""); + checkBAppInfo(tokenConfigsInput, address(bApps[i]), proxiedManager); } } @@ -238,40 +180,42 @@ contract BasedAppsManagerTest is UtilsTest { uint32[] memory sharedRiskLevelInput = new uint32[](2); sharedRiskLevelInput[0] = 102; sharedRiskLevelInput[1] = 102; + ICore.TokenConfig[] memory tokenConfigsInput = new ICore.TokenConfig[]( + tokensInput.length + ); + for (uint256 i = 0; i < tokensInput.length; i++) { + tokenConfigsInput[i] = ICore.TokenConfig({ + token: tokensInput[i], + sharedRiskLevel: sharedRiskLevelInput[i] + }); + } for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); - bApps[i].registerBApp(tokensInput, sharedRiskLevelInput, ""); - checkBAppInfo( - tokensInput, - sharedRiskLevelInput, - address(bApps[i]), - proxiedManager - ); + bApps[i].registerBApp(tokenConfigsInput, ""); + checkBAppInfo(tokenConfigsInput, address(bApps[i]), proxiedManager); } } function testRevertRegisterBAppWithSameTokens() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createTwoTokenAndRiskInputsWithTheSameToken(); + ICore.TokenConfig[] + memory tokenConfigsInput = createTwoTokenAndRiskInputsWithTheSameToken(); for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector( IBasedAppManager.TokenAlreadyAddedToBApp.selector, - tokensInput[0] + tokenConfigsInput[0].token ) ); - bApps[i].registerBApp(tokensInput, sharedRiskLevelInput, ""); + bApps[i].registerBApp(tokenConfigsInput, ""); } } function testRevertRegisterBAppWithTokenZero() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createSingleTokenAndSingleRiskLevel(address(0), 102); + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + address(0), + 102 + ); for (uint256 i = 0; i < bApps.length; i++) { vm.prank(USER1); vm.expectRevert( @@ -279,100 +223,55 @@ contract BasedAppsManagerTest is UtilsTest { ValidationLib.ZeroAddressNotAllowed.selector ) ); - bApps[i].registerBApp( - tokensInput, - sharedRiskLevelInput, - metadataURIs[i] - ); + bApps[i].registerBApp(tokenConfigsInput, metadataURIs[i]); } } function testRevertRegisterBAppTwice() public { vm.startPrank(USER1); - address[] memory tokensInput = new address[](1); - tokensInput[0] = address(erc20mock); - uint32[] memory sharedRiskLevelInput = new uint32[](1); - sharedRiskLevelInput[0] = 102; - bApp1.registerBApp(tokensInput, sharedRiskLevelInput, ""); + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + address(erc20mock), + 102 + ); + bApp1.registerBApp(tokenConfigsInput, ""); vm.expectRevert( abi.encodeWithSelector( IBasedAppManager.BAppAlreadyRegistered.selector ) ); - bApp1.registerBApp(tokensInput, sharedRiskLevelInput, ""); - bApp2.registerBApp(tokensInput, sharedRiskLevelInput, ""); + bApp1.registerBApp(tokenConfigsInput, ""); + bApp2.registerBApp(tokenConfigsInput, ""); vm.expectRevert( abi.encodeWithSelector( IBasedAppManager.BAppAlreadyRegistered.selector ) ); - bApp2.registerBApp(tokensInput, sharedRiskLevelInput, ""); - bApp3.registerBApp(tokensInput, sharedRiskLevelInput, ""); + bApp2.registerBApp(tokenConfigsInput, ""); + bApp3.registerBApp(tokenConfigsInput, ""); vm.expectRevert( abi.encodeWithSelector( IBasedAppManager.BAppAlreadyRegistered.selector ) ); - bApp3.registerBApp(tokensInput, sharedRiskLevelInput, ""); + bApp3.registerBApp(tokenConfigsInput, ""); vm.stopPrank(); } function testRegisterBAppFromNonBAppContract() public { vm.startPrank(USER1); - address[] memory tokensInput = new address[](1); - tokensInput[0] = address(erc20mock); - uint32[] memory sharedRiskLevelInput = new uint32[](1); - sharedRiskLevelInput[0] = 102; - nonCompliantBApp.registerBApp(tokensInput, sharedRiskLevelInput, ""); + ICore.TokenConfig[] memory tokenConfigsInput = createSingleTokenConfig( + address(erc20mock), + 102 + ); + nonCompliantBApp.registerBApp(tokenConfigsInput, ""); checkBAppInfo( - tokensInput, - sharedRiskLevelInput, + tokenConfigsInput, address(nonCompliantBApp), proxiedManager ); vm.stopPrank(); } - function testRevertRegisterBAppWithMismatchTokenRiskLengthOne() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createTwoTokenAndMoreRiskInputs(); - for (uint256 i = 0; i < bApps.length; i++) { - vm.prank(USER1); - vm.expectRevert( - abi.encodeWithSelector( - ValidationLib.LengthsNotMatching.selector - ) - ); - bApps[i].registerBApp( - tokensInput, - sharedRiskLevelInput, - metadataURIs[i] - ); - } - } - - function testRevertRegisterBAppWithMismatchTokenRiskLengthTwo() public { - ( - address[] memory tokensInput, - uint32[] memory sharedRiskLevelInput - ) = createTwoTokenAndLessRiskInputs(); - for (uint256 i = 0; i < bApps.length; i++) { - vm.prank(USER1); - vm.expectRevert( - abi.encodeWithSelector( - ValidationLib.LengthsNotMatching.selector - ) - ); - bApps[i].registerBApp( - tokensInput, - sharedRiskLevelInput, - metadataURIs[i] - ); - } - } - function testUpdateBAppMetadata() public { testRegisterBApp(); for (uint256 i = 0; i < bApps.length; i++) { @@ -433,28 +332,28 @@ contract BasedAppsManagerTest is UtilsTest { } } - function testSupportInterface() public { - vm.startPrank(USER1); + function testUpdateBAppTokens() public { + testRegisterBApp(); + ICore.TokenConfig[] + memory tokenConfigsInput = createTwoTokenAndRiskInputs(); + // ICore.TokenConfig[] memory tokenConfigs = new ICore.TokenConfig[]( + // tokensInput.length + // ); + // for (uint256 i = 0; i < tokensInput.length; i++) { + // tokenConfigs[i] = ICore.TokenConfig({ + // token: tokensInput[i], + // sharedRiskLevel: sharedRiskLevelInput[i] + 1000 + // }); + // } for (uint256 i = 0; i < bApps.length; i++) { - bool success = bApps[i].supportsInterface( - type(IBasedApp).interfaceId - ); - assertEq(success, true, "supportsInterface based app"); - bool failed = bApps[i].supportsInterface( - type(IBasedAppManager).interfaceId - ); - assertEq( - failed, - false, - "does not supportsInterface based app manager" - ); - bool failed2 = bApps[i].supportsInterface(type(IERC20).interfaceId); - assertEq(failed2, false, "does not supportsInterface"); - bool success2 = bApps[i].supportsInterface( - type(IERC165).interfaceId + vm.prank(USER1); + vm.expectEmit(true, true, false, false); + emit IBasedAppManager.BAppTokensUpdated( + address(bApps[i]), + tokenConfigsInput ); - assertEq(success2, true, "does supportsInterface of IERC165"); + bApps[i].updateBAppTokens(tokenConfigsInput); + checkBAppUpdatedTokens(tokenConfigsInput, address(bApps[i])); } - vm.stopPrank(); } } diff --git a/test/modules/DelegationManager.t.sol b/test/modules/DelegationManager.t.sol index 518cba61..e9326ddf 100644 --- a/test/modules/DelegationManager.t.sol +++ b/test/modules/DelegationManager.t.sol @@ -2,7 +2,9 @@ pragma solidity 0.8.29; import { Setup } from "@ssv/test/helpers/Setup.t.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; import { ValidationLib } from "@ssv/src/core/libraries/ValidationLib.sol"; contract BasedAppManagerDelegateTest is Setup { diff --git a/test/modules/ProtocolManager.t.sol b/test/modules/ProtocolManager.t.sol index d9d8ff97..64f321b0 100644 --- a/test/modules/ProtocolManager.t.sol +++ b/test/modules/ProtocolManager.t.sol @@ -1,10 +1,26 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { + Ownable2StepUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import { ETH_ADDRESS } from "@ssv/src/core/libraries/ValidationLib.sol"; import { Setup } from "@ssv/test/helpers/Setup.t.sol"; +import { + IProtocolManager +} from "@ssv/src/core/interfaces/IProtocolManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + ERC1967Proxy +} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { SSVBasedApps } from "@ssv/src/core/SSVBasedApps.sol"; +import { ISSVBasedApps } from "@ssv/src/core/interfaces/ISSVBasedApps.sol"; contract ProtocolManagerTest is Setup, Ownable2StepUpgradeable { function testUpdateFeeTimelockPeriod() public { @@ -67,6 +83,16 @@ contract ProtocolManagerTest is Setup, Ownable2StepUpgradeable { ); } + function testUpdateTokenUpdateTimelockPeriod() public { + vm.prank(OWNER); + proxiedManager.updateTokenUpdateTimelockPeriod(7 days); + assertEq( + proxiedManager.tokenUpdateTimelockPeriod(), + 7 days, + "TokenUpdate timelock update failed" + ); + } + function testMaxPercentage() public view { assertEq( proxiedManager.maxPercentage(), @@ -85,7 +111,7 @@ contract ProtocolManagerTest is Setup, Ownable2StepUpgradeable { function testUpdateMaxShares() public { vm.prank(OWNER); - uint256 newValue = 1e18; + uint256 newValue = 1e38; proxiedManager.updateMaxShares(newValue); assertEq( proxiedManager.maxShares(), @@ -167,7 +193,18 @@ contract ProtocolManagerTest is Setup, Ownable2StepUpgradeable { address(ATTACKER) ) ); - proxiedManager.updateObligationExpireTime(1 days); + proxiedManager.updateObligationExpireTime(59 minutes); + } + + function testRevertUpdateTokenUpdateTimelockPeriodWithNonOwner() public { + vm.prank(ATTACKER); + vm.expectRevert( + abi.encodeWithSelector( + OwnableUnauthorizedAccount.selector, + address(ATTACKER) + ) + ); + proxiedManager.updateTokenUpdateTimelockPeriod(7 days); } function testRevertUpdateMaxSharesWithNonOwner() public { @@ -192,4 +229,207 @@ contract ProtocolManagerTest is Setup, Ownable2StepUpgradeable { ); proxiedManager.updateMaxFeeIncrement(501); } + + /// @notice By default, no features should be disabled + function testDefaultDisabledFeaturesIsZero() public view { + assertEq( + proxiedManager.disabledFeatures(), + 0, + "default disabledFeatures should be zero" + ); + } + + /// @notice The initializer should respect `config.disabledFeatures` + function testInitializeDisabledFeaturesFromConfig() public { + // Override config in Setup + config.disabledFeatures = 3; // slashing & withdrawals disabled + + // Re-deploy a fresh proxy with the modified config + bytes memory initData = abi.encodeWithSelector( + implementation.initialize.selector, + address(OWNER), + IBasedAppManager(basedAppsManagerMod), + IStrategyManager(strategyManagerMod), + IProtocolManager(protocolManagerMod), + config + ); + ERC1967Proxy proxy = new ERC1967Proxy( + address(implementation), + initData + ); + SSVBasedApps proxiedManager = SSVBasedApps(payable(address(proxy))); + + // It should read back exactly what we set + assertEq( + proxiedManager.disabledFeatures(), + 3, + "initializer did not set disabledFeatures from config" + ); + } + + /// @notice Only the owner can update the feature mask + function testUpdateFeatureDisabledFlagsAsOwner() public { + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(2); + assertEq( + proxiedManager.disabledFeatures(), + 2, + "owner update of disabledFeatures failed" + ); + } + + /// @notice Updating the flags should emit DisabledFeaturesUpdated + function testEmitDisabledFeaturesUpdatedEvent() public { + vm.prank(OWNER); + vm.expectEmit(true, false, false, true); + emit IProtocolManager.DisabledFeaturesUpdated(5); + proxiedManager.updateDisabledFeatures(5); + } + + function testRevertUpdateDisabledFeaturesWithNonOwner() public { + vm.prank(ATTACKER); + vm.expectRevert( + abi.encodeWithSelector( + OwnableUnauthorizedAccount.selector, + address(ATTACKER) + ) + ); + proxiedManager.updateDisabledFeatures(1); + } + + function testSetIndividualDisabledFeatureBits() public { + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(1 << 0); + assertEq( + proxiedManager.disabledFeatures(), + 1, + "slashingDisabled bit not set correctly" + ); + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(1 << 1); + assertEq( + proxiedManager.disabledFeatures(), + 2, + "withdrawalsDisabled bit not set correctly" + ); + } + + function testClearFlags() public { + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(3); + assertEq(proxiedManager.disabledFeatures(), 3, "mask precondition"); + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(0); + assertEq(proxiedManager.disabledFeatures(), 0, "flags not cleared"); + } + + function testCombinedFlags() public { + uint32 mask = (1 << 0) | (1 << 2) | (1 << 4); + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(mask); + assertEq( + proxiedManager.disabledFeatures(), + mask, + "combined mask mismatch" + ); + } + + function testOtherParamsUnaffectedByFeatureMask() public { + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(type(uint32).max); + vm.prank(OWNER); + proxiedManager.updateFeeTimelockPeriod(2 days); + assertEq( + proxiedManager.feeTimelockPeriod(), + 2 days, + "feeTimelockPeriod should update despite flags" + ); + } + + function testRevertUpdateFeeTimelockPeriod() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidFeeTimelockPeriod.selector + ) + ); + proxiedManager.updateFeeTimelockPeriod(23 hours); + } + + function testRevertUpdateFeeExpireTime() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector(ISSVBasedApps.InvalidFeeExpireTime.selector) + ); + proxiedManager.updateFeeExpireTime(59 minutes); + } + + function testRevertUpdateWithdrawalTimelockPeriod() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidWithdrawalTimelockPeriod.selector + ) + ); + proxiedManager.updateWithdrawalTimelockPeriod(23 hours); + } + + function testRevertUpdateWithdrawalExpireTime() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidWithdrawalExpireTime.selector + ) + ); + proxiedManager.updateWithdrawalExpireTime(59 minutes); + } + + function testRevertUpdateObligationTimelockPeriod() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidObligationTimelockPeriod.selector + ) + ); + proxiedManager.updateObligationTimelockPeriod(23 hours); + } + + function testRevertUpdateObligationExpireTime() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidObligationExpireTime.selector + ) + ); + proxiedManager.updateObligationExpireTime(59 minutes); + } + + function testRevertUpdateTokenUpdateTimelockPeriod() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidTokenUpdateTimelockPeriod.selector + ) + ); + proxiedManager.updateTokenUpdateTimelockPeriod(23 hours); + } + + function testRevertUpdateMaxShares() public { + vm.prank(OWNER); + uint256 newValue = 1e37; + vm.expectRevert( + abi.encodeWithSelector(ISSVBasedApps.InvalidMaxShares.selector) + ); + proxiedManager.updateMaxShares(newValue); + } + + function testRevertUpdateMaxFeeIncrement() public { + vm.prank(OWNER); + vm.expectRevert( + abi.encodeWithSelector( + ISSVBasedApps.InvalidMaxFeeIncrement.selector + ) + ); + proxiedManager.updateMaxFeeIncrement(49); + } } diff --git a/test/modules/SlashingManager.bapp.t.sol b/test/modules/SlashingManager.bapp.t.sol index 4d157454..8dde6d1e 100644 --- a/test/modules/SlashingManager.bapp.t.sol +++ b/test/modules/SlashingManager.bapp.t.sol @@ -1,10 +1,18 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { IERC20, IStrategyManager, IBasedApp } from "@ssv/test/helpers/Setup.t.sol"; +import { + IERC20, + IStrategyManager, + IBasedApp +} from "@ssv/test/helpers/Setup.t.sol"; import { StrategyManagerTest } from "@ssv/test/modules/StrategyManager.t.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; -import { IBasedAppManager } from "@ssv/src/core/interfaces/IBasedAppManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; contract SlashingManagerTest is StrategyManagerTest { function testGetSlashableBalanceBasic() public { @@ -45,15 +53,20 @@ contract SlashingManagerTest is StrategyManagerTest { ); } - function testSlashBApp(uint256 slashAmount) public { + function testSlashBApp(uint32 slashPercentage) public { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); vm.assume( - slashAmount > 0 && - slashAmount <= - (depositAmount * percentage) / proxiedManager.maxPercentage() + slashPercentage > 0 && + slashPercentage <= proxiedManager.maxPercentage() ); + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); + testStrategyOptInToBApp(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -67,16 +80,17 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, address(bApp1) ); proxiedManager.slash( STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); + uint256 newStrategyBalance = depositAmount - slashAmount; checkTotalSharesAndTotalBalance( STRATEGY1, @@ -89,16 +103,22 @@ contract SlashingManagerTest is StrategyManagerTest { checkSlashingFund(address(bApp1), token, slashAmount); } - function testSlashBAppWithEth(uint256 slashAmount) public { + function testSlashBAppWithEth(uint32 slashPercentage) public { testStrategyOptInToBAppWithETH(); uint32 percentage = 10_000; uint256 depositAmount = 1 ether; address token = ETH_ADDRESS; vm.assume( - slashAmount > 0 && - slashAmount <= - (depositAmount * percentage) / proxiedManager.maxPercentage() + slashPercentage > 0 && + slashPercentage < proxiedManager.maxPercentage() + ); + + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage ); + vm.prank(USER2); proxiedManager.depositETH{ value: depositAmount }(STRATEGY1); vm.prank(USER1); @@ -107,14 +127,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, address(bApp1) ); proxiedManager.slash( STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -129,15 +149,17 @@ contract SlashingManagerTest is StrategyManagerTest { checkSlashingFund(address(bApp1), token, slashAmount); } - function testSlashBAppButInternalSlashRevert(uint256 slashAmount) public { + function testSlashBAppButInternalSlashRevert( + uint32 slashPercentage + ) public { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); vm.assume( - slashAmount > 0 && - slashAmount <= - (depositAmount * percentage) / proxiedManager.maxPercentage() + slashPercentage > 0 && + slashPercentage <= proxiedManager.maxPercentage() ); + testStrategyOptInToBApp(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -153,7 +175,7 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp2), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); checkTotalSharesAndTotalBalance( @@ -181,7 +203,7 @@ contract SlashingManagerTest is StrategyManagerTest { depositAmount ); vm.prank(USER1); - uint256 slashAmount = 1; + uint32 slashPercentage = 100; vm.expectRevert( abi.encodeWithSelector(IBasedAppManager.BAppNotRegistered.selector) ); @@ -189,7 +211,7 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, USER1, address(erc20mock), - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); } @@ -198,7 +220,7 @@ contract SlashingManagerTest is StrategyManagerTest { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); - uint256 slashAmount = 1000; + uint32 slashPercentage = 100; testStrategyOptInToBApp(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -214,27 +236,32 @@ contract SlashingManagerTest is StrategyManagerTest { bApps[i].slash( STRATEGY1, token, - slashAmount, + slashPercentage, + address(bApps[i]), abi.encodePacked("0x00") ); } } function testWithdrawSlashingFundErc20() public { - uint256 slashAmount = 100; - testSlashBApp(slashAmount); + uint32 slashPercentage = 9000; + testSlashBApp(slashPercentage); vm.expectEmit(true, true, true, true); - emit IStrategyManager.SlashingFundWithdrawn( - address(erc20mock), - slashAmount - ); + emit IStrategyManager.SlashingFundWithdrawn(address(erc20mock), 1); vm.prank(USER1); - bApp1.withdrawSlashingFund(address(erc20mock), slashAmount); + bApp1.withdrawSlashingFund(address(erc20mock), 1); } function testWithdrawSlashingFundEth() public { - uint256 slashAmount = 0.2 ether; - testSlashBAppWithEth(slashAmount); + uint32 slashPercentage = 9000; + uint32 percentage = 10000; + uint256 depositAmount = 1 ether; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); + testSlashBAppWithEth(slashPercentage); vm.expectEmit(true, true, true, true); emit IStrategyManager.SlashingFundWithdrawn(ETH_ADDRESS, slashAmount); vm.prank(USER1); @@ -242,8 +269,13 @@ contract SlashingManagerTest is StrategyManagerTest { } function testRevertWithdrawSlashingFundErc20WithEth() public { - uint256 slashAmount = 100; - testSlashBApp(slashAmount); + uint32 slashPercentage = 100; + uint256 slashAmount = calculateSlashAmount( + 1 ether, + proxiedManager.maxPercentage(), + slashPercentage + ); + testSlashBApp(slashPercentage); vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector(IStrategyManager.InvalidToken.selector) @@ -251,26 +283,9 @@ contract SlashingManagerTest is StrategyManagerTest { proxiedManager.withdrawSlashingFund(ETH_ADDRESS, slashAmount); } - function testRevertWithdrawSlashingFundErc20WithInsufficientBalance() - public - { - uint256 slashAmount = 100; - testSlashBApp(slashAmount); - vm.prank(address(bApp1)); - vm.expectRevert( - abi.encodeWithSelector( - IStrategyManager.InsufficientBalance.selector - ) - ); - proxiedManager.withdrawSlashingFund( - address(erc20mock), - slashAmount + 1 - ); - } - function testRevertWithdrawSlashingFundErc20WithZeroAmount() public { - uint256 slashAmount = 100; - testSlashBApp(slashAmount); + uint32 slashPercentage = 100; + testSlashBApp(slashPercentage); vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector(IStrategyManager.InvalidAmount.selector) @@ -282,7 +297,8 @@ contract SlashingManagerTest is StrategyManagerTest { public { uint256 slashAmount = 100; - testSlashBApp(slashAmount); + uint32 slashPercentage = 10000; + testSlashBApp(slashPercentage); vm.prank(address(bApp1)); vm.expectRevert( abi.encodeWithSelector( @@ -293,8 +309,8 @@ contract SlashingManagerTest is StrategyManagerTest { } function testRevertWithdrawETHSlashingFundErc20WithZeroAmount() public { - uint256 slashAmount = 100; - testSlashBApp(slashAmount); + uint32 slashPercentage = 100; + testSlashBApp(slashPercentage); vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector(IStrategyManager.InvalidAmount.selector) @@ -303,11 +319,16 @@ contract SlashingManagerTest is StrategyManagerTest { } function testFinalizeWithdrawalAfterSlashingRedeemsLowerAmount() public { - uint256 slashAmount = 100; + uint32 slashPercentage = 100; uint32 percentage = 9000; uint256 depositAmount = 100_000; uint256 withdrawalAmount = (depositAmount * 50) / 100; address token = address(erc20mock); + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); testStrategyOptInToBApp(percentage); @@ -334,14 +355,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, address(bApp1) ); proxiedManager.slash( STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -361,17 +382,23 @@ contract SlashingManagerTest is StrategyManagerTest { vm.prank(USER2); proxiedManager.finalizeWithdrawal(STRATEGY1, IERC20(erc20mock)); - checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_950); + checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_550); checkAccountShares(STRATEGY1, USER2, token, 50_000); checkSlashableBalance(STRATEGY1, address(bApp1), token, 0); checkSlashingFund(address(bApp1), token, slashAmount); } function testFinalizeWithdrawalETHAfterSlashingRedeemsLowerAmount() public { - uint256 slashAmount = 100; + uint32 slashPercentage = 100; uint256 depositAmount = 100_000; uint256 withdrawalAmount = (depositAmount * 50) / 100; address token = ETH_ADDRESS; + uint32 percentage = 10000; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); testStrategyOptInToBAppWithETH(); @@ -394,14 +421,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, address(bApp1) ); proxiedManager.slash( STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -421,7 +448,7 @@ contract SlashingManagerTest is StrategyManagerTest { vm.prank(USER2); proxiedManager.finalizeWithdrawalETH(STRATEGY1); - checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_950); + checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_500); checkAccountShares(STRATEGY1, USER2, token, 50_000); checkSlashableBalance(STRATEGY1, address(bApp1), token, 0); checkSlashingFund(address(bApp1), token, slashAmount); @@ -429,11 +456,17 @@ contract SlashingManagerTest is StrategyManagerTest { function testSlashBAppTotalBalance(uint256 depositAmount) public { uint32 percentage = 10_000; + uint32 slashPercentage = proxiedManager.maxPercentage(); vm.assume( depositAmount > 0 && depositAmount <= proxiedManager.maxShares() ); address token = address(erc20mock); - uint256 slashAmount = depositAmount; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); + testStrategyOptInToBApp(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -447,7 +480,7 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, address(bApp1) ); checkGeneration(STRATEGY1, token, 0); @@ -455,7 +488,7 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -510,7 +543,8 @@ contract SlashingManagerTest is StrategyManagerTest { function testSlashBAppWhenObligationIsZero() public { uint256 depositAmount = 100_000 * 10 ** 18; address token = address(erc20mock); - uint256 slashAmount = depositAmount / 2; + uint32 slashPercentage = 100; + testStrategyOptInToBAppWithMultipleTokensWithPercentageZero(); vm.prank(USER2); proxiedManager.depositERC20( @@ -529,7 +563,7 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); checkTotalSharesAndTotalBalance( @@ -547,10 +581,16 @@ contract SlashingManagerTest is StrategyManagerTest { function testFinalizeWithdrawalAfterPartialSlashBAppWithdrawSmallerAmount() public { - testStrategyOptInToBApp(proxiedManager.maxPercentage()); + uint32 percentage = proxiedManager.maxPercentage(); + testStrategyOptInToBApp(percentage); uint256 depositAmount = 1000; uint256 withdrawalAmount = 800; - uint256 slashAmount = 300; + uint32 slashPercentage = 10_00; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); address token = address(erc20mock); vm.startPrank(USER1); proxiedManager.depositERC20( @@ -563,10 +603,11 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp1), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); - uint256 newStrategyBalance = depositAmount - slashAmount; // 700 + + uint256 newStrategyBalance = depositAmount - slashAmount; checkTotalSharesAndTotalBalance( STRATEGY1, token, @@ -577,9 +618,10 @@ contract SlashingManagerTest is StrategyManagerTest { checkSlashableBalance(STRATEGY1, address(bApp1), token, 0); checkSlashingFund(address(bApp1), token, slashAmount); checkGeneration(STRATEGY1, token, 0); + vm.warp(block.timestamp + proxiedManager.withdrawalTimelockPeriod()); - proxiedManager.finalizeWithdrawal(STRATEGY1, IERC20(token)); // this ends up withdrawing 560 (800 * 70% since 30% was slashed) - uint256 effectiveWithdrawalAmount = 560; + proxiedManager.finalizeWithdrawal(STRATEGY1, IERC20(token)); // this ends up withdrawing 720 + uint256 effectiveWithdrawalAmount = 720; checkTotalSharesAndTotalBalance( STRATEGY1, token, @@ -601,7 +643,13 @@ contract SlashingManagerTest is StrategyManagerTest { uint32 percentage = 10_000; uint256 depositAmount = 100_000; address token = address(erc20mock); - uint256 slashAmount = 10_000; + uint32 slashPercentage = 100; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); + assertEq(slashAmount, 1000, "Slash amount should be 1000"); testStrategyOptInToBApp(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -615,14 +663,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, address(0) ); proxiedManager.slash( STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); (uint32 adjustedPercentage, ) = proxiedManager.obligations( @@ -630,7 +678,7 @@ contract SlashingManagerTest is StrategyManagerTest { address(bApp3), token ); - assertEq(adjustedPercentage, 9000, "Obligation percentage"); + assertEq(adjustedPercentage, 10000, "Adjusted obligation percentage"); uint256 newStrategyBalance = depositAmount - slashAmount; checkTotalSharesAndTotalBalance( STRATEGY1, @@ -639,12 +687,12 @@ contract SlashingManagerTest is StrategyManagerTest { newStrategyBalance ); checkAccountShares(STRATEGY1, USER2, token, depositAmount); - checkSlashableBalance(STRATEGY1, address(bApp3), token, 81_000); + checkSlashableBalance(STRATEGY1, address(bApp3), token, 99000); checkSlashingFund(address(0), token, slashAmount); } function testSlashBAppAdjust( - uint256 slashAmount, + uint32 slashPercentage, uint256 depositAmount ) public { uint32 percentage = 10_000; @@ -654,10 +702,13 @@ contract SlashingManagerTest is StrategyManagerTest { depositAmount <= proxiedManager.maxShares() && percentage > 0 && percentage <= proxiedManager.maxPercentage() && - slashAmount > 0 && - slashAmount != depositAmount && - slashAmount <= - (depositAmount * percentage) / proxiedManager.maxPercentage() + slashPercentage > 0 && + slashPercentage < proxiedManager.maxPercentage() + ); + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage ); testStrategyOptInToBApp(percentage); vm.prank(USER2); @@ -672,14 +723,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, address(0) ); proxiedManager.slash( STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint32 adjustedPercentage = checkAdjustedPercentage( @@ -712,7 +763,12 @@ contract SlashingManagerTest is StrategyManagerTest { vm.assume( depositAmount > 0 && depositAmount <= proxiedManager.maxShares() ); - uint256 slashAmount = depositAmount; + uint32 slashPercentage = 10_000; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); testStrategyOptInToBApp(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -726,14 +782,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, address(0) ); proxiedManager.slash( STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); (uint32 adjustedPercentage, ) = proxiedManager.obligations( @@ -755,7 +811,13 @@ contract SlashingManagerTest is StrategyManagerTest { function testSlashBAppAdjustBasicETHWithAdjust() public { uint256 depositAmount = 100 ether; address token = ETH_ADDRESS; - uint256 slashAmount = 10 ether; + uint32 slashPercentage = 100; + uint32 percentage = 10_000; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); testStrategyOptInToBAppWithETH(); vm.deal(USER2, depositAmount); vm.prank(USER2); @@ -766,14 +828,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, address(0) ); proxiedManager.slash( STRATEGY1, address(bApp3), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); (uint32 adjustedPercentage, ) = proxiedManager.obligations( @@ -781,7 +843,7 @@ contract SlashingManagerTest is StrategyManagerTest { address(bApp3), token ); - assertEq(adjustedPercentage, 9000, "Obligation percentage"); + assertEq(adjustedPercentage, 10000, "Obligation percentage"); uint256 newStrategyBalance = depositAmount - slashAmount; checkTotalSharesAndTotalBalance( STRATEGY1, @@ -790,14 +852,21 @@ contract SlashingManagerTest is StrategyManagerTest { newStrategyBalance ); checkAccountShares(STRATEGY1, USER2, token, depositAmount); - checkSlashableBalance(STRATEGY1, address(bApp3), token, 81 ether); + checkSlashableBalance(STRATEGY1, address(bApp3), token, 99.00 ether); checkSlashingFund(address(0), token, slashAmount); } function testSlashBAppAdjustBasicETH() public { uint256 depositAmount = 100 ether; address token = ETH_ADDRESS; - uint256 slashAmount = 10 ether; + uint32 slashPercentage = 100; + uint32 percentage = 10_000; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); + testStrategyOptInToBAppWithETH(); vm.deal(USER2, depositAmount); vm.prank(USER2); @@ -808,14 +877,14 @@ contract SlashingManagerTest is StrategyManagerTest { STRATEGY1, address(bApp4), token, - slashAmount, + slashPercentage, address(bApp4) ); proxiedManager.slash( STRATEGY1, address(bApp4), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); (uint32 adjustedPercentage, ) = proxiedManager.obligations( @@ -846,4 +915,33 @@ contract SlashingManagerTest is StrategyManagerTest { ); bApp4.withdrawETHSlashingFund(1 ether); } + + function testRevertSlashStrategyNotOptedInToBApp() public { + uint256 depositAmount = 100_000; + uint32 slashPercentage = 100; + + testCreateStrategies(); + testRegisterBAppWith2Tokens(); + + vm.prank(USER2); + proxiedManager.depositERC20( + STRATEGY1, + IERC20(erc20mock), + depositAmount + ); + vm.prank(USER1); + vm.expectRevert( + abi.encodeWithSelector( + IStrategyManager.BAppNotOptedIn.selector, + STRATEGY1 + ) + ); + proxiedManager.slash( + STRATEGY1, + address(bApp1), + address(erc20mock), + slashPercentage, + abi.encodePacked("0x00") + ); + } } diff --git a/test/modules/SlashingManager.eoa.t.sol b/test/modules/SlashingManager.eoa.t.sol index 379705bd..008004dd 100644 --- a/test/modules/SlashingManager.eoa.t.sol +++ b/test/modules/SlashingManager.eoa.t.sol @@ -1,8 +1,13 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.29; -import { IERC20, IStrategyManager, IBasedAppManager } from "@ssv/test/helpers/Setup.t.sol"; +import { + IERC20, + IStrategyManager, + IBasedAppManager +} from "@ssv/test/helpers/Setup.t.sol"; import { StrategyManagerTest } from "@ssv/test/modules/StrategyManager.t.sol"; +import { ValidationLib } from "@ssv/src/core/libraries/ValidationLib.sol"; contract SlashingManagerEOATest is StrategyManagerTest { function testGetSlashableBalanceBasic() public { @@ -42,7 +47,10 @@ contract SlashingManagerEOATest is StrategyManagerTest { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); - uint256 slashAmount = 1000; + uint32 slashPercentage = 100; + uint256 slashAmount = (((depositAmount * percentage) / + proxiedManager.maxPercentage()) * slashPercentage) / + proxiedManager.maxPercentage(); testStrategyOptInToBAppEOA(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -55,10 +63,10 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); - uint256 newStrategyBalance = depositAmount - slashAmount; // 100,000 - 1,000 = 99,000 ERC20 + uint256 newStrategyBalance = depositAmount - slashAmount; checkAccountShares(STRATEGY1, USER2, token, depositAmount); checkTotalSharesAndTotalBalance( STRATEGY1, @@ -66,18 +74,20 @@ contract SlashingManagerEOATest is StrategyManagerTest { depositAmount, newStrategyBalance ); - checkSlashableBalance(STRATEGY1, USER1, token, 0); // 99,000 * 90% = 89,100 ERC20 + checkSlashableBalance(STRATEGY1, USER1, token, 0); checkSlashingFund(USER1, token, slashAmount); } - function testSlashEOA(uint256 slashAmount) public { + function testSlashEOA(uint32 slashPercentage) public { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); + uint256 slashAmount = (depositAmount * percentage * slashPercentage) / + proxiedManager.maxPercentage() / + proxiedManager.maxPercentage(); vm.assume( - slashAmount > 0 && - slashAmount <= - (depositAmount * percentage) / proxiedManager.maxPercentage() + slashPercentage > 0 && + slashPercentage <= proxiedManager.maxPercentage() ); testStrategyOptInToBAppEOA(percentage); vm.prank(USER2); @@ -98,7 +108,7 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -113,17 +123,17 @@ contract SlashingManagerEOATest is StrategyManagerTest { checkSlashingFund(USER1, token, slashAmount); } - function testSlashEOAWithEth(uint256 slashAmount) public { + function testSlashEOAWithEth(uint32 slashPercentage) public { uint32 percentage = 9000; uint256 depositAmount = 3 ether; address token = ETH_ADDRESS; - vm.assume( - slashAmount > 0 && - slashAmount <= - (depositAmount * percentage) / proxiedManager.maxPercentage() + slashPercentage > 0 && + slashPercentage <= proxiedManager.maxPercentage() ); - + uint256 slashAmount = (depositAmount * percentage * slashPercentage) / + proxiedManager.maxPercentage() / + proxiedManager.maxPercentage(); testStrategyOptInToBAppEOAWithETH(percentage); vm.prank(USER2); proxiedManager.depositETH{ value: depositAmount }(STRATEGY1); @@ -140,7 +150,7 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -155,18 +165,19 @@ contract SlashingManagerEOATest is StrategyManagerTest { checkSlashingFund(USER1, token, slashAmount); } - function testRevertSlashEOAWithZeroAmount() public { + function testRevertSlashEOAWithZeroPercentage() public { testStrategyOptInToBAppEOA(1000); vm.prank(USER1); - uint256 slashAmount = 0; + uint32 slashPercentage = 0; + vm.expectRevert( - abi.encodeWithSelector(IStrategyManager.InvalidAmount.selector) + abi.encodeWithSelector(ValidationLib.InvalidPercentage.selector) ); proxiedManager.slash( STRATEGY1, USER1, address(erc20mock), - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); } @@ -174,7 +185,8 @@ contract SlashingManagerEOATest is StrategyManagerTest { function testRevertSlashEOAWithInsufficientBalance() public { uint32 percentage = 9000; address token = address(erc20mock); - uint256 slashAmount = 1; + uint32 slashPercentage = 100; + testStrategyOptInToBAppEOA(percentage); vm.prank(USER1); vm.expectRevert( @@ -186,7 +198,7 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); } @@ -195,7 +207,8 @@ contract SlashingManagerEOATest is StrategyManagerTest { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); - uint256 slashAmount = 1000; + uint32 slashPercentage = 100; + testStrategyOptInToBAppEOA(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -209,14 +222,15 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); } function testWithdrawSlashingFundErc20FromEOA() public { uint256 slashAmount = 100; - testSlashEOA(slashAmount); + uint32 slashPercentage = 9000; + testSlashEOA(slashPercentage); vm.expectEmit(true, true, true, true); emit IStrategyManager.SlashingFundWithdrawn( address(erc20mock), @@ -228,24 +242,26 @@ contract SlashingManagerEOATest is StrategyManagerTest { function testWithdrawSlashingFundEthFromEOA() public { uint256 slashAmount = 0.2 ether; - testSlashEOAWithEth(slashAmount); + uint32 slashPercentage = 1000; + testSlashEOAWithEth(slashPercentage); vm.expectEmit(true, true, true, true); emit IStrategyManager.SlashingFundWithdrawn(ETH_ADDRESS, slashAmount); vm.prank(USER1); proxiedManager.withdrawETHSlashingFund(slashAmount); } - // Slash Non Compatible BApp - - function testSlashNonCompatibleBApp(uint256 slashAmount) public { + // Slash Non Compatible BApp, does not implement the slash function inside the contract + function testSlashNonCompatibleBApp(uint32 slashPercentage) public { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); vm.assume( - slashAmount > 0 && - slashAmount <= - (depositAmount * percentage) / proxiedManager.maxPercentage() + slashPercentage > 0 && + slashPercentage <= proxiedManager.maxPercentage() ); + uint256 slashAmount = (depositAmount * percentage * slashPercentage) / + proxiedManager.maxPercentage() / + proxiedManager.maxPercentage(); testStrategyOptInToBAppNonCompliant(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -254,32 +270,16 @@ contract SlashingManagerEOATest is StrategyManagerTest { depositAmount ); vm.prank(USER1); - vm.expectEmit(true, true, true, true); - emit IStrategyManager.StrategySlashed( - STRATEGY1, - address(nonCompliantBApp), - token, - slashAmount, - address(nonCompliantBApp) - ); - nonCompliantBApp.slash(STRATEGY1, token, slashAmount); - uint256 newStrategyBalance = depositAmount - slashAmount; - checkTotalSharesAndTotalBalance( - STRATEGY1, - token, - depositAmount, - newStrategyBalance - ); - checkAccountShares(STRATEGY1, USER2, token, depositAmount); - checkSlashableBalance(STRATEGY1, address(nonCompliantBApp), token, 0); - checkSlashingFund(address(nonCompliantBApp), token, slashAmount); + vm.expectRevert(); + nonCompliantBApp.slash(STRATEGY1, token, slashPercentage); } function testRevertSlashNonCompatibleBAppWithNonOwner() public { uint32 percentage = 9000; uint256 depositAmount = 100_000; address token = address(erc20mock); - uint256 slashAmount = 2; + uint32 slashPercentage = 100; + testStrategyOptInToBAppNonCompliant(percentage); vm.startPrank(USER2); proxiedManager.depositERC20( @@ -287,18 +287,12 @@ contract SlashingManagerEOATest is StrategyManagerTest { IERC20(erc20mock), depositAmount ); - vm.expectRevert( - abi.encodeWithSelector( - IStrategyManager.InvalidBAppOwner.selector, - USER2, - address(nonCompliantBApp) - ) - ); + vm.expectRevert(); proxiedManager.slash( STRATEGY1, address(nonCompliantBApp), token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); vm.stopPrank(); @@ -306,6 +300,8 @@ contract SlashingManagerEOATest is StrategyManagerTest { function testRevertSlashEOANotRegistered() public { uint256 depositAmount = 100_000; + uint32 slashPercentage = 100; + vm.prank(USER2); proxiedManager.depositERC20( STRATEGY1, @@ -313,7 +309,6 @@ contract SlashingManagerEOATest is StrategyManagerTest { depositAmount ); vm.prank(USER1); - uint256 slashAmount = 1; vm.expectRevert( abi.encodeWithSelector(IBasedAppManager.BAppNotRegistered.selector) ); @@ -321,41 +316,54 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, address(erc20mock), - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); } - function testRevertWithdrawSlashingFundErc20WithEthEOA() public { - uint256 slashAmount = 100; - testSlashEOA(slashAmount); + function testRevertSlashStrategyNotOptedInToEOA() public { + uint256 depositAmount = 100_000; + uint32 slashPercentage = 100; + + testCreateStrategies(); + testRegisterBAppWithEOA(); + + vm.prank(USER2); + proxiedManager.depositERC20( + STRATEGY1, + IERC20(erc20mock), + depositAmount + ); vm.prank(USER1); vm.expectRevert( - abi.encodeWithSelector(IStrategyManager.InvalidToken.selector) + abi.encodeWithSelector( + IStrategyManager.BAppNotOptedIn.selector, + STRATEGY1 + ) + ); + proxiedManager.slash( + STRATEGY1, + USER1, + address(erc20mock), + slashPercentage, + abi.encodePacked("0x00") ); - proxiedManager.withdrawSlashingFund(ETH_ADDRESS, slashAmount); } - function testRevertWithdrawSlashingFundErc20WithInsufficientBalanceEOA() - public - { + function testRevertWithdrawSlashingFundErc20WithEthEOA() public { uint256 slashAmount = 100; - testSlashEOA(slashAmount); + uint32 slashPercentage = 100; + testSlashEOA(slashPercentage); vm.prank(USER1); vm.expectRevert( - abi.encodeWithSelector( - IStrategyManager.InsufficientBalance.selector - ) - ); - proxiedManager.withdrawSlashingFund( - address(erc20mock), - slashAmount + 1 + abi.encodeWithSelector(IStrategyManager.InvalidToken.selector) ); + proxiedManager.withdrawSlashingFund(ETH_ADDRESS, slashAmount); } function testRevertWithdrawSlashingFundErc20WithZeroAmountEOA() public { - uint256 slashAmount = 100; - testSlashEOA(slashAmount); + uint32 slashPercentage = 100; + testSlashEOA(slashPercentage); vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector(IStrategyManager.InvalidAmount.selector) @@ -367,7 +375,8 @@ contract SlashingManagerEOATest is StrategyManagerTest { public { uint256 slashAmount = 100; - testSlashEOA(slashAmount); + uint32 slashPercentage = 100; + testSlashEOA(slashPercentage); vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector( @@ -378,8 +387,8 @@ contract SlashingManagerEOATest is StrategyManagerTest { } function testRevertWithdrawETHSlashingFundErc20WithZeroAmountEOA() public { - uint256 slashAmount = 100; - testSlashEOA(slashAmount); + uint32 slashPercentage = 100; + testSlashEOA(slashPercentage); vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector(IStrategyManager.InvalidAmount.selector) @@ -388,11 +397,16 @@ contract SlashingManagerEOATest is StrategyManagerTest { } function testFinalizeWithdrawalAfterSlashingRedeemsLowerAmountEOA() public { - uint256 slashAmount = 100; uint32 percentage = 9000; uint256 depositAmount = 100_000; uint256 withdrawalAmount = (depositAmount * 50) / 100; address token = address(erc20mock); + uint32 slashPercentage = 100; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); testStrategyOptInToBAppEOA(percentage); @@ -419,14 +433,14 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, USER1 ); proxiedManager.slash( STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -446,7 +460,7 @@ contract SlashingManagerEOATest is StrategyManagerTest { vm.prank(USER2); proxiedManager.finalizeWithdrawal(STRATEGY1, IERC20(erc20mock)); - checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_950); + checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_550); checkAccountShares(STRATEGY1, USER2, token, 50_000); checkSlashableBalance(STRATEGY1, USER1, token, 0); checkSlashingFund(USER1, token, slashAmount); @@ -455,11 +469,16 @@ contract SlashingManagerEOATest is StrategyManagerTest { function testFinalizeWithdrawalETHAfterSlashingRedeemsLowerAmountEOA() public { - uint256 slashAmount = 100; uint32 percentage = 10_000; uint256 depositAmount = 100_000; uint256 withdrawalAmount = (depositAmount * 50) / 100; address token = ETH_ADDRESS; + uint32 slashPercentage = 100; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); testStrategyOptInToBAppEOAWithETH(percentage); @@ -482,14 +501,14 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, USER1 ); proxiedManager.slash( STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; @@ -509,7 +528,7 @@ contract SlashingManagerEOATest is StrategyManagerTest { vm.prank(USER2); proxiedManager.finalizeWithdrawalETH(STRATEGY1); - checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_950); + checkTotalSharesAndTotalBalance(STRATEGY1, token, 50_000, 49_500); checkAccountShares(STRATEGY1, USER2, token, 50_000); checkSlashableBalance(STRATEGY1, USER1, token, 0); checkSlashingFund(USER1, token, slashAmount); @@ -521,7 +540,13 @@ contract SlashingManagerEOATest is StrategyManagerTest { ); uint32 percentage = 10_000; address token = address(erc20mock); - uint256 slashAmount = depositAmount; + uint32 slashPercentage = proxiedManager.maxPercentage(); + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); + testStrategyOptInToBAppEOA(percentage); vm.prank(USER2); proxiedManager.depositERC20( @@ -535,7 +560,7 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); checkGeneration(STRATEGY1, token, 1); @@ -616,10 +641,16 @@ contract SlashingManagerEOATest is StrategyManagerTest { function testFinalizeWithdrawalAfterPartialSlashBAppWithdrawSmallerAmountEOA() public { - testStrategyOptInToBAppEOA(proxiedManager.maxPercentage()); + uint32 percentage = proxiedManager.maxPercentage(); + testStrategyOptInToBAppEOA(percentage); uint256 depositAmount = 1000; uint256 withdrawalAmount = 800; - uint256 slashAmount = 300; + uint32 slashPercentage = 10_00; + uint256 slashAmount = calculateSlashAmount( + depositAmount, + percentage, + slashPercentage + ); address token = address(erc20mock); vm.startPrank(USER1); proxiedManager.depositERC20( @@ -632,7 +663,7 @@ contract SlashingManagerEOATest is StrategyManagerTest { STRATEGY1, USER1, token, - slashAmount, + slashPercentage, abi.encodePacked("0x00") ); uint256 newStrategyBalance = depositAmount - slashAmount; // 700 @@ -647,8 +678,8 @@ contract SlashingManagerEOATest is StrategyManagerTest { checkSlashingFund(USER1, token, slashAmount); checkGeneration(STRATEGY1, token, 0); vm.warp(block.timestamp + proxiedManager.withdrawalTimelockPeriod()); - proxiedManager.finalizeWithdrawal(STRATEGY1, IERC20(token)); // this ends up withdrawing 560 (800 * 70% since 30% was slashed) - uint256 effectiveWithdrawalAmount = 560; + proxiedManager.finalizeWithdrawal(STRATEGY1, IERC20(token)); // this ends up withdrawing 720 + uint256 effectiveWithdrawalAmount = 720; checkTotalSharesAndTotalBalance( STRATEGY1, token, @@ -668,7 +699,8 @@ contract SlashingManagerEOATest is StrategyManagerTest { function testRevertWithdrawSlashingFundErc20WithEthOnEOA() public { uint256 slashAmount = 100; - testSlashEOA(slashAmount); + uint32 slashPercentage = 100; + testSlashEOA(slashPercentage); vm.prank(USER1); vm.expectRevert( abi.encodeWithSelector(IStrategyManager.InvalidToken.selector) diff --git a/test/modules/StrategyManager.t.sol b/test/modules/StrategyManager.t.sol index f27fe6c3..ee17802a 100644 --- a/test/modules/StrategyManager.t.sol +++ b/test/modules/StrategyManager.t.sol @@ -3,10 +3,15 @@ pragma solidity 0.8.29; import { IERC20, BasedAppMock } from "@ssv/test/helpers/Setup.t.sol"; import { BasedAppsManagerTest } from "@ssv/test/modules/BasedAppsManager.t.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; -import { IStrategyManager } from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + IStrategyManager +} from "@ssv/src/core/interfaces/IStrategyManager.sol"; +import { + IBasedAppManager +} from "@ssv/src/core/interfaces/IBasedAppManager.sol"; import { UtilsTest } from "@ssv/test/helpers/Utils.t.sol"; import { ValidationLib } from "@ssv/src/core/libraries/ValidationLib.sol"; +import { ICore } from "@ssv/src/core/interfaces/ICore.sol"; contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { function updateObligation( @@ -46,9 +51,36 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { STRATEGY1_INITIAL_FEE, "" ); + uint32[] memory strategies = proxiedManager.ownedStrategies(USER1); + assertEq( + strategies.length, + 1, + "Should have created 1 strategy for USER1" + ); + assertEq( + strategies[0], + strategyId1, + "Should have the correct ID for Strategy 1" + ); proxiedManager.createStrategy(STRATEGY2_INITIAL_FEE, ""); proxiedManager.createStrategy(STRATEGY3_INITIAL_FEE, ""); - + strategies = proxiedManager.ownedStrategies(USER1); + assertEq(strategies.length, 3, "Should have created the strategy"); + assertEq( + strategies[0], + strategyId1, + "Should have the correct ID for Strategy 1" + ); + assertEq( + strategies[1], + STRATEGY2, + "Should have the correct ID for Strategy 2" + ); + assertEq( + strategies[2], + STRATEGY3, + "Should have the correct ID for Strategy 3" + ); assertEq( strategyId1, STRATEGY1, @@ -70,6 +102,17 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { STRATEGY4_INITIAL_FEE, "" ); + strategies = proxiedManager.ownedStrategies(USER2); + assertEq( + strategies.length, + 1, + "Should have created one strategy for USER2" + ); + assertEq( + strategies[0], + strategyId4, + "Should have the correct ID for Strategy 4" + ); assertEq( strategyId4, STRATEGY4, @@ -390,7 +433,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), percentage, proxiedManager, - uint32(i) + 1, true ); } @@ -442,7 +484,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), percentage, proxiedManager, - 1, true ); uint32 counter = bApp1.counter(); @@ -485,7 +526,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), percentage, proxiedManager, - 1, true ); vm.stopPrank(); @@ -526,7 +566,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { ETH_ADDRESS, percentage, proxiedManager, - 1, true ); vm.stopPrank(); @@ -567,7 +606,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), percentage, proxiedManager, - 1, true ); vm.stopPrank(); @@ -600,7 +638,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), 0, proxiedManager, - 0, false ); uint32 counter = bApp1.counter(); @@ -633,8 +670,7 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { abi.encodePacked("0x00") ); checkBAppInfo( - new address[](0), - new uint32[](0), + new ICore.TokenConfig[](0), address(bApps[i]), proxiedManager ); @@ -645,7 +681,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), 0, proxiedManager, - 0, false ); } @@ -689,7 +724,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), percentage, proxiedManager, - 1, true ); checkStrategyInfo( @@ -699,7 +733,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock2), percentage, proxiedManager, - 1, true ); vm.stopPrank(); @@ -776,7 +809,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), percentage, proxiedManager, - uint32(i) + 1, true ); } @@ -848,7 +880,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(erc20mock), percentage, proxiedManager, - 0, true ); } @@ -890,7 +921,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { ETH_ADDRESS, percentage, proxiedManager, - uint32(i) + 1, true ); } @@ -1015,6 +1045,34 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { vm.stopPrank(); } + function testRevertStrategyOptingInToNonExistingBApp( + uint32 percentage + ) public { + vm.assume( + percentage > 0 && percentage <= proxiedManager.maxPercentage() + ); + testCreateStrategies(); + vm.startPrank(USER1); + address[] memory tokensInput = new address[](0); + uint32[] memory obligationPercentagesInput = new uint32[](0); + for (uint256 i = 0; i < bApps.length; i++) { + vm.expectRevert( + abi.encodeWithSelector( + IBasedAppManager.BAppNotRegistered.selector, + address(erc20mock2) + ) + ); + proxiedManager.optInToBApp( + STRATEGY1, + address(bApps[i]), + tokensInput, + obligationPercentagesInput, + abi.encodePacked("0x00") + ); + } + vm.stopPrank(); + } + function testStrategyOwnerDepositERC20WithNoObligation( uint256 amount ) public { @@ -1226,7 +1284,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(bApps[i]), token, percentage, - uint32(i) + 1, true, proxiedManager ); @@ -1320,7 +1377,7 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { returns (uint32 proposedFee) { testStrategyOptInToBApp(9000); - proposedFee = 20; + proposedFee = 505; vm.prank(USER1); vm.expectEmit(true, true, true, true); emit IStrategyManager.StrategyFeeUpdateProposed( @@ -1586,7 +1643,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { ); vm.startPrank(USER1); - checkUsedTokens(STRATEGY1, address(erc20mock), uint32(bApps.length)); vm.warp( block.timestamp + @@ -1616,7 +1672,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { true ); } - checkUsedTokens(STRATEGY1, address(erc20mock), 0); vm.stopPrank(); } @@ -1998,7 +2053,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(bApps[i]), address(erc20mock), percentage, - uint32(i) + 1, true, proxiedManager ); @@ -2021,7 +2075,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(bApps[i]), ETH_ADDRESS, proxiedManager.maxPercentage(), - uint32(i) + 1, true, proxiedManager ); @@ -2062,7 +2115,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(bApps[i]), address(erc20mock), 0, - 0, true, proxiedManager ); @@ -2085,7 +2137,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(bApps[i]), ETH_ADDRESS, 0, - 0, true, proxiedManager ); @@ -2116,7 +2167,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { address(bApps[i]), ETH_ADDRESS, 5000, - uint32(i) + 1, true, proxiedManager ); @@ -2207,346 +2257,6 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { vm.stopPrank(); } - function testUpdateUsedTokensCorrectly() public { - testCreateNewObligationSuccessful(); - vm.startPrank(USER1); - address token = address(erc20mock2); - - updateObligation(STRATEGY1, address(bApp1), token, 9700); - checkObligationInfo( - STRATEGY1, - address(bApp1), - token, - 9700, - uint32(bApps.length), - true, - proxiedManager - ); - - proxiedManager.proposeUpdateObligation( - STRATEGY1, - address(bApp1), - token, - 0 - ); - checkProposedObligation( - STRATEGY1, - address(bApp1), - token, - 9700, - 0, - block.timestamp, - true - ); - vm.warp(block.timestamp + proxiedManager.obligationTimelockPeriod()); - proxiedManager.finalizeUpdateObligation( - STRATEGY1, - address(bApp1), - token - ); - checkProposedObligation( - STRATEGY1, - address(bApp1), - token, - 0, - 0, - 0, - true - ); - checkObligationInfo( - STRATEGY1, - address(bApp1), - token, - 0, - uint32(bApps.length) - 1, - true, - proxiedManager - ); - - vm.expectRevert( - abi.encodeWithSelector( - IStrategyManager.ObligationAlreadySet.selector - ) - ); - proxiedManager.createObligation(STRATEGY1, address(bApp1), token, 1000); - updateObligation(STRATEGY1, address(bApp1), token, 1000); - checkObligationInfo( - STRATEGY1, - address(bApp1), - token, - 1000, - uint32(bApps.length), - true, - proxiedManager - ); - vm.stopPrank(); - } - - function testUpdateUsedTokens() public { - uint32 percentage = 10_000; - testCreateStrategies(); - testRegisterBAppWith2Tokens(); - - vm.startPrank(USER1); - proxiedManager.depositERC20(STRATEGY1, IERC20(erc20mock), 100_000); - ( - address[] memory tokensInput, - uint32[] memory obligationPercentagesInput - ) = createSingleTokenAndSingleObligationPercentage( - address(erc20mock), - percentage - ); - - proxiedManager.optInToBApp( - STRATEGY1, - address(bApp1), - tokensInput, - obligationPercentagesInput, - abi.encodePacked("0x00") - ); - proxiedManager.optInToBApp( - STRATEGY1, - address(bApp2), - tokensInput, - obligationPercentagesInput, - abi.encodePacked("0x00") - ); - - uint32 strategyId = proxiedManager.accountBAppStrategy( - USER1, - address(bApp1) - ); - assertEq( - strategyId, - STRATEGY1, - "BApp1 should have the strategy ID set correctly" - ); - - strategyId = proxiedManager.accountBAppStrategy(USER1, address(bApp2)); - assertEq( - strategyId, - STRATEGY1, - "BApp2 should have the strategy ID set correctly" - ); - - (uint256 obligationPercentage, bool isSet) = proxiedManager.obligations( - strategyId, - address(bApp1), - address(erc20mock) - ); - assertEq(isSet, true, "BApp1 Should have the obligation set"); - assertEq( - obligationPercentage, - percentage, - "Should have the obligation percentage set correctly" - ); - - (obligationPercentage, isSet) = proxiedManager.obligations( - strategyId, - address(bApp2), - address(erc20mock) - ); - assertEq(isSet, true, "BApp2 Should have the obligation set"); - assertEq( - obligationPercentage, - percentage, - "Should have the obligation percentage set correctly" - ); - - checkUsedTokens(strategyId, address(erc20mock), 2); - - proxiedManager.proposeUpdateObligation( - STRATEGY1, - address(bApp2), - address(erc20mock), - 0 - ); - checkUsedTokens(strategyId, address(erc20mock), 2); - - vm.warp(block.timestamp + proxiedManager.obligationTimelockPeriod()); - proxiedManager.finalizeUpdateObligation( - STRATEGY1, - address(bApp2), - address(erc20mock) - ); - checkUsedTokens(strategyId, address(erc20mock), 1); - - proxiedManager.proposeUpdateObligation( - STRATEGY1, - address(bApp1), - address(erc20mock), - 0 - ); - checkUsedTokens(strategyId, address(erc20mock), 1); - - vm.warp(block.timestamp + proxiedManager.obligationTimelockPeriod()); - proxiedManager.finalizeUpdateObligation( - STRATEGY1, - address(bApp1), - address(erc20mock) - ); - checkUsedTokens(strategyId, address(erc20mock), 0); - - proxiedManager.proposeUpdateObligation( - STRATEGY1, - address(bApp1), - address(erc20mock), - 1 - ); - checkUsedTokens(strategyId, address(erc20mock), 0); - - vm.warp(block.timestamp + proxiedManager.obligationTimelockPeriod()); - proxiedManager.finalizeUpdateObligation( - STRATEGY1, - address(bApp1), - address(erc20mock) - ); - checkUsedTokens(strategyId, address(erc20mock), 1); - - proxiedManager.proposeUpdateObligation( - STRATEGY1, - address(bApp2), - address(erc20mock), - 1 - ); - checkUsedTokens(strategyId, address(erc20mock), 1); - - vm.warp(block.timestamp + proxiedManager.obligationTimelockPeriod()); - proxiedManager.finalizeUpdateObligation( - STRATEGY1, - address(bApp2), - address(erc20mock) - ); - checkUsedTokens(strategyId, address(erc20mock), 2); - vm.stopPrank(); - } - - function testUpdateUsedTokensETH() public { - uint32 percentage = 10_000; - testCreateStrategies(); - testRegisterBAppWithETH(); - vm.startPrank(USER1); - proxiedManager.depositETH{ value: 1 ether }(STRATEGY1); - ( - address[] memory tokensInput, - uint32[] memory obligationPercentagesInput - ) = createSingleTokenAndSingleObligationPercentage( - ETH_ADDRESS, - percentage - ); - - proxiedManager.optInToBApp( - STRATEGY1, - address(bApp1), - tokensInput, - obligationPercentagesInput, - abi.encodePacked("0x00") - ); - checkObligationInfo( - STRATEGY1, - address(bApp1), - ETH_ADDRESS, - percentage, - 1, - true, - proxiedManager - ); - - proxiedManager.optInToBApp( - STRATEGY1, - address(bApp2), - tokensInput, - obligationPercentagesInput, - abi.encodePacked("0x00") - ); - checkObligationInfo( - STRATEGY1, - address(bApp2), - ETH_ADDRESS, - percentage, - 2, - true, - proxiedManager - ); - - proxiedManager.proposeUpdateObligation( - STRATEGY1, - address(bApp2), - ETH_ADDRESS, - 0 - ); - checkObligationInfo( - STRATEGY1, - address(bApp2), - ETH_ADDRESS, - percentage, - 2, - true, - proxiedManager - ); - - vm.warp(block.timestamp + proxiedManager.obligationTimelockPeriod()); - - proxiedManager.finalizeUpdateObligation( - STRATEGY1, - address(bApp2), - ETH_ADDRESS - ); - checkObligationInfo( - STRATEGY1, - address(bApp2), - ETH_ADDRESS, - 0, - 1, - true, - proxiedManager - ); - - proxiedManager.proposeUpdateObligation( - STRATEGY1, - address(bApp1), - ETH_ADDRESS, - 0 - ); - checkObligationInfo( - STRATEGY1, - address(bApp1), - ETH_ADDRESS, - percentage, - 1, - true, - proxiedManager - ); - - vm.warp(block.timestamp + proxiedManager.obligationTimelockPeriod()); - - proxiedManager.finalizeUpdateObligation( - STRATEGY1, - address(bApp1), - ETH_ADDRESS - ); - checkObligationInfo( - STRATEGY1, - address(bApp1), - ETH_ADDRESS, - 0, - 0, - true, - proxiedManager - ); - checkObligationInfo( - STRATEGY1, - address(bApp2), - ETH_ADDRESS, - 0, - 0, - true, - proxiedManager - ); - - vm.stopPrank(); - } - function testRevertProposeWithdrawInsufficientLiquidity() public { testCreateObligationETHWithZeroPercentage(); // Registers BApp and does the opt in vm.prank(USER1); @@ -2615,11 +2325,13 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { vm.startPrank(USER1); proxiedManager.depositETH{ value: 1 ether }(STRATEGY1); proxiedManager.proposeWithdrawalETH(STRATEGY1, 1 ether); + uint32 slashPercentage = 5000; proxiedManager.slash( STRATEGY1, address(bApp1), ETH_ADDRESS, - 0.5 ether, + // 0.5 ether, + slashPercentage, abi.encode("0x00") ); vm.warp(block.timestamp + proxiedManager.withdrawalTimelockPeriod()); @@ -2634,11 +2346,12 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { vm.startPrank(USER1); proxiedManager.depositETH{ value: 1 ether }(STRATEGY1); proxiedManager.proposeWithdrawalETH(STRATEGY1, 1 ether); + uint32 slashPercentage = 10_000; proxiedManager.slash( STRATEGY1, address(bApp1), ETH_ADDRESS, - 1 ether, + slashPercentage, abi.encode("0x00") ); vm.warp(block.timestamp + proxiedManager.withdrawalTimelockPeriod()); @@ -2678,4 +2391,172 @@ contract StrategyManagerTest is UtilsTest, BasedAppsManagerTest { newWithdrawalAmount ); } + + function testSlashWhenEnabled() public { + uint32 pct = proxiedManager.maxPercentage(); + // register & opt‐in + testStrategyOptInToBApp(pct); + // deposit 1 token + vm.startPrank(USER1); + erc20mock.approve(address(proxiedManager), 1); + proxiedManager.depositERC20(STRATEGY1, erc20mock, 1); + vm.stopPrank(); + // slash from bApp1 + vm.prank(address(bApp1)); + proxiedManager.slash( + STRATEGY1, + address(bApp1), + address(erc20mock), + 10_000, + "" + ); + // after slash, strategy balance should be zero + uint256 bal = proxiedManager.strategyTotalBalance( + STRATEGY1, + address(erc20mock) + ); + assertEq(bal, 0, "Strategy balance should have decreased by 1"); + } + + function testSlashRevertsWhenDisabled() public { + // disable slashing + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(1 << 0); + // now any slash call must revert + vm.prank(address(bApp1)); + vm.expectRevert( + abi.encodeWithSelector(IStrategyManager.SlashingDisabled.selector) + ); + proxiedManager.slash( + STRATEGY1, + address(bApp1), + address(erc20mock), + 10_000, + "" + ); + } + + function testSlashSucceedsAfterReenable() public { + // re‐enable slashing + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(0); + // prepare valid slash (opt-in + deposit) + uint32 pct = proxiedManager.maxPercentage(); + testStrategyOptInToBApp(pct); + vm.startPrank(USER1); + erc20mock.approve(address(proxiedManager), 1); + proxiedManager.depositERC20(STRATEGY1, erc20mock, 1); + vm.stopPrank(); + // should no longer revert + vm.prank(address(bApp1)); + proxiedManager.slash( + STRATEGY1, + address(bApp1), + address(erc20mock), + 10_000, + "" + ); + // confirm balance dropped + uint256 bal = proxiedManager.strategyTotalBalance( + STRATEGY1, + address(erc20mock) + ); + assertEq(bal, 0, "Slash should succeed once re-enabled"); + } + + function testProposeAndFinalizeWithdrawalWhenEnabled() public { + // set up a deposit + testCreateStrategyAndSingleDeposit(100); + // propose withdrawal + vm.prank(USER1); + proxiedManager.proposeWithdrawal(STRATEGY1, address(erc20mock), 50); + // fast-forward timelock + vm.warp(block.timestamp + proxiedManager.withdrawalTimelockPeriod()); + // finalize and check balances + vm.prank(USER1); + proxiedManager.finalizeWithdrawal(STRATEGY1, erc20mock); + uint256 remaining = proxiedManager.strategyAccountShares( + STRATEGY1, + USER1, + address(erc20mock) + ); + assertEq( + remaining, + 50, + "User should have 50 tokens left in the strategy" + ); + } + + function testProposeWithdrawalRevertsWhenDisabled() public { + // disable withdrawals (bit 1) + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(1 << 1); + + // attempt to propose an ERC20 withdrawal + vm.prank(USER1); + vm.expectRevert( + abi.encodeWithSelector( + IStrategyManager.WithdrawalsDisabled.selector + ) + ); + proxiedManager.proposeWithdrawal(STRATEGY1, address(erc20mock), 1); + } + + function testFinalizeWithdrawalRevertsWhenDisabled() public { + // first get a pending withdrawal + testProposeWithdrawalFromStrategy(); + + // disable withdrawals + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(1 << 1); + + // now finalize should revert + vm.warp(block.timestamp + proxiedManager.withdrawalTimelockPeriod()); + vm.prank(USER1); + vm.expectRevert( + abi.encodeWithSelector( + IStrategyManager.WithdrawalsDisabled.selector + ) + ); + proxiedManager.finalizeWithdrawal(STRATEGY1, erc20mock); + } + + function testProposeWithdrawalETHRevertsWhenDisabled() public { + // deposit some ETH first + testCreateStrategyETHAndDepositETH(); + + // disable withdrawals + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(1 << 1); + + // attempt to propose an ETH withdrawal + vm.prank(USER1); + vm.expectRevert( + abi.encodeWithSelector( + IStrategyManager.WithdrawalsDisabled.selector + ) + ); + proxiedManager.proposeWithdrawalETH(STRATEGY1, 1 ether); + } + + function testFinalizeWithdrawalETHRevertsWhenDisabled() public { + // get a pending ETH withdrawal + testProposeWithdrawalETHFromStrategy(0.5 ether); + + // disable withdrawals + vm.prank(OWNER); + proxiedManager.updateDisabledFeatures(1 << 1); + + // warp past timelock + vm.warp(block.timestamp + proxiedManager.withdrawalTimelockPeriod()); + + // now finalize should revert + vm.prank(USER1); + vm.expectRevert( + abi.encodeWithSelector( + IStrategyManager.WithdrawalsDisabled.selector + ) + ); + proxiedManager.finalizeWithdrawalETH(STRATEGY1); + } }