diff --git a/LICENSE.md b/LICENSE.md index 37ef213..513f058 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -13,7 +13,7 @@ Licensed Work: BendDAO V2 Protocol. The Licensed Work is (c) 2023 BendDAO Additional Use Grant: Any uses listed and defined at this [LICENSE](./LICENSE.md) -Change Date: The earlier of 1 Octor 2026 or a date specified at v2-license-date.benddao.eth +Change Date: The earlier of 1 September 2027 or a date specified at v2-license-date.benddao.eth Change License: MIT diff --git a/README.md b/README.md index 8b5a86c..41d439f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,46 @@ +``` +###### ###### # ####### +# # ###### # # ##### # # # # # # +# # # ## # # # # # # # # # +###### ##### # # # # # # # # # # # +# # # # # # # # # # ####### # # +# # # # ## # # # # # # # # +###### ###### # # ##### ###### # # ####### +``` + # BendDAO Protocol V2 +This repository contains the smart contracts source code and markets configuration for BendDAO V2 Protocol. The repository uses Foundry as development environment for compilation, testing and deployment tasks. + --- ## What are BendDAO Protocol V2? -TBD +BendDAO V2 Protocol brings you composable lending and leverage. It allows anyone to borrow in an overcollateralized fashion, leverage savings on MakerDAO, leverage stake on Lido, leverage restake with EigenLayer derivatives, bringing together lending and leverage in the same protocol! ---- +V2 Protocol has three user sides to it: + +Lenders deposit assets to earn passive yield. -## Contracts overview +Borrowers can use ERC20 & ERC721 as collaterals to borrow assets in an overcollateralized fashion. -TBD +Leverage users can use ERC721 as collaterals to borrow assets to create leverage positions, which can be used across DeFi, NFTs, RWA, etc. --- ## Documentation -TBD +[Doc Hub](https://docs.benddao.xyz) + +[User Guide](https://docs.benddao.xyz/portal/v/v2) + +[Dev Guide](https://docs.benddao.xyz/developers/v/v2-1) --- ## Audits -TBD +All audits are stored in the [audits](./audits/) folder and [online](https://docs.benddao.xyz/portal/v/v2/security-and-risks/audits). --- @@ -32,34 +50,6 @@ A bug bounty is open on Immunefi. The rewards and scope are defined [here](https --- -## Deployment Addresses - -TBD - ---- - -## Importing package - -Using npm: - -```bash -npm install @benddao/bend-v2 -``` - -Using forge: - -```bash -forge install @benddao/bend-v2@v1.0.0 -``` - -Using git submodules: - -```bash -git submodule add @benddao/bend-v2@v1.0.0 lib/bend-v2 -``` - ---- - ## Testing with [Foundry](https://github.com/foundry-rs/foundry) 🔨 For testing, make sure `yarn` and `foundry` are installed. @@ -74,18 +64,6 @@ npm run test --- -## Testing with Hardhat - -Only a few tests are run with Hardhat. - -Just run: - -```bash -yarn test:hardhat -``` - ---- - ## Test coverage Test coverage is reported using [foundry](https://github.com/foundry-rs/foundry) coverage with [lcov](https://github.com/linux-test-project/lcov) report formatting (and optionally, [genhtml](https://manpages.ubuntu.com/manpages/xenial/man1/genhtml.1.html) transformer). @@ -93,7 +71,7 @@ Test coverage is reported using [foundry](https://github.com/foundry-rs/foundry) To generate the `lcov` report, run the following: ```bash -npm run coverage:forge +npm run coverage:lcov ``` The report is then usable either: @@ -118,27 +96,7 @@ In the case the storage layout snapshots checked by `storage-layout.sh` are not ## Deployment & Upgrades -### Network mode (default) - -Run the Foundry deployment script with: - -```bash -npm run deploy:goerli -``` - -### Local mode - -First start a local EVM: - -```bash -npm run anvil:goerli -``` - -Then run the Foundry deployment script in a separate shell: - -```bash -npm run deploy:local -``` +Documents in the [script](./script/) folder. --- @@ -150,4 +108,6 @@ For any questions or feedback, you can send an email to [developer@benddao.xyz]( ## Licensing -The code is under the Business Source License 1.1, see [`LICENSE`](./LICENSE). +The primary license for BendDAO v2 is the Business Source License 1.1 (`BUSL-1.1`), see [`LICENSE`](./LICENSE). + +However, some files can also be licensed under `GPL-2.0-or-later` (as indicated in their SPDX headers). diff --git a/audits/2024-06-04-BendDAO-V2-VerilogSolutions-Report.pdf b/audits/2024-06-04-BendDAO-V2-VerilogSolutions-Report.pdf new file mode 100644 index 0000000..4e5fb0a Binary files /dev/null and b/audits/2024-06-04-BendDAO-V2-VerilogSolutions-Report.pdf differ diff --git a/audits/2024-09-03-BendDAO-V2-Code4rena-Report.md b/audits/2024-09-03-BendDAO-V2-Code4rena-Report.md new file mode 100644 index 0000000..0b45338 --- /dev/null +++ b/audits/2024-09-03-BendDAO-V2-Code4rena-Report.md @@ -0,0 +1 @@ +[Report URL](https://code4rena.com/reports/2024-07-benddao) diff --git a/config/eth-mainnet.json b/config/eth-mainnet.json index a288a7c..a32185f 100644 --- a/config/eth-mainnet.json +++ b/config/eth-mainnet.json @@ -1,13 +1,13 @@ { "chainId": 1, "rpcAlias": "mainnet", - "forkBlockNumber": 19369370, - "treasury": "0x472FcC65Fab565f75B1e0E861864A86FE5bcEd7B", + "forkBlockNumber": 20659780, + "treasury": "0xA14a73522D2f60D1ff96D52C5A8D1177940227a8", "wrappedNative": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "ProxyAdmin": "0x0000000000000000000000000000000000000000", - "AddressProvider": "0x0000000000000000000000000000000000000000", - "ACLAdmin": "0xe6b80f77a8B8FcD124aB748C720B7EAEA83dDb4C", - "ACLManager": "0x0000000000000000000000000000000000000000", - "PriceOracle": "0x0000000000000000000000000000000000000000", - "PoolManager": "0x0000000000000000000000000000000000000000" + "ProxyAdmin": "0x3b241a4338f0C3f67aFE0e130ccB653D0Ef3767C", + "AddressProvider": "0xa9Afc955d549D43DB056655b98FaB02870A45Fcd", + "ACLAdmin": "0x868964fa49a6fd6e116FE82c8f4165904406f479", + "ACLManager": "0xfc76aCB1C685fC427894652616feB4E303e4611a", + "PriceOracle": "0x3F8133A472C1d94Be4A562C44c337409c04Ae244", + "PoolManager": "0x0b870d974fB968B2E06798ABBD2563c80933D148" } diff --git a/release/abis/Configurator.json b/release/abis/Configurator.json index e9ff7ce..751867e 100644 --- a/release/abis/Configurator.json +++ b/release/abis/Configurator.json @@ -682,74 +682,6 @@ ], "anonymous": false }, - { - "type": "event", - "name": "AssetInterestBorrowDataUpdated", - "inputs": [ - { - "name": "poolId", - "type": "uint32", - "indexed": true, - "internalType": "uint32" - }, - { - "name": "asset", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "groupId", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "borrowRate", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "borrowIndex", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AssetInterestSupplyDataUpdated", - "inputs": [ - { - "name": "poolId", - "type": "uint32", - "indexed": true, - "internalType": "uint32" - }, - { - "name": "asset", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "supplyRate", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "supplyIndex", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, { "type": "event", "name": "Paused", diff --git a/release/abis/DefaultInterestRateModel.json b/release/abis/DefaultInterestRateModel.json new file mode 100644 index 0000000..17e0a7c --- /dev/null +++ b/release/abis/DefaultInterestRateModel.json @@ -0,0 +1,485 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "MAX_BORROW_RATE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_OPTIMAL_RATE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MIN_OPTIMAL_RATE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addressProvider", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IAddressProvider" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "calculateGroupBorrowRate", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "utilizationRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getBaseVariableBorrowRate", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getInterestRateParams", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct DefaultInterestRateModel.InterestRateParams", + "components": [ + { + "name": "optimalUtilizationRate", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "baseVariableBorrowRate", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "variableRateSlope1", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "variableRateSlope2", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMaxVariableBorrowRate", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOptimalUtilizationRate", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getVariableRateSlope1", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getVariableRateSlope2", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "provider", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setInterestRateParams", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "optimalRate", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "baseRate", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "slope1", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "slope2", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setInterestRateParams", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rateData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setInterestRateParams", + "inputs": [ + { + "name": "pool", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rateData", + "type": "tuple", + "internalType": "struct DefaultInterestRateModel.InterestRateParams", + "components": [ + { + "name": "optimalUtilizationRate", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "baseVariableBorrowRate", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "variableRateSlope1", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "variableRateSlope2", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "InterestRateParamsUpdate", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "indexed": true, + "internalType": "uint32" + }, + { + "name": "reserve", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "group", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "optimalUtilizationRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "baseVariableBorrowRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "variableRateSlope1", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "variableRateSlope2", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/release/abis/PoolLens.json b/release/abis/PoolLens.json index 712b9b5..a2366d3 100644 --- a/release/abis/PoolLens.json +++ b/release/abis/PoolLens.json @@ -1083,6 +1083,45 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getYieldStakerAssetData", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "staker", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "stakerCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "stakerBorrow", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "availableBorrow", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "isOperatorAuthorized", diff --git a/release/abis/PriceOracle.json b/release/abis/PriceOracle.json index fb5b45b..af7bdda 100644 --- a/release/abis/PriceOracle.json +++ b/release/abis/PriceOracle.json @@ -88,6 +88,25 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "assetOracleSourceTypes", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "bendNFTOracle", @@ -101,6 +120,19 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "bendTokenOracle", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IBendNFTOracle" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getAssetChainlinkAggregators", @@ -120,6 +152,25 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getAssetOracleSourceTypes", + "inputs": [ + { + "name": "assets", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [ + { + "name": "sourceTypes", + "type": "uint8[]", + "internalType": "uint8[]" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getAssetPrice", @@ -158,6 +209,25 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getAssetPriceFromBendTokenOracle", + "inputs": [ + { + "name": "asset", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getAssetPriceFromChainlink", @@ -190,6 +260,19 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getBendTokenOracle", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "initialize", @@ -254,6 +337,24 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "setAssetOracleSourceTypes", + "inputs": [ + { + "name": "assets", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "sourceTypes", + "type": "uint8[]", + "internalType": "uint8[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "setBendNFTOracle", @@ -267,6 +368,19 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "setBendTokenOracle", + "inputs": [ + { + "name": "bendTokenOracle_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "event", "name": "AssetAggregatorUpdated", @@ -286,6 +400,25 @@ ], "anonymous": false }, + { + "type": "event", + "name": "AssetOracleSourceTypeUpdated", + "inputs": [ + { + "name": "asset", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sourceType", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, { "type": "event", "name": "BendNFTOracleUpdated", @@ -299,6 +432,19 @@ ], "anonymous": false }, + { + "type": "event", + "name": "BendTokenOracleUpdated", + "inputs": [ + { + "name": "bendTokenOracle", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, { "type": "event", "name": "Initialized", diff --git a/release/abis/Yield.json b/release/abis/Yield.json index 15df997..594d196 100644 --- a/release/abis/Yield.json +++ b/release/abis/Yield.json @@ -78,6 +78,45 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getYieldStakerAssetData", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "staker", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "stakerCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "stakerBorrow", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "availableBorrow", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "moduleGitCommit", diff --git a/release/abis/YieldEthStakingEtherfi.json b/release/abis/YieldEthStakingEtherfi.json index 2e5c065..c7c4f05 100644 --- a/release/abis/YieldEthStakingEtherfi.json +++ b/release/abis/YieldEthStakingEtherfi.json @@ -487,7 +487,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" }, @@ -907,7 +907,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" }, @@ -1055,7 +1055,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" } @@ -1465,7 +1465,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "indexed": false, "internalType": "uint16" diff --git a/release/abis/YieldEthStakingLido.json b/release/abis/YieldEthStakingLido.json index f98f0e2..118ea2c 100644 --- a/release/abis/YieldEthStakingLido.json +++ b/release/abis/YieldEthStakingLido.json @@ -500,7 +500,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" }, @@ -912,7 +912,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" }, @@ -1060,7 +1060,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" } @@ -1483,7 +1483,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "indexed": false, "internalType": "uint16" diff --git a/release/abis/YieldSavingsDai.json b/release/abis/YieldSavingsDai.json index c868a79..b10ac38 100644 --- a/release/abis/YieldSavingsDai.json +++ b/release/abis/YieldSavingsDai.json @@ -465,7 +465,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" }, @@ -872,7 +872,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" }, @@ -1010,7 +1010,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "internalType": "uint16" } @@ -1430,7 +1430,7 @@ "internalType": "uint16" }, { - "name": "liquidationThreshold", + "name": "collateralFactor", "type": "uint16", "indexed": false, "internalType": "uint16" diff --git a/release/abis/YieldSavingsUSDS.json b/release/abis/YieldSavingsUSDS.json new file mode 100644 index 0000000..f816f92 --- /dev/null +++ b/release/abis/YieldSavingsUSDS.json @@ -0,0 +1,1541 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "accountYieldInWithdraws", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "accountYieldShares", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addressProvider", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IAddressProvider" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "batchRepay", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "batchStake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "borrowAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "batchUnstake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "batchUnstakeAndRepay", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "botAdmin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "claimedUnstakeFine", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "collectFeeToTreasury", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "convertToDebtAssets", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToDebtShares", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToYieldAssets", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToYieldShares", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "createYieldAccount", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAccountTotalUnstakedYield", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAccountTotalYield", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAccountYieldBalance", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftCollateralData", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "totalCollateral", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "totalBorrow", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "availabeBorrow", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftCollateralDataList", + "inputs": [ + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "totalCollaterals", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "totalBorrows", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "availabeBorrows", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftConfig", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "leverageFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftDebtInUnderlyingAsset", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftStakeData", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "state", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "debtAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "yieldAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftStakeDataList", + "inputs": [ + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "poolIds", + "type": "uint32[]", + "internalType": "uint32[]" + }, + { + "name": "states", + "type": "uint8[]", + "internalType": "uint8[]" + }, + { + "name": "debtAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "yieldAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftUnstakeData", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawReqId", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftUnstakeDataList", + "inputs": [ + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "unstakeFines", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "withdrawAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "withdrawReqIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftValueInUnderlyingAsset", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftYieldInUnderlyingAsset", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "yieldAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "yieldValue", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftYieldStakeDataStruct", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct YieldStakingBase.YieldStakeData", + "components": [ + { + "name": "yieldAccount", + "type": "address", + "internalType": "address" + }, + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "state", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "debtShare", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "yieldShare", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawReqId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "remainYieldAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTotalDebt", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTotalUnstakeFine", + "inputs": [], + "outputs": [ + { + "name": "totalFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "claimedFine", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getYieldAccount", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "addressProvider_", + "type": "address", + "internalType": "address" + }, + { + "name": "usds_", + "type": "address", + "internalType": "address" + }, + { + "name": "susds_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nftConfigs", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "leverageFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "poolManager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IPoolManager" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "poolYield", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IYield" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "repay", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setBotAdmin", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setNftActive", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "active", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setNftStakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "leverageFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setNftUnstakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setPause", + "inputs": [ + { + "name": "paused", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "stake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "borrowAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "stakeDatas", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "yieldAccount", + "type": "address", + "internalType": "address" + }, + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "state", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "debtShare", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "yieldShare", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawReqId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "remainYieldAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "susds", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ISavingsUSDS" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalDebtShare", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalUnstakeFine", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "underlyingAsset", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20Metadata" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "unstake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unstakeAndRepay", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "usds", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20Metadata" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "yieldAccounts", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "yieldRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IYieldRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "CollectFeeToTreasury", + "inputs": [ + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amountToCollect", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Paused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Repay", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RepayPart", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetBotAdmin", + "inputs": [ + { + "name": "oldAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetNftActive", + "inputs": [ + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "isActive", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetNftStakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "leverageFactor", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetNftUnstakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Stake", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unpaused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unstake", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/release/abis/YieldWUSDStaking.json b/release/abis/YieldWUSDStaking.json new file mode 100644 index 0000000..59598d6 --- /dev/null +++ b/release/abis/YieldWUSDStaking.json @@ -0,0 +1,1461 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "SECONDS_OF_YEAR", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint48", + "internalType": "uint48" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addressProvider", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IAddressProvider" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "batchRepay", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "batchStake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "borrowAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "wusdStakingPoolId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "batchUnstake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "botAdmin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "claimedUnstakeFine", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "collectFeeToTreasury", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "convertToDebtAssets", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToDebtShares", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "createYieldAccount", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getNftCollateralData", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "totalCollateral", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "totalBorrow", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "availabeBorrow", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftCollateralDataList", + "inputs": [ + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "totalCollaterals", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "totalBorrows", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "availabeBorrows", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftConfig", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "leverageFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftDebtInUnderlyingAsset", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftStakeData", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "state", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "debtAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "yieldAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftStakeDataList", + "inputs": [ + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "poolIds", + "type": "uint32[]", + "internalType": "uint32[]" + }, + { + "name": "states", + "type": "uint8[]", + "internalType": "uint8[]" + }, + { + "name": "debtAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "yieldAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftUnstakeData", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawReqId", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftUnstakeDataList", + "inputs": [ + { + "name": "nfts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokenIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "unstakeFines", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "withdrawAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "withdrawReqIds", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftValueInUnderlyingAsset", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftYieldInUnderlyingAsset", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "yieldAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "yieldValue", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNftYieldStakeDataStruct", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct YieldWUSDStaking.YieldStakeData", + "components": [ + { + "name": "yieldAccount", + "type": "address", + "internalType": "address" + }, + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "state", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "debtShare", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "wusdStakingPoolId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "stakingPlanId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "remainYieldAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTotalDebt", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTotalUnstakeFine", + "inputs": [], + "outputs": [ + { + "name": "totalFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "claimedFine", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getWUSDStakingPlan", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IWUSDStaking.StakingPlan", + "components": [ + { + "name": "stakingPoolId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "stakedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "apy", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "startTime", + "type": "uint48", + "internalType": "uint48" + }, + { + "name": "endTime", + "type": "uint48", + "internalType": "uint48" + }, + { + "name": "claimableTimestamp", + "type": "uint48", + "internalType": "uint48" + }, + { + "name": "yield", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "stakingStatus", + "type": "uint8", + "internalType": "enum IWUSDStaking.StakingStatus" + }, + { + "name": "claimType", + "type": "uint8", + "internalType": "enum IWUSDStaking.ClaimType" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getWUSDStakingPools", + "inputs": [], + "outputs": [ + { + "name": "stakingPoolIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "stakingPeriods", + "type": "uint48[]", + "internalType": "uint48[]" + }, + { + "name": "apys", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "minStakingAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getYieldAccount", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "addressProvider_", + "type": "address", + "internalType": "address" + }, + { + "name": "wusd_", + "type": "address", + "internalType": "address" + }, + { + "name": "wusdStaking_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nftConfigs", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "leverageFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "poolManager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IPoolManager" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "poolYield", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IYield" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "repay", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setBotAdmin", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setNftActive", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "active", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setNftStakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "leverageFactor", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setNftUnstakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setPause", + "inputs": [ + { + "name": "paused", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "stake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "borrowAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "wusdStakingPoolId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "stakeDatas", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "yieldAccount", + "type": "address", + "internalType": "address" + }, + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "state", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "debtShare", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "wusdStakingPoolId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "stakingPlanId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "remainYieldAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalDebtShare", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalUnstakeFine", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "underlyingAsset", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20Metadata" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "unstake", + "inputs": [ + { + "name": "poolId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nft", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "unstakeFine", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "wusdStaking", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IWUSDStaking" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "yieldAccounts", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "yieldRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IYieldRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "CollectFeeToTreasury", + "inputs": [ + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amountToCollect", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Paused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Repay", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RepayPart", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetBotAdmin", + "inputs": [ + { + "name": "oldAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetNftActive", + "inputs": [ + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "isActive", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetNftStakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "leverageFactor", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + }, + { + "name": "collateralFactor", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetNftUnstakeParams", + "inputs": [ + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "maxUnstakeFine", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "unstakeHeathFactor", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Stake", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unpaused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unstake", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nft", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/release/deployments/mainnet.json b/release/deployments/mainnet.json new file mode 100644 index 0000000..1d30b14 --- /dev/null +++ b/release/deployments/mainnet.json @@ -0,0 +1,34 @@ +{ + "Tokens": {}, + "AddressProvider": "0xa9Afc955d549D43DB056655b98FaB02870A45Fcd", + "ACLManager": "0xfc76aCB1C685fC427894652616feB4E303e4611a", + "PriceOracle": "0x3F8133A472C1d94Be4A562C44c337409c04Ae244", + "Oracles" : { + "SDAIPriceAdapter": "0x1432A03715dC2491B9cDE3A0dcbea4de4F0DedBf", + "EETHPriceAdapter": "0xA9e3696544dA3B41C774115c0F7Ef6e97436F5DB", + "SUSDSPriceAdapter": "0x510F816D2bCf423DF45f57026eCAE80fFdF777A5" + }, + "PoolManager": "0x0b870d974fB968B2E06798ABBD2563c80933D148", + "DefaultInterestRateModel": "0x209759aBCB4d2eA1B0Fc285042C2BCdbfd124123", + "Modules": { + "Installer": "0x1f71c54EDf2D4cf3C856987e8d3A7962F5308fea", + "Configurator": "0xAB3AB751136c4E6af6f2Bbd894A822DEB7A67c2f", + "BVault": "0x34659656E021C9b0730bD2a576B8536D9929A5eC", + "PoolLens": "0x4643f7791aC622863503d8D3e62942FcfF592A91", + "FlashLoan": "0xae23C7BFfd96B77667bf63Ff3e294c1335865fBC", + "Yield": "0x63e6BE62bE4d60017360EEE1E451eBc70bE40861", + "ConfiguratorPool": "0x780fE377c80b242F8Cabeb43aa73913178685710", + "CrossLending": "0xE3AD267696758Ec9882525C43b20171A98535c49", + "CrossLiquidation": "0x4e208A8D078836c68A0cC77E94f261829A5c5773", + "IsolateLending": "0xAF488A0ff6D54d6E457cfD5cEcCe49646d01FBf5", + "IsolateLiquidation": "0x94e044321fF98DDCC4060d362aFdde3Ceb6C05a4", + "UIPoolLens": "0x222bB44602746C18006E857E81F36899B86Afb87" + }, + "YieldRegistry": "0x8e72a9ea6a6d99Cbb5E50343BB5D741156C01154", + "YieldEthStakingLido": "0x61Ae6DCE4C7Cb1b8165aE244c734f20DF56efd73", + "YieldEthStakingEtherfi": "0x529a8822416c3c4ED1B77dE570118fDf1d474639", + "YieldSavingsDai": "0x6FA43C1a296db746937Ac4D97Ff61409E8c530cC", + "BendV1Migration": "0xf6EE27bb3F17E456078711D8c4b257377375D654", + "YieldSavingsUSDS": "0x0684c5ca33f3C9aD5BB405f91D4200B776Af477B", + "YieldWUSDStaking": "0x8C119f5D51209E6b5C508F90d23E8F3069a2DDBD" +} diff --git a/release/deployments/sepolia.json b/release/deployments/sepolia.json index 49b7ce2..9e360af 100644 --- a/release/deployments/sepolia.json +++ b/release/deployments/sepolia.json @@ -4,17 +4,20 @@ "ACLManager": "0xe4aE7754b99b470EedEfE7C403c88eE8e5062C59", "PriceOracle": "0xAc4177f05F2E57d9836Fe895b816a03CDE377905", "Oracles" : { - "SDAIPriceAdapter": "0x955018DdE44C05409096399BbB9dc39aF432646d" + "SDAIPriceAdapter": "0x955018DdE44C05409096399BbB9dc39aF432646d", + "EETHPriceAdapter": "0x1Ba0AaEC85481a08878e8Ca30ae117532e45dc06", + "SUSDSPriceAdapter": "0x4b953a2FDf836643718E34F8BB62f67a4AdA5F88" }, "PoolManager": "0xE16d9FB95eed698ee2384050a2C15BF1DdBA3E6C", + "DefaultInterestRateModel": "0x3a6a42c547B264AB3717aD127e5f07960Fe21306", "Modules": { "Installer": "0x7784861E78A4836fD21aE43135D05AbcCb5492CE", "Configurator": "0x0C29eB90EbD26F5db19D63CaB50e2D9F12e66DA0", - "ConfiguratorPool": "", "BVault": "0xc14Fc51A8F5eaB88AcaeFa887DBFB1BA57ad537e", "PoolLens": "0xd36a2B9495df19D378198908398F43D004A048EA", "FlashLoan": "0x42411426999dC2b91Cc18B6d558c5C9812DfD31b", "Yield": "0x8c071148Ab197a188D5adf2d49a51aD297425249", + "ConfiguratorPool": "0x4d5EB7413a5A9039519e133Be29fdd3B7AA269f8", "CrossLending": "0x6c5Cea4C4D71a95fD0970D65f17B4d132702D773", "CrossLiquidation": "0xbc893f39458552B694378fd2d205C4191A497786", "IsolateLending": "0x136eFb46b6c1C21c063159631f95fF6C97B01742", @@ -25,5 +28,7 @@ "YieldEthStakingLido": "0x59303f797B8Dd80fc3743047df63C76E44Ca7CBd", "YieldEthStakingEtherfi": "0x3234F1047E71421Ec67A576D87eaEe1B86E8A1Ea", "YieldSavingsDai": "0x7464a51fA6338A34b694b4bF4A152781fb2C4B70", - "BendV1Migration": "0x989c290B431DA780C3Fce9640488E7967C1bAB84" + "BendV1Migration": "0x989c290B431DA780C3Fce9640488E7967C1bAB84", + "YieldSavingsUSDS": "0x63d56158751A75493B4b5fEdc46A29ba6a68cc15", + "YieldWUSDStaking": "0x86FF757587515bbD5C708170a15b4235EaCa284C" } diff --git a/script/DeployContract.s.sol b/script/DeployContract.s.sol index 2fab6fe..42c78e0 100644 --- a/script/DeployContract.s.sol +++ b/script/DeployContract.s.sol @@ -34,7 +34,7 @@ contract DeployContract is DeployBase { uint256 chainId = config.getChainId(); if (chainId == 1) { // mainnet - revert('not support'); + v1AddressProvider = 0x24451F47CaF13B24f4b5034e1dF6c0E401ec0e46; } else if (chainId == 11155111) { // sepolia v1AddressProvider = 0x95e84AED75EB9A545D817c391A0011E0B34EAf5C; @@ -50,6 +50,7 @@ contract DeployContract is DeployBase { abi.encodeWithSelector(v1MigrationImpl.initialize.selector, address(addressProvider_), v1AddressProvider) ); BendV1Migration v1Migration = BendV1Migration((address(v1MigrationProxy))); + console.log('BendV1Migration:', address(v1Migration)); return address(v1Migration); } diff --git a/script/DeployPoolFull.s.sol b/script/DeployPoolFull.s.sol index 437e875..9d7795d 100644 --- a/script/DeployPoolFull.s.sol +++ b/script/DeployPoolFull.s.sol @@ -24,6 +24,8 @@ import {FlashLoan} from 'src/modules/FlashLoan.sol'; import {PoolLens} from 'src/modules/PoolLens.sol'; import {UIPoolLens} from 'src/modules/UIPoolLens.sol'; +import {DefaultInterestRateModel} from 'src/irm/DefaultInterestRateModel.sol'; + import {Configured, ConfigLib, Config} from 'config/Configured.sol'; import {DeployBase} from './DeployBase.s.sol'; @@ -33,6 +35,23 @@ contract DeployPoolFull is DeployBase { using ConfigLib for Config; function _deploy() internal virtual override { + // _deployPoolFull(); + + _deployPoolPart(); + } + + function _deployPoolPart() internal { + address proxyAdmin_ = config.getProxyAdmin(); + require(proxyAdmin_ != address(0), 'ProxyAdmin not exist in config'); + + address addressProvider_ = config.getAddressProvider(); + require(addressProvider_ != address(0), 'AddressProvider not exist in config'); + + address defaultIrm_ = _deployDefaultIRM(proxyAdmin_, addressProvider_); + console.log('DefaultIrm:', defaultIrm_); + } + + function _deployPoolFull() internal { address proxyAdmin_ = _deployProxyAdmin(); console.log('ProxyAdmin:', proxyAdmin_); @@ -47,6 +66,9 @@ contract DeployPoolFull is DeployBase { address poolManager_ = _deployPoolManager(addressProvider_); console.log('PoolManager:', poolManager_); + + address defaultIrm_ = _deployDefaultIRM(proxyAdmin_, addressProvider_); + console.log('DefaultIrm:', defaultIrm_); } function _deployProxyAdmin() internal returns (address) { @@ -131,6 +153,18 @@ contract DeployPoolFull is DeployBase { return address(priceOracle); } + function _deployDefaultIRM(address proxyAdmin_, address addressProvider_) internal returns (address) { + DefaultInterestRateModel defaultIrmImpl = new DefaultInterestRateModel(); + TransparentUpgradeableProxy defaultIrmProxy = new TransparentUpgradeableProxy( + address(defaultIrmImpl), + address(proxyAdmin_), + abi.encodeWithSelector(defaultIrmImpl.initialize.selector, address(addressProvider_)) + ); + DefaultInterestRateModel defaultIrm = DefaultInterestRateModel(address(defaultIrmProxy)); + + return address(defaultIrm); + } + struct DeployLocalVars { address addressInCfg; address[] modules; diff --git a/script/DeployPriceAdapter.s.sol b/script/DeployPriceAdapter.s.sol index 9985070..12b9e69 100644 --- a/script/DeployPriceAdapter.s.sol +++ b/script/DeployPriceAdapter.s.sol @@ -9,6 +9,8 @@ import {IAddressProvider} from 'src/interfaces/IAddressProvider.sol'; import {Constants} from 'src/libraries/helpers/Constants.sol'; import {SDAIPriceAdapter} from 'src/oracles/SDAIPriceAdapter.sol'; +import {EETHPriceAdapter} from 'src/oracles/EETHPriceAdapter.sol'; +import {SUSDSPriceAdapter} from 'src/oracles/SUSDSPriceAdapter.sol'; import {Configured, ConfigLib, Config} from 'config/Configured.sol'; import {DeployBase} from './DeployBase.s.sol'; @@ -25,7 +27,11 @@ contract DeployPriceAdapter is DeployBase { address addrProviderInCfg = config.getAddressProvider(); require(addrProviderInCfg != address(0), 'AddressProvider not exist in config'); - _deploySDAIPriceAdapter(proxyAdminInCfg, addrProviderInCfg); + // _deploySDAIPriceAdapter(proxyAdminInCfg, addrProviderInCfg); + + // _deployEETHPriceAdapter(proxyAdminInCfg, addrProviderInCfg); + + // _deployUSDSPriceAdapter(proxyAdminInCfg, addrProviderInCfg); } function _deploySDAIPriceAdapter(address /*proxyAdmin_*/, address /*addressProvider_*/) internal returns (address) { @@ -35,7 +41,8 @@ contract DeployPriceAdapter is DeployBase { uint256 chainId = config.getChainId(); if (chainId == 1) { // mainnet - revert('not support'); + daiAgg = 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; + ratePot = 0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7; } else if (chainId == 11155111) { // sepolia daiAgg = 0x14866185B1962B63C3Ea9E03Bc1da838bab34C19; @@ -45,7 +52,62 @@ contract DeployPriceAdapter is DeployBase { } SDAIPriceAdapter sdaiAdapter = new SDAIPriceAdapter(daiAgg, ratePot, 'sDAI / USD'); + console.log('SDAIPriceAdapter:', address(sdaiAdapter)); return address(sdaiAdapter); } + + function _deployEETHPriceAdapter(address /*proxyAdmin_*/, address /*addressProvider_*/) internal returns (address) { + address ethAggAddress = address(0); + address weETHAggAddress = address(0); + address weETHAddress = address(0); + + uint256 chainId = config.getChainId(); + if (chainId == 1) { + // mainnet + ethAggAddress = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; + weETHAggAddress = 0x5c9C449BbC9a6075A2c061dF312a35fd1E05fF22; + weETHAddress = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; + } else if (chainId == 11155111) { + // sepolia + ethAggAddress = 0x694AA1769357215DE4FAC081bf1f309aDC325306; + weETHAggAddress = 0x2CFb337f5699c1419AE0dfe2940F628FEF7FC682; + weETHAddress = 0xb944C510F81553E39F8Db2A89e3f8007A910827f; + } else { + revert('not support'); + } + + EETHPriceAdapter eEthAdapter = new EETHPriceAdapter( + ethAggAddress, + weETHAggAddress, + weETHAddress, + 'eETH / weETH / ETH / USD' + ); + console.log('EETHPriceAdapter:', address(eEthAdapter)); + + return address(eEthAdapter); + } + + function _deployUSDSPriceAdapter(address /*proxyAdmin_*/, address /*addressProvider_*/) internal returns (address) { + address usdsAgg = address(0); + address rateProvider = address(0); + + uint256 chainId = config.getChainId(); + if (chainId == 1) { + // mainnet + usdsAgg = 0xfF30586cD0F29eD462364C7e81375FC0C71219b1; + rateProvider = 0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD; + } else if (chainId == 11155111) { + // sepolia + usdsAgg = 0x7354784A7A705963b484Fed3a903FF8c9b6Ec617; + rateProvider = 0xf8B05Dab36ea0492E4e358FeF83a608e4297b3D4; + } else { + revert('not support'); + } + + SUSDSPriceAdapter susdsAdapter = new SUSDSPriceAdapter(usdsAgg, rateProvider, 'sUSDS / USD'); + console.log('SUSDSPriceAdapter:', address(susdsAdapter)); + + return address(susdsAdapter); + } } diff --git a/script/DeployYieldMock.s.sol b/script/DeployYieldMock.s.sol index 522b2dc..0fce389 100644 --- a/script/DeployYieldMock.s.sol +++ b/script/DeployYieldMock.s.sol @@ -11,9 +11,12 @@ import {MockUnstETH} from 'test/mocks/MockUnstETH.sol'; import {MockeETH} from 'test/mocks/MockeETH.sol'; import {MockEtherfiWithdrawRequestNFT} from 'test/mocks/MockEtherfiWithdrawRequestNFT.sol'; import {MockEtherfiLiquidityPool} from 'test/mocks/MockEtherfiLiquidityPool.sol'; +import {MockWeETH} from 'test/mocks/MockWeETH.sol'; import {MockSDAI} from 'test/mocks/MockSDAI.sol'; import {MockDAIPot} from 'test/mocks/MockDAIPot.sol'; +import {MockSUSDS} from 'test/mocks/MockSUSDS.sol'; +import {MockWUSDStaking} from 'test/mocks/MockWUSDStaking.sol'; import {Configured, ConfigLib, Config} from 'config/Configured.sol'; import {DeployBase} from './DeployBase.s.sol'; @@ -24,11 +27,11 @@ contract DeployYieldMock is DeployBase { using ConfigLib for Config; function _deploy() internal virtual override { - //_deployMockLido(); - - //_deployMockEtherfi(); - - _deployMockSDai(); + // _deployMockLido(); + // _deployMockEtherfi(); + // _deployMockSDai(); + // _deployMockSUSDS(); + // _deployMockWUSD(); } function _deployMockLido() internal { @@ -45,6 +48,9 @@ contract DeployYieldMock is DeployBase { eETH.setLiquidityPool(address(pool)); nft.setLiquidityPool(address(pool), address(eETH)); + + //MockWeETH weETH = new MockWeETH(address(pool), address(eETH)); + new MockWeETH(address(pool), address(eETH)); } function _deployMockSDai() internal { @@ -57,4 +63,20 @@ contract DeployYieldMock is DeployBase { new MockSDAI(address(dai)); } + + function _deployMockSUSDS() internal { + // USDS should be same with pool lending + // MockERC20 usds = new MockERC20('USDS Stablecoin', 'USDS', 18); + MockERC20 usds = MockERC20(0x99f5A9506504BB96d0019538608090015BA9EBDd); + + new MockSUSDS(address(usds)); + } + + function _deployMockWUSD() internal { + // USDS should be same with pool lending + // MockERC20 wusd = new MockERC20('WUSD Stablecoin', 'WUSD', 18); + MockERC20 wusd = MockERC20(0xdf98BFe3CDF4CA3C0a9F1dE2e34e6D9E049E2952); + + new MockWUSDStaking(address(wusd)); + } } diff --git a/script/DeployYieldStaking.s.sol b/script/DeployYieldStaking.s.sol index e1e6d10..1dfbad5 100644 --- a/script/DeployYieldStaking.s.sol +++ b/script/DeployYieldStaking.s.sol @@ -11,6 +11,8 @@ import {Constants} from 'src/libraries/helpers/Constants.sol'; import {YieldEthStakingLido} from 'src/yield/lido/YieldEthStakingLido.sol'; import {YieldEthStakingEtherfi} from 'src/yield/etherfi/YieldEthStakingEtherfi.sol'; import {YieldSavingsDai} from 'src/yield/sdai/YieldSavingsDai.sol'; +import {YieldSavingsUSDS} from 'src/yield/susds/YieldSavingsUSDS.sol'; +import {YieldWUSDStaking} from 'src/yield/wusd/YieldWUSDStaking.sol'; import {YieldRegistry} from 'src/yield/YieldRegistry.sol'; import {YieldAccount} from 'src/yield/YieldAccount.sol'; @@ -30,13 +32,17 @@ contract DeployYieldStaking is DeployBase { address addrProviderInCfg = config.getAddressProvider(); require(addrProviderInCfg != address(0), 'AddressProvider not exist in config'); - //_deployYieldRegistry(proxyAdminInCfg, addrProviderInCfg); + // _deployYieldRegistry(proxyAdminInCfg, addrProviderInCfg); - _deployYieldEthStakingLido(proxyAdminInCfg, addrProviderInCfg); + // _deployYieldEthStakingLido(proxyAdminInCfg, addrProviderInCfg); - _deployYieldEthStakingEtherfi(proxyAdminInCfg, addrProviderInCfg); + // _deployYieldEthStakingEtherfi(proxyAdminInCfg, addrProviderInCfg); - _deployYieldSavingsDai(proxyAdminInCfg, addrProviderInCfg); + // _deployYieldSavingsDai(proxyAdminInCfg, addrProviderInCfg); + + // _deployYieldSavingsUSDS(proxyAdminInCfg, addrProviderInCfg); + + // _deployYieldWUSDStaking(proxyAdminInCfg, addrProviderInCfg); } function _deployYieldRegistry(address proxyAdmin_, address addressProvider_) internal returns (address) { @@ -48,6 +54,7 @@ contract DeployYieldStaking is DeployBase { abi.encodeWithSelector(yieldRegistryImpl.initialize.selector, address(addressProvider_)) ); YieldRegistry yieldRegistry = YieldRegistry(address(yieldRegistryProxy)); + console.log('yieldRegistry:', address(yieldRegistry)); IAddressProvider(addressProvider_).setYieldRegistry(address(yieldRegistry)); @@ -66,7 +73,9 @@ contract DeployYieldStaking is DeployBase { uint256 chainId = config.getChainId(); if (chainId == 1) { // mainnet - revert('not support'); + weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + stETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + unstETH = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; } else if (chainId == 11155111) { // sepolia weth = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; @@ -84,6 +93,7 @@ contract DeployYieldStaking is DeployBase { abi.encodeWithSelector(yieldLidoImpl.initialize.selector, address(addressProvider_), weth, stETH, unstETH) ); YieldEthStakingLido yieldLido = YieldEthStakingLido(payable(yieldLidoProxy)); + console.log('YieldEthStakingLido:', address(yieldLido)); return address(yieldLido); } @@ -95,7 +105,8 @@ contract DeployYieldStaking is DeployBase { uint256 chainId = config.getChainId(); if (chainId == 1) { // mainnet - revert('not support'); + weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + etherfiPool = 0x308861A430be4cce5502d0A12724771Fc6DaF216; } else if (chainId == 11155111) { // sepolia weth = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; @@ -112,6 +123,7 @@ contract DeployYieldStaking is DeployBase { abi.encodeWithSelector(yieldEtherfiImpl.initialize.selector, address(addressProvider_), weth, etherfiPool) ); YieldEthStakingEtherfi yieldEtherfi = YieldEthStakingEtherfi(payable(yieldEtherfiProxy)); + console.log('YieldEthStakingEtherfi:', address(yieldEtherfi)); return address(yieldEtherfi); } @@ -123,7 +135,8 @@ contract DeployYieldStaking is DeployBase { uint256 chainId = config.getChainId(); if (chainId == 1) { // mainnet - revert('not support'); + dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + sdai = 0x83F20F44975D03b1b09e64809B757c47f942BEeA; } else if (chainId == 11155111) { // sepolia dai = 0xf9a88B0cc31f248c89F063C2928fA10e5A029B88; @@ -140,7 +153,68 @@ contract DeployYieldStaking is DeployBase { abi.encodeWithSelector(yieldSDaiImpl.initialize.selector, address(addressProvider_), dai, sdai) ); YieldSavingsDai yieldSDai = YieldSavingsDai(payable(yieldSDaiProxy)); + console.log('YieldSavingsDai:', address(yieldSDai)); return address(yieldSDai); } + + function _deployYieldSavingsUSDS(address proxyAdmin_, address addressProvider_) internal returns (address) { + address usds = address(0); + address susds = address(0); + + uint256 chainId = config.getChainId(); + if (chainId == 1) { + // mainnet + usds = 0xdC035D45d973E3EC169d2276DDab16f1e407384F; + susds = 0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD; + } else if (chainId == 11155111) { + // sepolia + usds = 0x99f5A9506504BB96d0019538608090015BA9EBDd; + susds = 0xf8B05Dab36ea0492E4e358FeF83a608e4297b3D4; + } else { + revert('not support'); + } + + YieldSavingsUSDS yieldSUSDSImpl = new YieldSavingsUSDS(); + + TransparentUpgradeableProxy yieldSUSDSProxy = new TransparentUpgradeableProxy( + address(yieldSUSDSImpl), + address(proxyAdmin_), + abi.encodeWithSelector(yieldSUSDSImpl.initialize.selector, address(addressProvider_), usds, susds) + ); + YieldSavingsUSDS yieldSUSDS = YieldSavingsUSDS(payable(yieldSUSDSProxy)); + console.log('YieldSavingsUSDS:', address(yieldSUSDS)); + + return address(yieldSUSDS); + } + + function _deployYieldWUSDStaking(address proxyAdmin_, address addressProvider_) internal returns (address) { + address wusd = address(0); + address wusdStaking = address(0); + + uint256 chainId = config.getChainId(); + if (chainId == 1) { + // mainnet + wusd = 0xb6667b04Cb61Aa16B59617f90FFA068722Cf21dA; + wusdStaking = 0x338b1646956854A27dbA6dF6B8a3D38949EEBc7f; + } else if (chainId == 11155111) { + // sepolia + wusd = 0xdf98BFe3CDF4CA3C0a9F1dE2e34e6D9E049E2952; + wusdStaking = 0x5d8a096A6E0983DD64157bBbB5ce721002D5861A; + } else { + revert('not support'); + } + + YieldWUSDStaking yieldImpl = new YieldWUSDStaking(); + + TransparentUpgradeableProxy yieldProxy = new TransparentUpgradeableProxy( + address(yieldImpl), + address(proxyAdmin_), + abi.encodeWithSelector(yieldImpl.initialize.selector, address(addressProvider_), wusd, wusdStaking) + ); + YieldWUSDStaking yield = YieldWUSDStaking(payable(yieldProxy)); + console.log('YieldWUSDStaking:', address(yield)); + + return address(yield); + } } diff --git a/script/InitConfigPool.s.sol b/script/InitConfigPool.s.sol index 8b7af5e..75dd0f7 100644 --- a/script/InitConfigPool.s.sol +++ b/script/InitConfigPool.s.sol @@ -25,6 +25,7 @@ contract InitConfigPool is DeployBase { address internal addrUSDT; address internal addrUSDC; address internal addrDAI; + address internal addrUSDS; address internal addrWPUNK; address internal addrBAYC; @@ -38,6 +39,12 @@ contract InitConfigPool is DeployBase { address internal addrMOONBIRD; address internal addrCloneX; + address internal addrNFTOracle; + address internal addrCLAggWETH; + address internal addrCLAggUSDT; + address internal addrCLAggUSDC; + address internal addrCLAggDAI; + AddressProvider internal addressProvider; PriceOracle internal priceOracle; ConfiguratorPool internal configuratorPool; @@ -55,11 +62,46 @@ contract InitConfigPool is DeployBase { uint8 internal constant highRateGroupId = 3; function _deploy() internal virtual override { - if (block.chainid == 11155111) { + if (block.chainid == 1) { + // mainnet + addrWETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + addrUSDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + addrUSDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + addrDAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + addrUSDS = 0xdC035D45d973E3EC169d2276DDab16f1e407384F; + + addrWPUNK = 0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6; + addrBAYC = 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D; + addrStBAYC = 0x08f5F0126aF89B4fD5499E942891D904A027624B; + addrMAYC = 0x60E4d786628Fea6478F785A6d7e704777c86a7c6; + addrStMAYC = 0xc1ED28E4b4d8e284A41E7474CA5522b010f3A64F; + addrPPG = 0xBd3531dA5CF5857e7CfAA92426877b022e612cf8; + addrAZUKI = 0xED5AF388653567Af2F388E6224dC7C4b3241C544; + addrMIL = 0x5Af0D9827E0c53E4799BB226655A1de152A425a5; + addrDOODLE = 0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e; + addrMOONBIRD = 0x23581767a106ae21c074b2276D25e5C3e136a68b; + addrCloneX = 0x49cF6f5d44E70224e2E23fDcdd2C053F30aDA28B; + + addrNFTOracle = 0x7C2A19e54e48718f6C60908a9Cff3396E4Ea1eBA; + addrCLAggWETH = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; + addrCLAggUSDT = 0x3E7d1eAB13ad0104d2750B8863b489D65364e32D; + addrCLAggUSDC = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; + addrCLAggDAI = 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; + + commonPoolId = 1; + + irmDefault = DefaultInterestRateModel(0x209759aBCB4d2eA1B0Fc285042C2BCdbfd124123); + irmYield = irmDefault; + irmLow = irmDefault; + irmMiddle = irmDefault; + irmHigh = irmDefault; + } else if (block.chainid == 11155111) { + // sepolia addrWETH = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; addrUSDT = 0x53cEd787Ba91B4f872b961914faE013ccF8b0236; addrUSDC = 0xC188f878304F37e7199dFBd114e2Af68D043d98c; addrDAI = 0xf9a88B0cc31f248c89F063C2928fA10e5A029B88; + addrUSDS = 0x99f5A9506504BB96d0019538608090015BA9EBDd; addrWPUNK = 0x647dc527Bd7dFEE4DD468cE6fC62FC50fa42BD8b; addrBAYC = 0xE15A78992dd4a9d6833eA7C9643650d3b0a2eD2B; @@ -73,9 +115,15 @@ contract InitConfigPool is DeployBase { addrMOONBIRD = 0x4e3A064eF42DD916347751DfA7Ca1dcbA49d3DA8; addrCloneX = 0x3BD0A71D39E67fc49D5A6645550f2bc95F5cb398; + addrNFTOracle = 0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A; + addrCLAggWETH = 0x694AA1769357215DE4FAC081bf1f309aDC325306; + addrCLAggUSDT = 0x382c1856D25CbB835D4C1d732EB69f3e0d9Ba104; + addrCLAggUSDC = 0xA2F78ab2355fe2f984D808B5CeE7FD0A93D5270E; + addrCLAggDAI = 0x14866185B1962B63C3Ea9E03Bc1da838bab34C19; + commonPoolId = 1; - irmDefault = DefaultInterestRateModel(0x10988B9c7e7048B83D590b14F0167FDe56728Ae9); + irmDefault = DefaultInterestRateModel(0x3a6a42c547B264AB3717aD127e5f07960Fe21306); irmYield = irmDefault; irmLow = irmDefault; irmMiddle = irmDefault; @@ -91,145 +139,279 @@ contract InitConfigPool is DeployBase { configuratorPool = ConfiguratorPool(addressProvider.getPoolModuleProxy(Constants.MODULEID__CONFIGURATOR_POOL)); configurator = Configurator(addressProvider.getPoolModuleProxy(Constants.MODULEID__CONFIGURATOR)); - //initInterestRateModels(); - - //setPoolInterestRateModels(1); - //setPoolInterestRateModels(2); - //setPoolInterestRateModels(3); - - //initOralces(); + // initCommonPools(); - //initCommonPools(); + // listingUSDS(commonPoolId); - //initPunksPools(); + // initInterestRateModels(1); + // initInterestRateModels(2); + // initInterestRateModels(3); - //initStableCoinPools(); - - //setFlashLoan(1); - - configurator.setAssetClassGroup(1, address(addrWPUNK), middleRateGroupId); - configurator.setAssetClassGroup(1, address(addrBAYC), middleRateGroupId); + // setPoolInterestRateModels(1); + // setPoolInterestRateModels(2); + // setPoolInterestRateModels(3); } - function initOralces() internal { - priceOracle.setBendNFTOracle(0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A); - - address[] memory assets = new address[](3); - assets[0] = address(addrWETH); - assets[1] = address(addrUSDT); - assets[2] = address(addrDAI); - address[] memory aggs = new address[](3); - aggs[0] = address(0x694AA1769357215DE4FAC081bf1f309aDC325306); - aggs[1] = address(0x382c1856D25CbB835D4C1d732EB69f3e0d9Ba104); - aggs[2] = address(0x14866185B1962B63C3Ea9E03Bc1da838bab34C19); - priceOracle.setAssetChainlinkAggregators(assets, aggs); - } - - function initInterestRateModels() internal { - // Interest Rate Model - irmDefault = new DefaultInterestRateModel(address(addressProvider)); - - // WETH + function listingUSDS(uint32 poolId) internal { irmDefault.setInterestRateParams( - addrWETH, + poolId, + addrUSDS, yieldRateGroupId, - (65 * WadRayMath.RAY) / 100, - (1 * WadRayMath.RAY) / 100, // baseRate + (75 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100, // baseRate (1 * WadRayMath.RAY) / 100, (20 * WadRayMath.RAY) / 100 ); irmDefault.setInterestRateParams( - addrWETH, + poolId, + addrUSDS, lowRateGroupId, - (65 * WadRayMath.RAY) / 100, - (5 * WadRayMath.RAY) / 100, // baseRate - (5 * WadRayMath.RAY) / 100, - (100 * WadRayMath.RAY) / 100 + (75 * WadRayMath.RAY) / 100, + (6 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 ); irmDefault.setInterestRateParams( - addrWETH, + poolId, + addrUSDS, middleRateGroupId, - (65 * WadRayMath.RAY) / 100, + (75 * WadRayMath.RAY) / 100, (8 * WadRayMath.RAY) / 100, // baseRate - (5 * WadRayMath.RAY) / 100, - (100 * WadRayMath.RAY) / 100 + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 ); irmDefault.setInterestRateParams( - addrWETH, + poolId, + addrUSDS, highRateGroupId, - (65 * WadRayMath.RAY) / 100, + (75 * WadRayMath.RAY) / 100, (10 * WadRayMath.RAY) / 100, // baseRate - (5 * WadRayMath.RAY) / 100, - (100 * WadRayMath.RAY) / 100 + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 ); + addUSDS(poolId); + + configurator.setAssetBorrowing(poolId, addrUSDS, true); + configurator.setAssetFlashLoan(poolId, addrUSDS, true); + } + + function initOralces() internal { + priceOracle.setBendNFTOracle(addrNFTOracle); + + address[] memory assets = new address[](4); + assets[0] = address(addrWETH); + assets[1] = address(addrUSDT); + assets[2] = address(addrUSDC); + assets[3] = address(addrDAI); + address[] memory aggs = new address[](4); + aggs[0] = address(addrCLAggWETH); + aggs[1] = address(addrCLAggUSDT); + aggs[2] = address(addrCLAggUSDC); + aggs[3] = address(addrCLAggDAI); + priceOracle.setAssetChainlinkAggregators(assets, aggs); + } + + function initInterestRateModels(uint32 poolId) internal { + // WETH + if (poolId == 1 || poolId == 2) { + irmDefault.setInterestRateParams( + poolId, + addrWETH, + yieldRateGroupId, + (75 * WadRayMath.RAY) / 100, + (1 * WadRayMath.RAY) / 100, // baseRate + (1 * WadRayMath.RAY) / 100, + (20 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrWETH, + lowRateGroupId, + (75 * WadRayMath.RAY) / 100, + (5 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrWETH, + middleRateGroupId, + (75 * WadRayMath.RAY) / 100, + (7 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrWETH, + highRateGroupId, + (75 * WadRayMath.RAY) / 100, + (9 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + } + // USDT - irmDefault.setInterestRateParams( - addrUSDT, - yieldRateGroupId, - (65 * WadRayMath.RAY) / 100, - (1 * WadRayMath.RAY) / 100, // baseRate - (1 * WadRayMath.RAY) / 100, - (20 * WadRayMath.RAY) / 100 - ); - irmDefault.setInterestRateParams( - addrUSDT, - lowRateGroupId, - (65 * WadRayMath.RAY) / 100, - (5 * WadRayMath.RAY) / 100, // baseRate - (1 * WadRayMath.RAY) / 100, - (20 * WadRayMath.RAY) / 100 - ); - irmDefault.setInterestRateParams( - addrUSDT, - middleRateGroupId, - (65 * WadRayMath.RAY) / 100, - (8 * WadRayMath.RAY) / 100, // baseRate - (5 * WadRayMath.RAY) / 100, - (100 * WadRayMath.RAY) / 100 - ); - irmDefault.setInterestRateParams( - addrUSDT, - highRateGroupId, - (65 * WadRayMath.RAY) / 100, - (10 * WadRayMath.RAY) / 100, // baseRate - (5 * WadRayMath.RAY) / 100, - (100 * WadRayMath.RAY) / 100 - ); + if (poolId == 1 || poolId == 2 || poolId == 3) { + irmDefault.setInterestRateParams( + poolId, + addrUSDT, + yieldRateGroupId, + (75 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100, // baseRate + (1 * WadRayMath.RAY) / 100, + (20 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDT, + lowRateGroupId, + (75 * WadRayMath.RAY) / 100, + (5 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDT, + middleRateGroupId, + (75 * WadRayMath.RAY) / 100, + (7 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDT, + highRateGroupId, + (75 * WadRayMath.RAY) / 100, + (9 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + } + + // USDC + if (poolId == 1) { + irmDefault.setInterestRateParams( + poolId, + addrUSDC, + yieldRateGroupId, + (75 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100, // baseRate + (1 * WadRayMath.RAY) / 100, + (20 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDC, + lowRateGroupId, + (75 * WadRayMath.RAY) / 100, + (5 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDC, + middleRateGroupId, + (75 * WadRayMath.RAY) / 100, + (7 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDC, + highRateGroupId, + (75 * WadRayMath.RAY) / 100, + (9 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + } // DAI - irmDefault.setInterestRateParams( - addrDAI, - yieldRateGroupId, - (65 * WadRayMath.RAY) / 100, - (1 * WadRayMath.RAY) / 100, // baseRate - (1 * WadRayMath.RAY) / 100, - (20 * WadRayMath.RAY) / 100 - ); - irmDefault.setInterestRateParams( - addrDAI, - lowRateGroupId, - (65 * WadRayMath.RAY) / 100, - (5 * WadRayMath.RAY) / 100, // baseRate - (1 * WadRayMath.RAY) / 100, - (20 * WadRayMath.RAY) / 100 - ); - irmDefault.setInterestRateParams( - addrDAI, - middleRateGroupId, - (65 * WadRayMath.RAY) / 100, - (8 * WadRayMath.RAY) / 100, // baseRate - (5 * WadRayMath.RAY) / 100, - (100 * WadRayMath.RAY) / 100 - ); - irmDefault.setInterestRateParams( - addrDAI, - highRateGroupId, - (65 * WadRayMath.RAY) / 100, - (10 * WadRayMath.RAY) / 100, // baseRate - (5 * WadRayMath.RAY) / 100, - (100 * WadRayMath.RAY) / 100 - ); + if (poolId == 1 || poolId == 3) { + irmDefault.setInterestRateParams( + poolId, + addrDAI, + yieldRateGroupId, + (75 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100, // baseRate + (1 * WadRayMath.RAY) / 100, + (20 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrDAI, + lowRateGroupId, + (75 * WadRayMath.RAY) / 100, + (6 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrDAI, + middleRateGroupId, + (75 * WadRayMath.RAY) / 100, + (8 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrDAI, + highRateGroupId, + (75 * WadRayMath.RAY) / 100, + (10 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + } + + // USDS + if (poolId == 1) { + irmDefault.setInterestRateParams( + poolId, + addrUSDS, + yieldRateGroupId, + (75 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100, // baseRate + (1 * WadRayMath.RAY) / 100, + (20 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDS, + lowRateGroupId, + (75 * WadRayMath.RAY) / 100, + (6 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDS, + middleRateGroupId, + (75 * WadRayMath.RAY) / 100, + (8 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + irmDefault.setInterestRateParams( + poolId, + addrUSDS, + highRateGroupId, + (75 * WadRayMath.RAY) / 100, + (10 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (80 * WadRayMath.RAY) / 100 + ); + } } function setPoolInterestRateModels(uint32 poolId) internal { @@ -241,26 +423,37 @@ contract InitConfigPool is DeployBase { setAssetInterestRateModels(poolId, addrUSDT); } + if (poolId == 1) { + setAssetInterestRateModels(poolId, addrUSDC); + } + if (poolId == 1 || poolId == 3) { setAssetInterestRateModels(poolId, addrDAI); } + + if (poolId == 1) { + setAssetInterestRateModels(poolId, addrUSDS); + } } function setAssetInterestRateModels(uint32 poolId, address asset) internal { configurator.setAssetLendingRate(poolId, asset, lowRateGroupId, address(irmDefault)); configurator.setAssetLendingRate(poolId, asset, middleRateGroupId, address(irmDefault)); configurator.setAssetLendingRate(poolId, asset, highRateGroupId, address(irmDefault)); - configurator.setAssetYieldRate(poolId, asset, address(irmDefault)); + if ((poolId == 1) && (asset == addrWETH || asset == addrDAI || asset == addrUSDS)) { + configurator.setAssetYieldRate(poolId, asset, address(irmDefault)); + } } function initCommonPools() internal { - commonPoolId = createNewPool('Common Pool'); + commonPoolId = createNewPool('BendDAO Pool'); // erc20 assets addWETH(commonPoolId); addUSDT(commonPoolId); addUSDC(commonPoolId); addDAI(commonPoolId); + addUSDS(commonPoolId); // erc721 assets addWPUNK(commonPoolId); @@ -317,7 +510,6 @@ contract InitConfigPool is DeployBase { configurator.setAssetProtocolFee(poolId, address(token), 1500); configurator.setAssetClassGroup(poolId, address(token), lowRateGroupId); configurator.setAssetActive(poolId, address(token), true); - configurator.setAssetBorrowing(poolId, address(token), true); configurator.setAssetSupplyCap(poolId, address(token), 100_000_000 * (10 ** decimals)); configurator.setAssetBorrowCap(poolId, address(token), 100_000_000 * (10 ** decimals)); @@ -336,7 +528,6 @@ contract InitConfigPool is DeployBase { configurator.setAssetProtocolFee(poolId, address(token), 1000); configurator.setAssetClassGroup(poolId, address(token), lowRateGroupId); configurator.setAssetActive(poolId, address(token), true); - configurator.setAssetBorrowing(poolId, address(token), true); configurator.setAssetSupplyCap(poolId, address(token), 100_000_000 * (10 ** decimals)); configurator.setAssetBorrowCap(poolId, address(token), 100_000_000 * (10 ** decimals)); @@ -349,13 +540,12 @@ contract InitConfigPool is DeployBase { IERC20Metadata token = IERC20Metadata(addrUSDC); uint8 decimals = token.decimals(); - //configurator.addAssetERC20(poolId, address(token)); + configurator.addAssetERC20(poolId, address(token)); - //configurator.setAssetCollateralParams(poolId, address(token), 7500, 7800, 450); - //configurator.setAssetProtocolFee(poolId, address(token), 1000); + configurator.setAssetCollateralParams(poolId, address(token), 7500, 7800, 450); + configurator.setAssetProtocolFee(poolId, address(token), 1000); configurator.setAssetClassGroup(poolId, address(token), lowRateGroupId); configurator.setAssetActive(poolId, address(token), true); - configurator.setAssetBorrowing(poolId, address(token), true); configurator.setAssetSupplyCap(poolId, address(token), 100_000_000 * (10 ** decimals)); configurator.setAssetBorrowCap(poolId, address(token), 100_000_000 * (10 ** decimals)); @@ -374,7 +564,6 @@ contract InitConfigPool is DeployBase { configurator.setAssetProtocolFee(poolId, address(token), 2500); configurator.setAssetClassGroup(poolId, address(token), lowRateGroupId); configurator.setAssetActive(poolId, address(token), true); - configurator.setAssetBorrowing(poolId, address(token), true); configurator.setAssetSupplyCap(poolId, address(token), 100_000_000 * (10 ** decimals)); configurator.setAssetBorrowCap(poolId, address(token), 100_000_000 * (10 ** decimals)); @@ -383,11 +572,38 @@ contract InitConfigPool is DeployBase { configurator.addAssetGroup(poolId, address(token), highRateGroupId, address(irmHigh)); } - function setFlashLoan(uint32 poolId) internal { + function addUSDS(uint32 poolId) internal { + IERC20Metadata token = IERC20Metadata(addrUSDS); + uint8 decimals = token.decimals(); + + configurator.addAssetERC20(poolId, address(token)); + + configurator.setAssetCollateralParams(poolId, address(token), 7500, 7800, 750); + configurator.setAssetProtocolFee(poolId, address(token), 1000); + configurator.setAssetClassGroup(poolId, address(token), lowRateGroupId); + configurator.setAssetActive(poolId, address(token), true); + configurator.setAssetSupplyCap(poolId, address(token), 100_000_000 * (10 ** decimals)); + configurator.setAssetBorrowCap(poolId, address(token), 100_000_000 * (10 ** decimals)); + + configurator.addAssetGroup(poolId, address(token), lowRateGroupId, address(irmLow)); + configurator.addAssetGroup(poolId, address(token), middleRateGroupId, address(irmMiddle)); + configurator.addAssetGroup(poolId, address(token), highRateGroupId, address(irmHigh)); + } + + function setAssetBorrowing(uint32 poolId) internal { + configurator.setAssetBorrowing(poolId, addrWETH, true); + configurator.setAssetBorrowing(poolId, addrUSDT, true); + configurator.setAssetBorrowing(poolId, addrUSDC, true); + configurator.setAssetBorrowing(poolId, addrDAI, true); + configurator.setAssetBorrowing(poolId, addrUSDS, true); + } + + function setAssetFlashLoan(uint32 poolId) internal { configurator.setAssetFlashLoan(poolId, addrWETH, true); configurator.setAssetFlashLoan(poolId, addrUSDT, true); configurator.setAssetFlashLoan(poolId, addrUSDC, true); configurator.setAssetFlashLoan(poolId, addrDAI, true); + configurator.setAssetFlashLoan(poolId, addrUSDS, true); } function addWPUNK(uint32 poolId) internal { diff --git a/script/InitConfigYield.s.sol b/script/InitConfigYield.s.sol index ead61cb..23ac548 100644 --- a/script/InitConfigYield.s.sol +++ b/script/InitConfigYield.s.sol @@ -16,6 +16,8 @@ import {DefaultInterestRateModel} from 'src/irm/DefaultInterestRateModel.sol'; import {YieldEthStakingLido} from 'src/yield/lido/YieldEthStakingLido.sol'; import {YieldEthStakingEtherfi} from 'src/yield/etherfi/YieldEthStakingEtherfi.sol'; import {YieldSavingsDai} from 'src/yield/sdai/YieldSavingsDai.sol'; +import {YieldSavingsUSDS} from 'src/yield/susds/YieldSavingsUSDS.sol'; +import {YieldWUSDStaking} from 'src/yield/wusd/YieldWUSDStaking.sol'; import {Configured, ConfigLib, Config} from 'config/Configured.sol'; import {DeployBase} from './DeployBase.s.sol'; @@ -27,14 +29,22 @@ contract InitConfigYield is DeployBase { address internal addrWETH; address internal addrDAI; + address internal addrUSDS; + address internal addrWUSD; address internal addrWPUNK; address internal addrBAYC; + address internal addrStBAYC; address internal addrMAYC; + address internal addrStMAYC; + address internal addrPPG; + address internal addrAZUKI; address internal addrYieldLido; address internal addrYieldEtherfi; address internal addrYieldSDai; + address internal addrYieldSUSDS; + address internal addrYieldWUSD; address internal addrIrmYield; uint32 commonPoolId; @@ -45,17 +55,47 @@ contract InitConfigYield is DeployBase { Configurator internal configurator; function _deploy() internal virtual override { - if (block.chainid == 11155111) { + if (block.chainid == 1) { + addrWETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + addrDAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + addrUSDS = 0xdC035D45d973E3EC169d2276DDab16f1e407384F; + addrWUSD = 0xb6667b04Cb61Aa16B59617f90FFA068722Cf21dA; + + addrWPUNK = 0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6; + addrBAYC = 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D; + addrStBAYC = 0x08f5F0126aF89B4fD5499E942891D904A027624B; + addrMAYC = 0x60E4d786628Fea6478F785A6d7e704777c86a7c6; + addrStMAYC = 0xc1ED28E4b4d8e284A41E7474CA5522b010f3A64F; + addrPPG = 0xBd3531dA5CF5857e7CfAA92426877b022e612cf8; + addrAZUKI = 0xED5AF388653567Af2F388E6224dC7C4b3241C544; + + addrYieldLido = 0x61Ae6DCE4C7Cb1b8165aE244c734f20DF56efd73; + addrYieldEtherfi = 0x529a8822416c3c4ED1B77dE570118fDf1d474639; + addrYieldSDai = 0x6FA43C1a296db746937Ac4D97Ff61409E8c530cC; + addrYieldSUSDS = 0x0684c5ca33f3C9aD5BB405f91D4200B776Af477B; + addrYieldWUSD = 0x8C119f5D51209E6b5C508F90d23E8F3069a2DDBD; + + commonPoolId = 1; + addrIrmYield = 0x8f6E743f1CDF1dC49dF342da221D3D966B658D00; + } else if (block.chainid == 11155111) { addrWETH = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; addrDAI = 0xf9a88B0cc31f248c89F063C2928fA10e5A029B88; + addrUSDS = 0x99f5A9506504BB96d0019538608090015BA9EBDd; + addrWUSD = 0xdf98BFe3CDF4CA3C0a9F1dE2e34e6D9E049E2952; addrWPUNK = 0x647dc527Bd7dFEE4DD468cE6fC62FC50fa42BD8b; addrBAYC = 0xE15A78992dd4a9d6833eA7C9643650d3b0a2eD2B; + addrStBAYC = 0x214455B76E5A5dECB48557417397B831efC6219b; addrMAYC = 0xD0ff8ae7E3D9591605505D3db9C33b96c4809CDC; + addrStMAYC = 0xE5165Aae8D50371A277f266eC5A0E00405B532C8; + addrPPG = 0x4041e6E3B54df2684c5b345d761CF13a1BC219b6; + addrAZUKI = 0x292F693048208184320C01e0C223D624268e5EE7; addrYieldLido = 0x59303f797B8Dd80fc3743047df63C76E44Ca7CBd; addrYieldEtherfi = 0x3234F1047E71421Ec67A576D87eaEe1B86E8A1Ea; addrYieldSDai = 0x7464a51fA6338A34b694b4bF4A152781fb2C4B70; + addrYieldSUSDS = 0x63d56158751A75493B4b5fEdc46A29ba6a68cc15; + addrYieldWUSD = 0x86FF757587515bbD5C708170a15b4235EaCa284C; commonPoolId = 1; addrIrmYield = 0x10988B9c7e7048B83D590b14F0167FDe56728Ae9; @@ -70,27 +110,37 @@ contract InitConfigYield is DeployBase { configuratorPool = ConfiguratorPool(addressProvider.getPoolModuleProxy(Constants.MODULEID__CONFIGURATOR_POOL)); configurator = Configurator(addressProvider.getPoolModuleProxy(Constants.MODULEID__CONFIGURATOR)); - //initYieldPools(); + // initYieldPools(4); - initYieldLido(); + // initYieldLido(); + // initYieldEtherfi(); + // initYieldSDai(); + // initYieldSUSDS(); + // initYieldWUSD(4); + } - initYieldEtherfi(); + function initYieldPools(uint32 poolId) internal { + configuratorPool.setPoolYieldEnable(poolId, true); - initYieldSDai(); - } + // IERC20Metadata weth = IERC20Metadata(addrWETH); + // configurator.setAssetYieldEnable(poolId, address(weth), true); + // configurator.setAssetYieldCap(poolId, address(weth), 2000); + // configurator.setAssetYieldRate(poolId, address(weth), address(addrIrmYield)); - function initYieldPools() internal { - configuratorPool.setPoolYieldEnable(commonPoolId, true); + // IERC20Metadata dai = IERC20Metadata(addrDAI); + // configurator.setAssetYieldEnable(poolId, address(dai), true); + // configurator.setAssetYieldCap(poolId, address(dai), 2000); + // configurator.setAssetYieldRate(poolId, address(dai), address(addrIrmYield)); - IERC20Metadata weth = IERC20Metadata(addrWETH); - configurator.setAssetYieldEnable(commonPoolId, address(weth), true); - configurator.setAssetYieldCap(commonPoolId, address(weth), 2000); - configurator.setAssetYieldRate(commonPoolId, address(weth), address(addrIrmYield)); + // IERC20Metadata usds = IERC20Metadata(addrUSDS); + // configurator.setAssetYieldEnable(poolId, address(usds), true); + // configurator.setAssetYieldCap(poolId, address(usds), 2000); + // configurator.setAssetYieldRate(poolId, address(usds), address(addrIrmYield)); - IERC20Metadata dai = IERC20Metadata(addrDAI); - configurator.setAssetYieldEnable(commonPoolId, address(dai), true); - configurator.setAssetYieldCap(commonPoolId, address(dai), 2000); - configurator.setAssetYieldRate(commonPoolId, address(dai), address(addrIrmYield)); + IERC20Metadata wusd = IERC20Metadata(addrWUSD); + configurator.setAssetYieldEnable(poolId, address(wusd), true); + configurator.setAssetYieldCap(poolId, address(wusd), 8000); + configurator.setAssetYieldRate(poolId, address(wusd), address(addrIrmYield)); } function initYieldLido() internal { @@ -105,6 +155,26 @@ contract InitConfigYield is DeployBase { yieldEthStakingLido.setNftActive(address(addrBAYC), true); yieldEthStakingLido.setNftStakeParams(address(addrBAYC), 50000, 9000); yieldEthStakingLido.setNftUnstakeParams(address(addrBAYC), 0.01 ether, 1.05e18); + + yieldEthStakingLido.setNftActive(address(addrStBAYC), true); + yieldEthStakingLido.setNftStakeParams(address(addrStBAYC), 50000, 9000); + yieldEthStakingLido.setNftUnstakeParams(address(addrStBAYC), 0.01 ether, 1.05e18); + + yieldEthStakingLido.setNftActive(address(addrMAYC), true); + yieldEthStakingLido.setNftStakeParams(address(addrMAYC), 50000, 9000); + yieldEthStakingLido.setNftUnstakeParams(address(addrMAYC), 0.01 ether, 1.05e18); + + yieldEthStakingLido.setNftActive(address(addrStMAYC), true); + yieldEthStakingLido.setNftStakeParams(address(addrStMAYC), 50000, 9000); + yieldEthStakingLido.setNftUnstakeParams(address(addrStMAYC), 0.01 ether, 1.05e18); + + yieldEthStakingLido.setNftActive(address(addrPPG), true); + yieldEthStakingLido.setNftStakeParams(address(addrPPG), 50000, 9000); + yieldEthStakingLido.setNftUnstakeParams(address(addrPPG), 0.01 ether, 1.05e18); + + yieldEthStakingLido.setNftActive(address(addrAZUKI), true); + yieldEthStakingLido.setNftStakeParams(address(addrAZUKI), 50000, 9000); + yieldEthStakingLido.setNftUnstakeParams(address(addrAZUKI), 0.01 ether, 1.05e18); } function initYieldEtherfi() internal { @@ -119,6 +189,26 @@ contract InitConfigYield is DeployBase { yieldEthStakingEtherfi.setNftActive(address(addrBAYC), true); yieldEthStakingEtherfi.setNftStakeParams(address(addrBAYC), 20000, 9000); yieldEthStakingEtherfi.setNftUnstakeParams(address(addrBAYC), 0.01 ether, 1.05e18); + + yieldEthStakingEtherfi.setNftActive(address(addrStBAYC), true); + yieldEthStakingEtherfi.setNftStakeParams(address(addrStBAYC), 20000, 9000); + yieldEthStakingEtherfi.setNftUnstakeParams(address(addrStBAYC), 0.01 ether, 1.05e18); + + yieldEthStakingEtherfi.setNftActive(address(addrMAYC), true); + yieldEthStakingEtherfi.setNftStakeParams(address(addrMAYC), 20000, 9000); + yieldEthStakingEtherfi.setNftUnstakeParams(address(addrMAYC), 0.01 ether, 1.05e18); + + yieldEthStakingEtherfi.setNftActive(address(addrStMAYC), true); + yieldEthStakingEtherfi.setNftStakeParams(address(addrStMAYC), 20000, 9000); + yieldEthStakingEtherfi.setNftUnstakeParams(address(addrStMAYC), 0.01 ether, 1.05e18); + + yieldEthStakingEtherfi.setNftActive(address(addrPPG), true); + yieldEthStakingEtherfi.setNftStakeParams(address(addrPPG), 20000, 9000); + yieldEthStakingEtherfi.setNftUnstakeParams(address(addrPPG), 0.01 ether, 1.05e18); + + yieldEthStakingEtherfi.setNftActive(address(addrAZUKI), true); + yieldEthStakingEtherfi.setNftStakeParams(address(addrAZUKI), 20000, 9000); + yieldEthStakingEtherfi.setNftUnstakeParams(address(addrAZUKI), 0.01 ether, 1.05e18); } function initYieldSDai() internal { @@ -133,5 +223,93 @@ contract InitConfigYield is DeployBase { yieldSDai.setNftActive(address(addrBAYC), true); yieldSDai.setNftStakeParams(address(addrBAYC), 50000, 9000); yieldSDai.setNftUnstakeParams(address(addrBAYC), 100e18, 1.05e18); + + yieldSDai.setNftActive(address(addrStBAYC), true); + yieldSDai.setNftStakeParams(address(addrStBAYC), 50000, 9000); + yieldSDai.setNftUnstakeParams(address(addrStBAYC), 100e18, 1.05e18); + + yieldSDai.setNftActive(address(addrMAYC), true); + yieldSDai.setNftStakeParams(address(addrMAYC), 50000, 9000); + yieldSDai.setNftUnstakeParams(address(addrMAYC), 100e18, 1.05e18); + + yieldSDai.setNftActive(address(addrStMAYC), true); + yieldSDai.setNftStakeParams(address(addrStMAYC), 50000, 9000); + yieldSDai.setNftUnstakeParams(address(addrStMAYC), 100e18, 1.05e18); + + yieldSDai.setNftActive(address(addrPPG), true); + yieldSDai.setNftStakeParams(address(addrPPG), 50000, 9000); + yieldSDai.setNftUnstakeParams(address(addrPPG), 100e18, 1.05e18); + + yieldSDai.setNftActive(address(addrAZUKI), true); + yieldSDai.setNftStakeParams(address(addrAZUKI), 50000, 9000); + yieldSDai.setNftUnstakeParams(address(addrAZUKI), 100e18, 1.05e18); + } + + function initYieldSUSDS() internal { + configurator.setManagerYieldCap(commonPoolId, address(addrYieldSUSDS), address(addrUSDS), 2000); + + YieldSavingsUSDS yieldSUSDS = YieldSavingsUSDS(payable(addrYieldSUSDS)); + + yieldSUSDS.setNftActive(address(addrWPUNK), true); + yieldSUSDS.setNftStakeParams(address(addrWPUNK), 50000, 9000); + yieldSUSDS.setNftUnstakeParams(address(addrWPUNK), 100e18, 1.05e18); + + yieldSUSDS.setNftActive(address(addrBAYC), true); + yieldSUSDS.setNftStakeParams(address(addrBAYC), 50000, 9000); + yieldSUSDS.setNftUnstakeParams(address(addrBAYC), 100e18, 1.05e18); + + yieldSUSDS.setNftActive(address(addrStBAYC), true); + yieldSUSDS.setNftStakeParams(address(addrStBAYC), 50000, 9000); + yieldSUSDS.setNftUnstakeParams(address(addrStBAYC), 100e18, 1.05e18); + + yieldSUSDS.setNftActive(address(addrMAYC), true); + yieldSUSDS.setNftStakeParams(address(addrMAYC), 50000, 9000); + yieldSUSDS.setNftUnstakeParams(address(addrMAYC), 100e18, 1.05e18); + + yieldSUSDS.setNftActive(address(addrStMAYC), true); + yieldSUSDS.setNftStakeParams(address(addrStMAYC), 50000, 9000); + yieldSUSDS.setNftUnstakeParams(address(addrStMAYC), 100e18, 1.05e18); + + yieldSUSDS.setNftActive(address(addrPPG), true); + yieldSUSDS.setNftStakeParams(address(addrPPG), 50000, 9000); + yieldSUSDS.setNftUnstakeParams(address(addrPPG), 100e18, 1.05e18); + + yieldSUSDS.setNftActive(address(addrAZUKI), true); + yieldSUSDS.setNftStakeParams(address(addrAZUKI), 50000, 9000); + yieldSUSDS.setNftUnstakeParams(address(addrAZUKI), 100e18, 1.05e18); + } + + function initYieldWUSD(uint32 poolId) internal { + YieldWUSDStaking yieldWUSD = YieldWUSDStaking(payable(addrYieldWUSD)); + + configurator.setManagerYieldCap(poolId, address(addrYieldWUSD), address(addrWUSD), 8000); + + yieldWUSD.setNftActive(address(addrWPUNK), true); + yieldWUSD.setNftStakeParams(address(addrWPUNK), 50000, 9000); + yieldWUSD.setNftUnstakeParams(address(addrWPUNK), 100e6, 1.05e18); + + yieldWUSD.setNftActive(address(addrBAYC), true); + yieldWUSD.setNftStakeParams(address(addrBAYC), 50000, 9000); + yieldWUSD.setNftUnstakeParams(address(addrBAYC), 100e6, 1.05e18); + + yieldWUSD.setNftActive(address(addrStBAYC), true); + yieldWUSD.setNftStakeParams(address(addrStBAYC), 50000, 9000); + yieldWUSD.setNftUnstakeParams(address(addrStBAYC), 100e6, 1.05e18); + + yieldWUSD.setNftActive(address(addrMAYC), true); + yieldWUSD.setNftStakeParams(address(addrMAYC), 50000, 9000); + yieldWUSD.setNftUnstakeParams(address(addrMAYC), 100e6, 1.05e18); + + yieldWUSD.setNftActive(address(addrStMAYC), true); + yieldWUSD.setNftStakeParams(address(addrStMAYC), 50000, 9000); + yieldWUSD.setNftUnstakeParams(address(addrStMAYC), 100e6, 1.05e18); + + yieldWUSD.setNftActive(address(addrPPG), true); + yieldWUSD.setNftStakeParams(address(addrPPG), 50000, 9000); + yieldWUSD.setNftUnstakeParams(address(addrPPG), 100e6, 1.05e18); + + yieldWUSD.setNftActive(address(addrAZUKI), true); + yieldWUSD.setNftStakeParams(address(addrAZUKI), 50000, 9000); + yieldWUSD.setNftUnstakeParams(address(addrAZUKI), 100e6, 1.05e18); } } diff --git a/script/InstallModule.s.sol b/script/InstallModule.s.sol index 28746bd..27f226d 100644 --- a/script/InstallModule.s.sol +++ b/script/InstallModule.s.sol @@ -33,18 +33,23 @@ contract InstallModule is DeployBase { Installer installer = Installer(poolManager.moduleIdToProxy(Constants.MODULEID__INSTALLER)); - address[] memory modules = _allModules(); - //address[] memory modules = _someModules(); + // address[] memory modules = _allModules(); + address[] memory modules = _someModules(); - installer.installModules(modules); + if (block.chainid != 1) { + installer.installModules(modules); + } } function _someModules() internal returns (address[] memory) { - address[] memory modules = new address[](1); + address[] memory modules = new address[](2); uint modIdx = 0; - UIPoolLens tsUIPoolLensImpl = new UIPoolLens(gitCommitHash); - modules[modIdx++] = address(tsUIPoolLensImpl); + PoolLens tsPoolLensImpl = new PoolLens(gitCommitHash); + modules[modIdx++] = address(tsPoolLensImpl); + + Yield tsYieldImpl = new Yield(gitCommitHash); + modules[modIdx++] = address(tsYieldImpl); return modules; } diff --git a/script/QueryPool.s.sol b/script/QueryPool.s.sol index 769b24a..b0ee448 100644 --- a/script/QueryPool.s.sol +++ b/script/QueryPool.s.sol @@ -11,27 +11,59 @@ import {PoolLens} from 'src/modules/PoolLens.sol'; import '@forge-std/Script.sol'; +interface IYieldStakingBase { + function underlyingAsset() external view returns (address); +} + contract QueryPool is QueryBase { using ConfigLib for Config; + PoolManager poolManager; + PoolLens poolLens; + function _query() internal virtual override { address addressInCfg = config.getPoolManager(); require(addressInCfg != address(0), 'PoolManager not exist in config'); - PoolManager poolManager = PoolManager(payable(addressInCfg)); + poolManager = PoolManager(payable(addressInCfg)); + poolLens = PoolLens(poolManager.moduleIdToProxy(Constants.MODULEID__POOL_LENS)); + + queryUserData(0x8b04B42962BeCb429a4dBFb5025b66D3d7D31d27, 1); + + // queryStakerData(address(0), 1); - PoolLens poolLens = PoolLens(poolManager.moduleIdToProxy(Constants.MODULEID__POOL_LENS)); + // queryStakerData(0x59303f797B8Dd80fc3743047df63C76E44Ca7CBd, 1); + // queryStakerData(0x3234F1047E71421Ec67A576D87eaEe1B86E8A1Ea, 1); + // queryStakerData(0x7464a51fA6338A34b694b4bF4A152781fb2C4B70, 1); + } + + function queryUserData(address user, uint32 pool) internal view { + (address[] memory assets /*uint8[] memory types*/, ) = poolLens.getPoolAssetList(pool); + + poolLens.getUserAssetList(user, pool); - poolLens.getUserAssetList(0xc24c9Af9007B8Eb713eFf069CDeC013DD86402E8, 1); + poolLens.getUserAccountData(user, pool); - poolLens.getUserAccountData(0xc24c9Af9007B8Eb713eFf069CDeC013DD86402E8, 1); + poolLens.getUserAccountGroupData(user, pool); + + for (uint i = 0; i < assets.length; i++) { + poolLens.getUserAssetScaledData(user, pool, assets[i]); + } + } - poolLens.getUserAccountGroupData(0xc24c9Af9007B8Eb713eFf069CDeC013DD86402E8, 1); + function queryStakerData(address staker, uint32 pool) internal view { + if (staker == address(0)) { + (address[] memory assets, uint8[] memory types) = poolLens.getPoolAssetList(pool); - poolLens.getUserAssetScaledData( - 0xc24c9Af9007B8Eb713eFf069CDeC013DD86402E8, - 1, - 0xf9a88B0cc31f248c89F063C2928fA10e5A029B88 - ); + for (uint i = 0; i < assets.length; i++) { + if (types[i] != Constants.ASSET_TYPE_ERC20) { + continue; + } + poolLens.getYieldStakerAssetData(pool, assets[i], staker); + } + } else { + address asset = IYieldStakingBase(staker).underlyingAsset(); + poolLens.getYieldStakerAssetData(pool, asset, staker); + } } } diff --git a/script/README.md b/script/README.md index 8ac6d4c..ab3d06e 100644 --- a/script/README.md +++ b/script/README.md @@ -1,17 +1,55 @@ # How to run the script -## env +## Env -fill .env file at project root. +Fill .env file at project root. -## Install new modules +## Deployment -. ./setup-env.sh && forge script ./script/InstallModule.s.sol -vvvvv --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url sepolia --broadcast --slow --verify +### GAS Price Unit + +5Gwei = 5000000000. +10Gwei = 10000000000. +60Gwei = 60000000000. +100Gwei = 100000000000. + +### Deploy Pool Contracts + +```shell +# Pool Contracts +. ./setup-env.sh && forge script ./script/DeployPoolFull.s.sol -vvvvv --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url {NETWORK} --broadcast --slow --verify --with-gas-price {GAS} + +# Init Pool Configs +. ./setup-env.sh && forge script ./script/InitConfigPool.s.sol -vvvvv --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url {NETWORK} --broadcast --slow --verify --with-gas-price {GAS} +``` + +### Deploy Yield Contracts + +```shell +# Price Adapters +. ./setup-env.sh && forge script ./script/DeployPriceAdapter.s.sol -vvvvv --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url {NETWORK} --broadcast --slow --verify --with-gas-price {GAS} + +# Pool Contracts +. ./setup-env.sh && forge script ./script/DeployYieldStaking.s.sol -vvvvv --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url {NETWORK} --broadcast --slow --verify --with-gas-price {GAS} + +# Init Yield Configs +. ./setup-env.sh && forge script ./script/InitConfigYield.s.sol -vvvvv --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url {NETWORK} --broadcast --slow --verify --with-gas-price {GAS} +``` + +## Install Modules + +```shell +. ./setup-env.sh && forge script ./script/InstallModule.s.sol -vvvvv --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url {NETWORK} --broadcast --slow --verify --with-gas-price {GAS} +``` ## Query +```shell forge script ./script/QueryPool.s.sol --rpc-url sepolia -vvvvv +``` ## Verify +```shell forge verify-contract --etherscan-api-key ${ETHERSCAN_KEY} --rpc-url sepolia ${ContractAddress} ${ContractFile}:${ContractName} +``` diff --git a/script/UpgradeContract.s.sol b/script/UpgradeContract.s.sol index 41c9912..9889165 100644 --- a/script/UpgradeContract.s.sol +++ b/script/UpgradeContract.s.sol @@ -18,6 +18,7 @@ import {YieldRegistry} from 'src/yield/YieldRegistry.sol'; import {YieldEthStakingLido} from 'src/yield/lido/YieldEthStakingLido.sol'; import {YieldEthStakingEtherfi} from 'src/yield/etherfi/YieldEthStakingEtherfi.sol'; import {YieldSavingsDai} from 'src/yield/sdai/YieldSavingsDai.sol'; +import {YieldWUSDStaking} from 'src/yield/wusd/YieldWUSDStaking.sol'; import {BendV1Migration} from 'src/migrations/BendV1Migration.sol'; @@ -29,13 +30,21 @@ contract UpgradeContract is DeployBase { address internal addrYieldLido; address internal addrYieldEtherfi; address internal addrYieldSDai; + address internal addrYieldWUSD; address internal addrBendV1Migration; function _deploy() internal virtual override { - if (block.chainid == 11155111) { + if (block.chainid == 1) { + addrYieldLido = 0x61Ae6DCE4C7Cb1b8165aE244c734f20DF56efd73; + addrYieldEtherfi = 0x529a8822416c3c4ED1B77dE570118fDf1d474639; + addrYieldSDai = 0x6FA43C1a296db746937Ac4D97Ff61409E8c530cC; + addrYieldWUSD = 0x8C119f5D51209E6b5C508F90d23E8F3069a2DDBD; + addrBendV1Migration = 0xf6EE27bb3F17E456078711D8c4b257377375D654; + } else if (block.chainid == 11155111) { addrYieldLido = 0x59303f797B8Dd80fc3743047df63C76E44Ca7CBd; addrYieldEtherfi = 0x3234F1047E71421Ec67A576D87eaEe1B86E8A1Ea; addrYieldSDai = 0x7464a51fA6338A34b694b4bF4A152781fb2C4B70; + addrYieldWUSD = 0x86FF757587515bbD5C708170a15b4235EaCa284C; addrBendV1Migration = 0x989c290B431DA780C3Fce9640488E7967C1bAB84; } else { revert('chainid not support'); @@ -55,6 +64,8 @@ contract UpgradeContract is DeployBase { //_upgradeYieldEthStakingLido(proxyAdminInCfg, addrProviderInCfg); //_upgradeYieldEthStakingEtherfi(proxyAdminInCfg, addrProviderInCfg); //_upgradeYieldSavingsDai(proxyAdminInCfg, addrProviderInCfg); + //_upgradeYieldWUSDStaking(proxyAdminInCfg, addrProviderInCfg); + //_upgradeBendV1Migration(proxyAdminInCfg, addrProviderInCfg); } @@ -102,6 +113,13 @@ contract UpgradeContract is DeployBase { proxyAdmin.upgrade(ITransparentUpgradeableProxy(addrYieldSDai), address(newImpl)); } + function _upgradeYieldWUSDStaking(address proxyAdmin_, address /*addressProvider_*/) internal { + YieldWUSDStaking newImpl = new YieldWUSDStaking(); + + ProxyAdmin proxyAdmin = ProxyAdmin(proxyAdmin_); + proxyAdmin.upgrade(ITransparentUpgradeableProxy(addrYieldWUSD), address(newImpl)); + } + function _upgradeBendV1Migration(address proxyAdmin_, address /*addressProvider_*/) internal { BendV1Migration newImpl = new BendV1Migration(); diff --git a/script/extractABI.sh b/script/extractABI.sh index 635c7ea..eda2b54 100644 --- a/script/extractABI.sh +++ b/script/extractABI.sh @@ -20,10 +20,14 @@ do jq '.abi' out/${fileWithExtension}/${filename}.json > release/abis/${filename}.json done +jq '.abi' out/DefaultInterestRateModel.sol/DefaultInterestRateModel.json > release/abis/DefaultInterestRateModel.json + jq '.abi' out/YieldRegistry.sol/YieldRegistry.json > release/abis/YieldRegistry.json jq '.abi' out/YieldAccount.sol/YieldAccount.json > release/abis/YieldAccount.json jq '.abi' out/YieldEthStakingLido.sol/YieldEthStakingLido.json > release/abis/YieldEthStakingLido.json jq '.abi' out/YieldEthStakingEtherfi.sol/YieldEthStakingEtherfi.json > release/abis/YieldEthStakingEtherfi.json jq '.abi' out/YieldSavingsDai.sol/YieldSavingsDai.json > release/abis/YieldSavingsDai.json +jq '.abi' out/YieldSavingsUSDS.sol/YieldSavingsUSDS.json > release/abis/YieldSavingsUSDS.json +jq '.abi' out/YieldWUSDStaking.sol/YieldWUSDStaking.json > release/abis/YieldWUSDStaking.json jq '.abi' out/BendV1Migration.sol/BendV1Migration.json > release/abis/BendV1Migration.json diff --git a/setup-env.sh b/setup-env.sh index ff6e767..04cb94e 100644 --- a/setup-env.sh +++ b/setup-env.sh @@ -11,6 +11,7 @@ source .env export GIT_COMMIT_HASH=`git rev-parse HEAD | cast to-bytes32` +echo $NETWORK echo $ETHERSCAN_KEY echo $GIT_COMMIT_HASH diff --git a/src/PriceOracle.sol b/src/PriceOracle.sol index 936e14a..6c49c6a 100644 --- a/src/PriceOracle.sol +++ b/src/PriceOracle.sol @@ -28,12 +28,18 @@ contract PriceOracle is IPriceOracle, Initializable { // Chainlink Aggregators for ERC20 tokens mapping(address => AggregatorV2V3Interface) public assetChainlinkAggregators; + // BendDAO Protocol ERC20 Token Oracle which used v2 + IBendNFTOracle public bendTokenOracle; + + // Asset Oracle Source Type + mapping(address => uint8) public assetOracleSourceTypes; + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[43] private __gap; + uint256[41] private __gap; modifier onlyOracleAdmin() { _onlyOracleAdmin(); @@ -69,6 +75,22 @@ contract PriceOracle is IPriceOracle, Initializable { NFT_BASE_CURRENCY_UNIT = nftBaseCurrencyUnit_; } + function setAssetOracleSourceTypes(address[] calldata assets, uint8[] calldata sourceTypes) public onlyOracleAdmin { + require(assets.length == sourceTypes.length, Errors.INCONSISTENT_PARAMS_LENGTH); + for (uint256 i = 0; i < assets.length; i++) { + require(assets[i] != address(0), Errors.INVALID_ADDRESS); + assetOracleSourceTypes[assets[i]] = sourceTypes[i]; + emit Events.AssetOracleSourceTypeUpdated(assets[i], sourceTypes[i]); + } + } + + function getAssetOracleSourceTypes(address[] calldata assets) public view returns (uint8[] memory sourceTypes) { + sourceTypes = new uint8[](assets.length); + for (uint256 i = 0; i < assets.length; i++) { + sourceTypes[i] = uint8(assetOracleSourceTypes[assets[i]]); + } + } + /// @notice Set Chainlink aggregators for sssets function setAssetChainlinkAggregators( address[] calldata assets, @@ -78,6 +100,8 @@ contract PriceOracle is IPriceOracle, Initializable { for (uint256 i = 0; i < assets.length; i++) { require(assets[i] != address(0), Errors.INVALID_ADDRESS); require(aggregators[i] != address(0), Errors.INVALID_ADDRESS); + uint256 decimalsUnit = 10 ** AggregatorV2V3Interface(aggregators[i]).decimals(); + require(BASE_CURRENCY_UNIT == decimalsUnit, Errors.INVALID_ASSET_DECIMALS); assetChainlinkAggregators[assets[i]] = AggregatorV2V3Interface(aggregators[i]); emit Events.AssetAggregatorUpdated(assets[i], aggregators[i]); } @@ -109,6 +133,19 @@ contract PriceOracle is IPriceOracle, Initializable { return address(bendNFTOracle); } + /// @notice Set the global BendDAO ERC20 Oracle + function setBendTokenOracle(address bendTokenOracle_) public onlyOracleAdmin { + require(bendTokenOracle_ != address(0), Errors.INVALID_ADDRESS); + uint256 decimalsUnit = 10 ** IBendNFTOracle(bendTokenOracle_).getDecimals(); + require(BASE_CURRENCY_UNIT == decimalsUnit, Errors.INVALID_ASSET_DECIMALS); + bendTokenOracle = IBendNFTOracle(bendTokenOracle_); + emit Events.BendTokenOracleUpdated(bendTokenOracle_); + } + + function getBendTokenOracle() public view returns (address) { + return address(bendTokenOracle); + } + /// @notice Query the price of asset function getAssetPrice(address asset) external view returns (uint256) { if (asset == BASE_CURRENCY) { @@ -119,7 +156,17 @@ contract PriceOracle is IPriceOracle, Initializable { return getAssetPriceFromChainlink(asset); } - return getAssetPriceFromBendNFTOracle(asset); + uint8 sourceType = assetOracleSourceTypes[asset]; + + if (sourceType == Constants.ORACLE_TYPE_BEND_NFT) { + return getAssetPriceFromBendNFTOracle(asset); + } + + if (sourceType == Constants.ORACLE_TYPE_BEND_TOKEN) { + return getAssetPriceFromBendTokenOracle(asset); + } + + revert(Errors.ASSET_ORACLE_NOT_EXIST); } /// @notice Query the price of asset from chainlink oracle @@ -153,4 +200,15 @@ contract PriceOracle is IPriceOracle, Initializable { uint256 nftPriceInBase = (nftPriceInNftBase * nftBaseCurrencyPriceInBase) / NFT_BASE_CURRENCY_UNIT; return nftPriceInBase; } + + /// @notice Query the price of asset from benddao token oracle + function getAssetPriceFromBendTokenOracle(address asset) public view returns (uint256) { + uint256 updatedAt = bendTokenOracle.getLatestTimestamp(asset); + require(updatedAt != 0, Errors.ORACLE_PRICE_IS_STALE); + + uint256 tokenPrice = bendTokenOracle.getAssetPrice(asset); + require(tokenPrice > 0, Errors.ASSET_PRICE_IS_ZERO); + + return tokenPrice; + } } diff --git a/src/interfaces/IBendNFTOracle.sol b/src/interfaces/IBendNFTOracle.sol index d5c0c9b..9f50e0d 100644 --- a/src/interfaces/IBendNFTOracle.sol +++ b/src/interfaces/IBendNFTOracle.sol @@ -11,4 +11,8 @@ interface IBendNFTOracle { function getAssetPrice(address _nftContract) external view returns (uint256); function getLatestTimestamp(address _nftContract) external view returns (uint256); + + function getPriceFeedLength(address _nftContract) external view returns (uint256 length); + + function getDecimals() external view returns (uint8); } diff --git a/src/interfaces/IDefaultInterestRateModel.sol b/src/interfaces/IDefaultInterestRateModel.sol index 47cec7e..ff44748 100644 --- a/src/interfaces/IDefaultInterestRateModel.sol +++ b/src/interfaces/IDefaultInterestRateModel.sol @@ -8,31 +8,31 @@ interface IDefaultInterestRateModel is IInterestRateModel { * @notice Returns the usage ratio at which the pool aims to obtain most competitive borrow rates. * @return The optimal usage ratio, expressed in ray. */ - function getOptimalUtilizationRate(address asset, uint256 group) external view returns (uint256); + function getOptimalUtilizationRate(uint32 pool, address asset, uint256 group) external view returns (uint256); /** * @notice Returns the base variable borrow rate * @return The base variable borrow rate, expressed in ray */ - function getBaseVariableBorrowRate(address asset, uint256 group) external view returns (uint256); + function getBaseVariableBorrowRate(uint32 pool, address asset, uint256 group) external view returns (uint256); /** * @notice Returns the rate slope below optimal usage ratio * @dev It's the variable rate when usage ratio > 0 and <= OPTIMAL_USAGE_RATIO * @return The variable rate slope, expressed in ray */ - function getVariableRateSlope1(address asset, uint256 group) external view returns (uint256); + function getVariableRateSlope1(uint32 pool, address asset, uint256 group) external view returns (uint256); /** * @notice Returns the variable rate slope above optimal usage ratio * @dev It's the variable rate when usage ratio > OPTIMAL_USAGE_RATIO * @return The variable rate slope, expressed in ray */ - function getVariableRateSlope2(address asset, uint256 group) external view returns (uint256); + function getVariableRateSlope2(uint32 pool, address asset, uint256 group) external view returns (uint256); /** * @notice Returns the maximum variable borrow rate * @return The maximum variable borrow rate, expressed in ray */ - function getMaxVariableBorrowRate(address asset, uint256 group) external view returns (uint256); + function getMaxVariableBorrowRate(uint32 pool, address asset, uint256 group) external view returns (uint256); } diff --git a/src/interfaces/IInterestRateModel.sol b/src/interfaces/IInterestRateModel.sol index d6585a8..0721790 100644 --- a/src/interfaces/IInterestRateModel.sol +++ b/src/interfaces/IInterestRateModel.sol @@ -15,6 +15,7 @@ interface IInterestRateModel { * @return borrowRate The group borrow rate expressed in rays */ function calculateGroupBorrowRate( + uint32 pool, address asset, uint256 groupId, uint256 utilizationRate diff --git a/src/irm/DefaultInterestRateModel.sol b/src/irm/DefaultInterestRateModel.sol index a909481..b6b111a 100644 --- a/src/irm/DefaultInterestRateModel.sol +++ b/src/irm/DefaultInterestRateModel.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; +import {Initializable} from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; + import {WadRayMath} from '../libraries/math/WadRayMath.sol'; import {PercentageMath} from '../libraries/math/PercentageMath.sol'; import {InputTypes} from '../libraries/types/InputTypes.sol'; @@ -17,11 +19,12 @@ import {IDefaultInterestRateModel} from '../interfaces/IDefaultInterestRateModel * @dev The model of interest rate is based on 2 slopes, one before the `OPTIMAL_USAGE_RATIO` * point of usage and another from that one to 100%. */ -contract DefaultInterestRateModel is IDefaultInterestRateModel { +contract DefaultInterestRateModel is IDefaultInterestRateModel, Initializable { using WadRayMath for uint256; using PercentageMath for uint256; event InterestRateParamsUpdate( + uint32 indexed poolId, address indexed reserve, uint256 group, uint256 optimalUtilizationRate, @@ -46,8 +49,8 @@ contract DefaultInterestRateModel is IDefaultInterestRateModel { uint256 public constant MAX_OPTIMAL_RATE = 99e25; // 99% IAddressProvider public addressProvider; - // asset => (group => params) - mapping(address => mapping(uint256 => InterestRateParams)) internal _interestRateParams; + // pool => (asset => (group => params)) + mapping(uint32 => mapping(address => mapping(uint256 => InterestRateParams))) internal _interestRateParams; // Modifiers modifier onlyPoolAdmin() { @@ -62,11 +65,16 @@ contract DefaultInterestRateModel is IDefaultInterestRateModel { /** * @dev Constructor. */ - constructor(address provider) { + constructor() { + _disableInitializers(); + } + + function initialize(address provider) public initializer { addressProvider = IAddressProvider(provider); } function setInterestRateParams( + uint32 pool, address asset, uint256 group, uint256 optimalRate, @@ -75,6 +83,7 @@ contract DefaultInterestRateModel is IDefaultInterestRateModel { uint256 slope2 ) public onlyPoolAdmin { _setInterestRateParams( + pool, asset, group, InterestRateParams({ @@ -86,40 +95,50 @@ contract DefaultInterestRateModel is IDefaultInterestRateModel { ); } - function setInterestRateParams(address asset, uint256 group, bytes calldata rateData) public onlyPoolAdmin { - _setInterestRateParams(asset, group, abi.decode(rateData, (InterestRateParams))); + function setInterestRateParams( + uint32 pool, + address asset, + uint256 group, + bytes calldata rateData + ) public onlyPoolAdmin { + _setInterestRateParams(pool, asset, group, abi.decode(rateData, (InterestRateParams))); } function setInterestRateParams( + uint32 pool, address asset, uint256 group, InterestRateParams calldata rateData ) public onlyPoolAdmin { - _setInterestRateParams(asset, group, rateData); + _setInterestRateParams(pool, asset, group, rateData); } - function getInterestRateParams(address asset, uint256 group) public view returns (InterestRateParams memory) { - return _interestRateParams[asset][group]; + function getInterestRateParams( + uint32 pool, + address asset, + uint256 group + ) public view returns (InterestRateParams memory) { + return _interestRateParams[pool][asset][group]; } - function getOptimalUtilizationRate(address asset, uint256 group) public view override returns (uint256) { - return _interestRateParams[asset][group].optimalUtilizationRate; + function getOptimalUtilizationRate(uint32 pool, address asset, uint256 group) public view override returns (uint256) { + return _interestRateParams[pool][asset][group].optimalUtilizationRate; } - function getBaseVariableBorrowRate(address asset, uint256 group) public view override returns (uint256) { - return _interestRateParams[asset][group].baseVariableBorrowRate; + function getBaseVariableBorrowRate(uint32 pool, address asset, uint256 group) public view override returns (uint256) { + return _interestRateParams[pool][asset][group].baseVariableBorrowRate; } - function getVariableRateSlope1(address asset, uint256 group) public view override returns (uint256) { - return _interestRateParams[asset][group].variableRateSlope1; + function getVariableRateSlope1(uint32 pool, address asset, uint256 group) public view override returns (uint256) { + return _interestRateParams[pool][asset][group].variableRateSlope1; } - function getVariableRateSlope2(address asset, uint256 group) public view override returns (uint256) { - return _interestRateParams[asset][group].variableRateSlope2; + function getVariableRateSlope2(uint32 pool, address asset, uint256 group) public view override returns (uint256) { + return _interestRateParams[pool][asset][group].variableRateSlope2; } - function getMaxVariableBorrowRate(address asset, uint256 group) public view override returns (uint256) { - InterestRateParams memory rateParams = _interestRateParams[asset][group]; + function getMaxVariableBorrowRate(uint32 pool, address asset, uint256 group) public view override returns (uint256) { + InterestRateParams memory rateParams = _interestRateParams[pool][asset][group]; return rateParams.baseVariableBorrowRate + (rateParams.variableRateSlope1) + (rateParams.variableRateSlope2); } @@ -132,11 +151,12 @@ contract DefaultInterestRateModel is IDefaultInterestRateModel { /// @inheritdoc IInterestRateModel function calculateGroupBorrowRate( + uint32 pool, address asset, uint256 group, uint256 utilizationRate ) public view override returns (uint256) { - InterestRateParams memory rateParams = _interestRateParams[asset][group]; + InterestRateParams memory rateParams = _interestRateParams[pool][asset][group]; require(rateParams.optimalUtilizationRate > 0, Errors.INVALID_OPTIMAL_USAGE_RATIO); CalcInterestRatesLocalVars memory vars; @@ -163,7 +183,12 @@ contract DefaultInterestRateModel is IDefaultInterestRateModel { * @param asset address of the underlying asset * @param rateParams Encoded interest rate params to apply */ - function _setInterestRateParams(address asset, uint256 group, InterestRateParams memory rateParams) internal { + function _setInterestRateParams( + uint32 pool, + address asset, + uint256 group, + InterestRateParams memory rateParams + ) internal { require(asset != address(0), Errors.INVALID_ADDRESS); require( @@ -179,9 +204,10 @@ contract DefaultInterestRateModel is IDefaultInterestRateModel { (rateParams.variableRateSlope2); require(maxBorrowRate <= MAX_BORROW_RATE, Errors.INVALID_MAX_RATE); - _interestRateParams[asset][group] = rateParams; + _interestRateParams[pool][asset][group] = rateParams; emit InterestRateParamsUpdate( + pool, asset, group, rateParams.optimalUtilizationRate, diff --git a/src/libraries/helpers/Constants.sol b/src/libraries/helpers/Constants.sol index 6c179f3..19debe1 100644 --- a/src/libraries/helpers/Constants.sol +++ b/src/libraries/helpers/Constants.sol @@ -111,4 +111,9 @@ library Constants { uint8 public constant YIELD_STATUS_UNSTAKE = 2; uint8 public constant YIELD_STATUS_CLAIM = 3; uint8 public constant YIELD_STATUS_REPAID = 4; + + // Oracle Source type + uint8 public constant ORACLE_TYPE_CHAIN_LINK = 0; + uint8 public constant ORACLE_TYPE_BEND_NFT = 1; + uint8 public constant ORACLE_TYPE_BEND_TOKEN = 2; } diff --git a/src/libraries/helpers/Errors.sol b/src/libraries/helpers/Errors.sol index f665849..dd2e3db 100644 --- a/src/libraries/helpers/Errors.sol +++ b/src/libraries/helpers/Errors.sol @@ -9,6 +9,7 @@ library Errors { string public constant MSG_VALUE_NOT_ZERO = '4'; string public constant TOKEN_ALLOWANCE_INSUFFICIENT = '5'; string public constant TOKEN_BALANCE_INSUFFICIENT = '6'; + string public constant UNSUPPORTED = '7'; string public constant REENTRANCY_ALREADY_LOCKED = '10'; @@ -103,6 +104,7 @@ library Errors { string public constant ASSET_SUPPLY_MODE_IS_SAME = '363'; string public constant ASSET_TOKEN_ALREADY_EXISTS = '364'; string public constant ASSET_LIQUIDITY_NOT_ZERO = '365'; + string public constant ASSET_ORACLE_NOT_EXIST = '366'; string public constant HEALTH_FACTOR_BELOW_LIQUIDATION_THRESHOLD = '400'; string public constant HEALTH_FACTOR_NOT_BELOW_LIQUIDATION_THRESHOLD = '401'; diff --git a/src/libraries/helpers/Events.sol b/src/libraries/helpers/Events.sol index ba75da3..7ecc886 100644 --- a/src/libraries/helpers/Events.sol +++ b/src/libraries/helpers/Events.sol @@ -11,6 +11,8 @@ library Events { /* Oracle Events */ event AssetAggregatorUpdated(address indexed asset, address aggregator); event BendNFTOracleUpdated(address bendNFTOracle); + event BendTokenOracleUpdated(address bendTokenOracle); + event AssetOracleSourceTypeUpdated(address indexed asset, uint8 sourceType); /* Pool Events */ event CreatePool(uint32 indexed poolId, string name); diff --git a/src/libraries/logic/ConfigureLogic.sol b/src/libraries/logic/ConfigureLogic.sol index da4835f..c95ef10 100644 --- a/src/libraries/logic/ConfigureLogic.sol +++ b/src/libraries/logic/ConfigureLogic.sol @@ -573,7 +573,8 @@ library ConfigureLogic { DataTypes.GroupData storage groupData = assetData.groupLookup[groupId]; groupData.rateModel = rateModel_; - InterestLogic.updateInterestRates(poolData, assetData, 0, 0); + // We intentionally commented this line of code cos upgrading the IRM contract may change the method signature + // InterestLogic.updateInterestRates(poolData, assetData, 0, 0); emit Events.SetAssetLendingRate(poolId, asset, groupId, rateModel_); } @@ -661,6 +662,7 @@ library ConfigureLogic { DataTypes.AssetData storage assetData = poolData.assetLookup[asset]; require(assetData.underlyingAsset != address(0), Errors.ASSET_NOT_EXISTS); require(assetData.assetType == Constants.ASSET_TYPE_ERC20, Errors.ASSET_TYPE_NOT_ERC20); + require(assetData.isYieldEnabled, Errors.ASSET_YIELD_NOT_ENABLE); // update index using old param before set new config InterestLogic.updateInterestIndexs(poolData, assetData); @@ -668,7 +670,8 @@ library ConfigureLogic { DataTypes.GroupData storage groupData = assetData.groupLookup[poolData.yieldGroup]; groupData.rateModel = rateModel_; - InterestLogic.updateInterestRates(poolData, assetData, 0, 0); + // We intentionally commented this line of code cos upgrading the IRM contract may change the method signature + // InterestLogic.updateInterestRates(poolData, assetData, 0, 0); emit Events.SetAssetYieldRate(poolId, asset, rateModel_); } diff --git a/src/libraries/logic/InterestLogic.sol b/src/libraries/logic/InterestLogic.sol index b8c96e8..89c640b 100644 --- a/src/libraries/logic/InterestLogic.sol +++ b/src/libraries/logic/InterestLogic.sol @@ -176,6 +176,7 @@ library InterestLogic { require(loopGroupData.rateModel != address(0), Errors.INVALID_RATE_MODEL); vars.nextGroupBorrowRate = IInterestRateModel(loopGroupData.rateModel).calculateGroupBorrowRate( + poolData.poolId, assetData.underlyingAsset, vars.loopGroupId, vars.assetUtilizationRate diff --git a/src/libraries/logic/IsolateLogic.sol b/src/libraries/logic/IsolateLogic.sol index bbf5688..af9d2fe 100644 --- a/src/libraries/logic/IsolateLogic.sol +++ b/src/libraries/logic/IsolateLogic.sol @@ -484,7 +484,7 @@ library IsolateLogic { // transfer erc721 to winning bidder if (params.supplyAsCollateral) { - VaultLogic.erc721TransferIsolateSupplyOnLiquidate(nftAssetData, vars.winningBidder, params.nftTokenIds); + VaultLogic.erc721TransferIsolateSupplyOnLiquidate(nftAssetData, vars.winningBidder, params.nftTokenIds, true); } else { VaultLogic.erc721DecreaseIsolateSupplyOnLiquidate(nftAssetData, params.nftTokenIds); diff --git a/src/libraries/logic/QueryLogic.sol b/src/libraries/logic/QueryLogic.sol index f23f709..6cf512c 100644 --- a/src/libraries/logic/QueryLogic.sol +++ b/src/libraries/logic/QueryLogic.sol @@ -759,6 +759,14 @@ library QueryLogic { loanData, IAddressProvider(ps.addressProvider).getPriceOracle() ); + + if (loanData.loanStatus == Constants.LOAN_STATUS_AUCTION) { + if (borrowAmount > loanData.bidAmount) { + liquidatePrice = borrowAmount; + } else { + liquidatePrice = loanData.bidAmount + borrowAmount.percentMul(PercentageMath.ONE_PERCENTAGE_FACTOR); + } + } } function getYieldERC20BorrowBalance(uint32 poolId, address asset, address staker) internal view returns (uint256) { @@ -771,6 +779,73 @@ library QueryLogic { return scaledBalance.rayMul(InterestLogic.getNormalizedBorrowDebt(assetData, groupData)); } + struct GetYieldStakerAssetDataLocalVars { + uint256 totalSupply; + uint256 totalCapAll; + uint256 normDebtIndex; + uint256 totalBorrowAll; + uint256 availableBorrowAll; + } + + function getYieldStakerAssetData( + uint32 poolId, + address asset, + address staker + ) internal view returns (uint256 stakerCap, uint256 stakerBorrow, uint256 availableBorrow) { + GetYieldStakerAssetDataLocalVars memory vars; + + DataTypes.PoolStorage storage ps = StorageSlot.getPoolStorage(); + DataTypes.PoolData storage poolData = ps.poolLookup[poolId]; + DataTypes.AssetData storage assetData = poolData.assetLookup[asset]; + DataTypes.GroupData storage groupData = assetData.groupLookup[poolData.yieldGroup]; + + // asset total suppy + vars.totalSupply = VaultLogic.erc20GetTotalScaledCrossSupply(assetData); + vars.totalSupply = vars.totalSupply.rayMul(InterestLogic.getNormalizedSupplyIncome(assetData)); + + vars.normDebtIndex = InterestLogic.getNormalizedBorrowDebt(assetData, groupData); + + // asset total cap for all stakers + vars.totalCapAll = vars.totalSupply.percentMul(assetData.yieldCap); + + // asset total borrow for all stakers + vars.totalBorrowAll = VaultLogic.erc20GetTotalScaledCrossBorrowInGroup(groupData); + vars.totalBorrowAll = vars.totalBorrowAll.rayMul(vars.normDebtIndex); + + // asset available borrow for all stakers + if (vars.totalCapAll > vars.totalBorrowAll) { + vars.availableBorrowAll = vars.totalCapAll - vars.totalBorrowAll; + } + + if (staker == address(0)) { + stakerCap = vars.totalCapAll; + stakerBorrow = vars.totalBorrowAll; + availableBorrow = vars.availableBorrowAll; + } else { + DataTypes.YieldManagerData storage ymData = assetData.yieldManagerLookup[staker]; + + // staker cap limit + stakerCap = vars.totalSupply.percentMul(ymData.yieldCap); + + // staker borrow + stakerBorrow = VaultLogic.erc20GetUserScaledCrossBorrowInGroup(groupData, staker); + stakerBorrow = stakerBorrow.rayMul(vars.normDebtIndex); + + if (stakerCap > stakerBorrow) { + availableBorrow = stakerCap - stakerBorrow; + } + + if (availableBorrow > vars.availableBorrowAll) { + availableBorrow = vars.availableBorrowAll; + } + } + + // check liquidity + if (availableBorrow > assetData.availableLiquidity) { + availableBorrow = assetData.availableLiquidity; + } + } + function getERC721TokenData( uint32 poolId, address asset, diff --git a/src/libraries/logic/VaultLogic.sol b/src/libraries/logic/VaultLogic.sol index 020ced2..11483b0 100644 --- a/src/libraries/logic/VaultLogic.sol +++ b/src/libraries/logic/VaultLogic.sol @@ -676,7 +676,8 @@ library VaultLogic { DataTypes.AssetData storage assetData, address from, address to, - uint256[] memory tokenIds + uint256[] memory tokenIds, + bool clearLockerAddr ) internal { for (uint256 i = 0; i < tokenIds.length; i++) { DataTypes.ERC721TokenData storage tokenData = assetData.erc721TokenData[tokenIds[i]]; @@ -684,6 +685,9 @@ library VaultLogic { require(tokenData.owner == from, Errors.INVALID_TOKEN_OWNER); tokenData.owner = to; + if (clearLockerAddr) { + tokenData.lockerAddr = address(0); + } } assetData.userScaledIsolateSupply[from] -= tokenIds.length; @@ -693,7 +697,8 @@ library VaultLogic { function erc721TransferIsolateSupplyOnLiquidate( DataTypes.AssetData storage assetData, address to, - uint256[] memory tokenIds + uint256[] memory tokenIds, + bool clearLockerAddr ) internal { for (uint256 i = 0; i < tokenIds.length; i++) { DataTypes.ERC721TokenData storage tokenData = assetData.erc721TokenData[tokenIds[i]]; @@ -703,6 +708,9 @@ library VaultLogic { assetData.userScaledIsolateSupply[to] += 1; tokenData.owner = to; + if (clearLockerAddr) { + tokenData.lockerAddr = address(0); + } } } diff --git a/src/modules/PoolLens.sol b/src/modules/PoolLens.sol index aef559e..45daef8 100644 --- a/src/modules/PoolLens.sol +++ b/src/modules/PoolLens.sol @@ -313,6 +313,14 @@ contract PoolLens is BaseModule { return QueryLogic.getYieldERC20BorrowBalance(poolId, asset, staker); } + function getYieldStakerAssetData( + uint32 poolId, + address asset, + address staker + ) public view returns (uint256 stakerCap, uint256 stakerBorrow, uint256 availableBorrow) { + return QueryLogic.getYieldStakerAssetData(poolId, asset, staker); + } + function getERC721TokenData( uint32 poolId, address asset, diff --git a/src/modules/Yield.sol b/src/modules/Yield.sol index d2cc6c4..b896a95 100644 --- a/src/modules/Yield.sol +++ b/src/modules/Yield.sol @@ -74,6 +74,14 @@ contract Yield is BaseModule, IYield { return QueryLogic.getYieldERC20BorrowBalance(poolId, asset, staker); } + function getYieldStakerAssetData( + uint32 poolId, + address asset, + address staker + ) public view returns (uint256 stakerCap, uint256 stakerBorrow, uint256 availableBorrow) { + return QueryLogic.getYieldStakerAssetData(poolId, asset, staker); + } + function getERC721TokenData( uint32 poolId, address asset, diff --git a/src/oracles/EETHPriceAdapter.sol b/src/oracles/EETHPriceAdapter.sol new file mode 100644 index 0000000..190a0de --- /dev/null +++ b/src/oracles/EETHPriceAdapter.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AggregatorV2V3Interface} from '@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol'; + +import {IWeETH} from './IWeETH.sol'; + +/** + * @title EETHPriceAdapter + * @notice Price adapter to calculate price of (eETH / USD) pair by using + * @notice Chainlink data feed for (ETH / USD), (weETH / ETH) and (weETH / eETH) ratio. + */ +contract EETHPriceAdapter { + uint256 public constant VERSION = 1; + + /** + * @notice Price feed for ETH / USD pair + */ + AggregatorV2V3Interface public immutable BASE_AGGREGATOR; + + /** + * @notice Price feed for weETH / ETH pair + */ + AggregatorV2V3Interface public immutable WEETH_AGGREGATOR; + + /** + * @notice rate provider for (weETH / eETH) + */ + IWeETH public immutable RATE_PROVIDER; + + /** + * @notice Number of decimals for eETH / eETH ratio + */ + uint8 public constant RATIO_DECIMALS = 18; + + /** + * @notice Number of decimals in the output of the base price adapter + */ + uint8 public immutable BASE_DECIMALS; + + /** + * @notice Number of decimals in the output of the weETH price adapter + */ + uint8 public immutable WEETH_DECIMALS; + + string private _description; + + /** + * @param ethAggAddress the address of ETH feed + * @param weETHAggAddress the address of ETH feed + * @param weETHAddress the address of the rate provider + * @param pairName the name identifier of sDAI paire + */ + constructor(address ethAggAddress, address weETHAggAddress, address weETHAddress, string memory pairName) { + BASE_AGGREGATOR = AggregatorV2V3Interface(ethAggAddress); + WEETH_AGGREGATOR = AggregatorV2V3Interface(weETHAggAddress); + RATE_PROVIDER = IWeETH(weETHAddress); + + BASE_DECIMALS = BASE_AGGREGATOR.decimals(); + WEETH_DECIMALS = WEETH_AGGREGATOR.decimals(); + + _description = pairName; + } + + function description() public view returns (string memory) { + return _description; + } + + function decimals() public view returns (uint8) { + return BASE_DECIMALS; + } + + function version() public pure returns (uint256) { + return VERSION; + } + + function latestAnswer() public view virtual returns (int256) { + int256 basePrice = BASE_AGGREGATOR.latestAnswer(); + int256 weETHPrice = WEETH_AGGREGATOR.latestAnswer(); + + return _convertWEETHPrice(basePrice, weETHPrice); + } + + function latestTimestamp() public view returns (uint256) { + return BASE_AGGREGATOR.latestTimestamp(); + } + + function latestRound() public view returns (uint256) { + return BASE_AGGREGATOR.latestRound(); + } + + function getAnswer(uint256 roundId) public view returns (int256) { + int256 basePrice = BASE_AGGREGATOR.getAnswer(roundId); + int256 weETHPrice = WEETH_AGGREGATOR.latestAnswer(); + + return _convertWEETHPrice(basePrice, weETHPrice); + } + + function getTimestamp(uint256 roundId) public view returns (uint256) { + return BASE_AGGREGATOR.getTimestamp(roundId); + } + + function latestRoundData() + public + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + ( + uint80 roundId_, + int256 basePrice, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ + ) = BASE_AGGREGATOR.latestRoundData(); + int256 weETHPrice = WEETH_AGGREGATOR.latestAnswer(); + + int256 eETHBasePrice = _convertWEETHPrice(basePrice, weETHPrice); + + return (roundId_, eETHBasePrice, startedAt_, updatedAt_, answeredInRound_); + } + + function getRoundData( + uint80 _roundId + ) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { + ( + uint80 roundId_, + int256 basePrice, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ + ) = BASE_AGGREGATOR.getRoundData(_roundId); + int256 weETHPrice = WEETH_AGGREGATOR.latestAnswer(); + + int256 eETHBasePrice = _convertWEETHPrice(basePrice, weETHPrice); + + return (roundId_, eETHBasePrice, startedAt_, updatedAt_, answeredInRound_); + } + + function _convertWEETHPrice(int256 basePrice, int256 weETHPrice) internal view returns (int256) { + int256 ratio = int256(RATE_PROVIDER.getWeETHByeETH(1 ether)); + + if (basePrice <= 0 || weETHPrice <= 0 || ratio <= 0) { + return 0; + } + + // calculate the price of the (eETH / ETH) from (weETH / ETH) + int256 eETHPrice = (weETHPrice * ratio) / int256(10 ** RATIO_DECIMALS); + + // calculate the price of the (eETH / ETH) from (ETH / USD) + return (basePrice * eETHPrice) / int256(10 ** WEETH_DECIMALS); + } +} diff --git a/src/oracles/IUSDSRate.sol b/src/oracles/IUSDSRate.sol new file mode 100644 index 0000000..856ce90 --- /dev/null +++ b/src/oracles/IUSDSRate.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +interface IUSDSRate { + function chi() external view returns (uint256); +} diff --git a/src/oracles/IWeETH.sol b/src/oracles/IWeETH.sol new file mode 100644 index 0000000..81a8c61 --- /dev/null +++ b/src/oracles/IWeETH.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice A simple version of the IWeETH interface allowing to get exchange ratio with eETH +interface IWeETH { + /// @notice Fetches the amount of weEth respective to the amount of eEth sent in + /// @param _eETHAmount amount sent in + /// @return The total number of shares for the specified amount + function getWeETHByeETH(uint256 _eETHAmount) external view returns (uint256); + + /// @notice Fetches the amount of eEth respective to the amount of weEth sent in + /// @param _weETHAmount amount sent in + /// @return The total amount for the number of shares sent in + function getEETHByWeETH(uint256 _weETHAmount) external view returns (uint256); + + // Amount of weETH for 1 eETH + function getRate() external view returns (uint256); +} diff --git a/src/oracles/SUSDSPriceAdapter.sol b/src/oracles/SUSDSPriceAdapter.sol new file mode 100644 index 0000000..09b37d9 --- /dev/null +++ b/src/oracles/SUSDSPriceAdapter.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AggregatorV2V3Interface} from '@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol'; + +import {IUSDSRate} from './IUSDSRate.sol'; + +/** + * @title SUSDSPriceAdapter + * @notice Price adapter to calculate price of sUSDS pair by using + * @notice Chainlink Data Feed for USDS and rate provider for sUSDS. + */ +contract SUSDSPriceAdapter { + /** + * @notice Price feed for USDS pair + */ + AggregatorV2V3Interface public immutable USDS_AGGREGATOR; + + /** + * @notice rate provider for (sUSDS / USDS) + */ + IUSDSRate public immutable RATE_PROVIDER; + + /** + * @notice Number of decimals for sUSDS / USDS ratio + */ + uint8 public constant RATIO_DECIMALS = 27; + + /** + * @notice Number of decimals in the output of this price adapter + */ + uint8 public immutable DECIMALS; + + string private _description; + + /** + * @param usdsAggregatorAddress the address of USDS feed + * @param rateProviderAddress the address of the rate provider + * @param pairName the name identifier of sUSDS paire + */ + constructor(address usdsAggregatorAddress, address rateProviderAddress, string memory pairName) { + USDS_AGGREGATOR = AggregatorV2V3Interface(usdsAggregatorAddress); + RATE_PROVIDER = IUSDSRate(rateProviderAddress); + + DECIMALS = USDS_AGGREGATOR.decimals(); + + _description = pairName; + } + + function description() public view returns (string memory) { + return _description; + } + + function decimals() public view returns (uint8) { + return DECIMALS; + } + + function version() public view returns (uint256) { + return USDS_AGGREGATOR.version(); + } + + function latestAnswer() public view virtual returns (int256) { + int256 usdsPrice = USDS_AGGREGATOR.latestAnswer(); + return _convertUSDSPrice(usdsPrice); + } + + function latestTimestamp() public view returns (uint256) { + return USDS_AGGREGATOR.latestTimestamp(); + } + + function latestRound() public view returns (uint256) { + return USDS_AGGREGATOR.latestRound(); + } + + function getAnswer(uint256 roundId) public view returns (int256) { + int256 usdsPrice = USDS_AGGREGATOR.getAnswer(roundId); + int256 susdsPrice = _convertUSDSPrice(usdsPrice); + return susdsPrice; + } + + function getTimestamp(uint256 roundId) public view returns (uint256) { + return USDS_AGGREGATOR.getTimestamp(roundId); + } + + function latestRoundData() + public + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + ( + uint80 roundId_, + int256 usdsPrice, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ + ) = USDS_AGGREGATOR.latestRoundData(); + + int256 susdsPrice = _convertUSDSPrice(usdsPrice); + + return (roundId_, susdsPrice, startedAt_, updatedAt_, answeredInRound_); + } + + function getRoundData( + uint80 _roundId + ) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { + ( + uint80 roundId_, + int256 usdsPrice, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ + ) = USDS_AGGREGATOR.getRoundData(_roundId); + + int256 susdsPrice = _convertUSDSPrice(usdsPrice); + + return (roundId_, susdsPrice, startedAt_, updatedAt_, answeredInRound_); + } + + function _convertUSDSPrice(int256 usdsPrice) internal view returns (int256) { + int256 ratio = int256(RATE_PROVIDER.chi()); + + if (usdsPrice <= 0 || ratio <= 0) { + return 0; + } + + return (usdsPrice * ratio) / int256(10 ** RATIO_DECIMALS); + } +} diff --git a/src/yield/YieldStakingBase.sol b/src/yield/YieldStakingBase.sol index 889c7a6..9c47da7 100644 --- a/src/yield/YieldStakingBase.sol +++ b/src/yield/YieldStakingBase.sol @@ -34,7 +34,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr using Math for uint256; event SetNftActive(address indexed nft, bool isActive); - event SetNftStakeParams(address indexed nft, uint16 leverageFactor, uint16 liquidationThreshold); + event SetNftStakeParams(address indexed nft, uint16 leverageFactor, uint16 collateralFactor); event SetNftUnstakeParams(address indexed nft, uint256 maxUnstakeFine, uint256 unstakeHeathFactor); event SetBotAdmin(address oldAdmin, address newAdmin); @@ -48,7 +48,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr struct YieldNftConfig { bool isActive; uint16 leverageFactor; // e.g. 50000 -> 500% - uint16 liquidationThreshold; // e.g. 9000 -> 90% + uint16 collateralFactor; // e.g. 9000 -> 90% uint256 maxUnstakeFine; // e.g. In underlying asset's decimals uint256 unstakeHeathFactor; // 18 decimals, e.g. 1.0 -> 1e18 } @@ -108,7 +108,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr underlyingAsset = IERC20Metadata(underlyingAsset_); - underlyingAsset.approve(address(poolManager), type(uint256).max); + underlyingAsset.safeApprove(address(poolManager), type(uint256).max); } /****************************************************************************/ @@ -122,16 +122,12 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr emit SetNftActive(nft, active); } - function setNftStakeParams( - address nft, - uint16 leverageFactor, - uint16 liquidationThreshold - ) public virtual onlyPoolAdmin { + function setNftStakeParams(address nft, uint16 leverageFactor, uint16 collateralFactor) public virtual onlyPoolAdmin { YieldNftConfig storage nc = nftConfigs[nft]; nc.leverageFactor = leverageFactor; - nc.liquidationThreshold = liquidationThreshold; + nc.collateralFactor = collateralFactor; - emit SetNftStakeParams(nft, leverageFactor, liquidationThreshold); + emit SetNftStakeParams(nft, leverageFactor, collateralFactor); } function setNftUnstakeParams( @@ -367,7 +363,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr accountYieldShares[address(vars.yieldAccout)] -= sd.yieldShare; sd.yieldShare = 0; - emit Unstake(msg.sender, nft, tokenId, sd.withdrawAmount); + emit Unstake(vars.nftOwner, nft, tokenId, sd.withdrawAmount); } struct RepayLocalVars { @@ -477,7 +473,9 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr } // repay debt to lending pool - poolYield.yieldRepayERC20(poolId, address(underlyingAsset), vars.repaidNftDebt); + if (vars.repaidNftDebt > 0) { + poolYield.yieldRepayERC20(poolId, address(underlyingAsset), vars.repaidNftDebt); + } // update shares sd.debtShare -= vars.repaidDebtShare; @@ -502,7 +500,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr delete stakeDatas[nft][tokenId]; - emit Repay(msg.sender, nft, tokenId, vars.nftDebt); + emit Repay(vars.nftOwner, nft, tokenId, vars.nftDebt); } /****************************************************************************/ @@ -518,13 +516,13 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr returns ( bool isActive, uint16 leverageFactor, - uint16 liquidationThreshold, + uint16 collateralFactor, uint256 maxUnstakeFine, uint256 unstakeHeathFactor ) { YieldNftConfig memory nc = nftConfigs[nft]; - return (nc.isActive, nc.leverageFactor, nc.liquidationThreshold, nc.maxUnstakeFine, nc.unstakeHeathFactor); + return (nc.isActive, nc.leverageFactor, nc.collateralFactor, nc.maxUnstakeFine, nc.unstakeHeathFactor); } function getYieldAccount(address user) public view virtual returns (address) { @@ -549,7 +547,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr YieldNftConfig storage nc = nftConfigs[nft]; uint256 nftPrice = getNftPriceInUnderlyingAsset(nft); - uint256 totalNftValue = nftPrice.percentMul(nc.liquidationThreshold); + uint256 totalNftValue = nftPrice.percentMul(nc.collateralFactor); return totalNftValue; } @@ -574,7 +572,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr uint256 nftPrice = getNftPriceInUnderlyingAsset(nft); - totalCollateral = nftPrice.percentMul(nc.liquidationThreshold); + totalCollateral = nftPrice.percentMul(nc.collateralFactor); availabeBorrow = nftPrice.percentMul(nc.leverageFactor); YieldStakeData storage sd = stakeDatas[nft][tokenId]; @@ -774,7 +772,7 @@ abstract contract YieldStakingBase is Initializable, PausableUpgradeable, Reentr YieldStakeData storage sd ) internal view virtual returns (uint256) { uint256 nftPrice = getNftPriceInUnderlyingAsset(nft); - uint256 totalNftValue = nftPrice.percentMul(nc.liquidationThreshold); + uint256 totalNftValue = nftPrice.percentMul(nc.collateralFactor); (, uint256 totalYieldValue) = _getNftYieldInUnderlyingAsset(sd); uint256 totalDebtValue = _getNftDebtInUnderlyingAsset(sd); diff --git a/src/yield/susds/ISavingsUSDS.sol b/src/yield/susds/ISavingsUSDS.sol new file mode 100644 index 0000000..680ae59 --- /dev/null +++ b/src/yield/susds/ISavingsUSDS.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; + +interface ISavingsUSDS is IERC20Metadata { + function usds() external view returns (address); + + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + function convertToShares(uint256 assets) external view returns (uint256); + + function convertToAssets(uint256 shares) external view returns (uint256); +} diff --git a/src/yield/susds/YieldSavingsUSDS.sol b/src/yield/susds/YieldSavingsUSDS.sol new file mode 100644 index 0000000..5df1fa2 --- /dev/null +++ b/src/yield/susds/YieldSavingsUSDS.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; + +import {IPriceOracleGetter} from 'src/interfaces/IPriceOracleGetter.sol'; +import {IYieldAccount} from 'src/interfaces/IYieldAccount.sol'; +import {IYieldRegistry} from 'src/interfaces/IYieldRegistry.sol'; + +import {ISavingsUSDS} from './ISavingsUSDS.sol'; + +import {Constants} from 'src/libraries/helpers/Constants.sol'; +import {Errors} from 'src/libraries/helpers/Errors.sol'; + +import {YieldStakingBase} from '../YieldStakingBase.sol'; + +contract YieldSavingsUSDS is YieldStakingBase { + using SafeERC20 for IERC20Metadata; + using Math for uint256; + + IERC20Metadata public usds; + ISavingsUSDS public susds; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[20] private __gap; + + constructor() { + _disableInitializers(); + } + + function initialize(address addressProvider_, address usds_, address susds_) public initializer { + require(addressProvider_ != address(0), Errors.ADDR_PROVIDER_CANNOT_BE_ZERO); + + require(usds_ != address(0), Errors.INVALID_ADDRESS); + require(susds_ != address(0), Errors.INVALID_ADDRESS); + + __YieldStakingBase_init(addressProvider_, usds_); + + usds = IERC20Metadata(usds_); + susds = ISavingsUSDS(susds_); + } + + function createYieldAccount(address user) public virtual override returns (address) { + super.createYieldAccount(user); + + IYieldAccount yieldAccount = IYieldAccount(yieldAccounts[user]); + yieldAccount.safeApprove(address(usds), address(susds), type(uint256).max); + + return address(yieldAccount); + } + + function batchUnstakeAndRepay( + uint32 poolId, + address[] calldata nfts, + uint256[] calldata tokenIds + ) public virtual whenNotPaused nonReentrant { + require(nfts.length == tokenIds.length, Errors.INCONSISTENT_PARAMS_LENGTH); + + for (uint i = 0; i < nfts.length; i++) { + _unstake(poolId, nfts[i], tokenIds[i], 0); + + _repay(poolId, nfts[i], tokenIds[i]); + } + } + + function unstakeAndRepay(uint32 poolId, address nft, uint256 tokenId) public virtual whenNotPaused nonReentrant { + _unstake(poolId, nft, tokenId, 0); + + _repay(poolId, nft, tokenId); + } + + function protocolDeposit(YieldStakeData storage sd, uint256 amount) internal virtual override returns (uint256) { + IYieldAccount yieldAccount = IYieldAccount(sd.yieldAccount); + + usds.safeTransfer(address(yieldAccount), amount); + + bytes memory result = yieldAccount.execute( + address(susds), + abi.encodeWithSelector(ISavingsUSDS.deposit.selector, amount, address(yieldAccount)) + ); + uint256 yieldShare = abi.decode(result, (uint256)); + require(yieldShare > 0, Errors.YIELD_ETH_DEPOSIT_FAILED); + + // We no need to convert to assets, cos it based on the balanceOf + return yieldShare; + } + + /* @dev Savings USDS no need 2 steps so keep it empty */ + function protocolRequestWithdrawal(YieldStakeData storage sd) internal virtual override { + require(sd.withdrawAmount > 0, Errors.YIELD_ETH_WITHDRAW_FAILED); + } + + function protocolClaimWithdraw(YieldStakeData storage sd) internal virtual override returns (uint256) { + IYieldAccount yieldAccount = IYieldAccount(sd.yieldAccount); + + uint256 claimedUsds = usds.balanceOf(address(this)); + + bytes memory result = yieldAccount.execute( + address(susds), + abi.encodeWithSelector(ISavingsUSDS.redeem.selector, sd.withdrawAmount, address(this), address(yieldAccount)) + ); + uint256 assetAmount = abi.decode(result, (uint256)); + require(assetAmount > 0, Errors.YIELD_ETH_CLAIM_FAILED); + + claimedUsds = usds.balanceOf(address(this)) - claimedUsds; + require(claimedUsds > 0, Errors.YIELD_ETH_CLAIM_FAILED); + + return claimedUsds; + } + + function protocolIsClaimReady(YieldStakeData storage sd) internal view virtual override returns (bool) { + if (super.protocolIsClaimReady(sd)) { + return true; + } + if (sd.state == Constants.YIELD_STATUS_UNSTAKE) { + return true; + } + return false; + } + + function getAccountTotalUnstakedYield(address account) public view virtual override returns (uint256) { + /* The withdrawing usds still in account when user do the unstake and waiting for claim (repay) */ + uint256 balance = getAccountYieldBalance(account); + uint256 inWithdraw = accountYieldInWithdraws[account]; + + require(balance >= inWithdraw, Errors.YIELD_ETH_ACCOUNT_INSUFFICIENT); + return balance - inWithdraw; + } + + function getAccountYieldBalance(address account) public view virtual override returns (uint256) { + return susds.balanceOf(account); + } + + function getProtocolTokenPriceInUnderlyingAsset() internal view virtual override returns (uint256) { + IPriceOracleGetter priceOracle = IPriceOracleGetter(addressProvider.getPriceOracle()); + uint256 sUSDSPriceInBase = priceOracle.getAssetPrice(address(susds)); + uint256 usdsPriceInBase = priceOracle.getAssetPrice(address(underlyingAsset)); + return sUSDSPriceInBase.mulDiv(10 ** underlyingAsset.decimals(), usdsPriceInBase); + } + + function getProtocolTokenAmountInUnderlyingAsset( + uint256 yieldAmount + ) internal view virtual override returns (uint256) { + return susds.convertToAssets(yieldAmount); + } + + function getProtocolTokenDecimals() internal view virtual override returns (uint8) { + return susds.decimals(); + } +} diff --git a/src/yield/wusd/IWUSDStaking.sol b/src/yield/wusd/IWUSDStaking.sol new file mode 100644 index 0000000..2330fa0 --- /dev/null +++ b/src/yield/wusd/IWUSDStaking.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +interface IWUSDStaking { + /** Datatypes */ + enum StakingStatus { + ACTIVE, + CLAIMABLE, + CLAIMED + } + enum ClaimType { + UNCLAIMED, + PREMATURED, + MATURED + } + + enum StakingPoolStatus { + INACTIVE, + ACTIVE + } + + struct StakingPool { + uint48 stakingPeriod; + uint256 apy; + uint256 minStakingAmount; + StakingPoolStatus status; + } + + struct StakingPoolDetail { + uint256 stakingPoolId; + StakingPool stakingPool; + } + + struct StakingPlan { + uint256 stakingPoolId; + uint256 stakedAmount; + uint256 apy; + uint48 startTime; + uint48 endTime; + uint48 claimableTimestamp; + uint256 yield; + StakingStatus stakingStatus; + ClaimType claimType; + } + + struct StakingPlanDetail { + uint256 stakingPlanId; + StakingPlan stakingPlan; + } + + function stake(uint256 stakingPoolId, uint256 stakingAmount) external returns (uint256 stakingPlanId); + + function terminate(uint256 stakingPlanId) external; + + function claim(uint256[] calldata stakingPlanIds) external; + + function getUserStakingPlan(address staker, uint256 stakingPlanId) external view returns (StakingPlan memory); + + function getBasicAPY() external view returns (uint256); + + function getGeneralStaking() external view returns (StakingPoolDetail[] memory stakingPoolsDetail); +} diff --git a/src/yield/wusd/YieldWUSDStaking.sol b/src/yield/wusd/YieldWUSDStaking.sol new file mode 100644 index 0000000..406e122 --- /dev/null +++ b/src/yield/wusd/YieldWUSDStaking.sol @@ -0,0 +1,863 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; +import {Initializable} from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; +import {PausableUpgradeable} from '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; +import {ReentrancyGuardUpgradeable} from '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol'; + +import {IAddressProvider} from 'src/interfaces/IAddressProvider.sol'; +import {IACLManager} from 'src/interfaces/IACLManager.sol'; +import {IPoolManager} from 'src/interfaces/IPoolManager.sol'; +import {IYield} from 'src/interfaces/IYield.sol'; +import {IPriceOracleGetter} from 'src/interfaces/IPriceOracleGetter.sol'; +import {IYieldAccount} from 'src/interfaces/IYieldAccount.sol'; +import {IYieldRegistry} from 'src/interfaces/IYieldRegistry.sol'; + +import {Constants} from 'src/libraries/helpers/Constants.sol'; +import {Errors} from 'src/libraries/helpers/Errors.sol'; + +import {PercentageMath} from 'src/libraries/math/PercentageMath.sol'; +import {WadRayMath} from 'src/libraries/math/WadRayMath.sol'; +import {MathUtils} from 'src/libraries/math/MathUtils.sol'; +import {ShareUtils} from 'src/libraries/math/ShareUtils.sol'; + +import {IWUSDStaking} from './IWUSDStaking.sol'; + +contract YieldWUSDStaking is Initializable, PausableUpgradeable, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20Metadata; + using PercentageMath for uint256; + using ShareUtils for uint256; + using WadRayMath for uint256; + using MathUtils for uint256; + using Math for uint256; + + event SetNftActive(address indexed nft, bool isActive); + event SetNftStakeParams(address indexed nft, uint16 leverageFactor, uint16 collateralFactor); + event SetNftUnstakeParams(address indexed nft, uint256 maxUnstakeFine, uint256 unstakeHeathFactor); + event SetBotAdmin(address oldAdmin, address newAdmin); + + event Stake(address indexed user, address indexed nft, uint256 indexed tokenId, uint256 amount); + event Unstake(address indexed user, address indexed nft, uint256 indexed tokenId, uint256 amount); + event Repay(address indexed user, address indexed nft, uint256 indexed tokenId, uint256 amount); + event RepayPart(address indexed user, address indexed nft, uint256 indexed tokenId, uint256 amount); + + event CollectFeeToTreasury(address indexed to, uint256 amountToCollect); + + struct YieldNftConfig { + bool isActive; + uint16 leverageFactor; // e.g. 50000 -> 500% + uint16 collateralFactor; // e.g. 9000 -> 90% + uint256 maxUnstakeFine; // e.g. In underlying asset's decimals + uint256 unstakeHeathFactor; // 18 decimals, e.g. 1.0 -> 1e18 + } + + struct YieldStakeData { + address yieldAccount; + uint32 poolId; + uint8 state; + uint256 debtShare; + uint256 wusdStakingPoolId; + uint256 stakingPlanId; + uint256 unstakeFine; + uint256 withdrawAmount; + uint256 remainYieldAmount; + } + + uint48 public constant SECONDS_OF_YEAR = 31536000; + uint256 constant DENOMINATOR = 1e6; + + IAddressProvider public addressProvider; + IPoolManager public poolManager; + IYield public poolYield; + IYieldRegistry public yieldRegistry; + IERC20Metadata public underlyingAsset; + IWUSDStaking public wusdStaking; + address public botAdmin; + uint256 public totalDebtShare; + uint256 public totalUnstakeFine; + mapping(address => address) public yieldAccounts; + mapping(address => YieldNftConfig) public nftConfigs; + mapping(address => mapping(uint256 => YieldStakeData)) public stakeDatas; + uint256 public claimedUnstakeFine; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[20] private __gap; + + modifier onlyPoolAdmin() { + __onlyPoolAdmin(); + _; + } + + function __onlyPoolAdmin() internal view { + require(IACLManager(addressProvider.getACLManager()).isPoolAdmin(msg.sender), Errors.CALLER_NOT_POOL_ADMIN); + } + + constructor() { + _disableInitializers(); + } + + function initialize(address addressProvider_, address wusd_, address wusdStaking_) public initializer { + __Pausable_init(); + __ReentrancyGuard_init(); + + addressProvider = IAddressProvider(addressProvider_); + + poolManager = IPoolManager(addressProvider.getPoolManager()); + poolYield = IYield(addressProvider.getPoolModuleProxy(Constants.MODULEID__YIELD)); + yieldRegistry = IYieldRegistry(addressProvider.getYieldRegistry()); + + underlyingAsset = IERC20Metadata(wusd_); + wusdStaking = IWUSDStaking(wusdStaking_); + + underlyingAsset.safeApprove(address(poolManager), type(uint256).max); + } + + /****************************************************************************/ + /* Configure Methods */ + /****************************************************************************/ + + function setNftActive(address nft, bool active) public virtual onlyPoolAdmin { + YieldNftConfig storage nc = nftConfigs[nft]; + nc.isActive = active; + + emit SetNftActive(nft, active); + } + + function setNftStakeParams(address nft, uint16 leverageFactor, uint16 collateralFactor) public virtual onlyPoolAdmin { + YieldNftConfig storage nc = nftConfigs[nft]; + nc.leverageFactor = leverageFactor; + nc.collateralFactor = collateralFactor; + + emit SetNftStakeParams(nft, leverageFactor, collateralFactor); + } + + function setNftUnstakeParams( + address nft, + uint256 maxUnstakeFine, + uint256 unstakeHeathFactor + ) public virtual onlyPoolAdmin { + YieldNftConfig storage nc = nftConfigs[nft]; + nc.maxUnstakeFine = maxUnstakeFine; + nc.unstakeHeathFactor = unstakeHeathFactor; + + emit SetNftUnstakeParams(nft, maxUnstakeFine, unstakeHeathFactor); + } + + function setBotAdmin(address newAdmin) public virtual onlyPoolAdmin { + require(newAdmin != address(0), Errors.INVALID_ADDRESS); + + address oldAdmin = botAdmin; + botAdmin = newAdmin; + + emit SetBotAdmin(oldAdmin, newAdmin); + } + + function setPause(bool paused) public virtual onlyPoolAdmin { + if (paused) { + _pause(); + } else { + _unpause(); + } + } + + function collectFeeToTreasury() public virtual onlyPoolAdmin { + address to = addressProvider.getTreasury(); + require(to != address(0), Errors.TREASURY_CANNOT_BE_ZERO); + + if (totalUnstakeFine > claimedUnstakeFine) { + uint256 amountToCollect = totalUnstakeFine - claimedUnstakeFine; + claimedUnstakeFine += amountToCollect; + + underlyingAsset.safeTransfer(to, amountToCollect); + + emit CollectFeeToTreasury(to, amountToCollect); + } + } + + /****************************************************************************/ + /* Service Methods */ + /****************************************************************************/ + + function createYieldAccount(address user) public virtual returns (address) { + require(user != address(0), Errors.INVALID_ADDRESS); + require(yieldAccounts[user] == address(0), Errors.YIELD_ACCOUNT_ALREADY_EXIST); + + address account = yieldRegistry.createYieldAccount(address(this)); + yieldAccounts[user] = account; + + IYieldAccount yieldAccount = IYieldAccount(account); + yieldAccount.safeApprove(address(underlyingAsset), address(wusdStaking), type(uint256).max); + + return account; + } + + struct StakeLocalVars { + IYieldAccount yieldAccout; + address nftOwner; + uint8 nftSupplyMode; + address nftLockerAddr; + uint256 totalDebtAmount; + uint256 nftPriceInUnderlyingAsset; + uint256 maxBorrowAmount; + uint256 debtShare; + } + + function batchStake( + uint32 poolId, + address[] calldata nfts, + uint256[] calldata tokenIds, + uint256[] calldata borrowAmounts, + uint256 wusdStakingPoolId + ) public virtual whenNotPaused nonReentrant { + require(nfts.length == tokenIds.length, Errors.INCONSISTENT_PARAMS_LENGTH); + require(nfts.length == borrowAmounts.length, Errors.INCONSISTENT_PARAMS_LENGTH); + + for (uint i = 0; i < nfts.length; i++) { + _stake(poolId, nfts[i], tokenIds[i], borrowAmounts[i], wusdStakingPoolId); + } + } + + function stake( + uint32 poolId, + address nft, + uint256 tokenId, + uint256 borrowAmount, + uint256 wusdStakingPoolId + ) public virtual whenNotPaused nonReentrant { + _stake(poolId, nft, tokenId, borrowAmount, wusdStakingPoolId); + } + + function _stake( + uint32 poolId, + address nft, + uint256 tokenId, + uint256 borrowAmount, + uint256 wusdStakingPoolId + ) internal virtual { + StakeLocalVars memory vars; + + require(borrowAmount > 0, Errors.INVALID_AMOUNT); + + vars.yieldAccout = IYieldAccount(yieldAccounts[msg.sender]); + require(address(vars.yieldAccout) != address(0), Errors.YIELD_ACCOUNT_NOT_EXIST); + + YieldNftConfig storage nc = nftConfigs[nft]; + require(nc.isActive, Errors.YIELD_ETH_NFT_NOT_ACTIVE); + require(nc.leverageFactor > 0, Errors.YIELD_ETH_NFT_LEVERAGE_FACTOR_ZERO); + + // check the nft ownership + (vars.nftOwner, vars.nftSupplyMode, vars.nftLockerAddr) = poolYield.getERC721TokenData(poolId, nft, tokenId); + require(vars.nftOwner == msg.sender, Errors.INVALID_CALLER); + require(vars.nftSupplyMode == Constants.SUPPLY_MODE_ISOLATE, Errors.INVALID_SUPPLY_MODE); + + // only one staking plan for each nft + require(vars.nftLockerAddr == address(0), Errors.YIELD_ETH_NFT_ALREADY_USED); + + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + require(sd.yieldAccount == address(0), Errors.YIELD_ETH_NFT_ALREADY_USED); + + sd.yieldAccount = address(vars.yieldAccout); + sd.poolId = poolId; + sd.state = Constants.YIELD_STATUS_ACTIVE; + sd.wusdStakingPoolId = wusdStakingPoolId; + + vars.totalDebtAmount = borrowAmount; + + vars.nftPriceInUnderlyingAsset = getNftPriceInUnderlyingAsset(nft); + vars.maxBorrowAmount = vars.nftPriceInUnderlyingAsset.percentMul(nc.leverageFactor); + require(vars.totalDebtAmount <= vars.maxBorrowAmount, Errors.YIELD_ETH_EXCEED_MAX_BORROWABLE); + + // calculate debt share before borrow + vars.debtShare = convertToDebtShares(poolId, borrowAmount); + + // borrow from lending pool + poolYield.yieldBorrowERC20(poolId, address(underlyingAsset), borrowAmount); + + // stake in protocol and got the yield + protocolDeposit(sd, borrowAmount); + + // update nft shares + sd.debtShare += vars.debtShare; + + // update global shares + totalDebtShare += vars.debtShare; + + poolYield.yieldSetERC721TokenData(poolId, nft, tokenId, true, address(underlyingAsset)); + + // check hf + uint256 hf = calculateHealthFactor(nft, nc, sd); + require(hf >= nc.unstakeHeathFactor, Errors.YIELD_ETH_HEATH_FACTOR_TOO_LOW); + + emit Stake(msg.sender, nft, tokenId, borrowAmount); + } + + struct UnstakeLocalVars { + IYieldAccount yieldAccout; + address nftOwner; + uint8 nftSupplyMode; + address nftLockerAddr; + } + + function batchUnstake( + uint32 poolId, + address[] calldata nfts, + uint256[] calldata tokenIds, + uint256 unstakeFine + ) public virtual whenNotPaused nonReentrant { + require(nfts.length == tokenIds.length, Errors.INCONSISTENT_PARAMS_LENGTH); + + for (uint i = 0; i < nfts.length; i++) { + _unstake(poolId, nfts[i], tokenIds[i], unstakeFine); + } + } + + function unstake( + uint32 poolId, + address nft, + uint256 tokenId, + uint256 unstakeFine + ) public virtual whenNotPaused nonReentrant { + _unstake(poolId, nft, tokenId, unstakeFine); + } + + function _unstake(uint32 poolId, address nft, uint256 tokenId, uint256 unstakeFine) internal virtual { + UnstakeLocalVars memory vars; + + YieldNftConfig storage nc = nftConfigs[nft]; + require(nc.isActive, Errors.YIELD_ETH_NFT_NOT_ACTIVE); + + // check the nft ownership + (vars.nftOwner, vars.nftSupplyMode, vars.nftLockerAddr) = poolYield.getERC721TokenData(poolId, nft, tokenId); + require(vars.nftOwner == msg.sender || botAdmin == msg.sender, Errors.INVALID_CALLER); + require(vars.nftSupplyMode == Constants.SUPPLY_MODE_ISOLATE, Errors.INVALID_SUPPLY_MODE); + require(vars.nftLockerAddr == address(this), Errors.YIELD_ETH_NFT_NOT_USED_BY_ME); + + vars.yieldAccout = IYieldAccount(yieldAccounts[vars.nftOwner]); + require(address(vars.yieldAccout) != address(0), Errors.YIELD_ACCOUNT_NOT_EXIST); + + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + require(sd.state == Constants.YIELD_STATUS_ACTIVE, Errors.YIELD_ETH_STATUS_NOT_ACTIVE); + require(sd.poolId == poolId, Errors.YIELD_ETH_POOL_NOT_SAME); + + // sender must be bot or nft owner + if (msg.sender == botAdmin) { + require(unstakeFine <= nc.maxUnstakeFine, Errors.YIELD_ETH_EXCEED_MAX_FINE); + + uint256 hf = calculateHealthFactor(nft, nc, sd); + require(hf < nc.unstakeHeathFactor, Errors.YIELD_ETH_HEATH_FACTOR_TOO_HIGH); + + sd.unstakeFine = unstakeFine; + totalUnstakeFine += unstakeFine; + } + + sd.state = Constants.YIELD_STATUS_UNSTAKE; + (sd.withdrawAmount, ) = _getNftYieldInUnderlyingAsset(sd, true); + + protocolRequestWithdrawal(sd); + + emit Unstake(vars.nftOwner, nft, tokenId, sd.withdrawAmount); + } + + struct RepayLocalVars { + IYieldAccount yieldAccout; + address nftOwner; + uint8 nftSupplyMode; + address nftLockerAddr; + uint256 nftDebt; + uint256 remainAmount; + uint256 extraAmount; + uint256 repaidNftDebt; + uint256 repaidDebtShare; + } + + function batchRepay( + uint32 poolId, + address[] calldata nfts, + uint256[] calldata tokenIds + ) public virtual whenNotPaused nonReentrant { + require(nfts.length == tokenIds.length, Errors.INCONSISTENT_PARAMS_LENGTH); + + for (uint i = 0; i < nfts.length; i++) { + _repay(poolId, nfts[i], tokenIds[i]); + } + } + + function repay(uint32 poolId, address nft, uint256 tokenId) public virtual whenNotPaused nonReentrant { + _repay(poolId, nft, tokenId); + } + + function _repay(uint32 poolId, address nft, uint256 tokenId) internal virtual { + RepayLocalVars memory vars; + + YieldNftConfig memory nc = nftConfigs[nft]; + require(nc.isActive, Errors.YIELD_ETH_NFT_NOT_ACTIVE); + + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + require( + sd.state == Constants.YIELD_STATUS_UNSTAKE || sd.state == Constants.YIELD_STATUS_CLAIM, + Errors.YIELD_ETH_STATUS_NOT_UNSTAKE + ); + require(sd.poolId == poolId, Errors.YIELD_ETH_POOL_NOT_SAME); + + // check the nft ownership + (vars.nftOwner, vars.nftSupplyMode, vars.nftLockerAddr) = poolYield.getERC721TokenData(poolId, nft, tokenId); + require(vars.nftOwner == msg.sender || botAdmin == msg.sender, Errors.INVALID_CALLER); + require(vars.nftSupplyMode == Constants.SUPPLY_MODE_ISOLATE, Errors.INVALID_SUPPLY_MODE); + require(vars.nftLockerAddr == address(this), Errors.YIELD_ETH_NFT_NOT_USED_BY_ME); + + vars.yieldAccout = IYieldAccount(yieldAccounts[vars.nftOwner]); + require(address(vars.yieldAccout) != address(0), Errors.YIELD_ACCOUNT_NOT_EXIST); + + // withdraw yield from protocol + if (sd.state == Constants.YIELD_STATUS_UNSTAKE) { + require(protocolIsClaimReady(sd), Errors.YIELD_ETH_WITHDRAW_NOT_READY); + + sd.remainYieldAmount = protocolClaimWithdraw(sd); + + sd.state = Constants.YIELD_STATUS_CLAIM; + } + + vars.nftDebt = _getNftDebtInUnderlyingAsset(sd); + + /* + case 1: yield >= debt + fine can repay full by bot; + case 2: yield > debt but can not cover the fine, need user do the repay; + case 3: yield < debt, need user do the repay; + + bot admin will try to repay debt asap, to reduce the debt interest; + */ + + // compute repay value + if (sd.remainYieldAmount >= vars.nftDebt) { + vars.repaidNftDebt = vars.nftDebt; + vars.repaidDebtShare = sd.debtShare; + + vars.remainAmount = sd.remainYieldAmount - vars.nftDebt; + // vars.extraAmount = 0; + } else { + if (msg.sender == botAdmin) { + // bot admin only repay debt from yield + vars.repaidNftDebt = sd.remainYieldAmount; + vars.repaidDebtShare = convertToDebtShares(poolId, vars.repaidNftDebt); + } else { + // sender (owner) must repay all debt + vars.repaidNftDebt = vars.nftDebt; + vars.repaidDebtShare = sd.debtShare; + } + + // vars.remainAmount = 0; + vars.extraAmount = vars.nftDebt - sd.remainYieldAmount; + } + + // compute fine value + if (vars.remainAmount >= sd.unstakeFine) { + vars.remainAmount = vars.remainAmount - sd.unstakeFine; + } else { + vars.extraAmount = vars.extraAmount + (sd.unstakeFine - vars.remainAmount); + } + + sd.remainYieldAmount = vars.remainAmount; + + // transfer eth from sender exclude bot admin + if ((vars.extraAmount > 0) && (msg.sender != botAdmin)) { + underlyingAsset.safeTransferFrom(msg.sender, address(this), vars.extraAmount); + } + + // repay debt to lending pool + if (vars.repaidNftDebt > 0) { + poolYield.yieldRepayERC20(poolId, address(underlyingAsset), vars.repaidNftDebt); + } + + // update shares + sd.debtShare -= vars.repaidDebtShare; + totalDebtShare -= vars.repaidDebtShare; + + // unlock nft when repaid all debt and fine + if (msg.sender == botAdmin) { + if ((sd.debtShare > 0) || (vars.extraAmount > 0)) { + emit RepayPart(msg.sender, nft, tokenId, vars.repaidNftDebt); + return; + } + } + + // send remain funds to owner + if (sd.remainYieldAmount > 0) { + underlyingAsset.safeTransfer(vars.nftOwner, sd.remainYieldAmount); + sd.remainYieldAmount = 0; + } + + // unlock nft + poolYield.yieldSetERC721TokenData(poolId, nft, tokenId, false, address(underlyingAsset)); + + delete stakeDatas[nft][tokenId]; + + emit Repay(vars.nftOwner, nft, tokenId, vars.nftDebt); + } + + /****************************************************************************/ + /* Query Methods */ + /****************************************************************************/ + + function getNftConfig( + address nft + ) + public + view + virtual + returns ( + bool isActive, + uint16 leverageFactor, + uint16 collateralFactor, + uint256 maxUnstakeFine, + uint256 unstakeHeathFactor + ) + { + YieldNftConfig memory nc = nftConfigs[nft]; + return (nc.isActive, nc.leverageFactor, nc.collateralFactor, nc.maxUnstakeFine, nc.unstakeHeathFactor); + } + + function getYieldAccount(address user) public view virtual returns (address) { + return yieldAccounts[user]; + } + + function getTotalDebt(uint32 poolId) public view virtual returns (uint256) { + return poolYield.getYieldERC20BorrowBalance(poolId, address(underlyingAsset), address(this)); + } + + function getNftValueInUnderlyingAsset(address nft) public view virtual returns (uint256) { + YieldNftConfig storage nc = nftConfigs[nft]; + + uint256 nftPrice = getNftPriceInUnderlyingAsset(nft); + uint256 totalNftValue = nftPrice.percentMul(nc.collateralFactor); + return totalNftValue; + } + + function getNftDebtInUnderlyingAsset(address nft, uint256 tokenId) public view virtual returns (uint256) { + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + return _getNftDebtInUnderlyingAsset(sd); + } + + function getNftYieldInUnderlyingAsset( + address nft, + uint256 tokenId + ) public view virtual returns (uint256 yieldAmount, uint256 yieldValue) { + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + return _getNftYieldInUnderlyingAsset(sd, false); + } + + function getNftCollateralData( + address nft, + uint256 tokenId + ) public view virtual returns (uint256 totalCollateral, uint256 totalBorrow, uint256 availabeBorrow) { + YieldNftConfig storage nc = nftConfigs[nft]; + + uint256 nftPrice = getNftPriceInUnderlyingAsset(nft); + + totalCollateral = nftPrice.percentMul(nc.collateralFactor); + availabeBorrow = nftPrice.percentMul(nc.leverageFactor); + + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + totalBorrow = _getNftDebtInUnderlyingAsset(sd); + + if (availabeBorrow > totalBorrow) { + availabeBorrow = availabeBorrow - totalBorrow; + } else { + availabeBorrow = 0; + } + } + + function getNftCollateralDataList( + address[] calldata nfts, + uint256[] calldata tokenIds + ) + public + view + virtual + returns (uint256[] memory totalCollaterals, uint256[] memory totalBorrows, uint256[] memory availabeBorrows) + { + totalCollaterals = new uint256[](nfts.length); + totalBorrows = new uint256[](nfts.length); + availabeBorrows = new uint256[](nfts.length); + + for (uint i = 0; i < nfts.length; i++) { + (totalCollaterals[i], totalBorrows[i], availabeBorrows[i]) = getNftCollateralData(nfts[i], tokenIds[i]); + } + } + + function getNftStakeData( + address nft, + uint256 tokenId + ) public view virtual returns (uint32 poolId, uint8 state, uint256 debtAmount, uint256 yieldAmount) { + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + + state = sd.state; + if (sd.state == Constants.YIELD_STATUS_UNSTAKE) { + if (protocolIsClaimReady(sd)) { + state = Constants.YIELD_STATUS_CLAIM; + } + } + + debtAmount = _getNftDebtInUnderlyingAsset(sd); + + if (sd.state == Constants.YIELD_STATUS_ACTIVE) { + (yieldAmount, ) = _getNftYieldInUnderlyingAsset(sd, false); + } else { + yieldAmount = sd.withdrawAmount; + } + + return (sd.poolId, state, debtAmount, yieldAmount); + } + + function getNftStakeDataList( + address[] calldata nfts, + uint256[] calldata tokenIds + ) + public + view + virtual + returns ( + uint32[] memory poolIds, + uint8[] memory states, + uint256[] memory debtAmounts, + uint256[] memory yieldAmounts + ) + { + poolIds = new uint32[](nfts.length); + states = new uint8[](nfts.length); + debtAmounts = new uint256[](nfts.length); + yieldAmounts = new uint256[](nfts.length); + + for (uint i = 0; i < nfts.length; i++) { + (poolIds[i], states[i], debtAmounts[i], yieldAmounts[i]) = getNftStakeData(nfts[i], tokenIds[i]); + } + } + + function getNftUnstakeData( + address nft, + uint256 tokenId + ) public view virtual returns (uint256 unstakeFine, uint256 withdrawAmount, uint256 withdrawReqId) { + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + return (sd.unstakeFine, sd.withdrawAmount, sd.stakingPlanId); + } + + function getNftUnstakeDataList( + address[] calldata nfts, + uint256[] calldata tokenIds + ) + public + view + virtual + returns (uint256[] memory unstakeFines, uint256[] memory withdrawAmounts, uint256[] memory withdrawReqIds) + { + unstakeFines = new uint256[](nfts.length); + withdrawAmounts = new uint256[](nfts.length); + withdrawReqIds = new uint256[](nfts.length); + + for (uint i = 0; i < nfts.length; i++) { + (unstakeFines[i], withdrawAmounts[i], withdrawReqIds[i]) = getNftUnstakeData(nfts[i], tokenIds[i]); + } + } + + function getTotalUnstakeFine() public view virtual returns (uint256 totalFine, uint256 claimedFine) { + return (totalUnstakeFine, claimedUnstakeFine); + } + + function getNftYieldStakeDataStruct( + address nft, + uint256 tokenId + ) public view virtual returns (YieldStakeData memory) { + return stakeDatas[nft][tokenId]; + } + + function getWUSDStakingPools() + public + view + virtual + returns ( + uint256[] memory stakingPoolIds, + uint48[] memory stakingPeriods, + uint256[] memory apys, + uint256[] memory minStakingAmounts + ) + { + IWUSDStaking.StakingPoolDetail[] memory stakingPoolsDetails = wusdStaking.getGeneralStaking(); + + stakingPoolIds = new uint256[](stakingPoolsDetails.length); + stakingPeriods = new uint48[](stakingPoolsDetails.length); + apys = new uint256[](stakingPoolsDetails.length); + minStakingAmounts = new uint256[](stakingPoolsDetails.length); + for (uint i = 0; i < stakingPoolsDetails.length; i++) { + stakingPoolIds[i] = stakingPoolsDetails[i].stakingPoolId; + stakingPeriods[i] = stakingPoolsDetails[i].stakingPool.stakingPeriod; + apys[i] = stakingPoolsDetails[i].stakingPool.apy; + minStakingAmounts[i] = stakingPoolsDetails[i].stakingPool.minStakingAmount; + } + } + + function getWUSDStakingPlan( + address nft, + uint256 tokenId + ) public view virtual returns (IWUSDStaking.StakingPlan memory) { + YieldStakeData storage sd = stakeDatas[nft][tokenId]; + return wusdStaking.getUserStakingPlan(sd.yieldAccount, sd.stakingPlanId); + } + + /****************************************************************************/ + /* Internal Methods */ + /****************************************************************************/ + function protocolDeposit(YieldStakeData storage sd, uint256 amount) internal virtual returns (uint256) { + IYieldAccount yieldAccount = IYieldAccount(sd.yieldAccount); + + underlyingAsset.safeTransfer(address(yieldAccount), amount); + + bytes memory result = yieldAccount.execute( + address(wusdStaking), + abi.encodeWithSelector(IWUSDStaking.stake.selector, sd.wusdStakingPoolId, amount) + ); + sd.stakingPlanId = abi.decode(result, (uint256)); + require(sd.stakingPlanId > 0, Errors.YIELD_ETH_DEPOSIT_FAILED); + + return amount; + } + + function protocolRequestWithdrawal(YieldStakeData storage sd) internal virtual { + // check the end time of staking plan + IWUSDStaking.StakingPlan memory stakingPlan = wusdStaking.getUserStakingPlan(sd.yieldAccount, sd.stakingPlanId); + if (uint256(stakingPlan.endTime) >= block.timestamp) { + IYieldAccount yieldAccount = IYieldAccount(sd.yieldAccount); + yieldAccount.execute( + address(wusdStaking), + abi.encodeWithSelector(IWUSDStaking.terminate.selector, sd.stakingPlanId) + ); + } + } + + function protocolClaimWithdraw(YieldStakeData storage sd) internal virtual returns (uint256) { + IYieldAccount yieldAccount = IYieldAccount(sd.yieldAccount); + + uint256 claimedAmount = underlyingAsset.balanceOf(address(yieldAccount)); + uint256[] memory stakingPlanIds = new uint256[](1); + stakingPlanIds[0] = sd.stakingPlanId; + yieldAccount.execute(address(wusdStaking), abi.encodeWithSelector(IWUSDStaking.claim.selector, stakingPlanIds)); + claimedAmount = underlyingAsset.balanceOf(address(yieldAccount)) - claimedAmount; + require(claimedAmount > 0, Errors.YIELD_ETH_CLAIM_FAILED); + + yieldAccount.safeTransfer(address(underlyingAsset), address(this), claimedAmount); + + return claimedAmount; + } + + function protocolIsClaimReady(YieldStakeData storage sd) internal view virtual returns (bool) { + if (sd.state == Constants.YIELD_STATUS_CLAIM) { + return true; + } + + if (sd.state == Constants.YIELD_STATUS_UNSTAKE) { + IWUSDStaking.StakingPlan memory stakingPlan = wusdStaking.getUserStakingPlan(sd.yieldAccount, sd.stakingPlanId); + uint48 currentTime = uint48(block.timestamp); + + if (stakingPlan.stakingStatus == IWUSDStaking.StakingStatus.CLAIMABLE) { + // mannually terminate the plan before end time of mature + if (currentTime >= stakingPlan.claimableTimestamp) { + return true; + } + } else if (stakingPlan.endTime <= currentTime) { + // directly claim after end time of mature + return true; + } + } + + return false; + } + + function convertToDebtShares(uint32 poolId, uint256 assets) public view virtual returns (uint256) { + return assets.convertToShares(totalDebtShare, getTotalDebt(poolId), Math.Rounding.Down); + } + + function convertToDebtAssets(uint32 poolId, uint256 shares) public view virtual returns (uint256) { + return shares.convertToAssets(totalDebtShare, getTotalDebt(poolId), Math.Rounding.Down); + } + + function _getNftDebtInUnderlyingAsset(YieldStakeData storage sd) internal view virtual returns (uint256) { + return convertToDebtAssets(sd.poolId, sd.debtShare); + } + + function _getNftYieldInUnderlyingAsset( + YieldStakeData storage sd, + bool isUnstake + ) internal view virtual returns (uint256, uint256) { + IWUSDStaking.StakingPlan memory stakingPlan = wusdStaking.getUserStakingPlan(sd.yieldAccount, sd.stakingPlanId); + + // there's no yield token for WUSD staking, just the same token + // the yield amount here should include the pricipal and rewards + // the under amount is same with yield amount + uint256 yieldAmount = stakingPlan.stakedAmount; + + uint48 calcEndTime; + uint256 calcAPY; + if (block.timestamp >= stakingPlan.endTime) { + calcEndTime = stakingPlan.endTime; + calcAPY = stakingPlan.apy; + } else { + calcEndTime = uint48(block.timestamp); + if (isUnstake) { + calcAPY = wusdStaking.getBasicAPY(); + } else { + calcAPY = stakingPlan.apy; + } + } + yieldAmount += _calculateYield(stakingPlan.stakedAmount, calcAPY, calcEndTime - stakingPlan.startTime); + + return (yieldAmount, yieldAmount); + } + + function getProtocolTokenDecimals() internal view virtual returns (uint8) { + return 6; + } + + function getProtocolTokenPriceInUnderlyingAsset() internal view virtual returns (uint256) { + return 1e6; + } + + function getProtocolTokenAmountInUnderlyingAsset(uint256 yieldAmount) internal view virtual returns (uint256) { + // WUSD is not rebase model, the share are fixed + return yieldAmount; + } + + function getNftPriceInUnderlyingAsset(address nft) internal view virtual returns (uint256) { + IPriceOracleGetter priceOracle = IPriceOracleGetter(addressProvider.getPriceOracle()); + uint256 nftPriceInBase = priceOracle.getAssetPrice(nft); + uint256 underlyingAssetPriceInBase = priceOracle.getAssetPrice(address(underlyingAsset)); + return nftPriceInBase.mulDiv(10 ** underlyingAsset.decimals(), underlyingAssetPriceInBase); + } + + function calculateHealthFactor( + address nft, + YieldNftConfig storage nc, + YieldStakeData storage sd + ) internal view virtual returns (uint256) { + uint256 nftPrice = getNftPriceInUnderlyingAsset(nft); + uint256 totalNftValue = nftPrice.percentMul(nc.collateralFactor); + + (, uint256 totalYieldValue) = _getNftYieldInUnderlyingAsset(sd, false); + uint256 totalDebtValue = _getNftDebtInUnderlyingAsset(sd); + + return (totalNftValue + totalYieldValue).wadDiv(totalDebtValue); + } + + function _calculateYield( + uint256 stakedAmount, + uint256 apy, + uint48 stakingDuration + ) internal pure virtual returns (uint256) { + return (stakedAmount * apy * stakingDuration) / SECONDS_OF_YEAR / DENOMINATOR; + } +} diff --git a/test/helpers/TestUser.sol b/test/helpers/TestUser.sol index d1cf31f..cfbc16b 100644 --- a/test/helpers/TestUser.sol +++ b/test/helpers/TestUser.sol @@ -32,6 +32,21 @@ contract TestUser is ERC721Holder { uint256 internal _uid; uint256[] internal _tokenIds; + struct AttckerData { + uint32 poolId; + address asset; + uint8[] groups; + uint256 amount; + uint256[] amounts; + address nftAsset; + uint256[] tokenIds; + uint8 supplyMode; + address onBehalf; + address receiver; + } + AttckerData internal _attackData; + uint256 internal _attackType; + constructor(PoolManager poolManager_, uint256 uid_) { _poolManager = poolManager_; _BVault = BVault(_poolManager.moduleIdToProxy(Constants.MODULEID__BVAULT)); @@ -91,6 +106,10 @@ contract TestUser is ERC721Holder { ERC721(token).setApprovalForAll(spender, val); } + function transferERC20(address token, address to, uint256 amount) public { + ERC20(token).safeTransfer(to, amount); + } + function depositERC20(uint32 poolId, address asset, uint256 amount, address onBehalf) public { if (asset == Constants.NATIVE_TOKEN_ADDRESS) { uint256 sendVal = amount; @@ -164,6 +183,72 @@ contract TestUser is ERC721Holder { } } + function crossLiquidateERC20( + uint32 poolId, + address borrower, + address collateralAsset, + address debtAsset, + uint256 debtToCover, + bool supplyAsCollateral + ) public { + if (debtAsset == Constants.NATIVE_TOKEN_ADDRESS) { + _crossLiquidation.crossLiquidateERC20{value: debtToCover}( + poolId, + borrower, + collateralAsset, + debtAsset, + debtToCover, + supplyAsCollateral + ); + } else { + _crossLiquidation.crossLiquidateERC20( + poolId, + borrower, + collateralAsset, + debtAsset, + debtToCover, + supplyAsCollateral + ); + } + } + + function crossLiquidateERC721( + uint32 poolId, + address borrower, + address collateralAsset, + uint256[] calldata collateralTokenIds, + address debtAsset, + bool supplyAsCollateral + ) public { + _crossLiquidation.crossLiquidateERC721( + poolId, + borrower, + collateralAsset, + collateralTokenIds, + debtAsset, + supplyAsCollateral + ); + } + + function crossLiquidateERC721ByNative( + uint32 poolId, + address borrower, + address collateralAsset, + uint256[] calldata collateralTokenIds, + bool supplyAsCollateral, + uint256 msgValue + ) public { + address debtAsset = Constants.NATIVE_TOKEN_ADDRESS; + _crossLiquidation.crossLiquidateERC721{value: msgValue}( + poolId, + borrower, + collateralAsset, + collateralTokenIds, + debtAsset, + supplyAsCollateral + ); + } + function isolateBorrow( uint32 poolId, address nftAsset, @@ -298,4 +383,116 @@ contract TestUser is ERC721Holder { function setAuthorization(uint32 poolId, address asset, address operator, bool approved) public { _BVault.setAuthorization(poolId, asset, operator, approved); } + + function getReentrantAttackTypes() public pure returns (uint256[] memory attackTypes) { + attackTypes = new uint256[](9); + for (uint i = 0; i < 9; i++) { + attackTypes[i] = i + 1; + } + } + + function setAttackType(uint256 attackType_) public { + _attackType = attackType_; + } + + function setAttackData(AttckerData memory attackData_) public { + _attackData = attackData_; + } + + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + if (_attackType == 0) { + return this.onERC721Received.selector; + } + + if (_attackType == 1) { + _BVault.depositERC20(_attackData.poolId, _attackData.asset, _attackData.amount, _attackData.onBehalf); + } + + if (_attackType == 2) { + _BVault.withdrawERC20( + _attackData.poolId, + _attackData.asset, + _attackData.amount, + _attackData.onBehalf, + _attackData.receiver + ); + } + + if (_attackType == 3) { + _BVault.depositERC721( + _attackData.poolId, + _attackData.asset, + _attackData.tokenIds, + _attackData.supplyMode, + _attackData.onBehalf + ); + } + + if (_attackType == 4) { + _BVault.withdrawERC721( + _attackData.poolId, + _attackData.asset, + _attackData.tokenIds, + _attackData.supplyMode, + _attackData.onBehalf, + _attackData.receiver + ); + } + + if (_attackType == 5) { + _BVault.setERC721SupplyMode( + _attackData.poolId, + _attackData.asset, + _attackData.tokenIds, + _attackData.supplyMode, + _attackData.onBehalf + ); + } + + if (_attackType == 6) { + _crossLending.crossBorrowERC20( + _attackData.poolId, + _attackData.asset, + _attackData.groups, + _attackData.amounts, + _attackData.onBehalf, + _attackData.receiver + ); + } + + if (_attackType == 7) { + _crossLending.crossRepayERC20( + _attackData.poolId, + _attackData.asset, + _attackData.groups, + _attackData.amounts, + _attackData.onBehalf + ); + } + + if (_attackType == 8) { + _isolateLending.isolateBorrow( + _attackData.poolId, + _attackData.nftAsset, + _attackData.tokenIds, + _attackData.asset, + _attackData.amounts, + _attackData.onBehalf, + _attackData.receiver + ); + } + + if (_attackType == 9) { + _isolateLending.isolateRepay( + _attackData.poolId, + _attackData.nftAsset, + _attackData.tokenIds, + _attackData.asset, + _attackData.amounts, + _attackData.onBehalf + ); + } + + return this.onERC721Received.selector; + } } diff --git a/test/integration/TestIntYieldBorrowERC20.t.sol b/test/integration/TestIntYieldBorrowERC20.t.sol index 46e37ab..84fd9b8 100644 --- a/test/integration/TestIntYieldBorrowERC20.t.sol +++ b/test/integration/TestIntYieldBorrowERC20.t.sol @@ -2,11 +2,18 @@ pragma solidity ^0.8.0; import 'src/libraries/helpers/Constants.sol'; +import 'src/libraries/helpers/Errors.sol'; import 'test/helpers/TestUser.sol'; import 'test/setup/TestWithBaseAction.sol'; contract TestIntYieldBorrowERC20 is TestWithBaseAction { + struct TestCaseVars { + uint256 stakerCap; + uint256 stakerBorrow; + uint256 availableBorrow; + } + function onSetUp() public virtual override { super.onSetUp(); @@ -18,6 +25,90 @@ contract TestIntYieldBorrowERC20 is TestWithBaseAction { uint256 borrowAmount = 10 ether; + TestCaseVars memory varsBefore; + (varsBefore.stakerCap, varsBefore.stakerBorrow, varsBefore.availableBorrow) = tsPoolLens.getYieldStakerAssetData( + tsCommonPoolId, + address(tsWETH), + address(tsStaker1) + ); + assertGt(varsBefore.stakerCap, borrowAmount, 'varsBefore.stakerCap'); + assertEq(varsBefore.stakerBorrow, 0, 'varsBefore.stakerBorrow'); + assertEq(varsBefore.availableBorrow, varsBefore.stakerCap, 'varsBefore.availableBorrow'); + tsStaker1.yieldBorrowERC20(tsCommonPoolId, address(tsWETH), borrowAmount); + + TestCaseVars memory varsAfter; + (varsAfter.stakerCap, varsAfter.stakerBorrow, varsAfter.availableBorrow) = tsPoolLens.getYieldStakerAssetData( + tsCommonPoolId, + address(tsWETH), + address(tsStaker1) + ); + assertEq(varsAfter.stakerBorrow, borrowAmount, 'varsAfter.stakerBorrow'); + assertEq(varsAfter.availableBorrow, (varsAfter.stakerCap - borrowAmount), 'varsAfter.availableBorrow'); + } + + function test_ReverfIf_BorrowWETH_AssetYield_Disable() public { + prepareWETH(tsDepositor1); + + tsHEVM.prank(tsPoolAdmin); + tsConfigurator.setAssetYieldEnable(tsCommonPoolId, address(tsWETH), false); + + uint256 borrowAmount = 10 ether; + tsHEVM.expectRevert(bytes(Errors.ASSET_YIELD_NOT_ENABLE)); + tsStaker1.yieldBorrowERC20(tsCommonPoolId, address(tsWETH), borrowAmount); + } + + function test_ReverfIf_BorrowWETH_Invalid_Staker_CapZero() public { + prepareWETH(tsDepositor1); + + tsHEVM.prank(tsPoolAdmin); + tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsStaker1), address(tsWETH), 0); + + uint256 borrowAmount = 10 ether; + tsHEVM.expectRevert(bytes(Errors.YIELD_EXCEED_STAKER_CAP_LIMIT)); + tsStaker1.yieldBorrowERC20(tsCommonPoolId, address(tsWETH), borrowAmount); + } + + function test_ReverfIf_BorrowWETH_Exceed_StakerYieldCap() public { + prepareWETH(tsDepositor1); + + tsHEVM.prank(tsPoolAdmin); + tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsStaker1), address(tsWETH), 100); // 1% + + TestCaseVars memory varsBefore; + (varsBefore.stakerCap, varsBefore.stakerBorrow, varsBefore.availableBorrow) = tsPoolLens.getYieldStakerAssetData( + tsCommonPoolId, + address(tsWETH), + address(tsStaker1) + ); + + // borrow all available + uint256 borrowAmount = varsBefore.availableBorrow; + tsStaker1.yieldBorrowERC20(tsCommonPoolId, address(tsWETH), borrowAmount); + + // borrow more + uint256 borrowAmount2 = 0.123 ether; + tsHEVM.expectRevert(bytes(Errors.YIELD_EXCEED_STAKER_CAP_LIMIT)); + tsStaker1.yieldBorrowERC20(tsCommonPoolId, address(tsWETH), borrowAmount2); + } + + function test_ReverfIf_BorrowWETH_Exceed_AssetYieldCap() public { + prepareWETH(tsDepositor1); + + TestCaseVars memory varsBefore; + (varsBefore.stakerCap, varsBefore.stakerBorrow, varsBefore.availableBorrow) = tsPoolLens.getYieldStakerAssetData( + tsCommonPoolId, + address(tsWETH), + address(tsStaker1) + ); + + // borrow all available + uint256 borrowAmount = varsBefore.availableBorrow; + tsStaker1.yieldBorrowERC20(tsCommonPoolId, address(tsWETH), borrowAmount); + + // borrow more + uint256 borrowAmount2 = 0.123 ether; + tsHEVM.expectRevert(bytes(Errors.YIELD_EXCEED_ASSET_CAP_LIMIT)); + tsStaker1.yieldBorrowERC20(tsCommonPoolId, address(tsWETH), borrowAmount2); } } diff --git a/test/integration/TestIntYieldRepayERC20.t.sol b/test/integration/TestIntYieldRepayERC20.t.sol index 08e2dde..38ffc1c 100644 --- a/test/integration/TestIntYieldRepayERC20.t.sol +++ b/test/integration/TestIntYieldRepayERC20.t.sol @@ -7,6 +7,12 @@ import 'test/helpers/TestUser.sol'; import 'test/setup/TestWithBaseAction.sol'; contract TestIntYieldRepayERC20 is TestWithBaseAction { + struct TestCaseVars { + uint256 stakerCap; + uint256 stakerBorrow; + uint256 availableBorrow; + } + function onSetUp() public virtual override { super.onSetUp(); @@ -23,12 +29,31 @@ contract TestIntYieldRepayERC20 is TestWithBaseAction { // make some interest advanceTimes(365 days); + TestCaseVars memory varsBefore; + (varsBefore.stakerCap, varsBefore.stakerBorrow, varsBefore.availableBorrow) = tsPoolLens.getYieldStakerAssetData( + tsCommonPoolId, + address(tsWETH), + address(tsStaker1) + ); + assertGt(varsBefore.stakerBorrow, borrowAmount, 'varsBefore.stakerBorrow'); + assertLt(varsBefore.availableBorrow, varsBefore.stakerCap, 'varsBefore.availableBorrow'); + // repay full uint256 repayAmount = tsPoolLens.getYieldERC20BorrowBalance(tsCommonPoolId, address(tsWETH), address(tsStaker1)); assertGt(repayAmount, borrowAmount, 'yield balance should have interest'); + assertEq(varsBefore.stakerBorrow, repayAmount, 'varsBefore.stakerBorrow not eq yield balance'); tsStaker1.approveERC20(address(tsWETH), type(uint256).max); tsStaker1.yieldRepayERC20(tsCommonPoolId, address(tsWETH), repayAmount); + + TestCaseVars memory varsAfter; + (varsAfter.stakerCap, varsAfter.stakerBorrow, varsAfter.availableBorrow) = tsPoolLens.getYieldStakerAssetData( + tsCommonPoolId, + address(tsWETH), + address(tsStaker1) + ); + assertEq(varsAfter.stakerBorrow, 0, 'varsAfter.stakerBorrow'); + assertEq(varsAfter.availableBorrow, varsAfter.stakerCap, 'varsAfter.availableBorrow'); } } diff --git a/test/integration/TestPriceOracle.t.sol b/test/integration/TestPriceOracle.t.sol index ad7b3c0..b3560f1 100644 --- a/test/integration/TestPriceOracle.t.sol +++ b/test/integration/TestPriceOracle.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {IPriceOracle} from 'src/interfaces/IPriceOracle.sol'; import {Errors} from 'src/libraries/helpers/Errors.sol'; +import {Constants} from 'src/libraries/helpers/Constants.sol'; import {PriceOracle} from 'src/PriceOracle.sol'; import 'test/mocks/MockERC20.sol'; @@ -19,10 +20,15 @@ contract TestPriceOracle is TestWithSetup { MockChainlinkAggregator mockCLAgg; MockERC721 mockErc721; MockBendNFTOracle mockNftOracle; + MockERC20 mockErc20InTO; + MockBendNFTOracle mockTokenOracle; address[] mockAssetAddrs; address[] mockClAggAddrs; + address[] sourceAssetAddrs; + uint8[] sourceTypes; + function onSetUp() public virtual override { super.onSetUp(); @@ -31,13 +37,24 @@ contract TestPriceOracle is TestWithSetup { mockCLAgg = new MockChainlinkAggregator(8, 'ETH / USD'); mockErc721 = new MockERC721('TNFT', 'TNFT'); - mockNftOracle = new MockBendNFTOracle(); + mockNftOracle = new MockBendNFTOracle(18); + + mockErc20InTO = new MockERC20('TITO', 'TITO', 18); + mockTokenOracle = new MockBendNFTOracle(8); mockAssetAddrs = new address[](1); mockAssetAddrs[0] = address(mockErc20); mockClAggAddrs = new address[](1); mockClAggAddrs[0] = address(mockCLAgg); + + sourceAssetAddrs = new address[](2); + sourceAssetAddrs[0] = address(mockErc721); + sourceAssetAddrs[1] = address(mockErc20InTO); + + sourceTypes = new uint8[](2); + sourceTypes[0] = Constants.ORACLE_TYPE_BEND_NFT; + sourceTypes[1] = Constants.ORACLE_TYPE_BEND_TOKEN; } function test_RevertIf_CallerNotAdmin() public { @@ -45,9 +62,17 @@ contract TestPriceOracle is TestWithSetup { tsHEVM.prank(address(tsHacker1)); tsPriceOracle.setAssetChainlinkAggregators(mockAssetAddrs, mockClAggAddrs); + tsHEVM.expectRevert(bytes(Errors.CALLER_NOT_ORACLE_ADMIN)); + tsHEVM.prank(address(tsHacker1)); + tsPriceOracle.setAssetOracleSourceTypes(sourceAssetAddrs, sourceTypes); + tsHEVM.expectRevert(bytes(Errors.CALLER_NOT_ORACLE_ADMIN)); tsHEVM.prank(address(tsHacker1)); tsPriceOracle.setBendNFTOracle(address(mockNftOracle)); + + tsHEVM.expectRevert(bytes(Errors.CALLER_NOT_ORACLE_ADMIN)); + tsHEVM.prank(address(tsHacker1)); + tsPriceOracle.setBendTokenOracle(address(mockTokenOracle)); } function test_Should_SetAggregators() public { @@ -59,6 +84,15 @@ contract TestPriceOracle is TestWithSetup { assertEq(retAggs[0], mockClAggAddrs[0], 'retAggs address not match'); } + function test_Should_SetSourceTypes() public { + tsHEVM.prank(tsOracleAdmin); + tsPriceOracle.setAssetOracleSourceTypes(sourceAssetAddrs, sourceTypes); + + uint8[] memory retTypes = tsPriceOracle.getAssetOracleSourceTypes(sourceAssetAddrs); + assertEq(retTypes.length, sourceAssetAddrs.length, 'retTypes length not match'); + assertEq(retTypes[0], sourceTypes[0], 'retTypes address not match'); + } + function test_Should_SetNftOracle() public { tsHEVM.prank(tsOracleAdmin); tsPriceOracle.setBendNFTOracle(address(mockNftOracle)); @@ -67,6 +101,14 @@ contract TestPriceOracle is TestWithSetup { assertEq(retNftOracle, address(mockNftOracle), 'retNftOracle address not match'); } + function test_Should_SetTokenOracle() public { + tsHEVM.prank(tsOracleAdmin); + tsPriceOracle.setBendTokenOracle(address(mockTokenOracle)); + + address retTokenOracle = tsPriceOracle.getBendTokenOracle(); + assertEq(retTokenOracle, address(mockTokenOracle), 'retTokenOracle address not match'); + } + function test_Should_GetAssetPriceFromChainlink() public { IPriceOracle oracle = IPriceOracle(tsPriceOracle); @@ -91,20 +133,66 @@ contract TestPriceOracle is TestWithSetup { tsHEVM.prank(tsOracleAdmin); tsPriceOracle.setBendNFTOracle(address(mockNftOracle)); - mockNftOracle.setAssetPrice(oracle.NFT_BASE_CURRENCY(), oracle.NFT_BASE_CURRENCY_UNIT()); - uint256 nftBaseCurrencyInBase = tsPriceOracle.getAssetPrice(oracle.NFT_BASE_CURRENCY()); - uint256 retPrice0 = tsPriceOracle.getAssetPriceFromBendNFTOracle(oracle.NFT_BASE_CURRENCY()); - assertEq(retPrice0, nftBaseCurrencyInBase, 'retPrice0 not match'); - mockNftOracle.setAssetPrice(address(mockErc721), 1234); - uint256 checkPrice2 = (1234 * nftBaseCurrencyInBase) / oracle.NFT_BASE_CURRENCY_UNIT(); + mockNftOracle.setAssetPrice(address(mockErc721), 1.234 ether); + uint256 checkPrice2 = (1.234 ether * nftBaseCurrencyInBase) / oracle.NFT_BASE_CURRENCY_UNIT(); uint256 retPrice2 = tsPriceOracle.getAssetPriceFromBendNFTOracle(address(mockErc721)); assertEq(retPrice2, checkPrice2, 'retPrice2 not match'); - mockNftOracle.setAssetPrice(address(mockErc721), 4321); - uint256 checkPrice3 = (4321 * nftBaseCurrencyInBase) / oracle.NFT_BASE_CURRENCY_UNIT(); + mockNftOracle.setAssetPrice(address(mockErc721), 4.321 ether); + uint256 checkPrice3 = (4.321 ether * nftBaseCurrencyInBase) / oracle.NFT_BASE_CURRENCY_UNIT(); uint256 retPrice3 = tsPriceOracle.getAssetPriceFromBendNFTOracle(address(mockErc721)); assertEq(retPrice3, checkPrice3, 'retPrice3 not match'); } + + function test_Should_getAssetPriceFromBendTokenOracle() public { + tsHEVM.prank(tsOracleAdmin); + tsPriceOracle.setBendTokenOracle(address(mockTokenOracle)); + + mockTokenOracle.setAssetPrice(address(mockErc20InTO), 1234); + uint256 retPrice2 = tsPriceOracle.getAssetPriceFromBendTokenOracle(address(mockErc20InTO)); + assertEq(retPrice2, 1234, 'retPrice2 not match'); + + mockTokenOracle.setAssetPrice(address(mockErc20InTO), 4321); + uint256 retPrice3 = tsPriceOracle.getAssetPriceFromBendTokenOracle(address(mockErc20InTO)); + assertEq(retPrice3, 4321, 'retPrice3 not match'); + } + + function test_Should_getAssetPrice() public { + IPriceOracle oracle = IPriceOracle(tsPriceOracle); + + tsHEVM.startPrank(tsOracleAdmin); + + tsPriceOracle.setBendTokenOracle(address(mockTokenOracle)); + + tsPriceOracle.setBendNFTOracle(address(mockNftOracle)); + + tsPriceOracle.setAssetChainlinkAggregators(mockAssetAddrs, mockClAggAddrs); + + tsPriceOracle.setAssetOracleSourceTypes(sourceAssetAddrs, sourceTypes); + + tsHEVM.stopPrank(); + + mockCLAgg.updateAnswer(1001); + mockNftOracle.setAssetPrice(address(mockErc721), 2.002 ether); + mockTokenOracle.setAssetPrice(address(mockErc20InTO), 3003); + + uint256 nftBaseCurrencyInBase = tsPriceOracle.getAssetPrice(oracle.NFT_BASE_CURRENCY()); + + uint256 retPrice1 = oracle.getAssetPrice(address(mockErc20)); + assertEq(retPrice1, 1001, 'retPrice1 not match'); + + uint256 retPrice2 = oracle.getAssetPrice(address(mockErc721)); + uint256 checkPrice2 = (2.002 ether * nftBaseCurrencyInBase) / oracle.NFT_BASE_CURRENCY_UNIT(); + assertEq(retPrice2, checkPrice2, 'retPrice2 not match'); + + uint256 retPrice3 = oracle.getAssetPrice(address(mockErc20InTO)); + assertEq(retPrice3, 3003, 'retPrice3 not match'); + } + + function test_RevertIf_getAssetPrice_NotExist() public { + tsHEVM.expectRevert(bytes(Errors.ASSET_ORACLE_NOT_EXIST)); + tsPriceOracle.getAssetPrice(address(mockErc20NotUsed)); + } } diff --git a/test/integration/TestReentrantAttack.t.sol b/test/integration/TestReentrantAttack.t.sol new file mode 100644 index 0000000..940db28 --- /dev/null +++ b/test/integration/TestReentrantAttack.t.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import 'src/libraries/helpers/Constants.sol'; +import 'src/libraries/helpers/Errors.sol'; + +import 'test/helpers/TestUser.sol'; +import 'test/setup/TestWithBaseAction.sol'; + +contract TestReentrantAttack is TestWithBaseAction { + function onSetUp() public virtual override { + super.onSetUp(); + + initCommonPools(); + } + + function test_RevertIf_CrossLiquidateERC721() public { + prepareUSDT(tsDepositor1); + + uint256[] memory depTokenIds = prepareCrossBAYC(tsBorrower1); + + TestUserAccountData memory accountDataBeforeBorrow = getUserAccountData(address(tsBorrower1), tsCommonPoolId); + + // borrow some eth + uint8[] memory borrowGroups = new uint8[](1); + borrowGroups[0] = tsLowRateGroupId; + + uint256[] memory borrowAmounts = new uint256[](1); + borrowAmounts[0] = + (accountDataBeforeBorrow.availableBorrowInBase * (10 ** tsUSDT.decimals())) / + tsPriceOracle.getAssetPrice(address(tsUSDT)); + + tsBorrower1.crossBorrowERC20( + tsCommonPoolId, + address(tsUSDT), + borrowGroups, + borrowAmounts, + address(tsBorrower1), + address(tsBorrower1) + ); + + // make some interest + advanceTimes(365 days); + + // drop down price and lower heath factor + uint256 baycCurPrice = tsBendNFTOracle.getAssetPrice(address(tsBAYC)); + uint256 baycNewPrice = (baycCurPrice * 75) / 100; + tsBendNFTOracle.setAssetPrice(address(tsBAYC), baycNewPrice); + + // liquidate some eth + tsLiquidator1.approveERC20(address(tsUSDT), type(uint256).max); + + uint256[] memory liqTokenIds = new uint256[](1); + liqTokenIds[0] = depTokenIds[0]; + + uint256[] memory attackTypes = tsLiquidator1.getReentrantAttackTypes(); + for (uint i = 0; i < attackTypes.length; i++) { + tsLiquidator1.setAttackType(attackTypes[i]); + + tsHEVM.expectRevert(bytes(Errors.REENTRANCY_ALREADY_LOCKED)); + tsLiquidator1.crossLiquidateERC721( + tsCommonPoolId, + address(tsBorrower1), + address(tsBAYC), + liqTokenIds, + address(tsUSDT), + false + ); + } + } + + function prepareBorrow(TestUser user, address nftAsset, uint256[] memory tokenIds, address debtAsset) internal { + TestLoanData memory loanDataBeforeBorrow = getIsolateCollateralData(tsCommonPoolId, nftAsset, 0, debtAsset); + + uint256[] memory borrowAmounts = new uint256[](tokenIds.length); + for (uint256 i = 0; i < tokenIds.length; i++) { + borrowAmounts[i] = loanDataBeforeBorrow.availableBorrow - (i + 1); + } + + user.isolateBorrow(tsCommonPoolId, nftAsset, tokenIds, debtAsset, borrowAmounts, address(user), address(user)); + } + + function prepareAuction(TestUser user, address nftAsset, uint256[] memory tokenIds, address debtAsset) internal { + user.approveERC20(debtAsset, type(uint256).max); + + uint256[] memory bidAmounts = new uint256[](tokenIds.length); + for (uint256 i = 0; i < tokenIds.length; i++) { + TestLoanData memory loanDataBeforeAuction = getIsolateLoanData(tsCommonPoolId, nftAsset, tokenIds[i]); + assertLt(loanDataBeforeAuction.healthFactor, 1e18, 'healthFactor not lt 1'); + bidAmounts[i] = (loanDataBeforeAuction.borrowAmount * 1011) / 1000; + + (uint256 borrowAmount /*uint256 thresholdPrice*/, , uint256 liquidatePrice) = tsPoolLens.getIsolateLiquidateData( + tsCommonPoolId, + address(tsBAYC), + tokenIds[i] + ); + assertLe(borrowAmount, liquidatePrice, 'borrowAmount not le liquidatePrice'); + } + + user.isolateAuction(tsCommonPoolId, nftAsset, tokenIds, debtAsset, bidAmounts); + } + + function test_RevertIf_IsolateLiquidate() public { + // deposit + prepareWETH(tsDepositor1); + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + // borrow + prepareBorrow(tsBorrower1, address(tsBAYC), tokenIds, address(tsWETH)); + + // make some interest + advanceTimes(365 days); + + // drop down nft price + actionSetNftPrice(address(tsBAYC), 5000); + + // auction + prepareAuction(tsLiquidator1, address(tsBAYC), tokenIds, address(tsWETH)); + + // end the auction + advanceTimes(25 hours); + + uint256[] memory liquidateAmounts = new uint256[](tokenIds.length); + + uint256[] memory attackTypes = tsLiquidator1.getReentrantAttackTypes(); + for (uint i = 0; i < attackTypes.length; i++) { + tsLiquidator1.setAttackType(attackTypes[i]); + + tsHEVM.expectRevert(bytes(Errors.REENTRANCY_ALREADY_LOCKED)); + tsLiquidator1.isolateLiquidate( + tsCommonPoolId, + address(tsBAYC), + tokenIds, + address(tsWETH), + liquidateAmounts, + false + ); + } + } +} diff --git a/test/mocks/MockBendNFTOracle.sol b/test/mocks/MockBendNFTOracle.sol index 526c7fc..e3523d0 100644 --- a/test/mocks/MockBendNFTOracle.sol +++ b/test/mocks/MockBendNFTOracle.sol @@ -4,8 +4,14 @@ pragma solidity ^0.8.0; import 'src/interfaces/IBendNFTOracle.sol'; contract MockBendNFTOracle is IBendNFTOracle { + uint8 public decimals; mapping(address => uint256) public prices; mapping(address => uint256) public latestTimestamps; + mapping(address => uint256) public priceFeedLengths; + + constructor(uint8 decimals_) { + decimals = decimals_; + } function getAssetPrice(address _nftContract) public view returns (uint256 price) { price = prices[_nftContract]; @@ -14,9 +20,18 @@ contract MockBendNFTOracle is IBendNFTOracle { function setAssetPrice(address _nftContract, uint256 price) public { prices[_nftContract] = price; latestTimestamps[_nftContract] = block.timestamp; + priceFeedLengths[_nftContract] += 1; } function getLatestTimestamp(address _nftContract) public view returns (uint256) { return latestTimestamps[_nftContract]; } + + function getDecimals() external view returns (uint8) { + return decimals; + } + + function getPriceFeedLength(address _nftContract) external view returns (uint256 length) { + return priceFeedLengths[_nftContract]; + } } diff --git a/test/mocks/MockSUSDS.sol b/test/mocks/MockSUSDS.sol new file mode 100644 index 0000000..6d0247d --- /dev/null +++ b/test/mocks/MockSUSDS.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +import {ISavingsUSDS, IERC20Metadata} from 'src/yield/susds/ISavingsUSDS.sol'; + +contract MockSUSDS is ISavingsUSDS, ERC20, Ownable2Step { + uint256 private constant RAY = 10 ** 27; + + address private _usds; + uint8 private _decimals; + uint256 private _ratio; + // Savings yield + uint192 private _chi; // The Rate Accumulator [ray] + uint64 private _rho; // Time of last drip [unix epoch time] + uint256 private _ssr; // The USDS Savings Rate [ray] + + constructor(address usds_) ERC20('Savings USDS', 'sUSDS') { + _usds = usds_; + _decimals = 18; + _ratio = (RAY * 1000) / 925; // 7.5% APR; + + _chi = uint192(RAY); + _rho = uint64(block.timestamp); + _ssr = RAY; + } + + function setShareRatio(uint256 ratio_) public onlyOwner { + _ratio = ratio_; + } + + function getShareRatio() public view returns (uint256) { + return _ratio; + } + + function usds() public view returns (address) { + return _usds; + } + + function setChi(uint192 chi_) public onlyOwner { + _chi = chi_; + } + + function chi() public view returns (uint192) { + return _chi; + } + + function setRho(uint64 rho_) public onlyOwner { + _rho = rho_; + } + + function rho() public view returns (uint64) { + return _rho; + } + + function setSsr(uint256 ssr_) public onlyOwner { + _ssr = ssr_; + } + + function ssr() public view returns (uint256) { + return _ssr; + } + + function deposit(uint256 assets, address receiver) public returns (uint256 shares) { + ERC20(_usds).transferFrom(msg.sender, address(this), assets); + + shares = convertToShares(assets); + _mint(receiver, shares); + + return shares; + } + + function redeem(uint256 shares, address receiver, address owner) public returns (uint256 assets) { + _burn(owner, shares); + + assets = convertToAssets(shares); + + ERC20(_usds).transfer(receiver, assets); + + return assets; + } + + function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares) { + shares = convertToShares(assets); + _burn(owner, shares); + + ERC20(_usds).transfer(receiver, assets); + + return shares; + } + + function convertToShares(uint256 assets) public view returns (uint256) { + return (assets * RAY) / _ratio; + } + + function convertToAssets(uint256 shares) public view returns (uint256) { + return (shares * _ratio) / RAY; + } + + function rebase(address receiver, uint256 amount) public returns (uint256) { + ERC20(_usds).transferFrom(msg.sender, address(this), amount); + + uint256 shares = convertToShares(amount); + _mint(receiver, shares); + + return shares; + } + + function decimals() public view override(ERC20, IERC20Metadata) returns (uint8) { + return _decimals; + } + + function transferUSDS(address receiver) public onlyOwner { + uint256 amount = ERC20(_usds).balanceOf(address(this)); + ERC20(_usds).transfer(receiver, amount); + } +} diff --git a/test/mocks/MockWUSDStaking.sol b/test/mocks/MockWUSDStaking.sol new file mode 100644 index 0000000..79d87d7 --- /dev/null +++ b/test/mocks/MockWUSDStaking.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +import {IWUSDStaking} from 'src/yield/wusd/IWUSDStaking.sol'; + +contract MockWUSDStaking is IWUSDStaking, Ownable2Step { + using SafeERC20 for IERC20; + + /** constant */ + uint48 constant FLEXIBLE_STAKING = type(uint48).max; + uint48 public constant SECONDS_OF_YEAR = 31536000; + uint256 constant DENOMINATOR = 1e6; + + address public WUSD; + address private _poolAddress; + uint256 private _numberOfStakingPools; + uint256 private _basicAPY; + uint48 private _cooldownDuration; + mapping(uint256 => StakingPool) private _stakingPools; + mapping(address => mapping(uint256 => StakingPlan)) private _userStakingPlans; + mapping(address => uint256) private _totalStakingPlansByAddress; + + constructor(address wusd) { + WUSD = wusd; + + _poolAddress = address(this); + _basicAPY = 50000; + _cooldownDuration = 172800; + } + + function stake(uint256 stakingPoolId, uint256 stakingAmount) external returns (uint256) { + StakingPool memory stakingPool = _stakingPools[stakingPoolId]; + + IERC20(WUSD).safeTransferFrom(_msgSender(), _poolAddress, stakingAmount); + + uint48 endTime = uint48(block.timestamp) + stakingPool.stakingPeriod; + uint256 stakingPlanId = ++_totalStakingPlansByAddress[_msgSender()]; + + _userStakingPlans[_msgSender()][stakingPlanId] = StakingPlan({ + stakingPoolId: stakingPoolId, + stakedAmount: stakingAmount, + apy: stakingPool.apy, + startTime: uint48(block.timestamp), + endTime: endTime, + claimableTimestamp: 0, + yield: 0, + stakingStatus: StakingStatus.ACTIVE, + claimType: ClaimType.UNCLAIMED + }); + + return stakingPlanId; + } + + function terminate(uint256 stakingPlanId) external { + StakingPlan storage stakingPlan = _userStakingPlans[_msgSender()][stakingPlanId]; + + if (stakingPlan.stakingStatus != StakingStatus.ACTIVE) { + revert('InvalidStakingPlan'); + } + if (uint256(stakingPlan.endTime) < block.timestamp) { + revert('MaturedStakingPlan'); + } + + stakingPlan.claimType = ClaimType.PREMATURED; + stakingPlan.yield = _calculateYield( + stakingPlan.stakedAmount, + _basicAPY, + uint48(block.timestamp) - stakingPlan.startTime + ); + stakingPlan.claimableTimestamp = uint48(block.timestamp) + _cooldownDuration; + stakingPlan.stakingStatus = StakingStatus.CLAIMABLE; + } + + function claim(uint256[] calldata stakingPlanIds) external { + uint256 totalReceived = 0; + + for (uint256 i = 0; i < stakingPlanIds.length; i++) { + StakingPlan storage stakingPlan = _claim(stakingPlanIds[i]); + totalReceived += stakingPlan.stakedAmount + stakingPlan.yield; + } + + if (_poolAddress == address(this)) { + IERC20(WUSD).safeTransfer(_msgSender(), totalReceived); + } else { + IERC20(WUSD).safeTransferFrom(_poolAddress, _msgSender(), totalReceived); + } + } + + function _claim(uint256 stakingPlanId) internal returns (StakingPlan storage) { + if (stakingPlanId == 0) { + revert('InvalidId'); + } + + StakingPlan storage stakingPlan = _userStakingPlans[_msgSender()][stakingPlanId]; + + if (stakingPlan.apy == 0) { + revert('InvalidStakingPlan'); + } + + if (stakingPlan.stakingStatus == StakingStatus.CLAIMED) { + revert('StakeIsClaimed'); + } + + uint48 currentTime = uint48(block.timestamp); + + if (stakingPlan.stakingStatus == StakingStatus.CLAIMABLE) { + if (currentTime < stakingPlan.claimableTimestamp) { + revert('ClaimableTimestampNotReached'); + } + } else if (stakingPlan.endTime > currentTime) { + revert('UnclaimableStakingPlan'); + } else { + stakingPlan.claimType = ClaimType.MATURED; + stakingPlan.yield = _calculateYield( + stakingPlan.stakedAmount, + stakingPlan.apy, + stakingPlan.endTime - stakingPlan.startTime + ); + } + stakingPlan.stakingStatus = StakingStatus.CLAIMED; + + return stakingPlan; + } + + function _calculateYield(uint256 stakedAmount, uint256 apy, uint48 stakingDuration) internal pure returns (uint256) { + return (stakedAmount * apy * stakingDuration) / SECONDS_OF_YEAR / DENOMINATOR; + } + + function getGeneralStaking() external view returns (StakingPoolDetail[] memory stakingPoolsDetail) { + stakingPoolsDetail = new StakingPoolDetail[](_numberOfStakingPools); + + // stakingPoolId starts from 1 + uint256 activePoolCount = 0; + for (uint256 i = 1; i <= _numberOfStakingPools; i++) { + StakingPool memory stakingPool = _stakingPools[i]; + + if (stakingPool.status == StakingPoolStatus.ACTIVE) { + stakingPoolsDetail[activePoolCount].stakingPoolId = i; + stakingPoolsDetail[activePoolCount].stakingPool = stakingPool; + activePoolCount++; + } + } + + assembly { + mstore(stakingPoolsDetail, activePoolCount) + } + + return stakingPoolsDetail; + } + + function getStakingPoolDetails(uint256 stakingPoolId) external view returns (StakingPool memory) { + return _stakingPools[stakingPoolId]; + } + + function getUserStakingPlans(address staker) external view returns (StakingPlanDetail[] memory stakingRecords) { + uint256 totalPlansByStaker = _totalStakingPlansByAddress[staker]; + stakingRecords = new StakingPlanDetail[](totalPlansByStaker); + + for (uint256 i = 0; i < totalPlansByStaker; i++) { + stakingRecords[i].stakingPlanId = i + 1; + stakingRecords[i].stakingPlan = _userStakingPlans[staker][i + 1]; + } + + return stakingRecords; + } + + function getUserStakingPlan(address staker, uint256 stakingPlanId) external view returns (StakingPlan memory) { + return _userStakingPlans[staker][stakingPlanId]; + } + + function setPoolAddress(address newPoolAddress) external onlyOwner { + _poolAddress = newPoolAddress; + } + + function getPoolAddress() external view returns (address) { + return _poolAddress; + } + + function createStakingPool(uint48 stakingPeriod, uint256 apy, uint256 minStakingAmount) external onlyOwner { + uint256 stakingPoolId = ++_numberOfStakingPools; + _stakingPools[stakingPoolId] = StakingPool({ + stakingPeriod: stakingPeriod, + apy: apy, + minStakingAmount: minStakingAmount, + status: StakingPoolStatus.ACTIVE + }); + } + + function updateStakingPoolAPY(uint256 stakingPoolId, uint256 newAPY) external onlyOwner { + _stakingPools[stakingPoolId].apy = newAPY; + } + + function updateStakingPoolStatus(uint256 stakingPoolId, StakingPoolStatus stakingPoolStatus) external onlyOwner { + _stakingPools[stakingPoolId].status = stakingPoolStatus; + } + + function setBasicAPY(uint256 newBasicAPY) external onlyOwner { + _basicAPY = newBasicAPY; + } + + function getBasicAPY() external view returns (uint256) { + return _basicAPY; + } + + function setCooldownDuration(uint48 newCooldownDuration) external onlyOwner { + _cooldownDuration = newCooldownDuration; + } + + function getCooldownDuration() external view returns (uint256) { + return _cooldownDuration; + } +} diff --git a/test/mocks/MockWeETH.sol b/test/mocks/MockWeETH.sol new file mode 100644 index 0000000..4ec9e24 --- /dev/null +++ b/test/mocks/MockWeETH.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {IeETH} from 'src/yield/etherfi/IeETH.sol'; +import {ILiquidityPool} from 'src/yield/etherfi/ILiquidityPool.sol'; + +contract MockWeETH is ERC20, Ownable2Step { + //-------------------------------------------------------------------------------------- + //--------------------------------- STATE-VARIABLES ---------------------------------- + //-------------------------------------------------------------------------------------- + + IeETH public eETH; + ILiquidityPool public liquidityPool; + + //-------------------------------------------------------------------------------------- + //---------------------------- STATE-CHANGING FUNCTIONS ------------------------------ + //-------------------------------------------------------------------------------------- + + constructor(address _liquidityPool, address _eETH) ERC20('Wrapped eETH', 'weETH') { + require(_liquidityPool != address(0), 'No zero addresses'); + require(_eETH != address(0), 'No zero addresses'); + eETH = IeETH(_eETH); + liquidityPool = ILiquidityPool(_liquidityPool); + } + + /// @dev name changed from the version initially deployed + function name() public view virtual override returns (string memory) { + return 'Wrapped eETH'; + } + + /// @notice Wraps eEth + /// @param _eETHAmount the amount of eEth to wrap + /// @return returns the amount of weEth the user receives + function wrap(uint256 _eETHAmount) public returns (uint256) { + require(_eETHAmount > 0, "weETH: can't wrap zero eETH"); + uint256 weEthAmount = liquidityPool.sharesForAmount(_eETHAmount); + _mint(msg.sender, weEthAmount); + eETH.transferFrom(msg.sender, address(this), _eETHAmount); + return weEthAmount; + } + + /// @notice Unwraps weETH + /// @param _weETHAmount the amount of weETH to unwrap + /// @return returns the amount of eEth the user receives + function unwrap(uint256 _weETHAmount) external returns (uint256) { + require(_weETHAmount > 0, 'Cannot unwrap a zero amount'); + uint256 eETHAmount = liquidityPool.amountForShare(_weETHAmount); + _burn(msg.sender, _weETHAmount); + eETH.transfer(msg.sender, eETHAmount); + return eETHAmount; + } + + //-------------------------------------------------------------------------------------- + //------------------------------------ GETTERS --------------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Fetches the amount of weEth respective to the amount of eEth sent in + /// @param _eETHAmount amount sent in + /// @return The total number of shares for the specified amount + function getWeETHByeETH(uint256 _eETHAmount) external view returns (uint256) { + return liquidityPool.sharesForAmount(_eETHAmount); + } + + /// @notice Fetches the amount of eEth respective to the amount of weEth sent in + /// @param _weETHAmount amount sent in + /// @return The total amount for the number of shares sent in + function getEETHByWeETH(uint256 _weETHAmount) public view returns (uint256) { + return liquidityPool.amountForShare(_weETHAmount); + } + + // Amount of weETH for 1 eETH + function getRate() external view returns (uint256) { + return getEETHByWeETH(1 ether); + } + + function transferStETH(address receiver) public onlyOwner { + uint256 amount = IERC20(eETH).balanceOf(address(this)); + IERC20(eETH).transfer(receiver, amount); + } +} diff --git a/test/setup/TestWithBaseAction.sol b/test/setup/TestWithBaseAction.sol index 93ad182..5995dca 100644 --- a/test/setup/TestWithBaseAction.sol +++ b/test/setup/TestWithBaseAction.sol @@ -733,6 +733,7 @@ abstract contract TestWithBaseAction is TestWithPrepare { } groupData.borrowRate = IInterestRateModel(groupData.rateModel).calculateGroupBorrowRate( + expectedAssetData.poolId, expectedAssetData.asset, i, expectedAssetData.utilizationRate diff --git a/test/setup/TestWithData.sol b/test/setup/TestWithData.sol index 4281593..314f24c 100644 --- a/test/setup/TestWithData.sol +++ b/test/setup/TestWithData.sol @@ -41,6 +41,7 @@ abstract contract TestWithData is TestWithSetup { } struct TestAssetData { + uint32 poolId; address asset; uint8 assetType; TestAssetConfig config; @@ -74,6 +75,7 @@ abstract contract TestWithData is TestWithSetup { } struct TestUserAssetData { + uint32 poolId; address user; // fields come from contract uint256 walletBalance; @@ -101,6 +103,7 @@ abstract contract TestWithData is TestWithSetup { } struct TestLoanData { + uint32 poolId; address nftAsset; uint256 nftTokenId; // collateral fields from contract @@ -126,6 +129,7 @@ abstract contract TestWithData is TestWithSetup { } struct TestContractData { + uint32 poolId; TestAssetData assetData; TestUserAssetData userAssetData; TestUserAccountData accountData; @@ -151,6 +155,7 @@ abstract contract TestWithData is TestWithSetup { address asset, uint8 assetType ) public view returns (TestAssetData memory assetData) { + assetData.poolId = poolId; assetData.asset = asset; assetData.assetType = assetType; @@ -209,6 +214,7 @@ abstract contract TestWithData is TestWithSetup { } function copyAssetData(TestAssetData memory assetDataOld) public pure returns (TestAssetData memory assetDataNew) { + assetDataNew.poolId = assetDataOld.poolId; assetDataNew.asset = assetDataOld.asset; assetDataNew.assetType = assetDataOld.assetType; @@ -263,6 +269,7 @@ abstract contract TestWithData is TestWithSetup { address asset, uint8 assetType ) public view returns (TestUserAssetData memory userAssetData) { + userAssetData.poolId = poolId; userAssetData.user = user; if (assetType == Constants.ASSET_TYPE_ERC20) { @@ -303,6 +310,7 @@ abstract contract TestWithData is TestWithSetup { function copyUserAssetData( TestUserAssetData memory userAssetDataOld ) public pure returns (TestUserAssetData memory userAssetDataNew) { + userAssetDataNew.poolId = userAssetDataOld.poolId; userAssetDataNew.user = userAssetDataOld.user; userAssetDataNew.walletBalance = userAssetDataOld.walletBalance; @@ -340,6 +348,7 @@ abstract contract TestWithData is TestWithSetup { address nftAsset, uint256 nftTokenId ) internal view returns (TestLoanData memory data) { + data.poolId = poolId; data.nftAsset = nftAsset; data.nftTokenId = nftTokenId; @@ -368,6 +377,7 @@ abstract contract TestWithData is TestWithSetup { uint256 nftTokenId, address debtAsset ) internal view returns (TestLoanData memory data) { + data.poolId = poolId; data.nftAsset = nftAsset; data.nftTokenId = nftTokenId; @@ -376,6 +386,7 @@ abstract contract TestWithData is TestWithSetup { } function copyLoanData(TestLoanData memory loanDataOld) public pure returns (TestLoanData memory loanDataNew) { + loanDataNew.poolId = loanDataOld.poolId; loanDataNew.nftAsset = loanDataOld.nftAsset; loanDataNew.nftTokenId = loanDataOld.nftTokenId; diff --git a/test/setup/TestWithPrepare.sol b/test/setup/TestWithPrepare.sol index b97fcc5..87baa0c 100644 --- a/test/setup/TestWithPrepare.sol +++ b/test/setup/TestWithPrepare.sol @@ -35,6 +35,16 @@ abstract contract TestWithPrepare is TestWithData { prepareERC20(user, address(tsDAI), depositAmount); } + function prepareUSDS(TestUser user) internal virtual { + uint256 depositAmount = 500_000 * (10 ** IERC20Metadata(tsUSDS).decimals()); + prepareERC20(user, address(tsUSDS), depositAmount); + } + + function prepareWUSD(TestUser user) internal virtual { + uint256 depositAmount = 500_000 * (10 ** IERC20Metadata(tsWUSD).decimals()); + prepareERC20(user, address(tsWUSD), depositAmount); + } + function prepareIsolateERC721(TestUser user, address asset, uint256[] memory tokenIds) internal virtual { user.setApprovalForAllERC721(asset, true); user.depositERC721(tsCommonPoolId, asset, tokenIds, Constants.SUPPLY_MODE_ISOLATE, address(user)); diff --git a/test/setup/TestWithSetup.sol b/test/setup/TestWithSetup.sol index 8080395..293a2ac 100644 --- a/test/setup/TestWithSetup.sol +++ b/test/setup/TestWithSetup.sol @@ -22,6 +22,8 @@ import {YieldRegistry} from 'src/yield/YieldRegistry.sol'; import {YieldEthStakingLido} from 'src/yield/lido/YieldEthStakingLido.sol'; import {YieldEthStakingEtherfi} from 'src/yield/etherfi/YieldEthStakingEtherfi.sol'; import {YieldSavingsDai} from 'src/yield/sdai/YieldSavingsDai.sol'; +import {YieldSavingsUSDS} from 'src/yield/susds/YieldSavingsUSDS.sol'; +import {YieldWUSDStaking} from 'src/yield/wusd/YieldWUSDStaking.sol'; import {Installer} from 'src/modules/Installer.sol'; import {ConfiguratorPool} from 'src/modules/ConfiguratorPool.sol'; @@ -51,10 +53,15 @@ import {MockChainlinkAggregator} from 'test/mocks/MockChainlinkAggregator.sol'; import {MockDAIPot} from 'test/mocks/MockDAIPot.sol'; import {MockSDAI} from 'test/mocks/MockSDAI.sol'; +import {MockSUSDS} from 'test/mocks/MockSUSDS.sol'; + import {SDAIPriceAdapter} from 'src/oracles/SDAIPriceAdapter.sol'; +import {SUSDSPriceAdapter} from 'src/oracles/SUSDSPriceAdapter.sol'; import {MockDelegateRegistryV2} from 'test/mocks/MockDelegateRegistryV2.sol'; +import {MockWUSDStaking} from 'test/mocks/MockWUSDStaking.sol'; + import {TestUser} from '../helpers/TestUser.sol'; import {TestWithUtils} from './TestWithUtils.sol'; @@ -76,6 +83,8 @@ abstract contract TestWithSetup is TestWithUtils { MockERC20 public tsWETH; MockERC20 public tsDAI; MockERC20 public tsUSDT; + MockERC20 public tsUSDS; + MockERC20 public tsWUSD; MockERC721 public tsWPUNK; MockERC721 public tsBAYC; MockERC721 public tsMAYC; @@ -86,15 +95,21 @@ abstract contract TestWithSetup is TestWithUtils { MockEtherfiLiquidityPool public tsEtherfiLiquidityPool; MockDAIPot public tsDAIPot; MockSDAI public tsSDAI; + MockSUSDS public tsSUSDS; MockDelegateRegistryV2 public tsDelegateRegistryV2; + MockWUSDStaking public tsWUSDStaking; MockBendNFTOracle public tsBendNFTOracle; + MockBendNFTOracle public tsBendTokenOracle; MockChainlinkAggregator tsCLAggregatorWETH; MockChainlinkAggregator tsCLAggregatorDAI; + MockChainlinkAggregator tsCLAggregatorUSDS; MockChainlinkAggregator tsCLAggregatorUSDT; MockChainlinkAggregator tsCLAggregatorStETH; MockChainlinkAggregator tsCLAggregatorEETH; SDAIPriceAdapter tsCLAggregatorSDAI; + SUSDSPriceAdapter tsCLAggregatorSUSDS; + MockChainlinkAggregator tsCLAggregatorWUSD; ProxyAdmin public tsProxyAdmin; AddressProvider public tsAddressProvider; @@ -106,6 +121,8 @@ abstract contract TestWithSetup is TestWithUtils { YieldEthStakingLido public tsYieldEthStakingLido; YieldEthStakingEtherfi public tsYieldEthStakingEtherfi; YieldSavingsDai public tsYieldSavingsDai; + YieldSavingsUSDS public tsYieldSavingsUSDS; + YieldWUSDStaking public tsYieldWUSDStaking; Installer public tsInstaller; ConfiguratorPool public tsConfiguratorPool; @@ -359,11 +376,51 @@ abstract contract TestWithSetup is TestWithUtils { tsHEVM.prank(tsPoolAdmin); tsYieldRegistry.addYieldManager(address(tsYieldSavingsDai)); + // YieldSavingsUSDS + YieldSavingsUSDS yieldSavingsUSDSImpl = new YieldSavingsUSDS(); + TransparentUpgradeableProxy yieldSavingsUSDSProxy = new TransparentUpgradeableProxy( + address(yieldSavingsUSDSImpl), + address(tsProxyAdmin), + abi.encodeWithSelector( + yieldSavingsUSDSImpl.initialize.selector, + address(tsAddressProvider), + address(tsUSDS), + address(tsSUSDS) + ) + ); + tsYieldSavingsUSDS = YieldSavingsUSDS(payable(address(yieldSavingsUSDSProxy))); + tsHEVM.prank(tsPoolAdmin); + tsYieldRegistry.addYieldManager(address(tsYieldSavingsUSDS)); + + // YieldWUSDStaking + YieldWUSDStaking yieldWUSDStakingImpl = new YieldWUSDStaking(); + TransparentUpgradeableProxy yieldWUSDStakingProxy = new TransparentUpgradeableProxy( + address(yieldWUSDStakingImpl), + address(tsProxyAdmin), + abi.encodeWithSelector( + yieldWUSDStakingImpl.initialize.selector, + address(tsAddressProvider), + address(tsWUSD), + address(tsWUSDStaking) + ) + ); + tsYieldWUSDStaking = YieldWUSDStaking(payable(address(yieldWUSDStakingProxy))); + tsHEVM.prank(tsPoolAdmin); + tsYieldRegistry.addYieldManager(address(tsYieldWUSDStaking)); + // Interest Rate Model tsLowRateGroupId = 1; tsMiddleRateGroupId = 2; tsHighRateGroupId = 3; - tsDefaultIRM = new DefaultInterestRateModel(address(tsAddressProvider)); + + DefaultInterestRateModel defaultIrmImpl = new DefaultInterestRateModel(); + TransparentUpgradeableProxy defaultIrmProxy = new TransparentUpgradeableProxy( + address(defaultIrmImpl), + address(tsProxyAdmin), + abi.encodeWithSelector(defaultIrmImpl.initialize.selector, address(tsAddressProvider)) + ); + tsDefaultIRM = DefaultInterestRateModel(address(defaultIrmProxy)); + tsYieldRateIRM = tsDefaultIRM; tsLowRateIRM = tsDefaultIRM; tsMiddleRateIRM = tsDefaultIRM; @@ -372,22 +429,40 @@ abstract contract TestWithSetup is TestWithUtils { // set price oracle tsHEVM.startPrank(tsOracleAdmin); tsPriceOracle.setBendNFTOracle(address(tsBendNFTOracle)); + tsPriceOracle.setBendTokenOracle(address(tsBendTokenOracle)); - address[] memory oracleAssets = new address[](6); + address[] memory oracleAssets = new address[](9); oracleAssets[0] = address(tsWETH); oracleAssets[1] = address(tsDAI); oracleAssets[2] = address(tsUSDT); oracleAssets[3] = address(tsStETH); oracleAssets[4] = address(tsEETH); oracleAssets[5] = address(tsSDAI); - address[] memory oracleAggs = new address[](6); + oracleAssets[6] = address(tsUSDS); + oracleAssets[7] = address(tsSUSDS); + oracleAssets[8] = address(tsWUSD); + address[] memory oracleAggs = new address[](9); oracleAggs[0] = address(tsCLAggregatorWETH); oracleAggs[1] = address(tsCLAggregatorDAI); oracleAggs[2] = address(tsCLAggregatorUSDT); oracleAggs[3] = address(tsCLAggregatorStETH); oracleAggs[4] = address(tsCLAggregatorEETH); oracleAggs[5] = address(tsCLAggregatorSDAI); + oracleAggs[6] = address(tsCLAggregatorUSDS); + oracleAggs[7] = address(tsCLAggregatorSUSDS); + oracleAggs[8] = address(tsCLAggregatorWUSD); tsPriceOracle.setAssetChainlinkAggregators(oracleAssets, oracleAggs); + + address[] memory sourceAssets = new address[](3); + sourceAssets[0] = address(tsWPUNK); + sourceAssets[1] = address(tsBAYC); + sourceAssets[2] = address(tsMAYC); + uint8[] memory sourceTypes = new uint8[](3); + sourceTypes[0] = Constants.ORACLE_TYPE_BEND_NFT; + sourceTypes[1] = Constants.ORACLE_TYPE_BEND_NFT; + sourceTypes[2] = Constants.ORACLE_TYPE_BEND_NFT; + tsPriceOracle.setAssetOracleSourceTypes(sourceAssets, sourceTypes); + tsHEVM.stopPrank(); } @@ -397,6 +472,8 @@ abstract contract TestWithSetup is TestWithUtils { tsWETH = MockERC20(tsFaucet.createMockERC20('MockWETH', 'WETH', 18)); tsDAI = MockERC20(tsFaucet.createMockERC20('MockDAI', 'DAI', 18)); tsUSDT = MockERC20(tsFaucet.createMockERC20('MockUSDT', 'USDT', 6)); + tsUSDS = MockERC20(tsFaucet.createMockERC20('MockUSDS', 'USDS', 18)); + tsWUSD = MockERC20(tsFaucet.createMockERC20('MockWUSD', 'WUSD', 6)); tsWPUNK = MockERC721(tsFaucet.createMockERC721('MockWPUNK', 'WPUNK')); tsBAYC = MockERC721(tsFaucet.createMockERC721('MockBAYC', 'BAYC')); @@ -418,7 +495,11 @@ abstract contract TestWithSetup is TestWithUtils { tsDAIPot = new MockDAIPot(); tsSDAI = new MockSDAI(address(tsDAI)); + tsSUSDS = new MockSUSDS(address(tsUSDS)); + tsDelegateRegistryV2 = new MockDelegateRegistryV2(); + + tsWUSDStaking = new MockWUSDStaking(address(tsWUSD)); } function initMockUsers() internal { @@ -492,6 +573,8 @@ abstract contract TestWithSetup is TestWithUtils { tsFaucet.privateMintERC20(address(tsDAI), address(user), TS_INITIAL_BALANCE * 1e18); tsFaucet.privateMintERC20(address(tsUSDT), address(user), TS_INITIAL_BALANCE * 1e6); + tsFaucet.privateMintERC20(address(tsUSDS), address(user), TS_INITIAL_BALANCE * 1e18); + tsFaucet.privateMintERC20(address(tsWUSD), address(user), TS_INITIAL_BALANCE * 1e6); uint256[] memory tokenIds = user.getTokenIds(); tsFaucet.privateMintERC721(address(tsWPUNK), address(user), tokenIds); @@ -508,10 +591,18 @@ abstract contract TestWithSetup is TestWithUtils { tsHEVM.label(address(tsCLAggregatorDAI), 'MockCLAggregator(DAI/USD)'); tsCLAggregatorDAI.updateAnswer(99984627); + tsCLAggregatorUSDS = new MockChainlinkAggregator(8, 'USDS / USD'); + tsHEVM.label(address(tsCLAggregatorUSDS), 'MockCLAggregator(USDS/USD)'); + tsCLAggregatorUSDS.updateAnswer(99984627); + tsCLAggregatorUSDT = new MockChainlinkAggregator(8, 'USDT / USD'); tsHEVM.label(address(tsCLAggregatorUSDT), 'MockCLAggregator(USDT/USD)'); tsCLAggregatorUSDT.updateAnswer(100053000); + tsCLAggregatorWUSD = new MockChainlinkAggregator(8, 'WUSD / USD'); + tsHEVM.label(address(tsCLAggregatorWUSD), 'MockCLAggregator(WUSD/USD)'); + tsCLAggregatorWUSD.updateAnswer(100063000); + tsCLAggregatorStETH = new MockChainlinkAggregator(8, 'stETH / USD'); tsHEVM.label(address(tsCLAggregatorStETH), 'MockCLAggregator(StETH/USD)'); tsCLAggregatorStETH.updateAnswer(204005904164); @@ -523,11 +614,18 @@ abstract contract TestWithSetup is TestWithUtils { tsCLAggregatorSDAI = new SDAIPriceAdapter(address(tsCLAggregatorDAI), address(tsDAIPot), 'sDAI / USD'); tsHEVM.label(address(tsCLAggregatorSDAI), 'SDAIPriceAdapter(sDAI/USD)'); - tsBendNFTOracle = new MockBendNFTOracle(); + tsCLAggregatorSUSDS = new SUSDSPriceAdapter(address(tsCLAggregatorDAI), address(tsDAIPot), 'sUSDS / USD'); + tsHEVM.label(address(tsCLAggregatorSUSDS), 'SUSDSPriceAdapter(sUSDS/USD)'); + + tsBendNFTOracle = new MockBendNFTOracle(18); tsHEVM.label(address(tsBendNFTOracle), 'MockBendNFTOracle'); tsBendNFTOracle.setAssetPrice(address(tsWPUNK), 58155486904761904761); tsBendNFTOracle.setAssetPrice(address(tsBAYC), 30919141261229331011); tsBendNFTOracle.setAssetPrice(address(tsMAYC), 5950381013403414953); + + tsBendTokenOracle = new MockBendNFTOracle(8); + tsHEVM.label(address(tsBendTokenOracle), 'MockBendTokenOracle'); + tsBendNFTOracle.setAssetPrice(address(tsUSDT), 100053000); } function setContractsLabels() internal { @@ -548,8 +646,11 @@ abstract contract TestWithSetup is TestWithUtils { function initInterestRateParams() internal { tsHEVM.startPrank(tsPoolAdmin); + uint32 poolId = 1; + // WETH tsDefaultIRM.setInterestRateParams( + poolId, address(tsWETH), 0, (65 * WadRayMath.RAY) / 100, @@ -558,6 +659,7 @@ abstract contract TestWithSetup is TestWithUtils { (100 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsWETH), tsLowRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -566,6 +668,7 @@ abstract contract TestWithSetup is TestWithUtils { (100 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsWETH), tsMiddleRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -574,6 +677,7 @@ abstract contract TestWithSetup is TestWithUtils { (100 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsWETH), tsHighRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -584,6 +688,7 @@ abstract contract TestWithSetup is TestWithUtils { // USDT tsDefaultIRM.setInterestRateParams( + poolId, address(tsUSDT), 0, (65 * WadRayMath.RAY) / 100, @@ -592,6 +697,7 @@ abstract contract TestWithSetup is TestWithUtils { (80 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsUSDT), tsLowRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -600,6 +706,7 @@ abstract contract TestWithSetup is TestWithUtils { (80 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsUSDT), tsMiddleRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -608,6 +715,7 @@ abstract contract TestWithSetup is TestWithUtils { (80 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsUSDT), tsHighRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -618,6 +726,7 @@ abstract contract TestWithSetup is TestWithUtils { // DAI tsDefaultIRM.setInterestRateParams( + poolId, address(tsDAI), 0, (65 * WadRayMath.RAY) / 100, @@ -626,6 +735,7 @@ abstract contract TestWithSetup is TestWithUtils { (90 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsDAI), tsLowRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -634,6 +744,7 @@ abstract contract TestWithSetup is TestWithUtils { (90 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsDAI), tsMiddleRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -642,6 +753,7 @@ abstract contract TestWithSetup is TestWithUtils { (90 * WadRayMath.RAY) / 100 ); tsDefaultIRM.setInterestRateParams( + poolId, address(tsDAI), tsHighRateGroupId, (65 * WadRayMath.RAY) / 100, @@ -650,6 +762,44 @@ abstract contract TestWithSetup is TestWithUtils { (90 * WadRayMath.RAY) / 100 ); + // USDS + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsUSDS), + 0, + (65 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100, // baseRate + (5 * WadRayMath.RAY) / 100, + (90 * WadRayMath.RAY) / 100 + ); + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsUSDS), + tsLowRateGroupId, + (65 * WadRayMath.RAY) / 100, + (4 * WadRayMath.RAY) / 100, // baseRate + (5 * WadRayMath.RAY) / 100, + (90 * WadRayMath.RAY) / 100 + ); + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsUSDS), + tsMiddleRateGroupId, + (65 * WadRayMath.RAY) / 100, + (6 * WadRayMath.RAY) / 100, // baseRate + (5 * WadRayMath.RAY) / 100, + (90 * WadRayMath.RAY) / 100 + ); + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsUSDS), + tsHighRateGroupId, + (65 * WadRayMath.RAY) / 100, + (8 * WadRayMath.RAY) / 100, // baseRate + (5 * WadRayMath.RAY) / 100, + (90 * WadRayMath.RAY) / 100 + ); + tsHEVM.stopPrank(); } @@ -690,6 +840,15 @@ abstract contract TestWithSetup is TestWithUtils { tsConfigurator.setAssetSupplyCap(tsCommonPoolId, address(tsUSDT), 100_000_000 * (10 ** tsWETH.decimals())); tsConfigurator.setAssetBorrowCap(tsCommonPoolId, address(tsUSDT), 100_000_000 * (10 ** tsWETH.decimals())); + tsConfigurator.addAssetERC20(tsCommonPoolId, address(tsUSDS)); + tsConfigurator.setAssetCollateralParams(tsCommonPoolId, address(tsUSDS), 7700, 8000, 500); + tsConfigurator.setAssetProtocolFee(tsCommonPoolId, address(tsUSDS), 2000); + tsConfigurator.setAssetClassGroup(tsCommonPoolId, address(tsUSDS), tsLowRateGroupId); + tsConfigurator.setAssetActive(tsCommonPoolId, address(tsUSDS), true); + tsConfigurator.setAssetBorrowing(tsCommonPoolId, address(tsUSDS), true); + tsConfigurator.setAssetSupplyCap(tsCommonPoolId, address(tsUSDS), 100_000_000 * (10 ** tsWETH.decimals())); + tsConfigurator.setAssetBorrowCap(tsCommonPoolId, address(tsUSDS), 100_000_000 * (10 ** tsWETH.decimals())); + // add interest group to assets tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsWETH), tsLowRateGroupId, address(tsLowRateIRM)); tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsWETH), tsHighRateGroupId, address(tsHighRateIRM)); @@ -697,6 +856,9 @@ abstract contract TestWithSetup is TestWithUtils { tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsDAI), tsLowRateGroupId, address(tsLowRateIRM)); tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsDAI), tsHighRateGroupId, address(tsHighRateIRM)); + tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsUSDS), tsLowRateGroupId, address(tsLowRateIRM)); + tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsUSDS), tsHighRateGroupId, address(tsHighRateIRM)); + tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsUSDT), tsLowRateGroupId, address(tsLowRateIRM)); tsConfigurator.addAssetGroup(tsCommonPoolId, address(tsUSDT), tsHighRateGroupId, address(tsHighRateIRM)); @@ -737,6 +899,12 @@ abstract contract TestWithSetup is TestWithUtils { tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsPoolManager), address(tsDAI), 2000); tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsStaker2), address(tsDAI), 2000); + tsConfigurator.setAssetYieldEnable(tsCommonPoolId, address(tsUSDS), true); + tsConfigurator.setAssetYieldCap(tsCommonPoolId, address(tsUSDS), 2000); + tsConfigurator.setAssetYieldRate(tsCommonPoolId, address(tsUSDS), address(tsYieldRateIRM)); + tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsPoolManager), address(tsUSDS), 2000); + tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsStaker2), address(tsUSDS), 2000); + tsHEVM.stopPrank(); } @@ -746,6 +914,7 @@ abstract contract TestWithSetup is TestWithUtils { tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsYieldEthStakingLido), address(tsWETH), 2000); tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsYieldEthStakingEtherfi), address(tsWETH), 2000); tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsYieldSavingsDai), address(tsDAI), 2000); + tsConfigurator.setManagerYieldCap(tsCommonPoolId, address(tsYieldSavingsUSDS), address(tsUSDS), 2000); tsYieldEthStakingLido.setNftActive(address(tsBAYC), true); tsYieldEthStakingLido.setNftStakeParams(address(tsBAYC), 50000, 9000); @@ -759,6 +928,93 @@ abstract contract TestWithSetup is TestWithUtils { tsYieldSavingsDai.setNftStakeParams(address(tsBAYC), 50000, 9000); tsYieldSavingsDai.setNftUnstakeParams(address(tsBAYC), 100e18, 1.05e18); + tsYieldSavingsUSDS.setNftActive(address(tsBAYC), true); + tsYieldSavingsUSDS.setNftStakeParams(address(tsBAYC), 50000, 9000); + tsYieldSavingsUSDS.setNftUnstakeParams(address(tsBAYC), 100e18, 1.05e18); + + tsHEVM.stopPrank(); + } + + function initPoolWUSD(uint32 poolId) internal { + tsHEVM.startPrank(tsPoolAdmin); + + // IRM + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsWUSD), + 0, + (75 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100, // baseRate + (1 * WadRayMath.RAY) / 100, + (2 * WadRayMath.RAY) / 100 + ); + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsWUSD), + tsLowRateGroupId, + (75 * WadRayMath.RAY) / 100, + (4 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (8 * WadRayMath.RAY) / 100 + ); + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsWUSD), + tsMiddleRateGroupId, + (65 * WadRayMath.RAY) / 100, + (6 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (8 * WadRayMath.RAY) / 100 + ); + tsDefaultIRM.setInterestRateParams( + poolId, + address(tsWUSD), + tsHighRateGroupId, + (65 * WadRayMath.RAY) / 100, + (8 * WadRayMath.RAY) / 100, // baseRate + (4 * WadRayMath.RAY) / 100, + (8 * WadRayMath.RAY) / 100 + ); + + // Asset + tsConfigurator.addAssetERC20(poolId, address(tsWUSD)); + tsConfigurator.setAssetCollateralParams(poolId, address(tsWUSD), 7700, 8000, 500); + tsConfigurator.setAssetProtocolFee(poolId, address(tsWUSD), 2000); + tsConfigurator.setAssetClassGroup(poolId, address(tsWUSD), tsLowRateGroupId); + tsConfigurator.setAssetActive(poolId, address(tsWUSD), true); + tsConfigurator.setAssetBorrowing(poolId, address(tsWUSD), true); + tsConfigurator.setAssetSupplyCap(poolId, address(tsWUSD), 100_000_000 * (10 ** tsWETH.decimals())); + tsConfigurator.setAssetBorrowCap(poolId, address(tsWUSD), 100_000_000 * (10 ** tsWETH.decimals())); + + tsConfigurator.addAssetGroup(poolId, address(tsWUSD), tsLowRateGroupId, address(tsLowRateIRM)); + tsConfigurator.addAssetGroup(poolId, address(tsWUSD), tsHighRateGroupId, address(tsHighRateIRM)); + + tsHEVM.stopPrank(); + } + + function initYieldWUSDStaking(uint32 poolId) internal { + tsHEVM.startPrank(tsPoolAdmin); + + tsConfigurator.setAssetYieldEnable(poolId, address(tsWUSD), true); + tsConfigurator.setAssetYieldCap(poolId, address(tsWUSD), 5000); + tsConfigurator.setAssetYieldRate(poolId, address(tsWUSD), address(tsYieldRateIRM)); + + // Yield + tsConfigurator.setManagerYieldCap(poolId, address(tsYieldWUSDStaking), address(tsWUSD), 5000); + + tsYieldWUSDStaking.setNftActive(address(tsBAYC), true); + tsYieldWUSDStaking.setNftStakeParams(address(tsBAYC), 50000, 9000); + tsYieldWUSDStaking.setNftUnstakeParams(address(tsBAYC), 100e18, 1.05e18); + + tsHEVM.stopPrank(); + + tsHEVM.startPrank(tsWUSDStaking.owner()); + + tsWUSDStaking.createStakingPool(600, 1, 0); + tsWUSDStaking.createStakingPool(604800, 80000, 0); + tsWUSDStaking.createStakingPool(2592000, 90000, 0); + tsWUSDStaking.createStakingPool(5184000, 95000, 0); + tsHEVM.stopPrank(); } } diff --git a/test/yield/YieldSavingsUSDS.t.sol b/test/yield/YieldSavingsUSDS.t.sol new file mode 100644 index 0000000..3d83582 --- /dev/null +++ b/test/yield/YieldSavingsUSDS.t.sol @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Constants} from 'src/libraries/helpers/Constants.sol'; + +import 'test/setup/TestWithPrepare.sol'; +import '@forge-std/Test.sol'; + +contract TestYieldSavingsUSDS is TestWithPrepare { + struct YieldTestVars { + uint32 poolId; + uint8 state; + uint256 debtAmount; + uint256 yieldAmount; + uint256 unstakeFine; + uint256 withdrawAmount; + uint256 withdrawReqId; + uint256 totalFineBefore; + uint256 totalFineAfter; + uint256 claimedFine; + } + + function onSetUp() public virtual override { + super.onSetUp(); + + initCommonPools(); + + initYieldEthStaking(); + } + + function test_Should_stake() public { + YieldTestVars memory testVars; + + prepareUSDS(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldSavingsUSDS.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldSavingsUSDS.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.poolId, tsCommonPoolId, 'poolId not eq'); + assertEq(testVars.state, Constants.YIELD_STATUS_ACTIVE, 'state not eq'); + testEquality(testVars.debtAmount, stakeAmount, 'testVars.debtAmount not eq'); + testEquality(testVars.yieldAmount, stakeAmount, 'testVars.yieldAmount not eq'); + + uint256 debtAmount = tsYieldSavingsUSDS.getNftDebtInUnderlyingAsset(address(tsBAYC), tokenIds[0]); + testEquality(debtAmount, stakeAmount, 'debtAmount not eq'); + + (uint256 yieldAmount, ) = tsYieldSavingsUSDS.getNftYieldInUnderlyingAsset(address(tsBAYC), tokenIds[0]); + testEquality(yieldAmount, stakeAmount, 'yieldAmount not eq'); + } + + function test_Should_unstake() public { + YieldTestVars memory testVars; + + prepareUSDS(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldSavingsUSDS.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + address yieldAccount = tsYieldSavingsUSDS.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount); + + // add some yield + uint256 deltaAmount = (stakeAmount * 35) / 1000; + tsHEVM.prank(address(tsDepositor1)); + tsUSDS.approve(address(tsSUSDS), type(uint256).max); + tsHEVM.prank(address(tsDepositor1)); + tsSUSDS.rebase(yieldAccount, deltaAmount); + + (uint256 underAmount, uint256 yieldAmount) = tsYieldSavingsUSDS.getNftYieldInUnderlyingAsset( + address(tsBAYC), + tokenIds[0] + ); + testEquality(underAmount, (stakeAmount + deltaAmount), 3, 'yieldAmount not eq'); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 0); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldSavingsUSDS.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, Constants.YIELD_STATUS_CLAIM, 'state not eq'); + assertEq(testVars.yieldAmount, yieldAmount, 'testVars.yieldAmount not eq'); + + (testVars.unstakeFine, testVars.withdrawAmount, testVars.withdrawReqId) = tsYieldSavingsUSDS.getNftUnstakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.unstakeFine, 0, 'unstakeFine not eq'); + assertEq(testVars.withdrawAmount, testVars.yieldAmount, 'withdrawAmount not eq'); + assertEq(testVars.withdrawReqId, 0, 'withdrawReqId not eq'); + } + + function test_Should_repay() public { + YieldTestVars memory testVars; + + prepareUSDS(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldSavingsUSDS.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.startPrank(address(tsBorrower1)); + + tsYieldSavingsUSDS.createYieldAccount(address(tsBorrower1)); + tsYieldSavingsUSDS.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount); + + // make some interest + advanceTimes(365 days); + + tsYieldSavingsUSDS.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 0); + + tsUSDS.approve(address(tsYieldSavingsUSDS), type(uint256).max); + tsYieldSavingsUSDS.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + tsHEVM.stopPrank(); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldSavingsUSDS.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, 0, 'state not eq'); + assertEq(testVars.debtAmount, 0, 'debtAmount not eq'); + assertEq(testVars.yieldAmount, 0, 'yieldAmount not eq'); + } + + function test_Should_unstakeAndRepay() public { + YieldTestVars memory testVars; + + prepareUSDS(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldSavingsUSDS.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.startPrank(address(tsBorrower1)); + + tsYieldSavingsUSDS.createYieldAccount(address(tsBorrower1)); + tsYieldSavingsUSDS.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount); + + // make some interest + advanceTimes(365 days); + + tsUSDS.approve(address(tsYieldSavingsUSDS), type(uint256).max); + tsYieldSavingsUSDS.unstakeAndRepay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + tsHEVM.stopPrank(); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldSavingsUSDS.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, 0, 'state not eq'); + assertEq(testVars.debtAmount, 0, 'debtAmount not eq'); + assertEq(testVars.yieldAmount, 0, 'yieldAmount not eq'); + } + + function test_Should_batch() public { + YieldTestVars memory testVars; + + prepareUSDS(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + address[] memory nfts = new address[](tokenIds.length); + for (uint i = 0; i < tokenIds.length; i++) { + nfts[i] = address(tsBAYC); + } + + uint256 stakeAmount = tsYieldSavingsUSDS.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 20) / 100; + + uint256[] memory stakeAmounts = new uint256[](tokenIds.length); + for (uint i = 0; i < tokenIds.length; i++) { + stakeAmounts[i] = stakeAmount; + } + + tsHEVM.startPrank(address(tsBorrower1)); + + tsYieldSavingsUSDS.createYieldAccount(address(tsBorrower1)); + + tsYieldSavingsUSDS.batchStake(tsCommonPoolId, nfts, tokenIds, stakeAmounts); + + // make some interest + advanceTimes(365 days); + + tsYieldSavingsUSDS.batchUnstake(tsCommonPoolId, nfts, tokenIds, 0); + + for (uint i = 0; i < tokenIds.length; i++) { + (testVars.unstakeFine, testVars.withdrawAmount, testVars.withdrawReqId) = tsYieldSavingsUSDS.getNftUnstakeData( + nfts[i], + tokenIds[i] + ); + } + + tsUSDS.approve(address(tsYieldSavingsUSDS), type(uint256).max); + tsYieldSavingsUSDS.batchRepay(tsCommonPoolId, nfts, tokenIds); + + tsHEVM.stopPrank(); + } + + function test_Should_collectFeeToTreasury() public { + YieldTestVars memory testVars; + + prepareUSDS(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldSavingsUSDS.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + address yieldAccount = tsYieldSavingsUSDS.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount); + + // add some yield + uint256 deltaAmount = (stakeAmount * 35) / 1000; + tsHEVM.prank(address(tsDepositor1)); + tsUSDS.approve(address(tsSUSDS), type(uint256).max); + tsHEVM.prank(address(tsDepositor1)); + tsSUSDS.rebase(yieldAccount, deltaAmount); + + // drop down price + adjustNftPrice(address(tsBAYC), 100); + + // ask bot to do the unstake forcely + tsHEVM.prank(address(tsPoolAdmin)); + tsYieldSavingsUSDS.setBotAdmin(address(tsBorrower2)); + + tsHEVM.prank(address(tsBorrower2)); + tsYieldSavingsUSDS.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 20e18); + + (testVars.unstakeFine, testVars.withdrawAmount, testVars.withdrawReqId) = tsYieldSavingsUSDS.getNftUnstakeData( + address(tsBAYC), + tokenIds[0] + ); + + // do the repay + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldSavingsUSDS.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, 0, 'state not eq'); + + // collect fee + (testVars.totalFineBefore, testVars.claimedFine) = tsYieldSavingsUSDS.getTotalUnstakeFine(); + assertEq(testVars.claimedFine, 0, 'claimedFine not eq'); + + tsHEVM.prank(address(tsPoolAdmin)); + tsYieldSavingsUSDS.collectFeeToTreasury(); + + (testVars.totalFineAfter, testVars.claimedFine) = tsYieldSavingsUSDS.getTotalUnstakeFine(); + assertEq(testVars.totalFineAfter, testVars.totalFineBefore, 'totalFineAfter not eq'); + assertEq(testVars.claimedFine, testVars.totalFineBefore, 'claimedFine not eq'); + } + + function test_Should_unstake_bot() public { + YieldTestVars memory testVars; + + prepareUSDS(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldSavingsUSDS.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount); + + // add some debt interest + advanceTimes(360 days); + + // drop down price + adjustNftPrice(address(tsBAYC), 100); + + // ask bot to do the unstake forcely + tsHEVM.prank(address(tsPoolAdmin)); + tsYieldSavingsUSDS.setBotAdmin(address(tsBorrower2)); + + tsHEVM.prank(address(tsBorrower2)); + tsYieldSavingsUSDS.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 20e18); + + (testVars.unstakeFine, testVars.withdrawAmount, testVars.withdrawReqId) = tsYieldSavingsUSDS.getNftUnstakeData( + address(tsBAYC), + tokenIds[0] + ); + + // ask bot to do the repay + tsHEVM.prank(address(tsBorrower2)); + tsYieldSavingsUSDS.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldSavingsUSDS.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, Constants.YIELD_STATUS_CLAIM, 'bot - state not eq'); + assertGt(testVars.debtAmount, 0, 'bot - debtAmount not gt'); + + // owner to do the remain repay + tsHEVM.prank(address(tsBorrower1)); + tsUSDS.approve(address(tsYieldSavingsUSDS), type(uint256).max); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldSavingsUSDS.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldSavingsUSDS.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, 0, 'owner - state not eq'); + } + + function adjustNftPrice(address nftAsset, uint256 percentage) internal { + uint256 oldPrice = tsBendNFTOracle.getAssetPrice(nftAsset); + uint256 newPrice = (oldPrice * percentage) / 1e4; + tsBendNFTOracle.setAssetPrice(nftAsset, newPrice); + } +} diff --git a/test/yield/YieldWUSDStaking.t.sol b/test/yield/YieldWUSDStaking.t.sol new file mode 100644 index 0000000..cf4caa0 --- /dev/null +++ b/test/yield/YieldWUSDStaking.t.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Constants} from 'src/libraries/helpers/Constants.sol'; +import {Errors} from 'src/libraries/helpers/Errors.sol'; +import {IWUSDStaking} from 'src/yield/wusd/IWUSDStaking.sol'; + +import 'test/setup/TestWithPrepare.sol'; +import '@forge-std/Test.sol'; + +contract TestYieldWUSDStaking is TestWithPrepare { + struct YieldTestVars { + uint32 poolId; + uint8 state; + uint256 debtAmount; + uint256 yieldAmount; + uint256 unstakeFine; + uint256 withdrawAmount; + uint256 withdrawReqId; + } + + uint256 wusdStakingPoolId; + + function onSetUp() public virtual override { + super.onSetUp(); + + initCommonPools(); + + initPoolWUSD(tsCommonPoolId); + initYieldWUSDStaking(tsCommonPoolId); + + wusdStakingPoolId = 2; + } + + function test_RevertIf_stake_invalid_caller() public { + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower2)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower2)); + + tsHEVM.prank(address(tsBorrower2)); + tsHEVM.expectRevert(bytes(Errors.INVALID_CALLER)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + } + + function test_RevertIf_stake_invalid_mode() public { + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareCrossBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsHEVM.expectRevert(bytes(Errors.INVALID_SUPPLY_MODE)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + } + + function test_Should_stake() public { + YieldTestVars memory testVars; + + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldWUSDStaking.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.poolId, tsCommonPoolId, 'poolId not eq'); + assertEq(testVars.state, Constants.YIELD_STATUS_ACTIVE, 'state not eq'); + testEquality(testVars.debtAmount, stakeAmount, 'debtAmount not eq'); + testEquality(testVars.yieldAmount, stakeAmount, 'yieldAmount not eq'); + + uint256 debtAmount = tsYieldWUSDStaking.getNftDebtInUnderlyingAsset(address(tsBAYC), tokenIds[0]); + testEquality(debtAmount, stakeAmount, 'debtAmount not eq'); + + (uint256 yieldAmount, ) = tsYieldWUSDStaking.getNftYieldInUnderlyingAsset(address(tsBAYC), tokenIds[0]); + testEquality(yieldAmount, stakeAmount, 'yieldAmount not eq'); + } + + function test_RevertIf_stake_again() public { + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + + tsHEVM.prank(address(tsBorrower1)); + tsHEVM.expectRevert(bytes(Errors.YIELD_ETH_NFT_ALREADY_USED)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + } + + function test_Should_unstake_before_mature() public { + YieldTestVars memory testVars; + + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + + // make some yield + IWUSDStaking.StakingPool memory stakingPool = tsWUSDStaking.getStakingPoolDetails(wusdStakingPoolId); + advanceTimes(stakingPool.stakingPeriod - 1 days); + + (uint256 yieldAmount, ) = tsYieldWUSDStaking.getNftYieldInUnderlyingAsset(address(tsBAYC), tokenIds[0]); + assertGt(yieldAmount, stakeAmount, 'yieldAmount not gt'); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 0); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldWUSDStaking.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, Constants.YIELD_STATUS_UNSTAKE, 'state not eq'); + + (testVars.unstakeFine, testVars.withdrawAmount, testVars.withdrawReqId) = tsYieldWUSDStaking.getNftUnstakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.unstakeFine, 0, 'unstakeFine not eq'); + assertGt(testVars.withdrawAmount, 0, 'withdrawAmount not gt 0'); + assertLe(testVars.withdrawAmount, yieldAmount, 'withdrawAmount not lt'); + assertGt(testVars.withdrawReqId, 0, 'withdrawReqId not gt'); + } + + function test_Should_repay_before_mature() public { + YieldTestVars memory testVars; + + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + + // make some yield + IWUSDStaking.StakingPool memory stakingPool = tsWUSDStaking.getStakingPoolDetails(wusdStakingPoolId); + advanceTimes(stakingPool.stakingPeriod - 1 days); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 0); + + // cooldown duration + advanceTimes(tsWUSDStaking.getCooldownDuration()); + + tsDepositor1.transferERC20(address(tsWUSD), address(tsWUSDStaking), 100_000e6); + + // tsHEVM.prank(address(tsBorrower1)); + // tsWUSD.approve(address(tsYieldWUSDStaking), type(uint256).max); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldWUSDStaking.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, 0, 'state not eq'); + assertEq(testVars.debtAmount, 0, 'debtAmount not eq'); + assertEq(testVars.yieldAmount, 0, 'yieldAmount not eq'); + } + + function test_Should_unstake_after_mature() public { + YieldTestVars memory testVars; + + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + + // make some yield + IWUSDStaking.StakingPool memory stakingPool = tsWUSDStaking.getStakingPoolDetails(wusdStakingPoolId); + advanceTimes(stakingPool.stakingPeriod + 1 days); + + (uint256 yieldAmount, ) = tsYieldWUSDStaking.getNftYieldInUnderlyingAsset(address(tsBAYC), tokenIds[0]); + assertGt(yieldAmount, stakeAmount, 'yieldAmount not gt'); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 0); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldWUSDStaking.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, Constants.YIELD_STATUS_CLAIM, 'state not eq'); + + (testVars.unstakeFine, testVars.withdrawAmount, testVars.withdrawReqId) = tsYieldWUSDStaking.getNftUnstakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.unstakeFine, 0, 'unstakeFine not eq'); + assertGt(testVars.withdrawAmount, 0, 'withdrawAmount not gt 0'); + assertLe(testVars.withdrawAmount, yieldAmount, 'withdrawAmount not lt'); + assertGt(testVars.withdrawReqId, 0, 'withdrawReqId not gt'); + } + + function test_Should_repay_after_mature() public { + YieldTestVars memory testVars; + + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + + // make some yield + IWUSDStaking.StakingPool memory stakingPool = tsWUSDStaking.getStakingPoolDetails(wusdStakingPoolId); + advanceTimes(stakingPool.stakingPeriod + 1 days); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 0); + + // cooldown duration + advanceTimes(tsWUSDStaking.getCooldownDuration()); + + tsDepositor1.transferERC20(address(tsWUSD), address(tsWUSDStaking), 100_000e6); + + // tsHEVM.prank(address(tsBorrower1)); + // tsWUSD.approve(address(tsYieldWUSDStaking), type(uint256).max); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldWUSDStaking.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, 0, 'state not eq'); + assertEq(testVars.debtAmount, 0, 'debtAmount not eq'); + assertEq(testVars.yieldAmount, 0, 'yieldAmount not eq'); + } + + function test_Should_batch() public { + prepareWUSD(tsDepositor1); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + address[] memory nfts = new address[](tokenIds.length); + for (uint i = 0; i < tokenIds.length; i++) { + nfts[i] = address(tsBAYC); + } + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + uint256[] memory stakeAmounts = new uint256[](tokenIds.length); + for (uint i = 0; i < tokenIds.length; i++) { + stakeAmounts[i] = stakeAmount; + } + + tsHEVM.startPrank(address(tsBorrower1)); + + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsYieldWUSDStaking.batchStake(tsCommonPoolId, nfts, tokenIds, stakeAmounts, wusdStakingPoolId); + + // make some yield + IWUSDStaking.StakingPool memory stakingPool = tsWUSDStaking.getStakingPoolDetails(wusdStakingPoolId); + advanceTimes(stakingPool.stakingPeriod - 1 days); + + tsYieldWUSDStaking.batchUnstake(tsCommonPoolId, nfts, tokenIds, 0); + + // cooldown duration + advanceTimes(tsWUSDStaking.getCooldownDuration()); + + tsDepositor1.transferERC20(address(tsWUSD), address(tsWUSDStaking), 100_000e6); + + tsWETH.approve(address(tsYieldWUSDStaking), type(uint256).max); + + tsYieldWUSDStaking.batchRepay(tsCommonPoolId, nfts, tokenIds); + + tsHEVM.stopPrank(); + } + + function test_Should_unstake_bot() public { + YieldTestVars memory testVars; + + prepareWUSD(tsDepositor1); + + // try to make yeild apr less than debt borrow rate + tsWUSDStaking.setBasicAPY(100); + tsWUSDStaking.updateStakingPoolAPY(wusdStakingPoolId, 100); + + uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1); + + uint256 stakeAmount = tsYieldWUSDStaking.getNftValueInUnderlyingAsset(address(tsBAYC)); + stakeAmount = (stakeAmount * 80) / 100; + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.createYieldAccount(address(tsBorrower1)); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.stake(tsCommonPoolId, address(tsBAYC), tokenIds[0], stakeAmount, wusdStakingPoolId); + + // add some debt interest + IWUSDStaking.StakingPool memory stakingPool = tsWUSDStaking.getStakingPoolDetails(wusdStakingPoolId); + advanceTimes(stakingPool.stakingPeriod - 1 days); + + // drop down price + adjustNftPrice(address(tsBAYC), 100); + + // ask bot to do the unstake forcely + tsHEVM.prank(address(tsPoolAdmin)); + tsYieldWUSDStaking.setBotAdmin(address(tsBorrower2)); + + tsHEVM.prank(address(tsBorrower2)); + tsYieldWUSDStaking.unstake(tsCommonPoolId, address(tsBAYC), tokenIds[0], 20e6); + + advanceTimes(tsWUSDStaking.getCooldownDuration()); + + tsDepositor1.transferERC20(address(tsWUSD), address(tsWUSDStaking), 100_000e6); + + // ask bot to do the repay + tsHEVM.prank(address(tsBorrower2)); + tsYieldWUSDStaking.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldWUSDStaking.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, Constants.YIELD_STATUS_CLAIM, 'bot - state not eq'); + assertGt(testVars.debtAmount, 0, 'bot - debtAmount not gt'); + + // owner to do the remain repay + tsHEVM.prank(address(tsBorrower1)); + tsWUSD.approve(address(tsYieldWUSDStaking), type(uint256).max); + + tsHEVM.prank(address(tsBorrower1)); + tsYieldWUSDStaking.repay(tsCommonPoolId, address(tsBAYC), tokenIds[0]); + + (testVars.poolId, testVars.state, testVars.debtAmount, testVars.yieldAmount) = tsYieldWUSDStaking.getNftStakeData( + address(tsBAYC), + tokenIds[0] + ); + assertEq(testVars.state, 0, 'owner - state not eq'); + } + + function adjustNftPrice(address nftAsset, uint256 percentage) internal { + uint256 oldPrice = tsBendNFTOracle.getAssetPrice(nftAsset); + uint256 newPrice = (oldPrice * percentage) / 1e4; + tsBendNFTOracle.setAssetPrice(nftAsset, newPrice); + } +}