From 3955800f3115f6ec275cd161fbed0780d23b3c62 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Fri, 10 Mar 2023 16:48:22 +0800 Subject: [PATCH 01/25] chore: add ETH instant withdraw implementation --- .../openzeppelin/contracts/Counters.sol | 43 ++ .../openzeppelin/contracts/EnumerableSet.sol | 167 ++++-- .../seaport/contracts/conduit/Conduit.sol | 2 +- contracts/deployments/ReservesSetupHelper.sol | 6 + contracts/interfaces/IBendPool.sol | 11 + contracts/interfaces/ICEther.sol | 6 + contracts/interfaces/IInstantNFTOracle.sol | 16 + contracts/interfaces/IInstantWithdrawNFT.sol | 11 + contracts/interfaces/ILoanVault.sol | 22 + contracts/interfaces/IPool.sol | 2 + contracts/interfaces/IPoolConfigurator.sol | 38 ++ contracts/interfaces/IPoolCore.sol | 2 + contracts/interfaces/IPoolInstantWithdraw.sol | 162 +++++ contracts/interfaces/IPoolParameters.sol | 8 +- .../interfaces/IProtocolDataProvider.sol | 21 +- .../IReserveInterestRateStrategy.sol | 10 +- contracts/interfaces/IStableDebtToken.sol | 158 +++++ contracts/interfaces/IWstETH.sol | 8 + contracts/misc/LoanVault.sol | 198 +++++++ contracts/misc/ProtocolDataProvider.sol | 55 +- contracts/mocks/MockedETHNFTOracle.sol | 35 ++ .../helpers/MockReserveConfiguration.sol | 11 + .../tests/MockReserveInterestRateStrategy.sol | 24 +- .../mocks/tokens/MockInstantWithdrawNFT.sol | 20 + .../upgradeability/MockStableDebtToken.sol | 13 + .../configuration/ReserveConfiguration.sol | 29 + .../protocol/libraries/helpers/Errors.sol | 4 + .../protocol/libraries/logic/BorrowLogic.sol | 2 + .../libraries/logic/ConfiguratorLogic.sol | 64 ++ .../protocol/libraries/logic/PoolLogic.sol | 1 + .../protocol/libraries/logic/ReserveLogic.sol | 49 +- .../libraries/logic/ValidationLogic.sol | 98 ++-- .../types/ConfiguratorInputTypes.sol | 3 + .../protocol/libraries/types/DataTypes.sol | 77 +++ .../DefaultReserveInterestRateStrategy.sol | 170 +++++- contracts/protocol/pool/PoolApeStaking.sol | 4 +- contracts/protocol/pool/PoolConfigurator.sol | 32 + .../protocol/pool/PoolInstantWithdraw.sol | 554 ++++++++++++++++++ contracts/protocol/pool/PoolParameters.sol | 3 +- .../tokenization/ATokenStableDebtToken.sol | 29 + .../tokenization/RebaseStableDebtToken.sol | 434 ++++++++++++++ .../protocol/tokenization/StableDebtToken.sol | 547 +++++++++++++++++ contracts/ui/UiPoolDataProvider.sol | 36 ++ contracts/ui/WETHGateway.sol | 49 ++ contracts/ui/WalletBalanceProvider.sol | 2 +- .../ui/interfaces/IUiPoolDataProvider.sol | 15 + contracts/ui/interfaces/IWETHGateway.sol | 10 + helpers/contracts-deployments.ts | 146 ++++- helpers/contracts-getters.ts | 36 ++ helpers/init-helpers.ts | 60 +- helpers/misc-utils.ts | 15 + helpers/types.ts | 19 + market-config/index.ts | 2 + market-config/rateStrategies.ts | 75 +++ market-config/reservesConfigs.ts | 30 + scripts/deployments/steps/06_pool.ts | 51 +- scripts/deployments/steps/11_allReserves.ts | 10 +- scripts/dev/5.rate-strategy.ts | 10 + test/_base_interest_rate_strategy.spec.ts | 101 +++- test/_base_reserve_configuration.spec.ts | 61 +- test/_pool_configurator.spec.ts | 31 +- test/_pool_initialization.spec.ts | 2 + test/helpers/uniswapv3-helper.ts | 2 +- test/pool_instant_withdraw.spec.ts | 117 ++++ test/xtoken_atoken_stable_debt_token.spec.ts | 194 ++++++ test/xtoken_stable_debt_token.spec.ts | 285 +++++++++ 66 files changed, 4340 insertions(+), 168 deletions(-) create mode 100644 contracts/dependencies/openzeppelin/contracts/Counters.sol create mode 100644 contracts/interfaces/IBendPool.sol create mode 100644 contracts/interfaces/ICEther.sol create mode 100644 contracts/interfaces/IInstantNFTOracle.sol create mode 100644 contracts/interfaces/IInstantWithdrawNFT.sol create mode 100644 contracts/interfaces/ILoanVault.sol create mode 100644 contracts/interfaces/IPoolInstantWithdraw.sol create mode 100644 contracts/interfaces/IStableDebtToken.sol create mode 100644 contracts/interfaces/IWstETH.sol create mode 100644 contracts/misc/LoanVault.sol create mode 100644 contracts/mocks/MockedETHNFTOracle.sol create mode 100644 contracts/mocks/tokens/MockInstantWithdrawNFT.sol create mode 100644 contracts/mocks/upgradeability/MockStableDebtToken.sol create mode 100644 contracts/protocol/pool/PoolInstantWithdraw.sol create mode 100644 contracts/protocol/tokenization/ATokenStableDebtToken.sol create mode 100644 contracts/protocol/tokenization/RebaseStableDebtToken.sol create mode 100644 contracts/protocol/tokenization/StableDebtToken.sol create mode 100644 test/pool_instant_withdraw.spec.ts create mode 100644 test/xtoken_atoken_stable_debt_token.spec.ts create mode 100644 test/xtoken_stable_debt_token.spec.ts diff --git a/contracts/dependencies/openzeppelin/contracts/Counters.sol b/contracts/dependencies/openzeppelin/contracts/Counters.sol new file mode 100644 index 000000000..6cd308487 --- /dev/null +++ b/contracts/dependencies/openzeppelin/contracts/Counters.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Counters.sol) + +pragma solidity ^0.8.0; + +/** + * @title Counters + * @author Matt Condon (@shrugs) + * @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number + * of elements in a mapping, issuing ERC721 ids, or counting request ids. + * + * Include with `using Counters for Counters.Counter;` + */ +library Counters { + struct Counter { + // This variable should never be directly accessed by users of the library: interactions must be restricted to + // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add + // this feature: see https://github.com/ethereum/solidity/issues/4637 + uint256 _value; // default: 0 + } + + function current(Counter storage counter) internal view returns (uint256) { + return counter._value; + } + + function increment(Counter storage counter) internal { + unchecked { + counter._value += 1; + } + } + + function decrement(Counter storage counter) internal { + uint256 value = counter._value; + require(value > 0, "Counter: decrement overflow"); + unchecked { + counter._value = value - 1; + } + } + + function reset(Counter storage counter) internal { + counter._value = 0; + } +} diff --git a/contracts/dependencies/openzeppelin/contracts/EnumerableSet.sol b/contracts/dependencies/openzeppelin/contracts/EnumerableSet.sol index ae0016b5f..4c701c26c 100644 --- a/contracts/dependencies/openzeppelin/contracts/EnumerableSet.sol +++ b/contracts/dependencies/openzeppelin/contracts/EnumerableSet.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol) +// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. -pragma solidity 0.8.10; +pragma solidity ^0.8.0; /** * @dev Library for managing @@ -25,6 +27,16 @@ pragma solidity 0.8.10; * * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) * and `uint256` (`UintSet`) are supported. + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableSet. + * ==== */ library EnumerableSet { // To implement this library for multiple types with as little code @@ -82,12 +94,12 @@ library EnumerableSet { uint256 lastIndex = set._values.length - 1; if (lastIndex != toDeleteIndex) { - bytes32 lastvalue = set._values[lastIndex]; + bytes32 lastValue = set._values[lastIndex]; // Move the last value to the index where the value to delete is - set._values[toDeleteIndex] = lastvalue; + set._values[toDeleteIndex] = lastValue; // Update the index for the moved value - set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex + set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex } // Delete the slot where the moved value was stored @@ -105,11 +117,7 @@ library EnumerableSet { /** * @dev Returns true if the value is in the set. O(1). */ - function _contains(Set storage set, bytes32 value) - private - view - returns (bool) - { + function _contains(Set storage set, bytes32 value) private view returns (bool) { return set._indexes[value] != 0; } @@ -130,14 +138,22 @@ library EnumerableSet { * * - `index` must be strictly less than {length}. */ - function _at(Set storage set, uint256 index) - private - view - returns (bytes32) - { + function _at(Set storage set, uint256 index) private view returns (bytes32) { return set._values[index]; } + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function _values(Set storage set) private view returns (bytes32[] memory) { + return set._values; + } + // Bytes32Set struct Bytes32Set { @@ -150,10 +166,7 @@ library EnumerableSet { * Returns true if the value was added to the set, that is if it was not * already present. */ - function add(Bytes32Set storage set, bytes32 value) - internal - returns (bool) - { + function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { return _add(set._inner, value); } @@ -163,21 +176,14 @@ library EnumerableSet { * Returns true if the value was removed from the set, that is if it was * present. */ - function remove(Bytes32Set storage set, bytes32 value) - internal - returns (bool) - { + function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { return _remove(set._inner, value); } /** * @dev Returns true if the value is in the set. O(1). */ - function contains(Bytes32Set storage set, bytes32 value) - internal - view - returns (bool) - { + function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { return _contains(set._inner, value); } @@ -198,14 +204,30 @@ library EnumerableSet { * * - `index` must be strictly less than {length}. */ - function at(Bytes32Set storage set, uint256 index) - internal - view - returns (bytes32) - { + function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { return _at(set._inner, index); } + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { + bytes32[] memory store = _values(set._inner); + bytes32[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + // AddressSet struct AddressSet { @@ -218,10 +240,7 @@ library EnumerableSet { * Returns true if the value was added to the set, that is if it was not * already present. */ - function add(AddressSet storage set, address value) - internal - returns (bool) - { + function add(AddressSet storage set, address value) internal returns (bool) { return _add(set._inner, bytes32(uint256(uint160(value)))); } @@ -231,21 +250,14 @@ library EnumerableSet { * Returns true if the value was removed from the set, that is if it was * present. */ - function remove(AddressSet storage set, address value) - internal - returns (bool) - { + function remove(AddressSet storage set, address value) internal returns (bool) { return _remove(set._inner, bytes32(uint256(uint160(value)))); } /** * @dev Returns true if the value is in the set. O(1). */ - function contains(AddressSet storage set, address value) - internal - view - returns (bool) - { + function contains(AddressSet storage set, address value) internal view returns (bool) { return _contains(set._inner, bytes32(uint256(uint160(value)))); } @@ -266,14 +278,30 @@ library EnumerableSet { * * - `index` must be strictly less than {length}. */ - function at(AddressSet storage set, uint256 index) - internal - view - returns (address) - { + function at(AddressSet storage set, uint256 index) internal view returns (address) { return address(uint160(uint256(_at(set._inner, index)))); } + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(AddressSet storage set) internal view returns (address[] memory) { + bytes32[] memory store = _values(set._inner); + address[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } + // UintSet struct UintSet { @@ -296,26 +324,19 @@ library EnumerableSet { * Returns true if the value was removed from the set, that is if it was * present. */ - function remove(UintSet storage set, uint256 value) - internal - returns (bool) - { + function remove(UintSet storage set, uint256 value) internal returns (bool) { return _remove(set._inner, bytes32(value)); } /** * @dev Returns true if the value is in the set. O(1). */ - function contains(UintSet storage set, uint256 value) - internal - view - returns (bool) - { + function contains(UintSet storage set, uint256 value) internal view returns (bool) { return _contains(set._inner, bytes32(value)); } /** - * @dev Returns the number of values on the set. O(1). + * @dev Returns the number of values in the set. O(1). */ function length(UintSet storage set) internal view returns (uint256) { return _length(set._inner); @@ -331,11 +352,27 @@ library EnumerableSet { * * - `index` must be strictly less than {length}. */ - function at(UintSet storage set, uint256 index) - internal - view - returns (uint256) - { + function at(UintSet storage set, uint256 index) internal view returns (uint256) { return uint256(_at(set._inner, index)); } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(UintSet storage set) internal view returns (uint256[] memory) { + bytes32[] memory store = _values(set._inner); + uint256[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; + } } diff --git a/contracts/dependencies/seaport/contracts/conduit/Conduit.sol b/contracts/dependencies/seaport/contracts/conduit/Conduit.sol index 1f67d062b..1b890df53 100644 --- a/contracts/dependencies/seaport/contracts/conduit/Conduit.sol +++ b/contracts/dependencies/seaport/contracts/conduit/Conduit.sol @@ -237,7 +237,7 @@ contract Conduit is ConduitInterface, TokenTransferrer { } if (_protocolDataProvider != address(0)) { - (address xTokenAddress, ) = IProtocolDataProvider( + (address xTokenAddress, , ) = IProtocolDataProvider( _protocolDataProvider ).getReserveTokensAddresses(item.token); if (xTokenAddress != address(0)) { diff --git a/contracts/deployments/ReservesSetupHelper.sol b/contracts/deployments/ReservesSetupHelper.sol index 72f0e3a13..039ab578d 100644 --- a/contracts/deployments/ReservesSetupHelper.sol +++ b/contracts/deployments/ReservesSetupHelper.sol @@ -20,6 +20,7 @@ contract ReservesSetupHelper is Ownable { uint256 reserveFactor; uint256 borrowCap; uint256 supplyCap; + bool stableBorrowingEnabled; bool borrowingEnabled; } @@ -52,6 +53,11 @@ inputParams[i].asset, inputParams[i].asset, inputParams[i].borrowCap ); + + configurator.setReserveStableRateBorrowing( + inputParams[i].asset, + inputParams[i].stableBorrowingEnabled + ); } configurator.setSupplyCap( inputParams[i].asset, diff --git a/contracts/interfaces/IBendPool.sol b/contracts/interfaces/IBendPool.sol new file mode 100644 index 000000000..f87ccb174 --- /dev/null +++ b/contracts/interfaces/IBendPool.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.10; + +interface IBendPool { + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; +} diff --git a/contracts/interfaces/ICEther.sol b/contracts/interfaces/ICEther.sol new file mode 100644 index 000000000..0a7b822e6 --- /dev/null +++ b/contracts/interfaces/ICEther.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.10; + +interface ICEther { + function mint() external payable; +} diff --git a/contracts/interfaces/IInstantNFTOracle.sol b/contracts/interfaces/IInstantNFTOracle.sol new file mode 100644 index 000000000..d6054b08d --- /dev/null +++ b/contracts/interfaces/IInstantNFTOracle.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +interface IInstantNFTOracle { + function getPresentValueAndDiscountRate(uint256 tokenId, uint256 borrowRate) + external + view + returns (uint256, uint256); + + function getPresentValueByDiscountRate( + uint256 tokenId, + uint256 discountRate + ) external view returns (uint256); + + function getEndTime(uint256 tokenId) external view returns (uint256); +} diff --git a/contracts/interfaces/IInstantWithdrawNFT.sol b/contracts/interfaces/IInstantWithdrawNFT.sol new file mode 100644 index 000000000..54850f413 --- /dev/null +++ b/contracts/interfaces/IInstantWithdrawNFT.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +/** + * @title IInstantWithdrawNFT + * + * @notice Defines the basic interface for an InstantWithdrawNFT contract. + **/ +interface IInstantWithdrawNFT { + function burn(uint256 tokenId) external; +} diff --git a/contracts/interfaces/ILoanVault.sol b/contracts/interfaces/ILoanVault.sol new file mode 100644 index 000000000..a38f96373 --- /dev/null +++ b/contracts/interfaces/ILoanVault.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +/** + * @title ILoanVault + * + * @notice Defines the basic interface for an LoanVault contract. + **/ +interface ILoanVault { + function transferCollateral( + address collateralAsset, + uint256 collateralTokenId, + address to + ) external; + + function settleCollateral( + address collateralAsset, + uint256 collateralTokenId + ) external; + + function swapETHToDerivativeAsset(address asset, uint256 amount) external; +} diff --git a/contracts/interfaces/IPool.sol b/contracts/interfaces/IPool.sol index 0dffc88a9..c80978027 100644 --- a/contracts/interfaces/IPool.sol +++ b/contracts/interfaces/IPool.sol @@ -6,6 +6,7 @@ import {IPoolMarketplace} from "./IPoolMarketplace.sol"; import {IPoolParameters} from "./IPoolParameters.sol"; import {IParaProxyInterfaces} from "./IParaProxyInterfaces.sol"; import "./IPoolApeStaking.sol"; +import "./IPoolInstantWithdraw.sol"; /** * @title IPool @@ -17,6 +18,7 @@ interface IPool is IPoolMarketplace, IPoolParameters, IPoolApeStaking, + IPoolInstantWithdraw, IParaProxyInterfaces { diff --git a/contracts/interfaces/IPoolConfigurator.sol b/contracts/interfaces/IPoolConfigurator.sol index ec0e8643f..719a73ff8 100644 --- a/contracts/interfaces/IPoolConfigurator.sol +++ b/contracts/interfaces/IPoolConfigurator.sol @@ -13,12 +13,14 @@ interface IPoolConfigurator { * @dev Emitted when a reserve is initialized. * @param asset The address of the underlying asset of the reserve * @param xToken The address of the associated xToken contract + * @param stableDebtToken The address of the associated stable rate debt token * @param variableDebtToken The address of the associated variable rate debt token * @param interestRateStrategyAddress The address of the interest rate strategy for the reserve **/ event ReserveInitialized( address indexed asset, address indexed xToken, + address stableDebtToken, address variableDebtToken, address interestRateStrategyAddress ); @@ -44,6 +46,13 @@ interface IPoolConfigurator { uint256 liquidationBonus ); + /** + * @dev Emitted when stable rate borrowing is enabled or disabled on a reserve + * @param asset The address of the underlying asset of the reserve + * @param enabled True if stable rate borrowing is enabled, false otherwise + **/ + event ReserveStableRateBorrowing(address indexed asset, bool enabled); + /** * @dev Emitted when a reserve is activated or deactivated * @param asset The address of the underlying asset of the reserve @@ -155,6 +164,18 @@ interface IPoolConfigurator { address indexed implementation ); + /** + * @dev Emitted when the implementation of a stable debt token is upgraded. + * @param asset The address of the underlying asset of the reserve + * @param proxy The stable debt token proxy address + * @param implementation The new xToken implementation + **/ + event StableDebtTokenUpgraded( + address indexed asset, + address indexed proxy, + address indexed implementation + ); + /** * @dev Emitted when the implementation of a variable debt token is upgraded. * @param asset The address of the underlying asset of the reserve @@ -203,6 +224,14 @@ interface IPoolConfigurator { ConfiguratorInputTypes.UpdateNTokenInput calldata input ) external; + /** + * @notice Updates the stable debt token implementation for the reserve. + * @param input The stableDebtToken update parameters + **/ + function updateStableDebtToken( + ConfiguratorInputTypes.UpdateDebtTokenInput calldata input + ) external; + /** * @notice Updates the variable debt token implementation for the asset. * @param input The variableDebtToken update parameters @@ -235,6 +264,15 @@ interface IPoolConfigurator { uint256 liquidationBonus ) external; + /** + * @notice Enable or disable stable rate borrowing on a reserve. + * @dev Can only be enabled (set to true) if borrowing is enabled + * @param asset The address of the underlying asset of the reserve + * @param enabled True if stable rate borrowing needs to be enabled, false otherwise + **/ + function setReserveStableRateBorrowing(address asset, bool enabled) + external; + /** * @notice Activate or deactivate a reserve * @param asset The address of the underlying asset of the reserve diff --git a/contracts/interfaces/IPoolCore.sol b/contracts/interfaces/IPoolCore.sol index 0ad6c1f7e..8bbdad7c0 100644 --- a/contracts/interfaces/IPoolCore.sol +++ b/contracts/interfaces/IPoolCore.sol @@ -70,6 +70,7 @@ interface IPoolCore { * initiator of the transaction on flashLoan() * @param onBehalfOf The address that will be getting the debt * @param amount The amount borrowed out + * @param interestRateMode The rate mode: 1 for Stable, 2 for Variable * @param borrowRate The numeric rate at which the user has borrowed, expressed in ray * @param referralCode The referral code used **/ @@ -78,6 +79,7 @@ interface IPoolCore { address user, address indexed onBehalfOf, uint256 amount, + DataTypes.InterestRateMode interestRateMode, uint256 borrowRate, uint16 indexed referralCode ); diff --git a/contracts/interfaces/IPoolInstantWithdraw.sol b/contracts/interfaces/IPoolInstantWithdraw.sol new file mode 100644 index 000000000..f6a380207 --- /dev/null +++ b/contracts/interfaces/IPoolInstantWithdraw.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import {IPoolAddressesProvider} from "./IPoolAddressesProvider.sol"; +import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; + +/** + * @title IPoolInstantWithdraw + * + * @notice Defines the basic interface for an ParaSpace Instant Withdraw. + **/ +interface IPoolInstantWithdraw { + /** + * @dev Emitted when the value of loan creation fee rate update + **/ + event LoanCreationFeeRateUpdated(uint256 oldValue, uint256 newValue); + + /** + * @dev Emitted when a loan is created + * @param user The address of the user who created the loan + * @param loanId The Id of the loan + * @param collateralAsset The collateral asset of the loan + * @param collateralTokenId The collateral token Id of the loan + * @param borrowAsset The borrow asset token address of the loan + * @param borrowAmount The borrow amount of the loan + * @param discountRate The discount rate of the collateral asset when created the loan + */ + event LoanCreated( + address indexed user, + uint256 indexed loanId, + address collateralAsset, + uint256 collateralTokenId, + address borrowAsset, + uint256 borrowAmount, + uint256 discountRate + ); + + /** + * @dev Emitted when a loan's collateral was swapped by user + * @param user The address swapped the collateral + * @param loanId The Id of the loan + * @param swapAsset The asset token address for the swap + * @param swapAmount The token amount for the swap + */ + event LoanCollateralSwapped( + address indexed user, + uint256 indexed loanId, + address swapAsset, + uint256 swapAmount + ); + + /** + * @dev Emitted when a loan is repaid by the borrower + * @param loanId The Id of the loan + * @param settler The address settled the loan + * @param repayAsset The repay asset token address + * @param repayAmount The repay token amount + */ + event LoanSettled( + uint256 indexed loanId, + address settler, + address repayAsset, + uint256 repayAmount + ); + + /** + * @notice add borrowable asset list for the specified collateral asset + * @dev Only callable by asset listing or pool admin + * @param collateralAsset The address of the collateral asset + * @param borrowAssets The address array of the borrowable asset list + **/ + function addBorrowableAssets( + address collateralAsset, + address[] calldata borrowAssets + ) external; + + /** + * @notice remove borrowable asset list for a specified collateral asset + * @dev Only callable by asset listing or pool admin + * @param collateralAsset The address of the collateral asset + * @param borrowAssets The address array of the borrowable asset list + **/ + function removeBorrowableAssets( + address collateralAsset, + address[] calldata borrowAssets + ) external; + + /** + * @notice update fee rate for creating loan + * @dev Only callable by asset listing or pool admin + * @param feeRate new fee rate + **/ + function setLoanCreationFeeRate(uint256 feeRate) external; + + /** + * @notice get borrowable asset list for the specified collateral asset + * @param collateralAsset The address of the collateral asset + **/ + function getBorrowableAssets(address collateralAsset) + external + view + returns (address[] memory); + + /** + * @notice get loan id list for the specified user address + * @param user The address of the specified user + **/ + function getUserLoanIdList(address user) + external + view + returns (uint256[] memory); + + /** + * @notice get detail loan info for the specified loan id + **/ + function getLoanInfo(uint256 loanId) + external + view + returns (DataTypes.TermLoanData memory); + + /** + * @notice get current present value of the specified loan's collateral asset + **/ + function getLoanCollateralPresentValue(uint256 loanId) + external + view + returns (uint256); + + /** + * @notice get current debt value of the specified loan + **/ + function getLoanDebtValue(uint256 loanId) external view returns (uint256); + + /** + * @notice create a term loan with the specified collateral asset + * @param collateralAsset The address of the collateral asset + * @param collateralTokenId The token id of the collateral asset + * @param borrowAsset The address of the asset user wanted to borrow + * @return the loan's borrow amount + **/ + function createLoan( + address collateralAsset, + uint256 collateralTokenId, + address borrowAsset, + uint16 referralCode + ) external returns (uint256); + + /** + * @notice swap a term loan collateral with the loan's borrow asset, + * the amount user need to pay is calculated by the present value of the collateral + * @param loanId The id for the specified loan + * @param receiver The address to receive the collateral asset + **/ + function swapLoanCollateral(uint256 loanId, address receiver) external; + + /** + * @notice settle a term loan collateral, The collateral asset will be + redeem as ETH to repay loan's debt + * @param loanId The id for the specified loan + **/ + function settleTermLoan(uint256 loanId) external; +} diff --git a/contracts/interfaces/IPoolParameters.sol b/contracts/interfaces/IPoolParameters.sol index 7b9a02001..e0dea2d96 100644 --- a/contracts/interfaces/IPoolParameters.sol +++ b/contracts/interfaces/IPoolParameters.sol @@ -14,6 +14,7 @@ interface IPoolParameters { * @dev Emitted when the state of a reserve is updated. * @param reserve The address of the underlying asset of the reserve * @param liquidityRate The next liquidity rate + * @param stableBorrowRate The next stable borrow rate * @param variableBorrowRate The next variable borrow rate * @param liquidityIndex The next liquidity index * @param variableBorrowIndex The next variable borrow index @@ -21,6 +22,7 @@ interface IPoolParameters { event ReserveDataUpdated( address indexed reserve, uint256 liquidityRate, + uint256 stableBorrowRate, uint256 variableBorrowRate, uint256 liquidityIndex, uint256 variableBorrowIndex @@ -37,12 +39,14 @@ interface IPoolParameters { * @dev Only callable by the PoolConfigurator contract * @param asset The address of the underlying asset of the reserve * @param xTokenAddress The address of the xToken that will be assigned to the reserve + * @param stableDebtAddress The address of the StableDebtToken that will be assigned to the reserve * @param variableDebtAddress The address of the VariableDebtToken that will be assigned to the reserve * @param interestRateStrategyAddress The address of the interest rate strategy contract **/ function initReserve( address asset, address xTokenAddress, + address stableDebtAddress, address variableDebtAddress, address interestRateStrategyAddress, address auctionStrategyAddress @@ -123,13 +127,13 @@ interface IPoolParameters { function revokeUnlimitedApprove(address token, address to) external; /** - * @notice undate fee percentage for claim ape for compound + * @notice update fee percentage for claim ape for compound * @param fee new fee percentage */ function setClaimApeForCompoundFee(uint256 fee) external; /** - * @notice undate ape compound strategy + * @notice update ape compound strategy * @param strategy new compound strategy */ function setApeCompoundStrategy( diff --git a/contracts/interfaces/IProtocolDataProvider.sol b/contracts/interfaces/IProtocolDataProvider.sol index 3b703c68c..517abd3fa 100644 --- a/contracts/interfaces/IProtocolDataProvider.sol +++ b/contracts/interfaces/IProtocolDataProvider.sol @@ -9,9 +9,12 @@ interface IProtocolDataProvider { * @param asset The address of the underlying asset of the reserve * @return accruedToTreasuryScaled The scaled amount of tokens accrued to treasury that is to be minted * @return totalPToken The total supply of the xToken + * @return totalStableDebt The total stable debt of the reserve * @return totalVariableDebt The total variable debt of the reserve * @return liquidityRate The liquidity rate of the reserve * @return variableBorrowRate The variable borrow rate of the reserve + * @return stableBorrowRate The stable borrow rate of the reserve + * @return averageStableBorrowRate The average stable borrow rate of the reserve * @return liquidityIndex The liquidity index of the reserve * @return variableBorrowIndex The variable borrow index of the reserve * @return lastUpdateTimestamp The timestamp of the last update of the reserve @@ -22,9 +25,12 @@ interface IProtocolDataProvider { returns ( uint256 accruedToTreasuryScaled, uint256 totalPToken, + uint256 totalStableDebt, uint256 totalVariableDebt, uint256 liquidityRate, uint256 variableBorrowRate, + uint256 stableBorrowRate, + uint256 averageStableBorrowRate, uint256 liquidityIndex, uint256 variableBorrowIndex, uint40 lastUpdateTimestamp @@ -114,6 +120,10 @@ interface IProtocolDataProvider { * @return currentVariableDebt The current variable debt of the user * @return scaledVariableDebt The scaled variable debt of the user * @return liquidityRate The liquidity rate of the reserve + * @return currentStableDebt The current stable debt of the user + * @return principalStableDebt The principal stable debt of the user + * @return stableBorrowRate The stable borrow rate of the user + * @return stableRateLastUpdated The timestamp of the last update of the user stable rate * @return usageAsCollateralEnabled True if the user is using the asset as collateral, false * otherwise **/ @@ -127,6 +137,10 @@ interface IProtocolDataProvider { uint256 currentVariableDebt, uint256 scaledVariableDebt, uint256 liquidityRate, + uint256 currentStableDebt, + uint256 principalStableDebt, + uint256 stableBorrowRate, + uint40 stableRateLastUpdated, bool usageAsCollateralEnabled ); @@ -134,12 +148,17 @@ interface IProtocolDataProvider { * @notice Returns the token addresses of the reserve * @param asset The address of the underlying asset of the reserve * @return xTokenAddress The PToken address of the reserve + * @return stableDebtTokenAddress The StableDebtToken address of the reserve * @return variableDebtTokenAddress The VariableDebtToken address of the reserve */ function getReserveTokensAddresses(address asset) external view - returns (address xTokenAddress, address variableDebtTokenAddress); + returns ( + address xTokenAddress, + address stableDebtTokenAddress, + address variableDebtTokenAddress + ); /** * @notice Returns the address of the Interest Rate strategy diff --git a/contracts/interfaces/IReserveInterestRateStrategy.sol b/contracts/interfaces/IReserveInterestRateStrategy.sol index 5f736d52d..fd3632ff7 100644 --- a/contracts/interfaces/IReserveInterestRateStrategy.sol +++ b/contracts/interfaces/IReserveInterestRateStrategy.sol @@ -25,9 +25,17 @@ interface IReserveInterestRateStrategy { * @notice Calculates the interest rates depending on the reserve's state and configurations * @param params The parameters needed to calculate interest rates * @return liquidityRate The liquidity rate expressed in rays + * @return stableBorrowRate The stable borrow rate expressed in rays * @return variableBorrowRate The variable borrow rate expressed in rays **/ function calculateInterestRates( DataTypes.CalculateInterestRatesParams memory params - ) external view returns (uint256, uint256); + ) + external + view + returns ( + uint256, + uint256, + uint256 + ); } diff --git a/contracts/interfaces/IStableDebtToken.sol b/contracts/interfaces/IStableDebtToken.sol new file mode 100644 index 000000000..b4ce08541 --- /dev/null +++ b/contracts/interfaces/IStableDebtToken.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import {IInitializableDebtToken} from "./IInitializableDebtToken.sol"; + +/** + * @title IStableDebtToken + * + * @notice Defines the interface for the stable debt token + * @dev It does not inherit from IERC20 to save in code size + **/ +interface IStableDebtToken is IInitializableDebtToken { + /** + * @dev Emitted when new stable debt is minted + * @param user The address of the user who triggered the minting + * @param onBehalfOf The recipient of stable debt tokens + * @param amount The amount minted (user entered amount + balance increase from interest) + * @param currentBalance The current balance of the user + * @param balanceIncrease The increase in balance since the last action of the user + * @param newRate The rate of the debt after the minting + * @param avgStableRate The next average stable rate after the minting + * @param newTotalSupply The next total supply of the stable debt token after the action + **/ + event Mint( + address indexed user, + address indexed onBehalfOf, + uint256 amount, + uint256 currentBalance, + uint256 balanceIncrease, + uint256 newRate, + uint256 avgStableRate, + uint256 newTotalSupply + ); + + /** + * @dev Emitted when new stable debt is burned + * @param from The address from which the debt will be burned + * @param amount The amount being burned (user entered amount - balance increase from interest) + * @param currentBalance The current balance of the user + * @param balanceIncrease The the increase in balance since the last action of the user + * @param avgStableRate The next average stable rate after the burning + * @param newTotalSupply The next total supply of the stable debt token after the action + **/ + event Burn( + address indexed from, + uint256 amount, + uint256 currentBalance, + uint256 balanceIncrease, + uint256 avgStableRate, + uint256 newTotalSupply + ); + + /** + * @notice Mints debt token to the `onBehalfOf` address. + * @dev The resulting rate is the weighted average between the rate of the new debt + * and the rate of the previous debt + * @param user The address receiving the borrowed underlying, being the delegatee in case + * of credit delegate, or same as `onBehalfOf` otherwise + * @param onBehalfOf The address receiving the debt tokens + * @param amount The amount of debt tokens to mint + * @param rate The rate of the debt being minted + * @return True if it is the first borrow, false otherwise + * @return The total stable debt + * @return The average stable borrow rate + **/ + function mint( + address user, + address onBehalfOf, + uint256 amount, + uint256 rate + ) + external + returns ( + bool, + uint256, + uint256 + ); + + /** + * @notice Burns debt of `user` + * @dev The resulting rate is the weighted average between the rate of the new debt + * and the rate of the previous debt + * @dev In some instances, a burn transaction will emit a mint event + * if the amount to burn is less than the interest the user earned + * @param from The address from which the debt will be burned + * @param amount The amount of debt tokens getting burned + * @return The total stable debt + * @return The average stable borrow rate + **/ + function burn(address from, uint256 amount) + external + returns (uint256, uint256); + + /** + * @notice Returns the average rate of all the stable rate loans. + * @return The average stable rate + **/ + function getAverageStableRate() external view returns (uint256); + + /** + * @notice Returns the stable rate of the user debt + * @param user The address of the user + * @return The stable rate of the user + **/ + function getUserStableRate(address user) external view returns (uint256); + + /** + * @notice Returns the timestamp of the last update of the user + * @param user The address of the user + * @return The timestamp + **/ + function getUserLastUpdated(address user) external view returns (uint40); + + /** + * @notice Returns the principal, the total supply, the average stable rate and the timestamp for the last update + * @return The principal + * @return The total supply + * @return The average stable rate + * @return The timestamp of the last update + **/ + function getSupplyData() + external + view + returns ( + uint256, + uint256, + uint256, + uint40 + ); + + /** + * @notice Returns the timestamp of the last update of the total supply + * @return The timestamp + **/ + function getTotalSupplyLastUpdated() external view returns (uint40); + + /** + * @notice Returns the total supply and the average stable rate + * @return The total supply + * @return The average rate + **/ + function getTotalSupplyAndAvgRate() + external + view + returns (uint256, uint256); + + /** + * @notice Returns the principal debt balance of the user + * @return The debt balance of the user since the last burn/mint action + **/ + function principalBalanceOf(address user) external view returns (uint256); + + /** + * @notice Returns the address of the underlying asset of this stableDebtToken (E.g. WETH for stableDebtWETH) + * @return The address of the underlying asset + **/ + function UNDERLYING_ASSET_ADDRESS() external view returns (address); +} diff --git a/contracts/interfaces/IWstETH.sol b/contracts/interfaces/IWstETH.sol new file mode 100644 index 000000000..dd3dda623 --- /dev/null +++ b/contracts/interfaces/IWstETH.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.10; + +interface IWstETH { + function stETH() external returns (address); + + function wrap(uint256 _stETHAmount) external returns (uint256); +} diff --git a/contracts/misc/LoanVault.sol b/contracts/misc/LoanVault.sol new file mode 100644 index 000000000..dae7e9576 --- /dev/null +++ b/contracts/misc/LoanVault.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import "../dependencies/openzeppelin/upgradeability/Initializable.sol"; +import "../dependencies/openzeppelin/upgradeability/OwnableUpgradeable.sol"; +import {IERC20} from "../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC721} from "../dependencies/openzeppelin/contracts/IERC721.sol"; +import {SafeERC20} from "../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {IInstantWithdrawNFT} from "../interfaces/IInstantWithdrawNFT.sol"; +import {IPool} from "../interfaces/IPool.sol"; +import {IWETH} from "./interfaces/IWETH.sol"; +import {ILido} from "../interfaces/ILido.sol"; +import {IAToken} from "../interfaces/IAToken.sol"; +import {IBendPool} from "../interfaces/IBendPool.sol"; +import {IWstETH} from "../interfaces/IWstETH.sol"; +import {ICEther} from "../interfaces/ICEther.sol"; + +/** + * @title LoanVault + **/ +contract LoanVault is Initializable, OwnableUpgradeable { + using SafeERC20 for IERC20; + + /** + * @dev Emitted during rescueERC20() + * @param token The address of the token + * @param to The address of the recipient + * @param amount The amount being rescued + **/ + event RescueERC20( + address indexed token, + address indexed to, + uint256 amount + ); + + /** + * @dev Emitted during rescueERC721() + * @param token The address of the token + * @param to The address of the recipient + * @param ids The ids of the tokens being rescued + **/ + event RescueERC721( + address indexed token, + address indexed to, + uint256[] ids + ); + + address private immutable lendingPool; + address private immutable wETH; + address private immutable aETH; + IPool private immutable aavePool; + /* + address private immutable astETH; + address private immutable awstETH; + address private immutable bendETH; + address private immutable bendPool; + address private immutable cETH; + address private immutable stETH; + address private immutable wstETH; + */ + + /** + * @dev Only pool can call functions marked by this modifier. + **/ + modifier onlyPool() { + require(_msgSender() == lendingPool, "caller must be pool"); + _; + } + + constructor( + address _lendingPool, + address _wETH, + address _aETH + ) + /* + address _bendETH, + address _cETH, + address _wstETH, + address _astETH, + address _awstETH +*/ + { + lendingPool = _lendingPool; + wETH = _wETH; + aETH = _aETH; + aavePool = IAToken(_aETH).POOL(); + /* + astETH = _astETH; + awstETH = _awstETH; + bendETH = _bendETH; + cETH = _cETH; + wstETH = _wstETH; + stETH = IWstETH(_wstETH).stETH(); + bendPool = address(IAToken(_bendETH).POOL()); + */ + } + + function initialize() public initializer { + __Ownable_init(); + + _unlimitedApproveToLendingPool(wETH); + _unlimitedApproveToLendingPool(aETH); + /* + _unlimitedApproveToLendingPool(astETH); + _unlimitedApproveToLendingPool(awstETH); + _unlimitedApproveToLendingPool(bendETH); + _unlimitedApproveToLendingPool(cETH); + _unlimitedApproveToLendingPool(stETH); + */ + } + + function _unlimitedApproveToLendingPool(address token) internal { + uint256 allowance = IERC20(token).allowance(address(this), lendingPool); + if (allowance == 0) { + IERC20(token).safeApprove(lendingPool, type(uint256).max); + } + } + + function transferCollateral( + address collateralAsset, + uint256 collateralTokenId, + address to + ) external onlyPool { + IERC721(collateralAsset).safeTransferFrom( + address(this), + to, + collateralTokenId + ); + } + + function settleCollateral( + address collateralAsset, + uint256 collateralTokenId + ) external onlyPool { + IInstantWithdrawNFT(collateralAsset).burn(collateralTokenId); + } + + function swapETHToDerivativeAsset(address asset, uint256 amount) + external + onlyPool + { + if (asset == wETH) { + IWETH(wETH).deposit{value: amount}(); + } else if (asset == aETH) { + IWETH(wETH).deposit{value: amount}(); + aavePool.supply(wETH, amount, address(this), 0); + /* + } else if (asset == astETH) { + ILido(stETH).submit{value: amount}(address(0)); + aavePool.supply(stETH, amount, address(this), 0); + } else if (asset == awstETH) { + ILido(stETH).submit{value: amount}(address(0)); + uint256 wstEthAmount = IWstETH(wstETH).wrap(amount); + aavePool.supply(wstETH, wstEthAmount, address(this), 0); + } else if (asset == bendETH) { + IWETH(wETH).deposit{value: amount}(); + IBendPool(bendPool).deposit(wETH, amount, address(this), 0); + } else if (asset == cETH) { + ICEther(cETH).mint{value: amount}(); + } else if (asset == stETH) { + ILido(stETH).submit{value: amount}(address(0)); + */ + } else { + revert("not support asset"); + } + } + + receive() external payable {} + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) external pure returns (bytes4) { + return this.onERC721Received.selector; + } + + function rescueERC20( + address token, + address to, + uint256 amount + ) external onlyOwner { + IERC20(token).safeTransfer(to, amount); + emit RescueERC20(token, to, amount); + } + + function rescueERC721( + address token, + address to, + uint256[] calldata ids + ) external onlyOwner { + for (uint256 i = 0; i < ids.length; i++) { + IERC721(token).safeTransferFrom(address(this), to, ids[i]); + } + emit RescueERC721(token, to, ids); + } +} diff --git a/contracts/misc/ProtocolDataProvider.sol b/contracts/misc/ProtocolDataProvider.sol index b752f1077..27591258f 100644 --- a/contracts/misc/ProtocolDataProvider.sol +++ b/contracts/misc/ProtocolDataProvider.sol @@ -8,6 +8,7 @@ import {UserConfiguration} from "../protocol/libraries/configuration/UserConfigu import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; import {WadRayMath} from "../protocol/libraries/math/WadRayMath.sol"; import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; +import {IStableDebtToken} from "../interfaces/IStableDebtToken.sol"; import {IVariableDebtToken} from "../interfaces/IVariableDebtToken.sol"; import {ICollateralizableERC721} from "../interfaces/ICollateralizableERC721.sol"; import {IScaledBalanceToken} from "../interfaces/IScaledBalanceToken.sol"; @@ -132,6 +133,7 @@ contract ProtocolDataProvider is IProtocolDataProvider { reserveData.isActive, reserveData.isFrozen, reserveData.borrowingEnabled, + reserveData.stableBorrowRateEnabled, reserveData.isPaused, ) = configuration.getFlags(); @@ -179,9 +181,12 @@ contract ProtocolDataProvider is IProtocolDataProvider { returns ( uint256 accruedToTreasuryScaled, uint256 totalPToken, + uint256 totalStableDebt, uint256 totalVariableDebt, uint256 liquidityRate, uint256 variableBorrowRate, + uint256 stableBorrowRate, + uint256 averageStableBorrowRate, uint256 liquidityIndex, uint256 variableBorrowIndex, uint40 lastUpdateTimestamp @@ -194,9 +199,13 @@ contract ProtocolDataProvider is IProtocolDataProvider { return ( reserve.accruedToTreasury, IERC20Detailed(reserve.xTokenAddress).totalSupply(), + IERC20Detailed(reserve.stableDebtTokenAddress).totalSupply(), IERC20Detailed(reserve.variableDebtTokenAddress).totalSupply(), reserve.currentLiquidityRate, reserve.currentVariableBorrowRate, + reserve.currentStableBorrowRate, + IStableDebtToken(reserve.stableDebtTokenAddress) + .getAverageStableRate(), reserve.liquidityIndex, reserve.variableBorrowIndex, reserve.lastUpdateTimestamp @@ -230,7 +239,9 @@ contract ProtocolDataProvider is IProtocolDataProvider { DataTypes.ReserveData memory reserve = IPool( ADDRESSES_PROVIDER.getPool() ).getReserveData(asset); - return IERC20Detailed(reserve.variableDebtTokenAddress).totalSupply(); + return + IERC20Detailed(reserve.stableDebtTokenAddress).totalSupply() + + IERC20Detailed(reserve.variableDebtTokenAddress).totalSupply(); } /// @inheritdoc IProtocolDataProvider @@ -244,6 +255,10 @@ contract ProtocolDataProvider is IProtocolDataProvider { uint256 currentVariableDebt, uint256 scaledVariableDebt, uint256 liquidityRate, + uint256 currentStableDebt, + uint256 principalStableDebt, + uint256 stableBorrowRate, + uint40 stableRateLastUpdated, bool usageAsCollateralEnabled ) { @@ -264,36 +279,52 @@ contract ProtocolDataProvider is IProtocolDataProvider { ); scaledXTokenBalance = IPToken(reserve.xTokenAddress) .scaledBalanceOf(user); + stableBorrowRate = IStableDebtToken(reserve.stableDebtTokenAddress) + .getUserStableRate(user); + stableRateLastUpdated = IStableDebtToken( + reserve.stableDebtTokenAddress + ).getUserLastUpdated(user); + currentVariableDebt = IERC20Detailed( + reserve.variableDebtTokenAddress + ).balanceOf(user); + currentStableDebt = IERC20Detailed(reserve.stableDebtTokenAddress) + .balanceOf(user); + principalStableDebt = IStableDebtToken( + reserve.stableDebtTokenAddress + ).principalBalanceOf(user); + scaledVariableDebt = IVariableDebtToken( + reserve.variableDebtTokenAddress + ).scaledBalanceOf(user); } else { currentXTokenBalance = INToken(reserve.xTokenAddress).balanceOf( user ); - scaledXTokenBalance = INToken(reserve.xTokenAddress).balanceOf( - user - ); + scaledXTokenBalance = currentXTokenBalance; collateralizedBalance = ICollateralizableERC721( reserve.xTokenAddress ).collateralizedBalanceOf(user); } - - currentVariableDebt = IERC20Detailed(reserve.variableDebtTokenAddress) - .balanceOf(user); - scaledVariableDebt = IVariableDebtToken( - reserve.variableDebtTokenAddress - ).scaledBalanceOf(user); } /// @inheritdoc IProtocolDataProvider function getReserveTokensAddresses(address asset) external view - returns (address xTokenAddress, address variableDebtTokenAddress) + returns ( + address xTokenAddress, + address stableDebtTokenAddress, + address variableDebtTokenAddress + ) { DataTypes.ReserveData memory reserve = IPool( ADDRESSES_PROVIDER.getPool() ).getReserveData(asset); - return (reserve.xTokenAddress, reserve.variableDebtTokenAddress); + return ( + reserve.xTokenAddress, + reserve.stableDebtTokenAddress, + reserve.variableDebtTokenAddress + ); } /// @inheritdoc IProtocolDataProvider diff --git a/contracts/mocks/MockedETHNFTOracle.sol b/contracts/mocks/MockedETHNFTOracle.sol new file mode 100644 index 000000000..c7bbc5752 --- /dev/null +++ b/contracts/mocks/MockedETHNFTOracle.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.10; + +import "../interfaces/IInstantNFTOracle.sol"; + +contract MockedETHNFTOracle is IInstantNFTOracle { + uint256 internal startTime; + uint256 internal endTime; + constructor() { + startTime = block.timestamp; + endTime = block.timestamp + 86400; + } + + function getPresentValueAndDiscountRate(uint256, uint256) + external + view + returns (uint256, uint256) { + return (_getPresentValue(), 9000); + } + + function getPresentValueByDiscountRate( + uint256, + uint256 + ) external view returns (uint256) { + return _getPresentValue(); + } + + function getEndTime(uint256) external view returns (uint256) { + return endTime; + } + + function _getPresentValue() internal view returns(uint256) { + return (block.timestamp - startTime) * 1e12 + 1e18; + } +} diff --git a/contracts/mocks/helpers/MockReserveConfiguration.sol b/contracts/mocks/helpers/MockReserveConfiguration.sol index d8f2e61be..c1af93bd8 100644 --- a/contracts/mocks/helpers/MockReserveConfiguration.sol +++ b/contracts/mocks/helpers/MockReserveConfiguration.sol @@ -79,6 +79,16 @@ contract MockReserveConfiguration { return configuration.getBorrowingEnabled(); } + function setStableRateBorrowingEnabled(bool enabled) external { + DataTypes.ReserveConfigurationMap memory config = configuration; + config.setStableRateBorrowingEnabled(enabled); + configuration = config; + } + + function getStableRateBorrowingEnabled() external view returns (bool) { + return configuration.getStableRateBorrowingEnabled(); + } + function setReserveFactor(uint256 reserveFactor) external { DataTypes.ReserveConfigurationMap memory config = configuration; config.setReserveFactor(reserveFactor); @@ -129,6 +139,7 @@ contract MockReserveConfiguration { bool, bool, bool, + bool, DataTypes.AssetType ) { diff --git a/contracts/mocks/tests/MockReserveInterestRateStrategy.sol b/contracts/mocks/tests/MockReserveInterestRateStrategy.sol index 82953a7cd..43151c56f 100644 --- a/contracts/mocks/tests/MockReserveInterestRateStrategy.sol +++ b/contracts/mocks/tests/MockReserveInterestRateStrategy.sol @@ -12,8 +12,11 @@ contract MockReserveInterestRateStrategy is IReserveInterestRateStrategy { uint256 internal immutable _baseVariableBorrowRate; uint256 internal immutable _variableRateSlope1; uint256 internal immutable _variableRateSlope2; + uint256 internal immutable _stableRateSlope1; + uint256 internal immutable _stableRateSlope2; uint256 internal _liquidityRate; + uint256 internal _stableBorrowRate; uint256 internal _variableBorrowRate; constructor( @@ -21,19 +24,27 @@ contract MockReserveInterestRateStrategy is IReserveInterestRateStrategy { uint256 optimalUsageRatio, uint256 baseVariableBorrowRate, uint256 variableRateSlope1, - uint256 variableRateSlope2 + uint256 variableRateSlope2, + uint256 stableRateSlope1, + uint256 stableRateSlope2 ) { OPTIMAL_USAGE_RATIO = optimalUsageRatio; ADDRESSES_PROVIDER = provider; _baseVariableBorrowRate = baseVariableBorrowRate; _variableRateSlope1 = variableRateSlope1; _variableRateSlope2 = variableRateSlope2; + _stableRateSlope1 = stableRateSlope1; + _stableRateSlope2 = stableRateSlope2; } function setLiquidityRate(uint256 liquidityRate) public { _liquidityRate = liquidityRate; } + function setStableBorrowRate(uint256 stableBorrowRate) public { + _stableBorrowRate = stableBorrowRate; + } + function setVariableBorrowRate(uint256 variableBorrowRate) public { _variableBorrowRate = variableBorrowRate; } @@ -46,10 +57,11 @@ contract MockReserveInterestRateStrategy is IReserveInterestRateStrategy { override returns ( uint256 liquidityRate, + uint256 stableBorrowRate, uint256 variableBorrowRate ) { - return (_liquidityRate, _variableBorrowRate); + return (_liquidityRate, _stableBorrowRate, _variableBorrowRate); } function getVariableRateSlope1() external view returns (uint256) { @@ -60,6 +72,14 @@ contract MockReserveInterestRateStrategy is IReserveInterestRateStrategy { return _variableRateSlope2; } + function getStableRateSlope1() external view returns (uint256) { + return _stableRateSlope1; + } + + function getStableRateSlope2() external view returns (uint256) { + return _stableRateSlope2; + } + function getBaseVariableBorrowRate() external view diff --git a/contracts/mocks/tokens/MockInstantWithdrawNFT.sol b/contracts/mocks/tokens/MockInstantWithdrawNFT.sol new file mode 100644 index 000000000..b4dc2793f --- /dev/null +++ b/contracts/mocks/tokens/MockInstantWithdrawNFT.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.10; + +import "../../interfaces/IInstantWithdrawNFT.sol"; +import "./MintableERC721.sol"; + +contract MockedInstantWithdrawNFT is MintableERC721, IInstantWithdrawNFT { + constructor( + string memory name, + string memory symbol, + string memory baseTokenURI + ) MintableERC721(name, symbol, baseTokenURI) { + } + + function burn(uint256 tokenId) external { + _burn(tokenId); + } + + receive() external payable {} +} diff --git a/contracts/mocks/upgradeability/MockStableDebtToken.sol b/contracts/mocks/upgradeability/MockStableDebtToken.sol new file mode 100644 index 000000000..8ac776eed --- /dev/null +++ b/contracts/mocks/upgradeability/MockStableDebtToken.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {StableDebtToken} from "../../protocol/tokenization/StableDebtToken.sol"; +import {IPool} from "../../interfaces/IPool.sol"; + +contract MockStableDebtToken is StableDebtToken { + constructor(IPool pool) StableDebtToken(pool) {} + + function getRevision() internal pure override returns (uint256) { + return 0x3; + } +} diff --git a/contracts/protocol/libraries/configuration/ReserveConfiguration.sol b/contracts/protocol/libraries/configuration/ReserveConfiguration.sol index 42d7a15f6..1f75942b2 100644 --- a/contracts/protocol/libraries/configuration/ReserveConfiguration.sol +++ b/contracts/protocol/libraries/configuration/ReserveConfiguration.sol @@ -349,6 +349,32 @@ library ReserveConfiguration { return (self.data & ~BORROWING_MASK) != 0; } + /** + * @notice Enables or disables stable rate borrowing on the reserve + * @param self The reserve configuration + * @param enabled True if the stable rate borrowing needs to be enabled, false otherwise + **/ + function setStableRateBorrowingEnabled( + DataTypes.ReserveConfigurationMap memory self, + bool enabled + ) internal pure { + self.data = + (self.data & STABLE_BORROWING_MASK) | + (uint256(enabled ? 1 : 0) << + STABLE_BORROWING_ENABLED_START_BIT_POSITION); + } + + /** + * @notice Gets the stable rate borrowing state of the reserve + * @param self The reserve configuration + * @return The stable rate borrowing state + **/ + function getStableRateBorrowingEnabled( + DataTypes.ReserveConfigurationMap memory self + ) internal pure returns (bool) { + return (self.data & ~STABLE_BORROWING_MASK) != 0; + } + /** * @notice Sets the reserve factor of the reserve * @param self The reserve configuration @@ -480,6 +506,7 @@ library ReserveConfiguration { * @return The state flag representing active * @return The state flag representing frozen * @return The state flag representing borrowing enabled + * @return The state flag representing stableRateBorrowing enabled * @return The state flag representing paused * @return The asset type **/ @@ -491,6 +518,7 @@ library ReserveConfiguration { bool, bool, bool, + bool, DataTypes.AssetType ) { @@ -500,6 +528,7 @@ library ReserveConfiguration { (dataLocal & ~ACTIVE_MASK) != 0, (dataLocal & ~FROZEN_MASK) != 0, (dataLocal & ~BORROWING_MASK) != 0, + (dataLocal & ~STABLE_BORROWING_MASK) != 0, (dataLocal & ~PAUSED_MASK) != 0, DataTypes.AssetType( (dataLocal & ~ASSET_TYPE_MASK) >> ASSET_TYPE_START_BIT_POSITION diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 6801d7fe4..8a44fdc64 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -125,4 +125,8 @@ library Errors { string public constant NOT_THE_BAKC_OWNER = "130"; //user is not the bakc owner. string public constant CALLER_NOT_EOA = "131"; //The caller of the function is not an EOA account string public constant MAKER_SAME_AS_TAKER = "132"; //maker and taker shouldn't be the same address + string public constant INVALID_LOAN_STATE = "133"; // invalid term loan status + string public constant INVALID_BORROW_ASSET = "134"; // invalid borrow asset for collateral + string public constant INVALID_PRESENT_VALUE = "135"; // invalid present value + string public constant USAGE_RATIO_TOO_HIGH = "136"; // usage ratio too high after borrow } diff --git a/contracts/protocol/libraries/logic/BorrowLogic.sol b/contracts/protocol/libraries/logic/BorrowLogic.sol index d93ac5571..f9e8dd559 100644 --- a/contracts/protocol/libraries/logic/BorrowLogic.sol +++ b/contracts/protocol/libraries/logic/BorrowLogic.sol @@ -29,6 +29,7 @@ library BorrowLogic { address user, address indexed onBehalfOf, uint256 amount, + DataTypes.InterestRateMode interestRateMode, uint256 borrowRate, uint16 indexed referralCode ); @@ -110,6 +111,7 @@ library BorrowLogic { params.user, params.onBehalfOf, params.amount, + DataTypes.InterestRateMode.VARIABLE, reserve.currentVariableBorrowRate, params.referralCode ); diff --git a/contracts/protocol/libraries/logic/ConfiguratorLogic.sol b/contracts/protocol/libraries/logic/ConfiguratorLogic.sol index 8f5f292e7..8d32110e7 100644 --- a/contracts/protocol/libraries/logic/ConfiguratorLogic.sol +++ b/contracts/protocol/libraries/logic/ConfiguratorLogic.sol @@ -23,6 +23,7 @@ library ConfiguratorLogic { event ReserveInitialized( address indexed asset, address indexed xToken, + address stableDebtToken, address variableDebtToken, address interestRateStrategyAddress, address auctionStrategyAddress @@ -37,6 +38,11 @@ library ConfiguratorLogic { address indexed proxy, address indexed implementation ); + event StableDebtTokenUpgraded( + address indexed asset, + address indexed proxy, + address indexed implementation + ); event VariableDebtTokenUpgraded( address indexed asset, address indexed proxy, @@ -84,6 +90,20 @@ library ConfiguratorLogic { ); } + address stableDebtTokenProxyAddress = _initTokenWithProxy( + input.stableDebtTokenImpl, + abi.encodeWithSelector( + IInitializableDebtToken.initialize.selector, + pool, + input.underlyingAsset, + input.incentivesController, + input.underlyingAssetDecimals, + input.stableDebtTokenName, + input.stableDebtTokenSymbol, + input.params + ) + ); + address variableDebtTokenProxyAddress = _initTokenWithProxy( input.variableDebtTokenImpl, abi.encodeWithSelector( @@ -101,6 +121,7 @@ library ConfiguratorLogic { pool.initReserve( input.underlyingAsset, xTokenProxyAddress, + stableDebtTokenProxyAddress, variableDebtTokenProxyAddress, input.interestRateStrategyAddress, input.auctionStrategyAddress @@ -120,6 +141,7 @@ library ConfiguratorLogic { emit ReserveInitialized( input.underlyingAsset, xTokenProxyAddress, + stableDebtTokenProxyAddress, variableDebtTokenProxyAddress, input.interestRateStrategyAddress, input.auctionStrategyAddress @@ -206,6 +228,48 @@ library ConfiguratorLogic { ); } + /** + * @notice Updates the stable debt token implementation and initializes it + * @dev Emits the `StableDebtTokenUpgraded` event + * @param cachedPool The Pool containing the reserve with the stable debt token + * @param input The parameters needed for the initialize call + */ + function executeUpdateStableDebtToken( + IPool cachedPool, + ConfiguratorInputTypes.UpdateDebtTokenInput calldata input + ) public { + DataTypes.ReserveData memory reserveData = cachedPool.getReserveData( + input.asset + ); + + (, , , uint256 decimals, ) = cachedPool + .getConfiguration(input.asset) + .getParams(); + + bytes memory encodedCall = abi.encodeWithSelector( + IInitializableDebtToken.initialize.selector, + cachedPool, + input.asset, + input.incentivesController, + decimals, + input.name, + input.symbol, + input.params + ); + + _upgradeTokenImplementation( + reserveData.stableDebtTokenAddress, + input.implementation, + encodedCall + ); + + emit StableDebtTokenUpgraded( + input.asset, + reserveData.stableDebtTokenAddress, + input.implementation + ); + } + /** * @notice Updates the variable debt token implementation and initializes it * @dev Emits the `VariableDebtTokenUpgraded` event diff --git a/contracts/protocol/libraries/logic/PoolLogic.sol b/contracts/protocol/libraries/logic/PoolLogic.sol index 2a3a27553..975a88e7d 100644 --- a/contracts/protocol/libraries/logic/PoolLogic.sol +++ b/contracts/protocol/libraries/logic/PoolLogic.sol @@ -46,6 +46,7 @@ library PoolLogic { } reservesData[params.asset].init( params.xTokenAddress, + params.stableDebtAddress, params.variableDebtAddress, params.interestRateStrategyAddress, params.auctionStrategyAddress diff --git a/contracts/protocol/libraries/logic/ReserveLogic.sol b/contracts/protocol/libraries/logic/ReserveLogic.sol index 0a38f2895..4ea68e5b3 100644 --- a/contracts/protocol/libraries/logic/ReserveLogic.sol +++ b/contracts/protocol/libraries/logic/ReserveLogic.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.10; import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; import {GPv2SafeERC20} from "../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol"; +import {IStableDebtToken} from "../../../interfaces/IStableDebtToken.sol"; import {IVariableDebtToken} from "../../../interfaces/IVariableDebtToken.sol"; import {IReserveInterestRateStrategy} from "../../../interfaces/IReserveInterestRateStrategy.sol"; import {ReserveConfiguration} from "../configuration/ReserveConfiguration.sol"; @@ -28,6 +29,7 @@ library ReserveLogic { event ReserveDataUpdated( address indexed reserve, uint256 liquidityRate, + uint256 stableBorrowRate, uint256 variableBorrowRate, uint256 liquidityIndex, uint256 variableBorrowIndex @@ -135,6 +137,7 @@ library ReserveLogic { function init( DataTypes.ReserveData storage reserve, address xTokenAddress, + address stableDebtTokenAddress, address variableDebtTokenAddress, address interestRateStrategyAddress, address auctionStrategyAddress @@ -147,6 +150,7 @@ library ReserveLogic { reserve.liquidityIndex = uint128(WadRayMath.RAY); reserve.variableBorrowIndex = uint128(WadRayMath.RAY); reserve.xTokenAddress = xTokenAddress; + reserve.stableDebtTokenAddress = stableDebtTokenAddress; reserve.variableDebtTokenAddress = variableDebtTokenAddress; reserve.interestRateStrategyAddress = interestRateStrategyAddress; reserve.auctionStrategyAddress = auctionStrategyAddress; @@ -154,8 +158,10 @@ library ReserveLogic { struct UpdateInterestRatesLocalVars { uint256 nextLiquidityRate; + uint256 nextStableRate; uint256 nextVariableRate; uint256 totalVariableDebt; + uint256 availableLiquidity; } /** @@ -181,13 +187,17 @@ library ReserveLogic { ( vars.nextLiquidityRate, + vars.nextStableRate, vars.nextVariableRate ) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress) .calculateInterestRates( DataTypes.CalculateInterestRatesParams({ liquidityAdded: liquidityAdded, liquidityTaken: liquidityTaken, + totalStableDebt: reserveCache.nextTotalStableDebt, totalVariableDebt: vars.totalVariableDebt, + averageStableBorrowRate: reserveCache + .nextAvgStableBorrowRate, reserveFactor: reserveCache.reserveFactor, reserve: reserveAddress, xToken: reserveCache.xTokenAddress @@ -195,11 +205,13 @@ library ReserveLogic { ); reserve.currentLiquidityRate = vars.nextLiquidityRate.toUint128(); + reserve.currentStableBorrowRate = vars.nextStableRate.toUint128(); reserve.currentVariableBorrowRate = vars.nextVariableRate.toUint128(); emit ReserveDataUpdated( reserveAddress, vars.nextLiquidityRate, + vars.nextStableRate, vars.nextVariableRate, reserveCache.nextLiquidityIndex, reserveCache.nextVariableBorrowIndex @@ -207,8 +219,10 @@ library ReserveLogic { } struct AccrueToTreasuryLocalVars { + uint256 prevTotalStableDebt; uint256 prevTotalVariableDebt; uint256 currTotalVariableDebt; + uint256 cumulatedStableInterest; uint256 totalDebtAccrued; uint256 amountToMint; } @@ -239,10 +253,23 @@ library ReserveLogic { reserveCache.nextVariableBorrowIndex ); + //calculate the stable debt until the last timestamp update + vars.cumulatedStableInterest = MathUtils.calculateCompoundedInterest( + reserveCache.currAvgStableBorrowRate, + reserveCache.stableDebtLastUpdateTimestamp, + reserveCache.reserveLastUpdateTimestamp + ); + + vars.prevTotalStableDebt = reserveCache.currPrincipalStableDebt.rayMul( + vars.cumulatedStableInterest + ); + //debt accrued is the sum of the current debt minus the sum of the debt at the last update vars.totalDebtAccrued = - vars.currTotalVariableDebt - - vars.prevTotalVariableDebt; + vars.currTotalVariableDebt + + reserveCache.currTotalStableDebt - + vars.prevTotalVariableDebt - + vars.prevTotalStableDebt; vars.amountToMint = vars.totalDebtAccrued.percentMul( reserveCache.reserveFactor @@ -320,7 +347,7 @@ library ReserveLogic { reserveCache.reserveConfiguration = reserve.configuration; reserveCache.xTokenAddress = reserve.xTokenAddress; - (, , , , DataTypes.AssetType reserveAssetType) = reserveCache + (, , , , , DataTypes.AssetType reserveAssetType) = reserveCache .reserveConfiguration .getFlags(); @@ -334,6 +361,8 @@ library ReserveLogic { reserveCache.currVariableBorrowRate = reserve .currentVariableBorrowRate; + reserveCache.stableDebtTokenAddress = reserve + .stableDebtTokenAddress; reserveCache.variableDebtTokenAddress = reserve .variableDebtTokenAddress; @@ -344,6 +373,20 @@ library ReserveLogic { .nextScaledVariableDebt = IVariableDebtToken( reserveCache.variableDebtTokenAddress ).scaledTotalSupply(); + + ( + reserveCache.currPrincipalStableDebt, + reserveCache.currTotalStableDebt, + reserveCache.currAvgStableBorrowRate, + reserveCache.stableDebtLastUpdateTimestamp + ) = IStableDebtToken(reserveCache.stableDebtTokenAddress) + .getSupplyData(); + + // by default the actions are considered as not affecting the debt balances. + // if the action involves mint/burn of debt, the cache needs to be updated + reserveCache.nextTotalStableDebt = reserveCache.currTotalStableDebt; + reserveCache.nextAvgStableBorrowRate = reserveCache + .currAvgStableBorrowRate; } return reserveCache; diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 18dde7465..ae4cfd08b 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -77,6 +77,7 @@ library ValidationLogic { bool isActive, bool isFrozen, , + , bool isPaused, DataTypes.AssetType reserveAssetType ) = reserveCache.reserveConfiguration.getFlags(); @@ -174,6 +175,7 @@ library ValidationLogic { bool isActive, , , + , bool isPaused, DataTypes.AssetType reserveAssetType ) = reserveCache.reserveConfiguration.getFlags(); @@ -196,6 +198,7 @@ library ValidationLogic { bool isActive, , , + , bool isPaused, DataTypes.AssetType reserveAssetType ) = reserveCache.reserveConfiguration.getFlags(); @@ -240,31 +243,26 @@ library ValidationLogic { bool isFrozen; bool isPaused; bool borrowingEnabled; + bool stableRateBorrowingEnabled; bool siloedBorrowingEnabled; DataTypes.AssetType assetType; } - /** - * @notice Validates a borrow action. - * @param reservesData The state of all the reserves - * @param reservesList The addresses of all the active reserves - * @param params Additional params needed for the validation - */ - function validateBorrow( - mapping(address => DataTypes.ReserveData) storage reservesData, - mapping(uint256 => address) storage reservesList, - DataTypes.ValidateBorrowParams memory params - ) internal view { - require(params.amount != 0, Errors.INVALID_AMOUNT); - ValidateBorrowLocalVars memory vars; + function validateBorrowAsset( + DataTypes.ReserveCache memory reserveCache, + uint256 amount, + ValidateBorrowLocalVars memory vars + ) internal pure { + require(amount != 0, Errors.INVALID_AMOUNT); ( vars.isActive, vars.isFrozen, vars.borrowingEnabled, + vars.stableRateBorrowingEnabled, vars.isPaused, vars.assetType - ) = params.reserveCache.reserveConfiguration.getFlags(); + ) = reserveCache.reserveConfiguration.getFlags(); require( vars.assetType == DataTypes.AssetType.ERC20, @@ -275,32 +273,18 @@ library ValidationLogic { require(!vars.isFrozen, Errors.RESERVE_FROZEN); require(vars.borrowingEnabled, Errors.BORROWING_NOT_ENABLED); - require( - params.priceOracleSentinel == address(0) || - IPriceOracleSentinel(params.priceOracleSentinel) - .isBorrowAllowed(), - Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED - ); - - vars.reserveDecimals = params - .reserveCache - .reserveConfiguration - .getDecimals(); - vars.borrowCap = params - .reserveCache - .reserveConfiguration - .getBorrowCap(); + vars.reserveDecimals = reserveCache.reserveConfiguration.getDecimals(); + vars.borrowCap = reserveCache.reserveConfiguration.getBorrowCap(); unchecked { vars.assetUnit = 10**vars.reserveDecimals; } if (vars.borrowCap != 0) { - vars.totalSupplyVariableDebt = params - .reserveCache + vars.totalSupplyVariableDebt = reserveCache .currScaledVariableDebt - .rayMul(params.reserveCache.nextVariableBorrowIndex); + .rayMul(reserveCache.nextVariableBorrowIndex); - vars.totalDebt = vars.totalSupplyVariableDebt + params.amount; + vars.totalDebt = vars.totalSupplyVariableDebt + amount; unchecked { require( @@ -309,6 +293,40 @@ library ValidationLogic { ); } } + } + + function validateInstantWithdrawBorrow( + DataTypes.ReserveCache memory reserveCache, + uint256 amount + ) internal pure { + ValidateBorrowLocalVars memory vars; + validateBorrowAsset(reserveCache, amount, vars); + require( + vars.stableRateBorrowingEnabled, + Errors.STABLE_BORROWING_NOT_ENABLED + ); + } + + /** + * @notice Validates a borrow action. + * @param reservesData The state of all the reserves + * @param reservesList The addresses of all the active reserves + * @param params Additional params needed for the validation + */ + function validateBorrow( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.ValidateBorrowParams memory params + ) internal view { + ValidateBorrowLocalVars memory vars; + validateBorrowAsset(params.reserveCache, params.amount, vars); + + require( + params.priceOracleSentinel == address(0) || + IPriceOracleSentinel(params.priceOracleSentinel) + .isBorrowAllowed(), + Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED + ); ( vars.userCollateralInBaseCurrency, @@ -384,6 +402,7 @@ library ValidationLogic { bool isActive, , , + , bool isPaused, DataTypes.AssetType assetType ) = reserveCache.reserveConfiguration.getFlags(); @@ -427,6 +446,7 @@ library ValidationLogic { bool isActive, , , + , bool isPaused, DataTypes.AssetType reserveAssetType ) = reserveCache.reserveConfiguration.getFlags(); @@ -449,6 +469,7 @@ library ValidationLogic { bool isActive, , , + , bool isPaused, DataTypes.AssetType reserveAssetType ) = reserveCache.reserveConfiguration.getFlags(); @@ -508,6 +529,7 @@ library ValidationLogic { vars.collateralReserveActive, , , + , vars.collateralReservePaused, vars.collateralReserveAssetType ) = collateralReserve.configuration.getFlags(); @@ -539,6 +561,7 @@ library ValidationLogic { vars.principalReserveActive, , , + , vars.principalReservePaused, ) = params.liquidationAssetReserveCache.reserveConfiguration.getFlags(); @@ -604,6 +627,7 @@ library ValidationLogic { vars.collateralReserveActive, , , + , vars.collateralReservePaused, vars.collateralReserveAssetType ) = collateralReserve.configuration.getFlags(); @@ -629,6 +653,7 @@ library ValidationLogic { vars.principalReserveActive, , , + , vars.principalReservePaused, ) = params.liquidationAssetReserveCache.reserveConfiguration.getFlags(); @@ -755,6 +780,7 @@ library ValidationLogic { vars.collateralReserveActive, , , + , vars.collateralReservePaused, vars.collateralReserveAssetType ) = collateralConfiguration.getFlags(); @@ -814,6 +840,7 @@ library ValidationLogic { vars.collateralReserveActive, , , + , vars.collateralReservePaused, vars.collateralReserveAssetType ) = collateralReserve.configuration.getFlags(); @@ -1024,7 +1051,7 @@ library ValidationLogic { DataTypes.SApeAddress ]; - (bool isActive, , , bool isPaused, ) = sApeReserve + (bool isActive, , , , bool isPaused, ) = sApeReserve .configuration .getFlags(); @@ -1053,6 +1080,7 @@ library ValidationLogic { bool isActive, , , + , bool isPaused, DataTypes.AssetType assetType ) = reserve.configuration.getFlags(); @@ -1182,6 +1210,7 @@ library ValidationLogic { vars.token0IsActive, vars.token0IsFrozen, , + , vars.token0IsPaused, ) = reservesData[token0].configuration.getFlags(); @@ -1190,6 +1219,7 @@ library ValidationLogic { vars.token1IsActive, vars.token1IsFrozen, , + , vars.token1IsPaused, ) = reservesData[token1].configuration.getFlags(); diff --git a/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol b/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol index 60b749529..cd0dd3de4 100644 --- a/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol +++ b/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol @@ -6,6 +6,7 @@ import {DataTypes} from "./DataTypes.sol"; library ConfiguratorInputTypes { struct InitReserveInput { address xTokenImpl; + address stableDebtTokenImpl; address variableDebtTokenImpl; uint8 underlyingAssetDecimals; address interestRateStrategyAddress; @@ -18,6 +19,8 @@ library ConfiguratorInputTypes { string xTokenSymbol; string variableDebtTokenName; string variableDebtTokenSymbol; + string stableDebtTokenName; + string stableDebtTokenSymbol; bytes params; } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index dcc0f7516..ccf709f0a 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.10; import {OfferItem, ConsiderationItem} from "../../../dependencies/seaport/contracts/lib/ConsiderationStructs.sol"; +import {EnumerableSet} from "../../../dependencies/openzeppelin/contracts/EnumerableSet.sol"; +import {Counters} from "../../../dependencies/openzeppelin/contracts/Counters.sol"; library DataTypes { enum AssetType { @@ -23,12 +25,16 @@ library DataTypes { uint128 variableBorrowIndex; //the current variable borrow rate. Expressed in ray uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; //timestamp of last update uint40 lastUpdateTimestamp; //the id of the reserve. Represents the position in the list of the active reserves uint16 id; //xToken address address xTokenAddress; + //stableDebtToken address + address stableDebtTokenAddress; //variableDebtToken address address variableDebtTokenAddress; //address of the interest rate strategy @@ -86,9 +92,20 @@ library DataTypes { bool isAuctioned; } + enum InterestRateMode { + NONE, + STABLE, + VARIABLE + } + struct ReserveCache { uint256 currScaledVariableDebt; uint256 nextScaledVariableDebt; + uint256 currPrincipalStableDebt; + uint256 currAvgStableBorrowRate; + uint256 currTotalStableDebt; + uint256 nextAvgStableBorrowRate; + uint256 nextTotalStableDebt; uint256 currLiquidityIndex; uint256 nextLiquidityIndex; uint256 currVariableBorrowIndex; @@ -98,8 +115,10 @@ library DataTypes { uint256 reserveFactor; ReserveConfigurationMap reserveConfiguration; address xTokenAddress; + address stableDebtTokenAddress; address variableDebtTokenAddress; uint40 reserveLastUpdateTimestamp; + uint40 stableDebtLastUpdateTimestamp; } struct ExecuteLiquidateParams { @@ -272,7 +291,9 @@ library DataTypes { struct CalculateInterestRatesParams { uint256 liquidityAdded; uint256 liquidityTaken; + uint256 totalStableDebt; uint256 totalVariableDebt; + uint256 averageStableBorrowRate; uint256 reserveFactor; address reserve; address xToken; @@ -281,6 +302,7 @@ library DataTypes { struct InitReserveParams { address asset; address xTokenAddress; + address stableDebtAddress; address variableDebtAddress; address interestRateStrategyAddress; address auctionStrategyAddress; @@ -370,6 +392,46 @@ library DataTypes { uint256 swapPercent; } + /** + * @dev Enum describing the current state of a loan + * State change flow: + * None -> Active -> RepaidByUser + * -> Settled + */ + enum LoanState { + // Default status + None, + // Status after the loan has been created + Active, + // The loan has been repaid by user, and the user will get the collateral. This is a terminal state. + Repaid, + // The loan has been repaid by system, and the collateral has redeemed to system asset + Settled + } + + struct TermLoanData { + //the id of the loan + uint256 loanId; + //the current state of the loan + LoanState state; + //loan start timestamp + uint40 startTime; + //loan end timestamp + uint40 endTime; + //address of borrower + address borrower; + //address of collateral asset token + address collateralAsset; + //the token id or collateral amount of collateral token + uint64 collateralTokenId; + //address of borrow asset + address borrowAsset; + //amount of borrow asset + uint256 borrowAmount; + //discount rate when created the loan + uint256 discountRate; + } + struct PoolStorage { // Map of reserves and their data (underlyingAssetOfReserve => reserveData) mapping(address => ReserveData) _reserves; @@ -386,6 +448,20 @@ library DataTypes { uint16 _apeCompoundFee; // Map of user's ape compound strategies mapping(address => ApeCompoundStrategy) _apeCompoundStrategies; + // Reserve storage for ape staking + uint256[20] __apeStakingReserve; + // loan creation fee rate + uint256 _loanCreationFeeRate; + // Map of collateral and borrowable ERC20 asset address set + mapping(address => EnumerableSet.AddressSet) _collateralBorrowableAsset; + // Loan Id Counter + Counters.Counter _loanIdCounter; + // Map of users address and their loanIds + mapping(address => uint256[]) _userLoanIds; + // Map of loanId and loan data + mapping(uint256 => DataTypes.TermLoanData) _termLoans; + // Reserve storage for ETH withdraw + uint256[20] __ethWithdrawReserve; } struct ReserveConfigData { @@ -396,6 +472,7 @@ library DataTypes { uint256 reserveFactor; bool usageAsCollateralEnabled; bool borrowingEnabled; + bool stableBorrowRateEnabled; bool isActive; bool isFrozen; bool isPaused; diff --git a/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol b/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol index 71076269a..969c8782e 100644 --- a/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol +++ b/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol @@ -29,6 +29,12 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { **/ uint256 public immutable OPTIMAL_USAGE_RATIO; + /** + * @dev This constant represents the optimal stable debt to total debt ratio of the reserve. + * Expressed in ray + */ + uint256 public immutable OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO; + /** * @dev This constant represents the excess usage ratio above the optimal. It's always equal to * 1-optimal usage ratio. Added as a constant here for gas optimizations. @@ -36,6 +42,13 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { **/ uint256 public immutable MAX_EXCESS_USAGE_RATIO; + /** + * @dev This constant represents the excess stable debt ratio above the optimal. It's always equal to + * 1-optimal stable to total debt ratio. Added as a constant here for gas optimizations. + * Expressed in ray + **/ + uint256 public immutable MAX_EXCESS_STABLE_TO_TOTAL_DEBT_RATIO; + IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; // Base variable borrow rate when usage rate = 0. Expressed in ray @@ -47,6 +60,18 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { // Slope of the variable interest curve when usage ratio > OPTIMAL_USAGE_RATIO. Expressed in ray uint256 internal immutable _variableRateSlope2; + // Slope of the stable interest curve when usage ratio > 0 and <= OPTIMAL_USAGE_RATIO. Expressed in ray + uint256 internal immutable _stableRateSlope1; + + // Slope of the stable interest curve when usage ratio > OPTIMAL_USAGE_RATIO. Expressed in ray + uint256 internal immutable _stableRateSlope2; + + // Premium on top of `_variableRateSlope1` for base stable borrowing rate + uint256 internal immutable _baseStableRateOffset; + + // Additional premium applied to stable rate when stable debt surpass `OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO` + uint256 internal immutable _stableRateExcessOffset; + /** * @dev Constructor. * @param provider The address of the PoolAddressesProvider contract @@ -54,24 +79,46 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { * @param baseVariableBorrowRate The base variable borrow rate * @param variableRateSlope1 The variable rate slope below optimal usage ratio * @param variableRateSlope2 The variable rate slope above optimal usage ratio + * @param stableRateSlope1 The stable rate slope below optimal usage ratio + * @param stableRateSlope2 The stable rate slope above optimal usage ratio + * @param baseStableRateOffset The premium on top of variable rate for base stable borrowing rate + * @param stableRateExcessOffset The premium on top of stable rate when there stable debt surpass the threshold + * @param optimalStableToTotalDebtRatio The optimal stable debt to total debt ratio of the reserve */ constructor( IPoolAddressesProvider provider, uint256 optimalUsageRatio, uint256 baseVariableBorrowRate, uint256 variableRateSlope1, - uint256 variableRateSlope2 + uint256 variableRateSlope2, + uint256 stableRateSlope1, + uint256 stableRateSlope2, + uint256 baseStableRateOffset, + uint256 stableRateExcessOffset, + uint256 optimalStableToTotalDebtRatio ) { require( WadRayMath.RAY >= optimalUsageRatio, Errors.INVALID_OPTIMAL_USAGE_RATIO ); + require( + WadRayMath.RAY >= optimalStableToTotalDebtRatio, + Errors.INVALID_OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO + ); OPTIMAL_USAGE_RATIO = optimalUsageRatio; MAX_EXCESS_USAGE_RATIO = WadRayMath.RAY - optimalUsageRatio; + OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO = optimalStableToTotalDebtRatio; + MAX_EXCESS_STABLE_TO_TOTAL_DEBT_RATIO = + WadRayMath.RAY - + optimalStableToTotalDebtRatio; ADDRESSES_PROVIDER = provider; _baseVariableBorrowRate = baseVariableBorrowRate; _variableRateSlope1 = variableRateSlope1; _variableRateSlope2 = variableRateSlope2; + _stableRateSlope1 = stableRateSlope1; + _stableRateSlope2 = stableRateSlope2; + _baseStableRateOffset = baseStableRateOffset; + _stableRateExcessOffset = stableRateExcessOffset; } /** @@ -92,6 +139,41 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { return _variableRateSlope2; } + /** + * @notice Returns the stable rate slope below optimal usage ratio + * @dev Its the stable rate when usage ratio > 0 and <= OPTIMAL_USAGE_RATIO + * @return The stable rate slope + **/ + function getStableRateSlope1() external view returns (uint256) { + return _stableRateSlope1; + } + + /** + * @notice Returns the stable rate slope above optimal usage ratio + * @dev Its the variable rate when usage ratio > OPTIMAL_USAGE_RATIO + * @return The stable rate slope + **/ + function getStableRateSlope2() external view returns (uint256) { + return _stableRateSlope2; + } + + /** + * @notice Returns the stable rate excess offset + * @dev An additional premium applied to the stable when stable debt > OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO + * @return The stable rate excess offset + */ + function getStableRateExcessOffset() external view returns (uint256) { + return _stableRateExcessOffset; + } + + /** + * @notice Returns the base stable borrow rate + * @return The base stable borrow rate + **/ + function getBaseStableBorrowRate() public view returns (uint256) { + return _variableRateSlope1 + _baseStableRateOffset; + } + /// @inheritdoc IReserveInterestRateStrategy function getBaseVariableBorrowRate() external @@ -117,24 +199,39 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { uint256 availableLiquidity; uint256 totalDebt; uint256 currentVariableBorrowRate; + uint256 currentStableBorrowRate; uint256 currentLiquidityRate; uint256 borrowUsageRatio; uint256 supplyUsageRatio; + uint256 stableToTotalDebtRatio; uint256 availableLiquidityPlusDebt; } /// @inheritdoc IReserveInterestRateStrategy function calculateInterestRates( DataTypes.CalculateInterestRatesParams calldata params - ) external view override returns (uint256, uint256) { + ) + external + view + override + returns ( + uint256, + uint256, + uint256 + ) + { CalcInterestRatesLocalVars memory vars; - vars.totalDebt = params.totalVariableDebt; + vars.totalDebt = params.totalStableDebt + params.totalVariableDebt; vars.currentLiquidityRate = 0; vars.currentVariableBorrowRate = _baseVariableBorrowRate; + vars.currentStableBorrowRate = getBaseStableBorrowRate(); if (vars.totalDebt != 0) { + vars.stableToTotalDebtRatio = params.totalStableDebt.rayDiv( + vars.totalDebt + ); vars.availableLiquidity = IToken(params.reserve).balanceOf(params.xToken) + params.liquidityAdded - @@ -155,22 +252,79 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { uint256 excessBorrowUsageRatio = (vars.borrowUsageRatio - OPTIMAL_USAGE_RATIO).rayDiv(MAX_EXCESS_USAGE_RATIO); + vars.currentStableBorrowRate += + _stableRateSlope1 + + _stableRateSlope2.rayMul(excessBorrowUsageRatio); + vars.currentVariableBorrowRate += _variableRateSlope1 + _variableRateSlope2.rayMul(excessBorrowUsageRatio); } else { + vars.currentStableBorrowRate += _stableRateSlope1 + .rayMul(vars.borrowUsageRatio) + .rayDiv(OPTIMAL_USAGE_RATIO); + vars.currentVariableBorrowRate += _variableRateSlope1 .rayMul(vars.borrowUsageRatio) .rayDiv(OPTIMAL_USAGE_RATIO); } - vars.currentLiquidityRate = vars - .currentVariableBorrowRate - .rayMul(vars.supplyUsageRatio) - .percentMul( + if (vars.stableToTotalDebtRatio > OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO) { + uint256 excessStableDebtRatio = (vars.stableToTotalDebtRatio - + OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO).rayDiv( + MAX_EXCESS_STABLE_TO_TOTAL_DEBT_RATIO + ); + vars.currentStableBorrowRate += _stableRateExcessOffset.rayMul( + excessStableDebtRatio + ); + } + + vars.currentLiquidityRate = _getOverallBorrowRate( + params.totalStableDebt, + params.totalVariableDebt, + vars.currentVariableBorrowRate, + params.averageStableBorrowRate + ).rayMul(vars.supplyUsageRatio).percentMul( PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor ); - return (vars.currentLiquidityRate, vars.currentVariableBorrowRate); + return ( + vars.currentLiquidityRate, + vars.currentStableBorrowRate, + vars.currentVariableBorrowRate + ); + } + + /** + * @dev Calculates the overall borrow rate as the weighted average between the total variable debt and total stable + * debt + * @param totalStableDebt The total borrowed from the reserve at a stable rate + * @param totalVariableDebt The total borrowed from the reserve at a variable rate + * @param currentVariableBorrowRate The current variable borrow rate of the reserve + * @param currentAverageStableBorrowRate The current weighted average of all the stable rate loans + * @return The weighted averaged borrow rate + **/ + function _getOverallBorrowRate( + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 currentVariableBorrowRate, + uint256 currentAverageStableBorrowRate + ) internal pure returns (uint256) { + uint256 totalDebt = totalStableDebt + totalVariableDebt; + + if (totalDebt == 0) return 0; + + uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul( + currentVariableBorrowRate + ); + + uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul( + currentAverageStableBorrowRate + ); + + uint256 overallBorrowRate = (weightedVariableRate + weightedStableRate) + .rayDiv(totalDebt.wadToRay()); + + return overallBorrowRate; } } diff --git a/contracts/protocol/pool/PoolApeStaking.sol b/contracts/protocol/pool/PoolApeStaking.sol index 7dc4702e7..9d03c2aae 100644 --- a/contracts/protocol/pool/PoolApeStaking.sol +++ b/contracts/protocol/pool/PoolApeStaking.sol @@ -622,7 +622,9 @@ contract PoolApeStaking is DataTypes.SApeAddress ]; - (bool isActive, , , bool isPaused, ) = reserve.configuration.getFlags(); + (bool isActive, , , , bool isPaused, ) = reserve + .configuration + .getFlags(); require(isActive, Errors.RESERVE_INACTIVE); require(!isPaused, Errors.RESERVE_PAUSED); diff --git a/contracts/protocol/pool/PoolConfigurator.sol b/contracts/protocol/pool/PoolConfigurator.sol index c064bac3f..357e04b9c 100644 --- a/contracts/protocol/pool/PoolConfigurator.sol +++ b/contracts/protocol/pool/PoolConfigurator.sol @@ -108,6 +108,13 @@ contract PoolConfigurator is VersionedInitializable, IPoolConfigurator { ConfiguratorLogic.executeUpdateNToken(_pool, input); } + /// @inheritdoc IPoolConfigurator + function updateStableDebtToken( + ConfiguratorInputTypes.UpdateDebtTokenInput calldata input + ) external override onlyPoolAdmin { + ConfiguratorLogic.executeUpdateStableDebtToken(_pool, input); + } + /// @inheritdoc IPoolConfigurator function updateVariableDebtToken( ConfiguratorInputTypes.UpdateDebtTokenInput calldata input @@ -123,6 +130,12 @@ contract PoolConfigurator is VersionedInitializable, IPoolConfigurator { { DataTypes.ReserveConfigurationMap memory currentConfig = _pool .getConfiguration(asset); + if (!enabled) { + require( + !currentConfig.getStableRateBorrowingEnabled(), + Errors.STABLE_BORROWING_ENABLED + ); + } currentConfig.setBorrowingEnabled(enabled); _pool.setConfiguration(asset, currentConfig); emit ReserveBorrowing(asset, enabled); @@ -180,6 +193,25 @@ contract PoolConfigurator is VersionedInitializable, IPoolConfigurator { ); } + /// @inheritdoc IPoolConfigurator + function setReserveStableRateBorrowing(address asset, bool enabled) + external + override + onlyRiskOrPoolAdmins + { + DataTypes.ReserveConfigurationMap memory currentConfig = _pool + .getConfiguration(asset); + if (enabled) { + require( + currentConfig.getBorrowingEnabled(), + Errors.BORROWING_NOT_ENABLED + ); + } + currentConfig.setStableRateBorrowingEnabled(enabled); + _pool.setConfiguration(asset, currentConfig); + emit ReserveStableRateBorrowing(asset, enabled); + } + /// @inheritdoc IPoolConfigurator function setReserveActive(address asset, bool active) external diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol new file mode 100644 index 000000000..572b9369d --- /dev/null +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -0,0 +1,554 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {ParaVersionedInitializable} from "../libraries/paraspace-upgradeability/ParaVersionedInitializable.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {DataTypes} from "../libraries/types/DataTypes.sol"; +import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC20WithPermit} from "../../interfaces/IERC20WithPermit.sol"; +import {IERC20Detailed} from "../../dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import {IERC721} from "../../dependencies/openzeppelin/contracts/IERC721.sol"; +import {IPoolAddressesProvider} from "../../interfaces/IPoolAddressesProvider.sol"; +import {IPriceOracleGetter} from "../../interfaces/IPriceOracleGetter.sol"; +import {IPoolInstantWithdraw} from "../../interfaces/IPoolInstantWithdraw.sol"; +import {IInstantNFTOracle} from "../../interfaces/IInstantNFTOracle.sol"; +import {IACLManager} from "../../interfaces/IACLManager.sol"; +import {IReserveInterestRateStrategy} from "../../interfaces/IReserveInterestRateStrategy.sol"; +import {IStableDebtToken} from "../../interfaces/IStableDebtToken.sol"; +import {IPToken} from "../../interfaces/IPToken.sol"; +import {ILoanVault} from "../../interfaces/ILoanVault.sol"; +import {IWETH} from "../../misc/interfaces/IWETH.sol"; +import {PoolStorage} from "./PoolStorage.sol"; +import {ValidationLogic} from "../libraries/logic/ValidationLogic.sol"; +import {ReserveLogic} from "../libraries/logic/ReserveLogic.sol"; +import {Address} from "../../dependencies/openzeppelin/contracts/Address.sol"; +import {Counters} from "../../dependencies/openzeppelin/contracts/Counters.sol"; +import {ParaReentrancyGuard} from "../libraries/paraspace-upgradeability/ParaReentrancyGuard.sol"; +import {WadRayMath} from "../libraries/math/WadRayMath.sol"; +import {EnumerableSet} from "../../dependencies/openzeppelin/contracts/EnumerableSet.sol"; +import {PercentageMath} from "../libraries/math/PercentageMath.sol"; +import {Math} from "../../dependencies/openzeppelin/contracts/Math.sol"; +import {SafeCast} from "../../dependencies/openzeppelin/contracts/SafeCast.sol"; +import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {MathUtils} from "../libraries/math/MathUtils.sol"; + +/** + * @title Pool Instant Withdraw contract + * + * @notice Main point of interaction with an ParaSpace protocol's market + * @dev To be covered by a proxy contract, owned by the PoolAddressesProvider of the specific market + * @dev All admin functions are callable by the PoolConfigurator contract defined also in the + * PoolAddressesProvider + **/ +contract PoolInstantWithdraw is + ParaVersionedInitializable, + ParaReentrancyGuard, + PoolStorage, + IPoolInstantWithdraw +{ + using ReserveLogic for DataTypes.ReserveData; + using PercentageMath for uint256; + using WadRayMath for uint256; + using EnumerableSet for EnumerableSet.AddressSet; + using Counters for Counters.Counter; + using SafeCast for uint256; + using SafeERC20 for IERC20; + + IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; + address internal immutable VAULT_CONTRACT; + address internal immutable WITHDRAW_ORACLE; + uint256 internal constant POOL_REVISION = 145; + + // See `IPoolCore` for descriptions + event Borrow( + address indexed reserve, + address user, + address indexed onBehalfOf, + uint256 amount, + DataTypes.InterestRateMode interestRateMode, + uint256 borrowRate, + uint16 indexed referralCode + ); + event Repay( + address indexed reserve, + address indexed user, + address indexed repayer, + uint256 amount, + bool usePTokens + ); + + /** + * @dev Only asset listing or pool admin can call functions marked by this modifier. + **/ + modifier onlyAssetListingOrPoolAdmins() { + _onlyAssetListingOrPoolAdmins(); + _; + } + + function _onlyAssetListingOrPoolAdmins() internal view { + IACLManager aclManager = IACLManager( + ADDRESSES_PROVIDER.getACLManager() + ); + require( + aclManager.isAssetListingAdmin(msg.sender) || + aclManager.isPoolAdmin(msg.sender), + Errors.CALLER_NOT_ASSET_LISTING_OR_POOL_ADMIN + ); + } + + /** + * @dev Constructor. + * @param provider The address of the PoolAddressesProvider contract + */ + constructor( + IPoolAddressesProvider provider, + address vault, + address withdrawOracle + ) { + ADDRESSES_PROVIDER = provider; + VAULT_CONTRACT = vault; + WITHDRAW_ORACLE = withdrawOracle; + } + + function getRevision() internal pure virtual override returns (uint256) { + return POOL_REVISION; + } + + /// @inheritdoc IPoolInstantWithdraw + function addBorrowableAssets( + address collateralAsset, + address[] calldata borrowableAssets + ) external virtual override onlyAssetListingOrPoolAdmins { + DataTypes.PoolStorage storage ps = poolStorage(); + EnumerableSet.AddressSet storage marketSets = ps + ._collateralBorrowableAsset[collateralAsset]; + uint256 assetLength = borrowableAssets.length; + for (uint256 i = 0; i < assetLength; i++) { + address asset = borrowableAssets[i]; + if (!marketSets.contains(asset)) { + marketSets.add(asset); + } + } + } + + /// @inheritdoc IPoolInstantWithdraw + function removeBorrowableAssets( + address collateralAsset, + address[] calldata borrowableAssets + ) external virtual override onlyAssetListingOrPoolAdmins { + DataTypes.PoolStorage storage ps = poolStorage(); + EnumerableSet.AddressSet storage marketSets = ps + ._collateralBorrowableAsset[collateralAsset]; + uint256 assetLength = borrowableAssets.length; + for (uint256 i = 0; i < assetLength; i++) { + address asset = borrowableAssets[i]; + if (marketSets.contains(asset)) { + marketSets.remove(asset); + } + } + } + + /// @inheritdoc IPoolInstantWithdraw + function setLoanCreationFeeRate(uint256 feeRate) + external + virtual + override + onlyAssetListingOrPoolAdmins + { + require( + feeRate < PercentageMath.HALF_PERCENTAGE_FACTOR, + "Value Too High" + ); + DataTypes.PoolStorage storage ps = poolStorage(); + uint256 oldValue = ps._loanCreationFeeRate; + if (oldValue != feeRate) { + ps._loanCreationFeeRate = feeRate; + emit LoanCreationFeeRateUpdated(oldValue, feeRate); + } + } + + /// @inheritdoc IPoolInstantWithdraw + function getBorrowableAssets(address collateralAsset) + external + view + virtual + override + returns (address[] memory) + { + DataTypes.PoolStorage storage ps = poolStorage(); + EnumerableSet.AddressSet storage marketSets = ps + ._collateralBorrowableAsset[collateralAsset]; + return marketSets.values(); + } + + /// @inheritdoc IPoolInstantWithdraw + function getUserLoanIdList(address user) + external + view + returns (uint256[] memory) + { + DataTypes.PoolStorage storage ps = poolStorage(); + return ps._userLoanIds[user]; + } + + /// @inheritdoc IPoolInstantWithdraw + function getLoanInfo(uint256 loanId) + external + view + returns (DataTypes.TermLoanData memory) + { + DataTypes.PoolStorage storage ps = poolStorage(); + return ps._termLoans[loanId]; + } + + /// @inheritdoc IPoolInstantWithdraw + function getLoanCollateralPresentValue(uint256 loanId) + external + view + returns (uint256) + { + DataTypes.PoolStorage storage ps = poolStorage(); + DataTypes.TermLoanData storage loan = ps._termLoans[loanId]; + require( + loan.state == DataTypes.LoanState.Active, + Errors.INVALID_LOAN_STATE + ); + + uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) + .getPresentValueByDiscountRate( + loan.collateralTokenId, + loan.discountRate + ); + + return presentValue; + } + + /// @inheritdoc IPoolInstantWithdraw + function getLoanDebtValue(uint256 loanId) external view returns (uint256) { + DataTypes.PoolStorage storage ps = poolStorage(); + DataTypes.TermLoanData storage loan = ps._termLoans[loanId]; + require( + loan.state == DataTypes.LoanState.Active, + Errors.INVALID_LOAN_STATE + ); + + uint256 loanDebt = _calculateLoanStableDebt( + loan.borrowAmount, + loan.discountRate, + loan.startTime + ); + + return loanDebt; + } + + /// @inheritdoc IPoolInstantWithdraw + function createLoan( + address collateralAsset, + uint256 collateralTokenId, + address borrowAsset, + uint16 referralCode + ) external nonReentrant returns (uint256) { + DataTypes.PoolStorage storage ps = poolStorage(); + _validateBorrowAsset(ps, collateralAsset, borrowAsset); + + DataTypes.ReserveCache memory reserveCache; + DataTypes.ReserveData storage reserve = ps._reserves[borrowAsset]; + reserveCache = reserve.cache(); + reserve.updateState(reserveCache); + + uint256 presentValue; + uint256 discountRate; + uint256 borrowAmount; + // calculate borrow amount + { + // fetch present value and discount rate from Oracle + (presentValue, discountRate) = IInstantNFTOracle(WITHDRAW_ORACLE) + .getPresentValueAndDiscountRate( + collateralTokenId, + reserve.currentStableBorrowRate + ); + + uint256 presentValueInBorrowAsset = _calculatePresentValueInBorrowAsset( + borrowAsset, + presentValue + ); + borrowAmount = presentValueInBorrowAsset.percentMul( + PercentageMath.PERCENTAGE_FACTOR - ps._loanCreationFeeRate + ); + } + + // validate borrow asset can be borrowed from lending pool + ValidationLogic.validateInstantWithdrawBorrow( + reserveCache, + borrowAmount + ); + + // handle asset + { + // transfer collateralAsset to reserveAddress + IERC721(collateralAsset).safeTransferFrom( + msg.sender, + VAULT_CONTRACT, + collateralTokenId + ); + + // mint debt token for reserveAddress and transfer borrow asset to borrower + ( + , + reserveCache.nextTotalStableDebt, + reserveCache.nextAvgStableBorrowRate + ) = IStableDebtToken(reserveCache.stableDebtTokenAddress).mint( + VAULT_CONTRACT, + VAULT_CONTRACT, + borrowAmount, + discountRate + ); + IPToken(reserveCache.xTokenAddress).transferUnderlyingTo( + msg.sender, + borrowAmount + ); + + // update borrow asset interest rate + reserve.updateInterestRates(reserveCache, borrowAsset, 0, 0); + } + + // create Loan + uint256 loanId = ps._loanIdCounter.current(); + ps._loanIdCounter.increment(); + ps._termLoans[loanId] = DataTypes.TermLoanData({ + loanId: loanId, + state: DataTypes.LoanState.Active, + startTime: uint40(block.timestamp), + endTime: uint40( + IInstantNFTOracle(WITHDRAW_ORACLE).getEndTime(collateralTokenId) + ), + borrower: msg.sender, + collateralAsset: collateralAsset, + collateralTokenId: collateralTokenId.toUint64(), + borrowAsset: borrowAsset, + borrowAmount: borrowAmount, + discountRate: discountRate + }); + ps._userLoanIds[msg.sender].push(loanId); + + emit Borrow( + borrowAsset, + VAULT_CONTRACT, + VAULT_CONTRACT, + borrowAmount, + DataTypes.InterestRateMode.STABLE, + discountRate, + referralCode + ); + emit LoanCreated( + msg.sender, + loanId, + collateralAsset, + collateralTokenId, + borrowAsset, + borrowAmount, + discountRate + ); + + return borrowAmount; + } + + /// @inheritdoc IPoolInstantWithdraw + function swapLoanCollateral(uint256 loanId, address receiver) + external + override + nonReentrant + { + DataTypes.PoolStorage storage ps = poolStorage(); + DataTypes.TermLoanData storage loan = ps._termLoans[loanId]; + // check loan state + require( + loan.state == DataTypes.LoanState.Active, + Errors.INVALID_LOAN_STATE + ); + + address collateralAsset = loan.collateralAsset; + uint256 collateralTokenId = uint256(loan.collateralTokenId); + address borrowAsset = loan.borrowAsset; + //here oracle need to guarantee presentValue > debtValue. + uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) + .getPresentValueByDiscountRate( + collateralTokenId, + loan.discountRate + ); + //repayableBorrowAssetAmount + uint256 presentValueInBorrowAsset = _calculatePresentValueInBorrowAsset( + borrowAsset, + presentValue + ); + + // update borrow asset state + DataTypes.ReserveCache memory reserveCache; + DataTypes.ReserveData storage reserve = ps._reserves[borrowAsset]; + reserveCache = reserve.cache(); + reserve.updateState(reserveCache); + + // repay borrow asset debt and update interest rate + uint256 loanDebt = _calculateLoanStableDebt( + loan.borrowAmount, + loan.discountRate, + loan.startTime + ); + require( + presentValueInBorrowAsset >= loanDebt, + Errors.INVALID_PRESENT_VALUE + ); + _repayLoanDebt(reserveCache, borrowAsset, loanDebt, msg.sender); + reserve.updateInterestRates(reserveCache, borrowAsset, 0, 0); + + // transfer to vault contract if got excess amount + if (presentValueInBorrowAsset != loanDebt) { + IERC20(borrowAsset).safeTransferFrom( + msg.sender, + VAULT_CONTRACT, + presentValueInBorrowAsset - loanDebt + ); + } + + // transfer collateral asset to receiver + ILoanVault(VAULT_CONTRACT).transferCollateral( + collateralAsset, + collateralTokenId, + receiver + ); + + // update loan + loan.state = DataTypes.LoanState.Repaid; + + emit Repay( + borrowAsset, + VAULT_CONTRACT, + VAULT_CONTRACT, + loanDebt, + false + ); + emit LoanCollateralSwapped( + msg.sender, + loanId, + borrowAsset, + presentValueInBorrowAsset + ); + } + + /// @inheritdoc IPoolInstantWithdraw + function settleTermLoan(uint256 loanId) external override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + DataTypes.TermLoanData storage loan = ps._termLoans[loanId]; + // check loan state + require( + loan.state == DataTypes.LoanState.Active, + Errors.INVALID_LOAN_STATE + ); + + address collateralAsset = loan.collateralAsset; + uint256 collateralTokenId = uint256(loan.collateralTokenId); + address borrowAsset = loan.borrowAsset; + + // update borrow asset state + DataTypes.ReserveCache memory reserveCache; + DataTypes.ReserveData storage reserve = ps._reserves[borrowAsset]; + reserveCache = reserve.cache(); + reserve.updateState(reserveCache); + + ILoanVault LoanVault = ILoanVault(VAULT_CONTRACT); + LoanVault.settleCollateral(collateralAsset, collateralTokenId); + + // repay borrow asset debt and update interest rate + // rename to loanTotalDebt. + uint256 loanDebt = _calculateLoanStableDebt( + loan.borrowAmount, + loan.discountRate, + loan.startTime + ); + uint256 vaultBalance = IERC20(borrowAsset).balanceOf(VAULT_CONTRACT); + if (loanDebt > vaultBalance) { + LoanVault.swapETHToDerivativeAsset( + borrowAsset, + loanDebt - vaultBalance + ); + } + vaultBalance = IERC20(borrowAsset).balanceOf(VAULT_CONTRACT); + // repay Math.min(totalDebt, vaultBalance) to prevent rebase token precision issue + _repayLoanDebt( + reserveCache, + borrowAsset, + Math.min(loanDebt, vaultBalance), + VAULT_CONTRACT + ); + reserve.updateInterestRates(reserveCache, borrowAsset, 0, 0); + + // update loan + loan.state = DataTypes.LoanState.Settled; + + emit Repay( + borrowAsset, + VAULT_CONTRACT, + VAULT_CONTRACT, + loanDebt, + false + ); + emit LoanSettled(loanId, msg.sender, borrowAsset, loanDebt); + } + + function _validateBorrowAsset( + DataTypes.PoolStorage storage ps, + address collateralAsset, + address borrowAsset + ) internal view { + EnumerableSet.AddressSet storage marketSets = ps + ._collateralBorrowableAsset[collateralAsset]; + require(marketSets.contains(borrowAsset), Errors.INVALID_BORROW_ASSET); + } + + function _calculatePresentValueInBorrowAsset(address asset, uint256 value) + internal + view + returns (uint256) + { + address paraOracle = ADDRESSES_PROVIDER.getPriceOracle(); + uint256 assetPrice = IPriceOracleGetter(paraOracle).getAssetPrice( + asset + ); + uint256 assetDecimals = IERC20Detailed(asset).decimals(); + uint256 assetUnit = 10**assetDecimals; + return (value * assetUnit) / assetPrice; + } + + function _calculateLoanStableDebt( + uint256 principalStableDebt, + uint256 stableRate, + uint40 lastUpdateTime + ) internal view returns (uint256) { + uint256 compoundedInterest = MathUtils.calculateCompoundedInterest( + stableRate, + lastUpdateTime + ); + + return principalStableDebt.rayMul(compoundedInterest); + } + + function _repayLoanDebt( + DataTypes.ReserveCache memory reserveCache, + address borrowAsset, + uint256 repayAmount, + address payer + ) internal { + ( + reserveCache.nextTotalStableDebt, + reserveCache.nextAvgStableBorrowRate + ) = IStableDebtToken(reserveCache.stableDebtTokenAddress).burn( + VAULT_CONTRACT, + repayAmount + ); + IERC20(borrowAsset).safeTransferFrom( + payer, + reserveCache.xTokenAddress, + repayAmount + ); + } +} diff --git a/contracts/protocol/pool/PoolParameters.sol b/contracts/protocol/pool/PoolParameters.sol index b1ea4438d..764a77a6d 100644 --- a/contracts/protocol/pool/PoolParameters.sol +++ b/contracts/protocol/pool/PoolParameters.sol @@ -21,7 +21,6 @@ import {FlashClaimLogic} from "../libraries/logic/FlashClaimLogic.sol"; import {Address} from "../../dependencies/openzeppelin/contracts/Address.sol"; import {IERC721Receiver} from "../../dependencies/openzeppelin/contracts/IERC721Receiver.sol"; import {IMarketplace} from "../../interfaces/IMarketplace.sol"; -import {Errors} from "../libraries/helpers/Errors.sol"; import {ParaReentrancyGuard} from "../libraries/paraspace-upgradeability/ParaReentrancyGuard.sol"; import {IAuctionableERC721} from "../../interfaces/IAuctionableERC721.sol"; import {IReserveAuctionStrategy} from "../../interfaces/IReserveAuctionStrategy.sol"; @@ -112,6 +111,7 @@ contract PoolParameters is function initReserve( address asset, address xTokenAddress, + address stableDebtAddress, address variableDebtAddress, address interestRateStrategyAddress, address auctionStrategyAddress @@ -125,6 +125,7 @@ contract PoolParameters is DataTypes.InitReserveParams({ asset: asset, xTokenAddress: xTokenAddress, + stableDebtAddress: stableDebtAddress, variableDebtAddress: variableDebtAddress, interestRateStrategyAddress: interestRateStrategyAddress, auctionStrategyAddress: auctionStrategyAddress, diff --git a/contracts/protocol/tokenization/ATokenStableDebtToken.sol b/contracts/protocol/tokenization/ATokenStableDebtToken.sol new file mode 100644 index 000000000..978e92375 --- /dev/null +++ b/contracts/protocol/tokenization/ATokenStableDebtToken.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IPool} from "../../interfaces/IPool.sol"; +import {RebaseStableDebtToken} from "./RebaseStableDebtToken.sol"; +import {WadRayMath} from "../libraries/math/WadRayMath.sol"; +import {IAToken} from "../../interfaces/IAToken.sol"; + +/** + * @title aToken Rebasing Debt Token + * + * @notice Implementation of the interest bearing token for the ParaSpace protocol + */ +contract ATokenStableDebtToken is RebaseStableDebtToken { + constructor(IPool pool) RebaseStableDebtToken(pool) { + //intentionally empty + } + + /** + * @return Current rebasing index of aToken in RAY + **/ + function lastRebasingIndex() internal view override returns (uint256) { + // Returns Aave aToken liquidity index + return + IAToken(_underlyingAsset).POOL().getReserveNormalizedIncome( + IAToken(_underlyingAsset).UNDERLYING_ASSET_ADDRESS() + ); + } +} diff --git a/contracts/protocol/tokenization/RebaseStableDebtToken.sol b/contracts/protocol/tokenization/RebaseStableDebtToken.sol new file mode 100644 index 000000000..4b1f62f41 --- /dev/null +++ b/contracts/protocol/tokenization/RebaseStableDebtToken.sol @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {MathUtils} from "../libraries/math/MathUtils.sol"; +import {WadRayMath} from "../libraries/math/WadRayMath.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {IRewardController} from "../../interfaces/IRewardController.sol"; +import {IStableDebtToken} from "../../interfaces/IStableDebtToken.sol"; +import {IPool} from "../../interfaces/IPool.sol"; +import {EIP712Base} from "./base/EIP712Base.sol"; +import {StableDebtToken} from "./StableDebtToken.sol"; +import {IncentivizedERC20} from "./base/IncentivizedERC20.sol"; +import {SafeCast} from "../../dependencies/openzeppelin/contracts/SafeCast.sol"; + +/** + * @title StableDebtToken + * + * @notice Implements a stable debt token to track the borrowing positions of users + * at stable rate mode + * @dev Transfer and approve functionalities are disabled since its a non-transferable token + **/ +contract RebaseStableDebtToken is StableDebtToken { + using WadRayMath for uint256; + using SafeCast for uint256; + + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor(IPool pool) StableDebtToken(pool) { + //intentionally empty + } + + /** + * @return Current rebasing index in RAY + **/ + function lastRebasingIndex() internal view virtual returns (uint256) { + // returns 1 RAY by default which makes it identical to StableDebtToken in behaviour + return WadRayMath.RAY; + } + + /** + * @dev Calculates the balance of the user: principal balance + debt interest accrued by the principal + * @param account The user address whose balance is calculated + * @return The balance of the user + **/ + function balanceOf(address account) + public + view + virtual + override + returns (uint256) + { + uint256 shareBalance = _userState[account].balance; + if (shareBalance == 0) { + return 0; + } + return + _calculateBalanceForTimestamp( + shareBalance, + lastRebasingIndex(), + _userState[account].additionalData, + uint40(block.timestamp), + _timestamps[account] + ); + } + + /// @inheritdoc IStableDebtToken + function mint( + address user, + address onBehalfOf, + uint256 amount, + uint256 rate + ) + external + virtual + override + onlyPool + returns ( + bool, + uint256, + uint256 + ) + { + MintLocalVars memory vars; + + if (user != onBehalfOf) { + _decreaseBorrowAllowance(onBehalfOf, user, amount); + } + + uint256 rebaseIndex = lastRebasingIndex(); + ( + , + uint256 currentBalance, + uint256 balanceIncrease + ) = _calculateBalanceIncrease(onBehalfOf, rebaseIndex); + + vars.currentAvgStableRate = _avgStableRate; + vars.previousSupply = _calcTotalSupply( + vars.currentAvgStableRate, + rebaseIndex + ); + vars.nextSupply = vars.previousSupply + amount; + _totalSupply = vars.nextSupply.rayDiv(rebaseIndex); + + vars.amountInRay = amount.wadToRay(); + + vars.currentStableRate = _userState[onBehalfOf].additionalData; + vars.nextStableRate = (vars.currentStableRate.rayMul( + currentBalance.wadToRay() + ) + vars.amountInRay.rayMul(rate)).rayDiv( + (currentBalance + amount).wadToRay() + ); + + _userState[onBehalfOf].additionalData = vars.nextStableRate.toUint128(); + + //solium-disable-next-line + _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40( + block.timestamp + ); + + // Calculates the updated average stable rate + vars.currentAvgStableRate = _avgStableRate = ( + (vars.currentAvgStableRate.rayMul(vars.previousSupply.wadToRay()) + + rate.rayMul(vars.amountInRay)).rayDiv( + vars.nextSupply.wadToRay() + ) + ).toUint128(); + + uint256 amountToMint = amount + balanceIncrease; + uint256 shareAmountToMint = amountToMint.rayDiv(rebaseIndex); + uint256 totalShareSupply = vars.previousSupply.rayDiv(rebaseIndex); + _mint(onBehalfOf, shareAmountToMint, totalShareSupply); + + emit Transfer(address(0), onBehalfOf, amountToMint); + emit Mint( + user, + onBehalfOf, + amountToMint, + currentBalance, + balanceIncrease, + vars.nextStableRate, + vars.currentAvgStableRate, + vars.nextSupply + ); + + return ( + currentBalance == 0, + vars.nextSupply, + vars.currentAvgStableRate + ); + } + + /// @inheritdoc IStableDebtToken + function burn(address from, uint256 amount) + external + virtual + override + onlyPool + returns (uint256, uint256) + { + uint256 rebaseIndex = lastRebasingIndex(); + ( + , + uint256 currentBalance, + uint256 balanceIncrease + ) = _calculateBalanceIncrease(from, rebaseIndex); + + uint256 currentAvgStableRate = uint256(_avgStableRate); + uint256 previousSupply = _calcTotalSupply( + currentAvgStableRate, + rebaseIndex + ); + uint256 nextAvgStableRate = 0; + uint256 nextSupply = 0; + uint256 userStableRate = _userState[from].additionalData; + + // Since the total supply and each single user debt accrue separately, + // there might be accumulation errors so that the last borrower repaying + // might actually try to repay more than the available debt supply. + // In this case we simply set the total supply and the avg stable rate to 0 + if (previousSupply <= amount) { + _avgStableRate = 0; + _totalSupply = 0; + } else { + nextSupply = previousSupply - amount; + _totalSupply = nextSupply.rayDiv(rebaseIndex); + uint256 firstTerm = uint256(currentAvgStableRate).rayMul( + previousSupply.wadToRay() + ); + uint256 secondTerm = userStableRate.rayMul(amount.wadToRay()); + + // For the same reason described above, when the last user is repaying it might + // happen that user rate * user balance > avg rate * total supply. In that case, + // we simply set the avg rate to 0 + if (secondTerm >= firstTerm) { + nextAvgStableRate = _totalSupply = _avgStableRate = 0; + } else { + nextAvgStableRate = _avgStableRate = ( + (firstTerm - secondTerm).rayDiv(nextSupply.wadToRay()) + ).toUint128(); + } + } + + if (amount == currentBalance) { + _userState[from].additionalData = 0; + _timestamps[from] = 0; + } else { + //solium-disable-next-line + _timestamps[from] = uint40(block.timestamp); + } + //solium-disable-next-line + _totalSupplyTimestamp = uint40(block.timestamp); + + if (balanceIncrease > amount) { + uint256 amountToMint = balanceIncrease - amount; + uint256 shareAmountToMint = amountToMint.rayDiv(rebaseIndex); + uint256 totalShareSupply = previousSupply.rayDiv(rebaseIndex); + _mint(from, shareAmountToMint, totalShareSupply); + emit Transfer(address(0), from, amountToMint); + emit Mint( + from, + from, + amountToMint, + currentBalance, + balanceIncrease, + userStableRate, + nextAvgStableRate, + nextSupply + ); + } else { + uint256 amountToBurn = amount - balanceIncrease; + uint256 shareAmountToBurn = amountToBurn.rayDiv(rebaseIndex); + uint256 totalShareSupply = previousSupply.rayDiv(rebaseIndex); + _burn(from, shareAmountToBurn, totalShareSupply); + emit Transfer(from, address(0), amountToBurn); + emit Burn( + from, + amountToBurn, + currentBalance, + balanceIncrease, + nextAvgStableRate, + nextSupply + ); + } + + return (nextSupply, nextAvgStableRate); + } + + function getSupplyData() + external + view + virtual + override + returns ( + uint256, + uint256, + uint256, + uint40 + ) + { + uint256 avgRate = _avgStableRate; + uint256 rebaseIndex = lastRebasingIndex(); + + return ( + _totalSupply.rayMul(rebaseIndex), + _calcTotalSupply(avgRate, rebaseIndex), + avgRate, + _totalSupplyTimestamp + ); + } + + /// @inheritdoc IStableDebtToken + function getTotalSupplyAndAvgRate() + external + view + override + returns (uint256, uint256) + { + uint256 avgRate = _avgStableRate; + return (_calcTotalSupply(avgRate, lastRebasingIndex()), avgRate); + } + + /// @inheritdoc IERC20 + function totalSupply() public view virtual override returns (uint256) { + return _calcTotalSupply(_avgStableRate, lastRebasingIndex()); + } + + /// @inheritdoc IStableDebtToken + function principalBalanceOf(address user) + external + view + virtual + override + returns (uint256) + { + uint256 shareBalance = _userState[user].balance; + return shareBalance.rayMul(lastRebasingIndex()); + } + + /** + * @notice Calculates the increase in balance since the last user interaction + * @param user The address of the user for which the interest is being accumulated + * @param rebaseIndex Current rebase index + * @return The previous principal balance + * @return The new principal balance + * @return The balance increase + **/ + function _calculateBalanceIncrease(address user, uint256 rebaseIndex) + internal + view + returns ( + uint256, + uint256, + uint256 + ) + { + uint256 shareBalance = _userState[user].balance; + if (shareBalance == 0) { + return (0, 0, 0); + } + + uint256 stableRate = _userState[user].additionalData; + uint40 lastUpdateTimestamp = _timestamps[user]; + + uint256 previousBalance = _calculateBalanceForTimestamp( + shareBalance, + rebaseIndex, + stableRate, + lastUpdateTimestamp, + lastUpdateTimestamp + ); + uint256 newBalance = _calculateBalanceForTimestamp( + shareBalance, + rebaseIndex, + stableRate, + uint40(block.timestamp), + lastUpdateTimestamp + ); + + return (previousBalance, newBalance, newBalance - previousBalance); + } + + function _calculateBalanceForTimestamp( + uint256 shareBalance, + uint256 rebaseIndex, + uint256 stableRate, + uint40 timestamp, + uint40 lastUpdateTimestamp + ) internal pure returns (uint256) { + uint256 scaledBalance = shareBalance.rayMul(rebaseIndex); + uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest( + stableRate, + lastUpdateTimestamp, + timestamp + ); + return scaledBalance.rayMul(cumulatedInterest); + } + + /** + * @notice Calculates the total supply + * @param avgRate The average rate at which the total supply increases + * @param rebaseIndex Current rebase index + * @return The debt balance of the user since the last burn/mint action + **/ + function _calcTotalSupply(uint256 avgRate, uint256 rebaseIndex) + internal + view + returns (uint256) + { + uint256 principalSupply = _totalSupply; + if (principalSupply == 0) { + return 0; + } + + return + _calculateBalanceForTimestamp( + principalSupply, + rebaseIndex, + avgRate, + uint40(block.timestamp), + _totalSupplyTimestamp + ); + } + + /** + * @notice Mints stable debt tokens to a user + * @param account The account receiving the debt tokens + * @param shareAmount The share amount being minted + * @param oldTotalShareSupply The total share supply before the minting event + **/ + function _mint( + address account, + uint256 shareAmount, + uint256 oldTotalShareSupply + ) internal override { + uint128 castShareAmount = shareAmount.toUint128(); + uint128 oldShareBalance = _userState[account].balance; + _userState[account].balance = oldShareBalance + castShareAmount; + + if (address(_rewardController) != address(0)) { + _rewardController.handleAction( + account, + oldTotalShareSupply, + oldShareBalance + ); + } + } + + /** + * @notice Burns stable debt tokens of a user + * @param account The user getting his debt burned + * @param shareAmount The share amount being burned + * @param oldTotalShareSupply The total share supply before the burning event + **/ + function _burn( + address account, + uint256 shareAmount, + uint256 oldTotalShareSupply + ) internal override { + uint128 castShareAmount = shareAmount.toUint128(); + uint128 oldShareBalance = _userState[account].balance; + _userState[account].balance = oldShareBalance - castShareAmount; + + if (address(_rewardController) != address(0)) { + _rewardController.handleAction( + account, + oldTotalShareSupply, + oldShareBalance + ); + } + } +} diff --git a/contracts/protocol/tokenization/StableDebtToken.sol b/contracts/protocol/tokenization/StableDebtToken.sol new file mode 100644 index 000000000..2475ef53e --- /dev/null +++ b/contracts/protocol/tokenization/StableDebtToken.sol @@ -0,0 +1,547 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {VersionedInitializable} from "../libraries/paraspace-upgradeability/VersionedInitializable.sol"; +import {MathUtils} from "../libraries/math/MathUtils.sol"; +import {WadRayMath} from "../libraries/math/WadRayMath.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {IRewardController} from "../../interfaces/IRewardController.sol"; +import {IInitializableDebtToken} from "../../interfaces/IInitializableDebtToken.sol"; +import {IStableDebtToken} from "../../interfaces/IStableDebtToken.sol"; +import {IPool} from "../../interfaces/IPool.sol"; +import {EIP712Base} from "./base/EIP712Base.sol"; +import {DebtTokenBase} from "./base/DebtTokenBase.sol"; +import {IncentivizedERC20} from "./base/IncentivizedERC20.sol"; +import {SafeCast} from "../../dependencies/openzeppelin/contracts/SafeCast.sol"; + +/** + * @title StableDebtToken + * + * @notice Implements a stable debt token to track the borrowing positions of users + * at stable rate mode + * @dev Transfer and approve functionalities are disabled since its a non-transferable token + **/ +contract StableDebtToken is DebtTokenBase, IncentivizedERC20, IStableDebtToken { + using WadRayMath for uint256; + using SafeCast for uint256; + + uint256 public constant DEBT_TOKEN_REVISION = 145; + + // Map of users address and the timestamp of their last update (userAddress => lastUpdateTimestamp) + mapping(address => uint40) internal _timestamps; + + uint128 internal _avgStableRate; + + // Timestamp of the last update of the total supply + uint40 internal _totalSupplyTimestamp; + + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor(IPool pool) + DebtTokenBase() + IncentivizedERC20( + pool, + "STABLE_DEBT_TOKEN_IMPL", + "STABLE_DEBT_TOKEN_IMPL", + 0 + ) + { + // Intentionally left blank + } + + /// @inheritdoc IInitializableDebtToken + function initialize( + IPool initializingPool, + address underlyingAsset, + IRewardController incentivesController, + uint8 debtTokenDecimals, + string memory debtTokenName, + string memory debtTokenSymbol, + bytes calldata params + ) external override initializer { + require(initializingPool == POOL, Errors.POOL_ADDRESSES_DO_NOT_MATCH); + _setName(debtTokenName); + _setSymbol(debtTokenSymbol); + _setDecimals(debtTokenDecimals); + + _underlyingAsset = underlyingAsset; + _rewardController = incentivesController; + + _domainSeparator = _calculateDomainSeparator(); + + emit Initialized( + underlyingAsset, + address(POOL), + address(incentivesController), + debtTokenDecimals, + debtTokenName, + debtTokenSymbol, + params + ); + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return DEBT_TOKEN_REVISION; + } + + /// @inheritdoc IStableDebtToken + function getAverageStableRate() + external + view + virtual + override + returns (uint256) + { + return _avgStableRate; + } + + /// @inheritdoc IStableDebtToken + function getUserLastUpdated(address user) + external + view + virtual + override + returns (uint40) + { + return _timestamps[user]; + } + + /// @inheritdoc IStableDebtToken + function getUserStableRate(address user) + external + view + virtual + override + returns (uint256) + { + return _userState[user].additionalData; + } + + /// @inheritdoc IERC20 + function balanceOf(address account) + public + view + virtual + override + returns (uint256) + { + uint256 accountBalance = super.balanceOf(account); + if (accountBalance == 0) { + return 0; + } + uint256 stableRate = _userState[account].additionalData; + uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest( + stableRate, + _timestamps[account] + ); + return accountBalance.rayMul(cumulatedInterest); + } + + struct MintLocalVars { + uint256 previousSupply; + uint256 nextSupply; + uint256 amountInRay; + uint256 currentStableRate; + uint256 nextStableRate; + uint256 currentAvgStableRate; + } + + /// @inheritdoc IStableDebtToken + function mint( + address user, + address onBehalfOf, + uint256 amount, + uint256 rate + ) + external + virtual + override + onlyPool + returns ( + bool, + uint256, + uint256 + ) + { + MintLocalVars memory vars; + + if (user != onBehalfOf) { + _decreaseBorrowAllowance(onBehalfOf, user, amount); + } + + ( + , + uint256 currentBalance, + uint256 balanceIncrease + ) = _calculateBalanceIncrease(onBehalfOf); + + vars.previousSupply = totalSupply(); + vars.currentAvgStableRate = _avgStableRate; + vars.nextSupply = _totalSupply = vars.previousSupply + amount; + + vars.amountInRay = amount.wadToRay(); + + vars.currentStableRate = _userState[onBehalfOf].additionalData; + vars.nextStableRate = (vars.currentStableRate.rayMul( + currentBalance.wadToRay() + ) + vars.amountInRay.rayMul(rate)).rayDiv( + (currentBalance + amount).wadToRay() + ); + + _userState[onBehalfOf].additionalData = vars.nextStableRate.toUint128(); + + //solium-disable-next-line + _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40( + block.timestamp + ); + + // Calculates the updated average stable rate + vars.currentAvgStableRate = _avgStableRate = ( + (vars.currentAvgStableRate.rayMul(vars.previousSupply.wadToRay()) + + rate.rayMul(vars.amountInRay)).rayDiv( + vars.nextSupply.wadToRay() + ) + ).toUint128(); + + uint256 amountToMint = amount + balanceIncrease; + _mint(onBehalfOf, amountToMint, vars.previousSupply); + + emit Transfer(address(0), onBehalfOf, amountToMint); + emit Mint( + user, + onBehalfOf, + amountToMint, + currentBalance, + balanceIncrease, + vars.nextStableRate, + vars.currentAvgStableRate, + vars.nextSupply + ); + + return ( + currentBalance == 0, + vars.nextSupply, + vars.currentAvgStableRate + ); + } + + /// @inheritdoc IStableDebtToken + function burn(address from, uint256 amount) + external + virtual + override + onlyPool + returns (uint256, uint256) + { + ( + , + uint256 currentBalance, + uint256 balanceIncrease + ) = _calculateBalanceIncrease(from); + + uint256 previousSupply = totalSupply(); + uint256 nextAvgStableRate = 0; + uint256 nextSupply = 0; + uint256 userStableRate = _userState[from].additionalData; + + // Since the total supply and each single user debt accrue separately, + // there might be accumulation errors so that the last borrower repaying + // might actually try to repay more than the available debt supply. + // In this case we simply set the total supply and the avg stable rate to 0 + if (previousSupply <= amount) { + _avgStableRate = 0; + _totalSupply = 0; + } else { + nextSupply = _totalSupply = previousSupply - amount; + uint256 firstTerm = uint256(_avgStableRate).rayMul( + previousSupply.wadToRay() + ); + uint256 secondTerm = userStableRate.rayMul(amount.wadToRay()); + + // For the same reason described above, when the last user is repaying it might + // happen that user rate * user balance > avg rate * total supply. In that case, + // we simply set the avg rate to 0 + if (secondTerm >= firstTerm) { + nextAvgStableRate = _totalSupply = _avgStableRate = 0; + } else { + nextAvgStableRate = _avgStableRate = ( + (firstTerm - secondTerm).rayDiv(nextSupply.wadToRay()) + ).toUint128(); + } + } + + if (amount == currentBalance) { + _userState[from].additionalData = 0; + _timestamps[from] = 0; + } else { + //solium-disable-next-line + _timestamps[from] = uint40(block.timestamp); + } + //solium-disable-next-line + _totalSupplyTimestamp = uint40(block.timestamp); + + if (balanceIncrease > amount) { + uint256 amountToMint = balanceIncrease - amount; + _mint(from, amountToMint, previousSupply); + emit Transfer(address(0), from, amountToMint); + emit Mint( + from, + from, + amountToMint, + currentBalance, + balanceIncrease, + userStableRate, + nextAvgStableRate, + nextSupply + ); + } else { + uint256 amountToBurn = amount - balanceIncrease; + _burn(from, amountToBurn, previousSupply); + emit Transfer(from, address(0), amountToBurn); + emit Burn( + from, + amountToBurn, + currentBalance, + balanceIncrease, + nextAvgStableRate, + nextSupply + ); + } + + return (nextSupply, nextAvgStableRate); + } + + /** + * @notice Calculates the increase in balance since the last user interaction + * @param user The address of the user for which the interest is being accumulated + * @return The previous principal balance + * @return The new principal balance + * @return The balance increase + **/ + function _calculateBalanceIncrease(address user) + internal + view + returns ( + uint256, + uint256, + uint256 + ) + { + uint256 previousPrincipalBalance = super.balanceOf(user); + if (previousPrincipalBalance == 0) { + return (0, 0, 0); + } + + uint256 newPrincipalBalance = balanceOf(user); + + return ( + previousPrincipalBalance, + newPrincipalBalance, + newPrincipalBalance - previousPrincipalBalance + ); + } + + /// @inheritdoc IStableDebtToken + function getSupplyData() + external + view + virtual + override + returns ( + uint256, + uint256, + uint256, + uint40 + ) + { + uint256 avgRate = _avgStableRate; + return ( + super.totalSupply(), + _calcTotalSupply(avgRate), + avgRate, + _totalSupplyTimestamp + ); + } + + /// @inheritdoc IStableDebtToken + function getTotalSupplyAndAvgRate() + external + view + virtual + override + returns (uint256, uint256) + { + uint256 avgRate = _avgStableRate; + return (_calcTotalSupply(avgRate), avgRate); + } + + /// @inheritdoc IERC20 + function totalSupply() public view virtual override returns (uint256) { + return _calcTotalSupply(_avgStableRate); + } + + /// @inheritdoc IStableDebtToken + function getTotalSupplyLastUpdated() + external + view + override + returns (uint40) + { + return _totalSupplyTimestamp; + } + + /// @inheritdoc IStableDebtToken + function principalBalanceOf(address user) + external + view + virtual + override + returns (uint256) + { + return super.balanceOf(user); + } + + /// @inheritdoc IStableDebtToken + function UNDERLYING_ASSET_ADDRESS() + external + view + override + returns (address) + { + return _underlyingAsset; + } + + /** + * @notice Calculates the total supply + * @param avgRate The average rate at which the total supply increases + * @return The debt balance of the user since the last burn/mint action + **/ + function _calcTotalSupply(uint256 avgRate) internal view returns (uint256) { + uint256 principalSupply = super.totalSupply(); + if (principalSupply == 0) { + return 0; + } + + uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest( + avgRate, + _totalSupplyTimestamp + ); + + return principalSupply.rayMul(cumulatedInterest); + } + + /** + * @notice Mints stable debt tokens to a user + * @param account The account receiving the debt tokens + * @param amount The amount being minted + * @param oldTotalSupply The total supply before the minting event + **/ + function _mint( + address account, + uint256 amount, + uint256 oldTotalSupply + ) internal virtual { + uint128 castAmount = amount.toUint128(); + uint128 oldAccountBalance = _userState[account].balance; + _userState[account].balance = oldAccountBalance + castAmount; + + if (address(_rewardController) != address(0)) { + _rewardController.handleAction( + account, + oldTotalSupply, + oldAccountBalance + ); + } + } + + /** + * @notice Burns stable debt tokens of a user + * @param account The user getting his debt burned + * @param amount The amount being burned + * @param oldTotalSupply The total supply before the burning event + **/ + function _burn( + address account, + uint256 amount, + uint256 oldTotalSupply + ) internal virtual { + uint128 castAmount = amount.toUint128(); + uint128 oldAccountBalance = _userState[account].balance; + _userState[account].balance = oldAccountBalance - castAmount; + + if (address(_rewardController) != address(0)) { + _rewardController.handleAction( + account, + oldTotalSupply, + oldAccountBalance + ); + } + } + + /// @inheritdoc EIP712Base + function _EIP712BaseId() internal view override returns (string memory) { + return name(); + } + + /** + * @dev Being non transferrable, the debt token does not implement any of the + * standard ERC20 functions for transfer and allowance. + **/ + function transfer(address, uint256) + external + virtual + override + returns (bool) + { + revert(Errors.OPERATION_NOT_SUPPORTED); + } + + function allowance(address, address) + external + view + virtual + override + returns (uint256) + { + revert(Errors.OPERATION_NOT_SUPPORTED); + } + + function approve(address, uint256) + external + virtual + override + returns (bool) + { + revert(Errors.OPERATION_NOT_SUPPORTED); + } + + function transferFrom( + address, + address, + uint256 + ) external virtual override returns (bool) { + revert(Errors.OPERATION_NOT_SUPPORTED); + } + + function increaseAllowance(address, uint256) + external + virtual + override + returns (bool) + { + revert(Errors.OPERATION_NOT_SUPPORTED); + } + + function decreaseAllowance(address, uint256) + external + virtual + override + returns (bool) + { + revert(Errors.OPERATION_NOT_SUPPORTED); + } +} diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index 7fa06d8dc..9739a1ce0 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -15,6 +15,7 @@ import {XTokenType, IXTokenType} from "../interfaces/IXTokenType.sol"; import {IAuctionableERC721} from "../interfaces/IAuctionableERC721.sol"; import {INToken} from "../interfaces/INToken.sol"; import {IVariableDebtToken} from "../interfaces/IVariableDebtToken.sol"; +import {IStableDebtToken} from "../interfaces/IStableDebtToken.sol"; import {WadRayMath} from "../protocol/libraries/math/WadRayMath.sol"; import {ReserveConfiguration} from "../protocol/libraries/configuration/ReserveConfiguration.sol"; import {UserConfiguration} from "../protocol/libraries/configuration/UserConfiguration.sol"; @@ -27,6 +28,7 @@ import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; import {IUniswapV3OracleWrapper} from "../interfaces/IUniswapV3OracleWrapper.sol"; import {UinswapV3PositionData} from "../interfaces/IUniswapV3PositionInfoProvider.sol"; import {Helpers} from "../protocol/libraries/helpers/Helpers.sol"; +import "hardhat/console.sol"; contract UiPoolDataProvider is IUiPoolDataProvider { using WadRayMath for uint256; @@ -58,6 +60,12 @@ contract UiPoolDataProvider is IUiPoolDataProvider { .getVariableRateSlope1(); interestRates.variableRateSlope2 = interestRateStrategy .getVariableRateSlope2(); + interestRates.stableRateSlope1 = interestRateStrategy + .getStableRateSlope1(); + interestRates.stableRateSlope2 = interestRateStrategy + .getStableRateSlope2(); + interestRates.baseStableBorrowRate = interestRateStrategy + .getBaseStableBorrowRate(); interestRates.baseVariableBorrowRate = interestRateStrategy .getBaseVariableBorrowRate(); interestRates.optimalUsageRatio = interestRateStrategy @@ -105,8 +113,12 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.liquidityRate = baseData.currentLiquidityRate; //the current variable borrow rate. Expressed in ray reserveData.variableBorrowRate = baseData.currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + reserveData.stableBorrowRate = baseData.currentStableBorrowRate; reserveData.lastUpdateTimestamp = baseData.lastUpdateTimestamp; reserveData.xTokenAddress = baseData.xTokenAddress; + reserveData.stableDebtTokenAddress = baseData + .stableDebtTokenAddress; reserveData.variableDebtTokenAddress = baseData .variableDebtTokenAddress; @@ -126,6 +138,13 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.underlyingAsset ); + ( + reserveData.totalPrincipalStableDebt, + , + reserveData.averageStableRate, + reserveData.stableDebtLastUpdateTimestamp + ) = IStableDebtToken(reserveData.stableDebtTokenAddress) + .getSupplyData(); reserveData.totalScaledVariableDebt = IVariableDebtToken( reserveData.variableDebtTokenAddress ).scaledTotalSupply(); @@ -137,6 +156,7 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.isActive, reserveData.isFrozen, reserveData.borrowingEnabled, + reserveData.stableBorrowRateEnabled, isPaused, assetType ) = reserveConfigurationMap.getFlags(); @@ -204,6 +224,10 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.variableRateSlope1 = interestRates.variableRateSlope1; reserveData.variableRateSlope2 = interestRates.variableRateSlope2; + reserveData.stableRateSlope1 = interestRates.stableRateSlope1; + reserveData.stableRateSlope2 = interestRates.stableRateSlope2; + reserveData.baseStableBorrowRate = interestRates + .baseStableBorrowRate; reserveData.baseVariableBorrowRate = interestRates .baseVariableBorrowRate; reserveData.optimalUsageRatio = interestRates.optimalUsageRatio; @@ -414,6 +438,18 @@ contract UiPoolDataProvider is IUiPoolDataProvider { userReservesData[i].scaledVariableDebt = IVariableDebtToken( baseData.variableDebtTokenAddress ).scaledBalanceOf(user); + userReservesData[i].principalStableDebt = IStableDebtToken( + baseData.stableDebtTokenAddress + ).principalBalanceOf(user); + if (userReservesData[i].principalStableDebt != 0) { + userReservesData[i].stableBorrowRate = IStableDebtToken( + baseData.stableDebtTokenAddress + ).getUserStableRate(user); + userReservesData[i] + .stableBorrowLastUpdateTimestamp = IStableDebtToken( + baseData.stableDebtTokenAddress + ).getUserLastUpdated(user); + } } } diff --git a/contracts/ui/WETHGateway.sol b/contracts/ui/WETHGateway.sol index 6c7028eb8..f6b609a1d 100644 --- a/contracts/ui/WETHGateway.sol +++ b/contracts/ui/WETHGateway.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.10; import {OwnableUpgradeable} from "../dependencies/openzeppelin/upgradeability/OwnableUpgradeable.sol"; import {IERC20} from "../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC721} from "../dependencies/openzeppelin/contracts/IERC721.sol"; import {IWETH} from "../misc/interfaces/IWETH.sol"; import {IWETHGateway} from "./interfaces/IWETHGateway.sol"; import {IPool} from "../interfaces/IPool.sol"; @@ -175,6 +176,54 @@ contract WETHGateway is ReentrancyGuard, IWETHGateway, OwnableUpgradeable { _safeTransferETH(to, amountToWithdraw); } + /** + * @notice create a ETH term loan with the specified collateral asset + * @param collateralAsset The address of the collateral asset + * @param collateralTokenId The token id of the collateral asset + **/ + function createLoan( + address collateralAsset, + uint256 collateralTokenId, + uint16 referralCode + ) external override nonReentrant { + IERC721(collateralAsset).safeTransferFrom( + msg.sender, + address(this), + collateralTokenId + ); + IERC721(collateralAsset).setApprovalForAll(pool, true); + uint256 ethAmount = IPool(pool).createLoan( + collateralAsset, + collateralTokenId, + weth, + referralCode + ); + WETH.withdraw(ethAmount); + _safeTransferETH(msg.sender, ethAmount); + } + + /** + * @notice swap a term loan collateral with ETH, + * the amount user need to pay is calculated by the present value of the collateral + * @param loanId The id for the specified loan + * @param receiver The address to receive the collateral asset + **/ + function swapLoanCollateral(uint256 loanId, address receiver) + external + payable + override + nonReentrant + {} + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) external pure returns (bytes4) { + return this.onERC721Received.selector; + } + /** * @dev transfer ETH to an address, revert if it fails. * @param to recipient of the transfer diff --git a/contracts/ui/WalletBalanceProvider.sol b/contracts/ui/WalletBalanceProvider.sol index 7ba486e22..be623ee66 100644 --- a/contracts/ui/WalletBalanceProvider.sol +++ b/contracts/ui/WalletBalanceProvider.sol @@ -105,7 +105,7 @@ contract WalletBalanceProvider { DataTypes.ReserveConfigurationMap memory configuration = pool .getConfiguration(reservesWithEth[j]); - (bool isActive, , , , ) = configuration.getFlags(); + (bool isActive, , , , , ) = configuration.getFlags(); if (!isActive) { balances[j] = 0; diff --git a/contracts/ui/interfaces/IUiPoolDataProvider.sol b/contracts/ui/interfaces/IUiPoolDataProvider.sol index 579ee2322..48e4826a6 100644 --- a/contracts/ui/interfaces/IUiPoolDataProvider.sol +++ b/contracts/ui/interfaces/IUiPoolDataProvider.sol @@ -8,6 +8,9 @@ interface IUiPoolDataProvider { struct InterestRates { uint256 variableRateSlope1; uint256 variableRateSlope2; + uint256 stableRateSlope1; + uint256 stableRateSlope2; + uint256 baseStableBorrowRate; uint256 baseVariableBorrowRate; uint256 optimalUsageRatio; } @@ -23,6 +26,7 @@ interface IUiPoolDataProvider { uint256 reserveFactor; bool usageAsCollateralEnabled; bool borrowingEnabled; + bool stableBorrowRateEnabled; bool auctionEnabled; bool isActive; bool isFrozen; @@ -33,17 +37,25 @@ interface IUiPoolDataProvider { uint128 variableBorrowIndex; uint128 liquidityRate; uint128 variableBorrowRate; + uint128 stableBorrowRate; uint40 lastUpdateTimestamp; address xTokenAddress; + address stableDebtTokenAddress; address variableDebtTokenAddress; address interestRateStrategyAddress; address auctionStrategyAddress; uint256 availableLiquidity; + uint256 totalPrincipalStableDebt; + uint256 averageStableRate; + uint256 stableDebtLastUpdateTimestamp; uint256 totalScaledVariableDebt; uint256 priceInMarketReferenceCurrency; address priceOracle; uint256 variableRateSlope1; uint256 variableRateSlope2; + uint256 stableRateSlope1; + uint256 stableRateSlope2; + uint256 baseStableBorrowRate; uint256 baseVariableBorrowRate; uint256 optimalUsageRatio; uint128 accruedToTreasury; @@ -59,7 +71,10 @@ interface IUiPoolDataProvider { uint256 scaledXTokenBalance; uint256 collateralizedBalance; bool usageAsCollateralEnabledOnUser; + uint256 stableBorrowRate; uint256 scaledVariableDebt; + uint256 principalStableDebt; + uint256 stableBorrowLastUpdateTimestamp; uint256 avgMultiplier; } diff --git a/contracts/ui/interfaces/IWETHGateway.sol b/contracts/ui/interfaces/IWETHGateway.sol index 0850dab5b..cbb4b3283 100644 --- a/contracts/ui/interfaces/IWETHGateway.sol +++ b/contracts/ui/interfaces/IWETHGateway.sol @@ -24,4 +24,14 @@ interface IWETHGateway { bytes32 permitR, bytes32 permitS ) external; + + function createLoan( + address collateralAsset, + uint256 collateralTokenId, + uint16 referralCode + ) external; + + function swapLoanCollateral(uint256 loanId, address receiver) + external + payable; } diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 05347713f..3b5ab20fc 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -247,6 +247,18 @@ import { HelperContract__factory, ParaSpaceAirdrop__factory, ParaSpaceAirdrop, + StableDebtToken, + StableDebtToken__factory, + MockStableDebtToken__factory, + PoolInstantWithdraw__factory, + LoanVault__factory, + LoanVault, + MockedETHNFTOracle, + MockedETHNFTOracle__factory, + MockedInstantWithdrawNFT__factory, + MockedInstantWithdrawNFT, + ATokenStableDebtToken, + ATokenStableDebtToken__factory, } from "../types"; import {MockContract} from "ethereum-waffle"; import { @@ -467,6 +479,10 @@ export const getPoolSignatures = () => { PoolApeStaking__factory.abi ); + const poolInstantWithdrawSelectors = getFunctionSignatures( + PoolInstantWithdraw__factory.abi + ); + const poolProxySelectors = getFunctionSignatures(ParaProxy__factory.abi); const poolParaProxyInterfacesSelectors = getFunctionSignatures( @@ -479,6 +495,7 @@ export const getPoolSignatures = () => { ...poolParametersSelectors, ...poolMarketplaceSelectors, ...poolApeStakingSelectors, + ...poolInstantWithdrawSelectors, ...poolProxySelectors, ...poolParaProxyInterfacesSelectors, ]; @@ -499,6 +516,7 @@ export const getPoolSignatures = () => { poolParametersSelectors, poolMarketplaceSelectors, poolApeStakingSelectors, + poolInstantWithdrawSelectors, poolParaProxyInterfacesSelectors, }; }; @@ -577,6 +595,7 @@ export const deployPoolComponents = async ( poolParametersSelectors, poolMarketplaceSelectors, poolApeStakingSelectors, + poolInstantWithdrawSelectors, poolParaProxyInterfacesSelectors, } = getPoolSignatures(); @@ -641,6 +660,9 @@ export const deployPoolComponents = async ( poolParametersSelectors: poolParametersSelectors.map((s) => s.signature), poolMarketplaceSelectors: poolMarketplaceSelectors.map((s) => s.signature), poolApeStakingSelectors: poolApeStakingSelectors.map((s) => s.signature), + poolInstantWithdrawSelectors: poolInstantWithdrawSelectors.map( + (s) => s.signature + ), poolParaProxyInterfacesSelectors: poolParaProxyInterfacesSelectors.map( (s) => s.signature ), @@ -762,7 +784,18 @@ export const deployReserveAuctionStrategy = async ( export const deployReserveInterestRateStrategy = async ( strategyName: string, - args: [tEthereumAddress, string, string, string, string], + args: [ + tEthereumAddress, + string, + string, + string, + string, + string, + string, + string, + string, + string + ], verify?: boolean ) => withSaveAndVerify( @@ -2699,3 +2732,114 @@ export const deployMockedDelegateRegistry = async (verify?: boolean) => [], verify ) as Promise; + +export const deployGenericStableDebtToken = async ( + poolAddress: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new StableDebtToken__factory(await getFirstSigner()), + eContractid.StableDebtToken, + [poolAddress], + verify + ) as Promise; + +export const deployATokenStableDebtToken = async ( + poolAddress: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new ATokenStableDebtToken__factory(await getFirstSigner()), + eContractid.ATokenStableDebtToken, + [poolAddress], + verify + ) as Promise; + +export const deployMockStableDebtToken = async ( + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + string, + string, + string + ], + verify?: boolean +) => { + const instance = await withSaveAndVerify( + new MockStableDebtToken__factory(await getFirstSigner()), + eContractid.MockStableDebtToken, + [args[0]], + verify + ); + + await instance.initialize( + args[0], + args[1], + args[2], + "18", + args[3], + args[4], + args[5] + ); + + return instance; +}; + +export const deployLoanVaultImpl = async ( + poolAddress: string, + verify?: boolean +) => { + const allTokens = await getAllTokens(); + const args = [poolAddress, allTokens.WETH.address, allTokens.aWETH.address]; + + return withSaveAndVerify( + new LoanVault__factory(await getFirstSigner()), + eContractid.LoanVaultImpl, + [...args], + verify + ) as Promise; +}; + +export const deployLoanVault = async ( + poolAddress: string, + verify?: boolean +) => { + const implementation = await deployLoanVaultImpl(poolAddress, verify); + + const deployer = await getFirstSigner(); + const deployerAddress = await deployer.getAddress(); + + const initData = implementation.interface.encodeFunctionData("initialize"); + + const proxyInstance = await withSaveAndVerify( + new InitializableAdminUpgradeabilityProxy__factory(await getFirstSigner()), + eContractid.LoanVault, + [], + verify + ); + + await waitForTx( + await (proxyInstance as InitializableAdminUpgradeabilityProxy)[ + "initialize(address,address,bytes)" + ](implementation.address, deployerAddress, initData, GLOBAL_OVERRIDES) + ); + + return proxyInstance as LoanVault; +}; + +export const deployMockETHNFTOracle = async (verify?: boolean) => + withSaveAndVerify( + new MockedETHNFTOracle__factory(await getFirstSigner()), + eContractid.MockETHNFTOracle, + [], + verify + ) as Promise; + +export const deployMockedInstantWithdrawNFT = async (verify?: boolean) => + withSaveAndVerify( + new MockedInstantWithdrawNFT__factory(await getFirstSigner()), + eContractid.MockedInstantWithdrawNFT, + ["MockETHNFT", "MockETHNFT", ""], + verify + ) as Promise; diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 908d18d8b..a1bcbf598 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -85,6 +85,9 @@ import { AutoYieldApe__factory, PYieldToken__factory, HelperContract__factory, + StableDebtToken__factory, + MockStableDebtToken__factory, + LoanVault__factory, } from "../types"; import { getEthersSigners, @@ -1201,3 +1204,36 @@ export const getBAYCSewerPass = async (address?: tEthereumAddress) => ).address, await getFirstSigner() ); + +export const getStableDebtToken = async (address?: tEthereumAddress) => + await StableDebtToken__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.StableDebtToken}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getMockStableDebtToken = async (address?: tEthereumAddress) => + await MockStableDebtToken__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.MockStableDebtToken}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getLoanVault = async (address?: tEthereumAddress) => + await LoanVault__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.LoanVault}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); diff --git a/helpers/init-helpers.ts b/helpers/init-helpers.ts index 0b1b97f3b..d89a31b85 100644 --- a/helpers/init-helpers.ts +++ b/helpers/init-helpers.ts @@ -46,6 +46,8 @@ import { deployNTokenBAKCImpl, deployPYieldToken, deployAutoYieldApe, + deployGenericStableDebtToken, + deployATokenStableDebtToken, } from "./contracts-deployments"; import {ZERO_ADDRESS} from "./constants"; @@ -53,6 +55,7 @@ export const initReservesByHelper = async ( reserves: [string, IReserveParams][], tokenAddresses: {[symbol: string]: tEthereumAddress}, xTokenNamePrefix: string, + stableDebtTokenNamePrefix: string, variableDebtTokenNamePrefix: string, symbolPrefix: string, admin: tEthereumAddress, @@ -61,6 +64,7 @@ export const initReservesByHelper = async ( verify: boolean, genericPTokenImplAddress?: tEthereumAddress, genericNTokenImplAddress?: tEthereumAddress, + genericStableDebtTokenAddress?: tEthereumAddress, genericVariableDebtTokenAddress?: tEthereumAddress, defaultReserveInterestRateStrategyAddress?: tEthereumAddress, defaultReserveAuctionStrategyAddress?: tEthereumAddress, @@ -86,6 +90,7 @@ export const initReservesByHelper = async ( const initInputParams: { xTokenImpl: string; assetType: BigNumberish; + stableDebtTokenImpl: string; variableDebtTokenImpl: string; underlyingAssetDecimals: BigNumberish; interestRateStrategyAddress: string; @@ -98,6 +103,8 @@ export const initReservesByHelper = async ( xTokenSymbol: string; variableDebtTokenName: string; variableDebtTokenSymbol: string; + stableDebtTokenName: string; + stableDebtTokenSymbol: string; params: string; atomicPricing?: boolean; }[] = []; @@ -120,9 +127,11 @@ export const initReservesByHelper = async ( let nTokenUniSwapV3ImplementationAddress = ""; let nTokenBAYCImplementationAddress = ""; let nTokenMAYCImplementationAddress = ""; + let stableDebtTokenImplementationAddress = genericStableDebtTokenAddress; let variableDebtTokenImplementationAddress = genericVariableDebtTokenAddress; let stETHVariableDebtTokenImplementationAddress = ""; let aTokenVariableDebtTokenImplementationAddress = ""; + let aTokenStableDebtTokenImplementationAddress = ""; let PsApeVariableDebtTokenImplementationAddress = ""; let nTokenBAKCImplementationAddress = ""; @@ -155,23 +164,6 @@ export const initReservesByHelper = async ( ); } - // const reserves = Object.entries(reservesParams).filter( - // ([, {xTokenImpl}]) => - // xTokenImpl === eContractid.DelegationAwarePTokenImpl || - // xTokenImpl === eContractid.PTokenImpl || - // xTokenImpl === eContractid.NTokenImpl || - // xTokenImpl === eContractid.NTokenBAYCImpl || - // xTokenImpl === eContractid.NTokenMAYCImpl || - // xTokenImpl === eContractid.NTokenMoonBirdsImpl || - // xTokenImpl === eContractid.NTokenUniswapV3Impl || - // xTokenImpl === eContractid.PTokenStETHImpl || - // xTokenImpl === eContractid.PTokenATokenImpl || - // xTokenImpl === eContractid.PTokenSApeImpl || - // xTokenImpl === eContractid.PTokenCApeImpl || - // xTokenImpl === eContractid.PYieldTokenImpl || - // xTokenImpl === eContractid.NTokenBAKCImpl - // ) as [string, IReserveParams][]; - for (const [symbol, params] of reserves) { if (!tokenAddresses[symbol]) { if (symbol === ERC20TokenContractId.yAPE) { @@ -190,6 +182,11 @@ export const initReservesByHelper = async ( baseVariableBorrowRate, variableRateSlope1, variableRateSlope2, + stableRateSlope1, + stableRateSlope2, + baseStableRateOffset, + stableRateExcessOffset, + optimalStableToTotalDebtRatio, } = strategy; const { maxPriceMultiplier, @@ -219,6 +216,11 @@ export const initReservesByHelper = async ( baseVariableBorrowRate, variableRateSlope1, variableRateSlope2, + stableRateSlope1, + stableRateSlope2, + baseStableRateOffset, + stableRateExcessOffset, + optimalStableToTotalDebtRatio, ], verify ) @@ -295,6 +297,7 @@ export const initReservesByHelper = async ( initInputParams.push({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion xTokenImpl: "", + stableDebtTokenImpl: "", variableDebtTokenImpl: "", assetType: xTokenType[reserveSymbols[i]] == "nft" ? 1 : 0, underlyingAssetDecimals: reserveInitDecimals[i], @@ -309,6 +312,8 @@ export const initReservesByHelper = async ( xTokenType[reserveSymbols[i]] === "nft" ? `n${symbolPrefix}${reserveSymbols[i]}` : `p${symbolPrefix}${reserveSymbols[i]}`, + stableDebtTokenName: `${stableDebtTokenNamePrefix} ${symbolPrefix}${reserveSymbols[i]}`, + stableDebtTokenSymbol: `sDebt${symbolPrefix}${reserveSymbols[i]}`, variableDebtTokenName: `${variableDebtTokenNamePrefix} ${symbolPrefix}${reserveSymbols[i]}`, variableDebtTokenSymbol: `vDebt${symbolPrefix}${reserveSymbols[i]}`, params: "0x10", @@ -336,6 +341,7 @@ export const initReservesByHelper = async ( for (let i = 0; i < inputs.length; i += 1) { let xTokenToUse = ""; + let stableDebtTokenToUse = ""; let variableDebtTokenToUse = ""; const reserveSymbol = inputs[i].underlyingAssetName; console.log("IS ", reserveSymbol); @@ -367,6 +373,12 @@ export const initReservesByHelper = async ( ).address; } variableDebtTokenToUse = aTokenVariableDebtTokenImplementationAddress; + if (!aTokenStableDebtTokenImplementationAddress) { + aTokenStableDebtTokenImplementationAddress = ( + await deployATokenStableDebtToken(pool.address, verify) + ).address; + } + stableDebtTokenToUse = aTokenStableDebtTokenImplementationAddress; } else if (reserveSymbol === ERC20TokenContractId.sAPE) { if (!pTokenSApeImplementationAddress) { const protocolDataProvider = await getProtocolDataProvider(); @@ -502,15 +514,24 @@ export const initReservesByHelper = async ( if (!variableDebtTokenToUse) { if (!variableDebtTokenImplementationAddress) { - variableDebtTokenImplementationAddress = await await ( + variableDebtTokenImplementationAddress = ( await deployGenericVariableDebtToken(pool.address, verify) ).address; } variableDebtTokenToUse = variableDebtTokenImplementationAddress; } + if (!stableDebtTokenToUse) { + if (!stableDebtTokenImplementationAddress) { + stableDebtTokenImplementationAddress = ( + await deployGenericStableDebtToken(pool.address, verify) + ).address; + } + stableDebtTokenToUse = stableDebtTokenImplementationAddress; + } inputs[i].xTokenImpl = xTokenToUse; inputs[i].variableDebtTokenImpl = variableDebtTokenToUse; + inputs[i].stableDebtTokenImpl = stableDebtTokenToUse; } console.log( @@ -564,6 +585,7 @@ export const configureReservesByHelper = async ( reserveFactor: BigNumberish; borrowCap: BigNumberish; supplyCap: BigNumberish; + stableBorrowingEnabled: boolean; borrowingEnabled: boolean; }[] = []; @@ -577,6 +599,7 @@ export const configureReservesByHelper = async ( reserveFactor, borrowCap, supplyCap, + stableBorrowRateEnabled, borrowingEnabled, }, ] of reserves) { @@ -614,6 +637,7 @@ export const configureReservesByHelper = async ( reserveFactor, borrowCap, supplyCap, + stableBorrowingEnabled: stableBorrowRateEnabled, borrowingEnabled: borrowingEnabled, }); diff --git a/helpers/misc-utils.ts b/helpers/misc-utils.ts index 1bffd1512..f2e777fd5 100644 --- a/helpers/misc-utils.ts +++ b/helpers/misc-utils.ts @@ -194,3 +194,18 @@ export const notFalsyOrZeroAddress = ( } return isAddress(address) && !isZeroAddress(address); }; + +export const impersonateAccountsHardhat = async ( + accounts: tEthereumAddress[] +) => { + if (DRE.network.name !== "hardhat") { + return; + } + + for (const account of accounts) { + await DRE.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [account], + }); + } +}; diff --git a/helpers/types.ts b/helpers/types.ts index f57ad2f0b..b84de5b82 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -11,6 +11,10 @@ import {NTokenMAYCLibraryAddresses} from "../types/factories/protocol/tokenizati import {NTokenMoonBirdsLibraryAddresses} from "../types/factories/protocol/tokenization/NTokenMoonBirds__factory"; import {NTokenUniswapV3LibraryAddresses} from "../types/factories/protocol/tokenization/NTokenUniswapV3__factory"; import {NTokenLibraryAddresses} from "../types/factories/protocol/tokenization/NToken__factory"; +import { + deployLoanVaultImpl, + deployMockETHNFTOracle, +} from "./contracts-deployments"; export enum AssetType { ERC20 = 0, @@ -98,6 +102,8 @@ export enum eEthereumNetwork { export enum eContractid { PoolAddressesProvider = "PoolAddressesProvider", + MockStableDebtToken = "MockStableDebtToken", + StableDebtToken = "StableDebtToken", MintableERC20 = "MintableERC20", MintableERC721 = "MintableERC721", MintableDelegationERC20 = "MintableDelegationERC20", @@ -222,10 +228,12 @@ export enum eContractid { MockAirdropProject = "MockAirdropProject", PoolCoreImpl = "PoolCoreImpl", PoolMarketplaceImpl = "PoolMarketplaceImpl", + PoolETHWithdrawImpl = "PoolETHWithdrawImpl", PoolParametersImpl = "PoolParametersImpl", PoolApeStakingImpl = "PoolApeStakingImpl", ApeCoinStaking = "ApeCoinStaking", ATokenDebtToken = "ATokenDebtToken", + ATokenStableDebtToken = "ATokenStableDebtToken", StETHDebtToken = "StETHDebtToken", CApeDebtToken = "CApeDebtToken", ApeStakingLogic = "ApeStakingLogic", @@ -251,6 +259,10 @@ export enum eContractid { ParaProxyInterfacesImpl = "ParaProxyInterfacesImpl", MockedDelegateRegistry = "MockedDelegateRegistry", MockMultiAssetAirdropProject = "MockMultiAssetAirdropProject", + MockETHNFTOracle = "MockETHNFTOracle", + MockedInstantWithdrawNFT = "MockedInstantWithdrawNFT", + LoanVault = "LoanVault", + LoanVaultImpl = "LoanVaultImpl", ParaSpaceAirdrop = "ParaSpaceAirdrop", } @@ -539,6 +551,11 @@ export interface IInterestRateStrategyParams { baseVariableBorrowRate: string; variableRateSlope1: string; variableRateSlope2: string; + stableRateSlope1: string; + stableRateSlope2: string; + baseStableRateOffset: string; + stableRateExcessOffset: string; + optimalStableToTotalDebtRatio: string; } export interface IAuctionStrategyParams { @@ -555,6 +572,7 @@ export interface IAuctionStrategyParams { export interface IReserveBorrowParams { borrowingEnabled: boolean; + stableBorrowRateEnabled: boolean; reserveDecimals: string; borrowCap: string; } @@ -671,6 +689,7 @@ export interface ICommonConfiguration { MarketId: string; ParaSpaceTeam: tEthereumAddress; PTokenNamePrefix: string; + StableDebtTokenNamePrefix: string; VariableDebtTokenNamePrefix: string; SymbolPrefix: string; ProviderId: number; diff --git a/market-config/index.ts b/market-config/index.ts index 0a6d055fd..455dfedf0 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -49,6 +49,7 @@ export const CommonConfig: Pick< | "MarketId" | "PTokenNamePrefix" | "VariableDebtTokenNamePrefix" + | "StableDebtTokenNamePrefix" | "SymbolPrefix" | "ProviderId" | "AuctionRecoveryHealthFactor" @@ -66,6 +67,7 @@ export const CommonConfig: Pick< WrappedNativeTokenId: ERC20TokenContractId.WETH, MarketId: "ParaSpaceMM", PTokenNamePrefix: "ParaSpace Derivative Token", + StableDebtTokenNamePrefix: "ParaSpace Stable Debt Token", VariableDebtTokenNamePrefix: "ParaSpace Variable Debt Token", SymbolPrefix: "", ProviderId: 1, diff --git a/market-config/rateStrategies.ts b/market-config/rateStrategies.ts index 090031ba9..324924bcb 100644 --- a/market-config/rateStrategies.ts +++ b/market-config/rateStrategies.ts @@ -10,6 +10,11 @@ export const rateStrategyDAI: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.04", 27).toString(), variableRateSlope2: utils.parseUnits("0.75", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyUSDC: IInterestRateStrategyParams = { @@ -18,6 +23,11 @@ export const rateStrategyUSDC: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.04", 27).toString(), variableRateSlope2: utils.parseUnits("0.60", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyUSDT: IInterestRateStrategyParams = { @@ -26,6 +36,11 @@ export const rateStrategyUSDT: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.04", 27).toString(), variableRateSlope2: utils.parseUnits("0.75", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyWETH: IInterestRateStrategyParams = { @@ -34,6 +49,11 @@ export const rateStrategyWETH: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0.025", 27).toString(), variableRateSlope1: utils.parseUnits("0.08", 27).toString(), variableRateSlope2: utils.parseUnits("0.9", 27).toString(), + stableRateSlope1: utils.parseUnits("0.1", 27).toString(), + stableRateSlope2: utils.parseUnits("1", 27).toString(), + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyWBTC: IInterestRateStrategyParams = { @@ -42,6 +62,11 @@ export const rateStrategyWBTC: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0.025", 27).toString(), variableRateSlope1: utils.parseUnits("0.07", 27).toString(), variableRateSlope2: utils.parseUnits("1", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyAPE: IInterestRateStrategyParams = { @@ -50,6 +75,11 @@ export const rateStrategyAPE: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0.70", 27).toString(), variableRateSlope1: utils.parseUnits("0.45", 27).toString(), variableRateSlope2: utils.parseUnits("0.55", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategycAPE: IInterestRateStrategyParams = { @@ -58,6 +88,11 @@ export const rateStrategycAPE: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0.05", 27).toString(), variableRateSlope1: utils.parseUnits("0.1", 27).toString(), variableRateSlope2: utils.parseUnits("0.23", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyXCDOT: IInterestRateStrategyParams = { @@ -66,6 +101,11 @@ export const rateStrategyXCDOT: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0.02", 27).toString(), variableRateSlope1: utils.parseUnits("0.25", 27).toString(), variableRateSlope2: utils.parseUnits("0.2", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyWGLMR: IInterestRateStrategyParams = { @@ -74,6 +114,11 @@ export const rateStrategyWGLMR: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.25", 27).toString(), variableRateSlope2: utils.parseUnits("0.25", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyBLUR: IInterestRateStrategyParams = { @@ -82,6 +127,11 @@ export const rateStrategyBLUR: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.04", 27).toString(), variableRateSlope2: utils.parseUnits("0.60", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; export const rateStrategyNFT: IInterestRateStrategyParams = { @@ -90,6 +140,11 @@ export const rateStrategyNFT: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.07", 27).toString(), variableRateSlope2: utils.parseUnits("3", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; //////////////////////////////////////////////////////////// @@ -101,6 +156,11 @@ export const rateStrategySTETH: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0.2", 27).toString(), variableRateSlope1: utils.parseUnits("0.08", 27).toString(), variableRateSlope2: utils.parseUnits("0.60", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; //////////////////////////////////////////////////////////// @@ -113,6 +173,11 @@ export const rateStrategyStableTwo: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.04", 27).toString(), variableRateSlope2: utils.parseUnits("0.75", 27).toString(), + stableRateSlope1: utils.parseUnits("0.02", 27).toString(), + stableRateSlope2: utils.parseUnits("0.75", 27).toString(), + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; // WETH, StETH, Punk @@ -122,6 +187,11 @@ export const rateStrategyXETH: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.08", 27).toString(), variableRateSlope2: utils.parseUnits("1", 27).toString(), + stableRateSlope1: utils.parseUnits("0.1", 27).toString(), + stableRateSlope2: utils.parseUnits("1", 27).toString(), + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; // BAT ENJ LINK MANA MKR REN YFI ZRX @@ -131,4 +201,9 @@ export const rateStrategyVolatileOne: IInterestRateStrategyParams = { baseVariableBorrowRate: utils.parseUnits("0", 27).toString(), variableRateSlope1: utils.parseUnits("0.07", 27).toString(), variableRateSlope2: utils.parseUnits("3", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; diff --git a/market-config/reservesConfigs.ts b/market-config/reservesConfigs.ts index 6ae66d45a..f3019c608 100644 --- a/market-config/reservesConfigs.ts +++ b/market-config/reservesConfigs.ts @@ -40,6 +40,7 @@ export const strategyDAI: IReserveParams = { liquidationProtocolFeePercentage: "0", liquidationBonus: "10400", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -55,6 +56,7 @@ export const strategyUSDC: IReserveParams = { liquidationProtocolFeePercentage: "0", liquidationBonus: "10450", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "6", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -70,6 +72,7 @@ export const strategyUSDT: IReserveParams = { liquidationProtocolFeePercentage: "0", liquidationBonus: "10500", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "6", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -85,6 +88,7 @@ export const strategyWETH: IReserveParams = { liquidationProtocolFeePercentage: "0", liquidationBonus: "10450", borrowingEnabled: true, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -100,6 +104,7 @@ export const strategyWBTC: IReserveParams = { liquidationThreshold: "8200", liquidationBonus: "10500", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "8", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "2000", @@ -115,6 +120,7 @@ export const strategyAPE: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "250", @@ -130,6 +136,7 @@ export const strategySAPE: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenSApeImpl, reserveFactor: "250", @@ -145,6 +152,7 @@ export const strategyCAPE: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenCApeImpl, reserveFactor: "1000", @@ -160,6 +168,7 @@ export const strategyYAPE: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PYieldTokenImpl, reserveFactor: "1000", @@ -175,6 +184,7 @@ export const strategyXCDOT: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "10", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -190,6 +200,7 @@ export const strategyWGLMR: IReserveParams = { liquidationThreshold: "4500", liquidationBonus: "10500", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -205,6 +216,7 @@ export const strategyBAYC: IReserveParams = { liquidationThreshold: "8000", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenBAYCImpl, reserveFactor: "0", @@ -220,6 +232,7 @@ export const strategyMAYC: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenMAYCImpl, reserveFactor: "0", @@ -235,6 +248,7 @@ export const strategyBAKC: IReserveParams = { liquidationThreshold: "8000", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenBAKCImpl, reserveFactor: "0", @@ -250,6 +264,7 @@ export const strategyDoodles: IReserveParams = { liquidationThreshold: "6500", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -265,6 +280,7 @@ export const strategyOthr: IReserveParams = { liquidationThreshold: "6500", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -280,6 +296,7 @@ export const strategyClonex: IReserveParams = { liquidationThreshold: "6500", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -295,6 +312,7 @@ export const strategyMoonbird: IReserveParams = { liquidationThreshold: "6500", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenMoonBirdsImpl, reserveFactor: "0", @@ -310,6 +328,7 @@ export const strategyMeebits: IReserveParams = { liquidationThreshold: "6500", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -325,6 +344,7 @@ export const strategyAzuki: IReserveParams = { liquidationThreshold: "6500", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -340,6 +360,7 @@ export const strategyWPunks: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -355,6 +376,7 @@ export const strategyUniswapV3: IReserveParams = { liquidationThreshold: "7000", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenUniswapV3Impl, reserveFactor: "0", @@ -370,6 +392,7 @@ export const strategySEWER: IReserveParams = { liquidationThreshold: "0000", liquidationBonus: "00000", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -385,6 +408,7 @@ export const strategyPudgyPenguins: IReserveParams = { liquidationThreshold: "6500", liquidationBonus: "10500", borrowingEnabled: false, + stableBorrowRateEnabled: false, reserveDecimals: "0", xTokenImpl: eContractid.NTokenImpl, reserveFactor: "0", @@ -403,6 +427,7 @@ export const strategySTETH: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenStETHImpl, reserveFactor: "1000", @@ -418,6 +443,7 @@ export const strategyWSTETH: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -433,6 +459,7 @@ export const strategyAWETH: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenATokenImpl, reserveFactor: "1000", @@ -448,6 +475,7 @@ export const strategyCETH: IReserveParams = { liquidationProtocolFeePercentage: "0", liquidationBonus: "10750", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "8", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -464,6 +492,7 @@ export const strategyPUNK: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -479,6 +508,7 @@ export const strategyBLUR: IReserveParams = { liquidationThreshold: "3500", liquidationBonus: "11000", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", diff --git a/scripts/deployments/steps/06_pool.ts b/scripts/deployments/steps/06_pool.ts index 1b50407e8..4b4e95c79 100644 --- a/scripts/deployments/steps/06_pool.ts +++ b/scripts/deployments/steps/06_pool.ts @@ -1,16 +1,30 @@ import {ZERO_ADDRESS} from "../../../helpers/constants"; -import {deployPoolComponents} from "../../../helpers/contracts-deployments"; +import { + deployLoanVault, + deployMockETHNFTOracle, + deployPoolComponents, +} from "../../../helpers/contracts-deployments"; import { getPoolProxy, getPoolAddressesProvider, getAutoCompoundApe, getAllTokens, getUniswapV3SwapRouter, + getFirstSigner, } from "../../../helpers/contracts-getters"; -import {registerContractInDb} from "../../../helpers/contracts-helpers"; +import { + getContractAddressInDb, + getFunctionSignatures, + registerContractInDb, + withSaveAndVerify, +} from "../../../helpers/contracts-helpers"; import {GLOBAL_OVERRIDES} from "../../../helpers/hardhat-constants"; import {waitForTx} from "../../../helpers/misc-utils"; import {eContractid, ERC20TokenContractId} from "../../../helpers/types"; +import { + PoolInstantWithdraw, + PoolInstantWithdraw__factory, +} from "../../../types"; export const step_06 = async (verify = false) => { const addressesProvider = await getPoolAddressesProvider(); @@ -26,6 +40,7 @@ export const step_06 = async (verify = false) => { poolParametersSelectors, poolMarketplaceSelectors, poolApeStakingSelectors, + poolInstantWithdrawSelectors, poolParaProxyInterfacesSelectors, } = await deployPoolComponents(addressesProvider.address, verify); @@ -110,6 +125,38 @@ export const step_06 = async (verify = false) => { ) ); + const loanVaultAddress = + (await getContractAddressInDb(eContractid.LoanVault)) || + (await deployLoanVault(poolAddress, verify)).address; + const nFTOracleAddress = + (await getContractAddressInDb(eContractid.MockETHNFTOracle)) || + (await deployMockETHNFTOracle(verify)).address; + // create PoolETHWithdraw here instead of in deployPoolComponents since LoanVault have a dependency for Pool address + const poolInstantWithdraw = (await withSaveAndVerify( + new PoolInstantWithdraw__factory(await getFirstSigner()), + eContractid.PoolETHWithdrawImpl, + [addressesProvider.address, loanVaultAddress, nFTOracleAddress], + verify, + false, + undefined, + getFunctionSignatures(PoolInstantWithdraw__factory.abi) + )) as PoolInstantWithdraw; + + await waitForTx( + await addressesProvider.updatePoolImpl( + [ + { + implAddress: poolInstantWithdraw.address, + action: 0, + functionSelectors: poolInstantWithdrawSelectors, + }, + ], + ZERO_ADDRESS, + "0x", + GLOBAL_OVERRIDES + ) + ); + const poolProxy = await getPoolProxy(poolAddress); const cAPE = await getAutoCompoundApe(); const uniswapV3Router = await getUniswapV3SwapRouter(); diff --git a/scripts/deployments/steps/11_allReserves.ts b/scripts/deployments/steps/11_allReserves.ts index 3080e476c..6a41497fa 100644 --- a/scripts/deployments/steps/11_allReserves.ts +++ b/scripts/deployments/steps/11_allReserves.ts @@ -39,8 +39,12 @@ export const step_11 = async (verify = false) => { const config = getParaSpaceConfig(); - const {PTokenNamePrefix, VariableDebtTokenNamePrefix, SymbolPrefix} = - config; + const { + PTokenNamePrefix, + StableDebtTokenNamePrefix, + VariableDebtTokenNamePrefix, + SymbolPrefix, + } = config; const treasuryAddress = config.Treasury; // Add an IncentivesController @@ -101,6 +105,7 @@ export const step_11 = async (verify = false) => { reserves, allTokenAddresses, PTokenNamePrefix, + StableDebtTokenNamePrefix, VariableDebtTokenNamePrefix, SymbolPrefix, paraSpaceAdminAddress, @@ -111,6 +116,7 @@ export const step_11 = async (verify = false) => { undefined, undefined, undefined, + undefined, auctionStrategy ); diff --git a/scripts/dev/5.rate-strategy.ts b/scripts/dev/5.rate-strategy.ts index 2ef0ea52c..2d72b025f 100644 --- a/scripts/dev/5.rate-strategy.ts +++ b/scripts/dev/5.rate-strategy.ts @@ -13,6 +13,11 @@ const deployRateStrategy = async () => { baseVariableBorrowRate: utils.parseUnits("0.6", 27).toString(), variableRateSlope1: utils.parseUnits("0.5", 27).toString(), variableRateSlope2: utils.parseUnits("0.6", 27).toString(), + stableRateSlope1: "0", + stableRateSlope2: "0", + baseStableRateOffset: utils.parseUnits("0.02", 27).toString(), + stableRateExcessOffset: utils.parseUnits("0.05", 27).toString(), + optimalStableToTotalDebtRatio: utils.parseUnits("0.2", 27).toString(), }; const newStrategy = await deployReserveInterestRateStrategy( strategy.name, @@ -22,6 +27,11 @@ const deployRateStrategy = async () => { strategy.baseVariableBorrowRate, strategy.variableRateSlope1, strategy.variableRateSlope2, + strategy.stableRateSlope1, + strategy.stableRateSlope2, + strategy.baseStableRateOffset, + strategy.stableRateExcessOffset, + strategy.optimalStableToTotalDebtRatio, ], false ); diff --git a/test/_base_interest_rate_strategy.spec.ts b/test/_base_interest_rate_strategy.spec.ts index 94d48ca56..def194742 100644 --- a/test/_base_interest_rate_strategy.spec.ts +++ b/test/_base_interest_rate_strategy.spec.ts @@ -19,6 +19,7 @@ import { VariableDebtToken__factory, MockReserveInterestRateStrategy__factory, PToken__factory, + StableDebtToken__factory, } from "../types"; import {strategyDAI} from "../market-config/reservesConfigs"; import {rateStrategyStableTwo} from "../market-config/rateStrategies"; @@ -41,7 +42,9 @@ import {ETHERSCAN_VERIFICATION} from "../helpers/hardhat-constants"; type CalculateInterestRatesParams = { liquidityAdded: BigNumberish; liquidityTaken: BigNumberish; + totalStableDebt: BigNumberish; totalVariableDebt: BigNumberish; + averageStableBorrowRate: BigNumberish; reserveFactor: BigNumberish; reserve: string; xToken: string; @@ -55,6 +58,9 @@ describe("Interest Rate Tests", () => { let strategyInstance: DefaultReserveInterestRateStrategy; let dai: MintableERC20; let pDai: PToken; + const baseStableRate = BigNumber.from( + rateStrategyStableTwo.variableRateSlope1 + ).add(rateStrategyStableTwo.baseStableRateOffset); const {INVALID_OPTIMAL_USAGE_RATIO} = ProtocolErrors; @@ -72,6 +78,11 @@ describe("Interest Rate Tests", () => { rateStrategyStableTwo.baseVariableBorrowRate, rateStrategyStableTwo.variableRateSlope1, rateStrategyStableTwo.variableRateSlope2, + rateStrategyStableTwo.stableRateSlope1, + rateStrategyStableTwo.stableRateSlope2, + rateStrategyStableTwo.baseStableRateOffset, + rateStrategyStableTwo.stableRateExcessOffset, + rateStrategyStableTwo.optimalStableToTotalDebtRatio, ], ETHERSCAN_VERIFICATION ); @@ -81,16 +92,25 @@ describe("Interest Rate Tests", () => { const params: CalculateInterestRatesParams = { liquidityAdded: 0, liquidityTaken: 0, + totalStableDebt: 0, totalVariableDebt: 0, + averageStableBorrowRate: 0, reserveFactor: strategyDAI.reserveFactor, reserve: dai.address, xToken: pDai.address, }; - const {0: currentLiquidityRate, 1: currentVariableBorrowRate} = - await strategyInstance.calculateInterestRates(params); + const { + 0: currentLiquidityRate, + 1: currentStableBorrowRate, + 2: currentVariableBorrowRate, + } = await strategyInstance.calculateInterestRates(params); expect(currentLiquidityRate).to.be.equal(0, "Invalid liquidity rate"); + expect(currentStableBorrowRate).to.be.equal( + baseStableRate, + "Invalid stable rate" + ); expect(currentVariableBorrowRate).to.be.equal( rateStrategyStableTwo.baseVariableBorrowRate, "Invalid variable rate" @@ -101,14 +121,19 @@ describe("Interest Rate Tests", () => { const params: CalculateInterestRatesParams = { liquidityAdded: "200000000000000000", liquidityTaken: 0, + totalStableDebt: 0, totalVariableDebt: "800000000000000000", + averageStableBorrowRate: 0, reserveFactor: strategyDAI.reserveFactor, reserve: dai.address, xToken: pDai.address, }; - const {0: currentLiquidityRate, 1: currentVariableBorrowRate} = - await strategyInstance.calculateInterestRates(params); + const { + 0: currentLiquidityRate, + 1: currentStableBorrowRate, + 2: currentVariableBorrowRate, + } = await strategyInstance.calculateInterestRates(params); const expectedVariableRate = BigNumber.from( rateStrategyStableTwo.baseVariableBorrowRate @@ -127,20 +152,30 @@ describe("Interest Rate Tests", () => { expectedVariableRate, "Invalid variable rate" ); + + expect(currentStableBorrowRate).to.be.equal( + baseStableRate.add(rateStrategyStableTwo.stableRateSlope1), + "Invalid stable rate" + ); }); it("TC-interest-rate-strategy-03 Checks rates at 100% usage ratio", async () => { const params: CalculateInterestRatesParams = { liquidityAdded: "0", liquidityTaken: 0, + totalStableDebt: 0, totalVariableDebt: "1000000000000000000", + averageStableBorrowRate: 0, reserveFactor: strategyDAI.reserveFactor, reserve: dai.address, xToken: pDai.address, }; - const {0: currentLiquidityRate, 1: currentVariableBorrowRate} = - await strategyInstance.calculateInterestRates(params); + const { + 0: currentLiquidityRate, + 1: currentStableBorrowRate, + 2: currentVariableBorrowRate, + } = await strategyInstance.calculateInterestRates(params); const expectedVariableRate = BigNumber.from( rateStrategyStableTwo.baseVariableBorrowRate @@ -159,20 +194,32 @@ describe("Interest Rate Tests", () => { expectedVariableRate, "Invalid variable rate" ); + + expect(currentStableBorrowRate).to.be.equal( + baseStableRate + .add(rateStrategyStableTwo.stableRateSlope1) + .add(rateStrategyStableTwo.stableRateSlope2), + "Invalid stable rate" + ); }); it("TC-interest-rate-strategy-04 Checks rates at 0.8% usage", async () => { const params: CalculateInterestRatesParams = { liquidityAdded: "9920000000000000000000", liquidityTaken: 0, + totalStableDebt: "0", totalVariableDebt: "80000000000000000000", + averageStableBorrowRate: "0", reserveFactor: strategyDAI.reserveFactor, reserve: dai.address, xToken: pDai.address, }; - const {0: currentLiquidityRate, 1: currentVariableBorrowRate} = - await strategyInstance.calculateInterestRates(params); + const { + 0: currentLiquidityRate, + 1: currentStableBorrowRate, + 2: currentVariableBorrowRate, + } = await strategyInstance.calculateInterestRates(params); const usageRatio = BigNumber.from(1).ray().percentMul(80); const OPTIMAL_USAGE_RATIO = BigNumber.from( @@ -200,6 +247,15 @@ describe("Interest Rate Tests", () => { expectedVariableRate, "Invalid variable rate" ); + + expect(currentStableBorrowRate).to.be.equal( + baseStableRate.add( + BigNumber.from(rateStrategyStableTwo.stableRateSlope1).rayMul( + usageRatio.rayDiv(OPTIMAL_USAGE_RATIO) + ) + ), + "Invalid stable rate" + ); }); it("TC-interest-rate-strategy-05 Checks getters", async () => { @@ -215,6 +271,12 @@ describe("Interest Rate Tests", () => { expect(await strategyInstance.getVariableRateSlope2()).to.be.eq( rateStrategyStableTwo.variableRateSlope2 ); + expect(await strategyInstance.getStableRateSlope1()).to.be.eq( + rateStrategyStableTwo.stableRateSlope1 + ); + expect(await strategyInstance.getStableRateSlope2()).to.be.eq( + rateStrategyStableTwo.stableRateSlope2 + ); expect(await strategyInstance.getMaxVariableBorrowRate()).to.be.eq( BigNumber.from(rateStrategyStableTwo.baseVariableBorrowRate) .add(BigNumber.from(rateStrategyStableTwo.variableRateSlope1)) @@ -223,6 +285,16 @@ describe("Interest Rate Tests", () => { expect(await strategyInstance.MAX_EXCESS_USAGE_RATIO()).to.be.eq( BigNumber.from(1).ray().sub(rateStrategyStableTwo.optimalUsageRatio) ); + expect( + await strategyInstance.MAX_EXCESS_STABLE_TO_TOTAL_DEBT_RATIO() + ).to.be.eq( + BigNumber.from(1) + .ray() + .sub(rateStrategyStableTwo.optimalStableToTotalDebtRatio) + ); + expect(await strategyInstance.getStableRateExcessOffset()).to.be.eq( + rateStrategyStableTwo.stableRateExcessOffset + ); }); it("TC-interest-rate-strategy-06 Deploy an interest rate strategy with optimalUsageRatio out of range (expect revert)", async () => { @@ -237,6 +309,11 @@ describe("Interest Rate Tests", () => { rateStrategyStableTwo.baseVariableBorrowRate, rateStrategyStableTwo.variableRateSlope1, rateStrategyStableTwo.variableRateSlope2, + rateStrategyStableTwo.stableRateSlope1, + rateStrategyStableTwo.stableRateSlope2, + rateStrategyStableTwo.baseStableRateOffset, + rateStrategyStableTwo.stableRateExcessOffset, + rateStrategyStableTwo.optimalStableToTotalDebtRatio, ] ) ).to.be.revertedWith(INVALID_OPTIMAL_USAGE_RATIO); @@ -289,6 +366,9 @@ describe("Interest Rate Tests", () => { await getFirstSigner() ).deploy("MOCK", "MOCK", "18"); + const stableDebtTokenImplementation = await new StableDebtToken__factory( + await getFirstSigner() + ).deploy(pool.address); const variableDebtTokenImplementation = await new VariableDebtToken__factory(await getFirstSigner()).deploy( pool.address @@ -299,7 +379,7 @@ describe("Interest Rate Tests", () => { mockRateStrategy = await new MockReserveInterestRateStrategy__factory( await getFirstSigner() - ).deploy(addressesProvider.address, 0, 0, 0, 0); + ).deploy(addressesProvider.address, 0, 0, 0, 0, 0, 0); mockAuctionStrategy = await deployReserveAuctionStrategy( eContractid.DefaultReserveAuctionStrategy, @@ -318,6 +398,7 @@ describe("Interest Rate Tests", () => { const initInputParams: ConfiguratorInputTypes.InitReserveInputStruct[] = [ { xTokenImpl: xTokenImplementation.address, + stableDebtTokenImpl: stableDebtTokenImplementation.address, variableDebtTokenImpl: variableDebtTokenImplementation.address, underlyingAssetDecimals: 18, interestRateStrategyAddress: mockRateStrategy.address, @@ -330,6 +411,8 @@ describe("Interest Rate Tests", () => { xTokenSymbol: "PMOCK", variableDebtTokenName: "VMOCK", variableDebtTokenSymbol: "VMOCK", + stableDebtTokenName: "SMOCK", + stableDebtTokenSymbol: "SMOCK", params: "0x10", }, ]; diff --git a/test/_base_reserve_configuration.spec.ts b/test/_base_reserve_configuration.spec.ts index c7ed5f0e0..e288f61e4 100644 --- a/test/_base_reserve_configuration.spec.ts +++ b/test/_base_reserve_configuration.spec.ts @@ -14,6 +14,7 @@ import { MockReserveInterestRateStrategy__factory, PoolCore__factory, PToken__factory, + StableDebtToken__factory, VariableDebtToken__factory, } from "../types"; import {ProtocolErrors} from "../helpers/types"; @@ -118,6 +119,7 @@ describe("ReserveConfiguration", async () => { false, false, false, + false, 0, ]); expect(await configMock.getFrozen()).to.be.false; @@ -128,6 +130,7 @@ describe("ReserveConfiguration", async () => { true, false, false, + false, 0, ]); expect(await configMock.getFrozen()).to.be.true; @@ -137,6 +140,7 @@ describe("ReserveConfiguration", async () => { false, false, false, + false, 0, ]); expect(await configMock.getFrozen()).to.be.false; @@ -148,6 +152,7 @@ describe("ReserveConfiguration", async () => { false, false, false, + false, 0, ]); expect(await configMock.getAssetType()).to.be.eq(0); @@ -159,6 +164,7 @@ describe("ReserveConfiguration", async () => { false, false, false, + false, 1, ]); expect(await configMock.getAssetType()).to.be.eq(1); @@ -168,6 +174,7 @@ describe("ReserveConfiguration", async () => { false, false, false, + false, 0, ]); }); @@ -178,6 +185,7 @@ describe("ReserveConfiguration", async () => { false, false, false, + false, 0, ]); expect(await configMock.getBorrowingEnabled()).to.be.false; @@ -188,6 +196,7 @@ describe("ReserveConfiguration", async () => { false, true, false, + false, 0, ]); expect(await configMock.getBorrowingEnabled()).to.be.true; @@ -197,11 +206,45 @@ describe("ReserveConfiguration", async () => { false, false, false, + false, 0, ]); expect(await configMock.getBorrowingEnabled()).to.be.false; }); + it("TC-reserve-configuration-06 getStableRateBorrowingEnabled()", async () => { + expect(await configMock.getFlags()).to.be.eql([ + false, + false, + false, + false, + false, + 0, + ]); + expect(await configMock.getStableRateBorrowingEnabled()).to.be.false; + expect(await configMock.setStableRateBorrowingEnabled(true)); + // borrowing is the 3rd flag + expect(await configMock.getFlags()).to.be.eql([ + false, + false, + false, + true, + false, + 0, + ]); + expect(await configMock.getStableRateBorrowingEnabled()).to.be.true; + expect(await configMock.setStableRateBorrowingEnabled(false)); + expect(await configMock.getFlags()).to.be.eql([ + false, + false, + false, + false, + false, + 0, + ]); + expect(await configMock.getStableRateBorrowingEnabled()).to.be.false; + }); + it("TC-reserve-configuration-07 getReserveFactor()", async () => { expect(bigNumbersToArrayString(await configMock.getParams())).to.be.eql( bigNumbersToArrayString([ZERO, ZERO, ZERO, ZERO, ZERO]) @@ -430,6 +473,7 @@ describe("ReserveConfiguration", async () => { pool.connect(configSigner).initReserve( dai.address, config.xTokenAddress, // just need a non-used reserve token + config.stableDebtTokenAddress, config.variableDebtTokenAddress, ZERO_ADDRESS, ZERO_ADDRESS @@ -466,6 +510,7 @@ describe("ReserveConfiguration", async () => { .initReserve( config.xTokenAddress, ZERO_ADDRESS, + config.stableDebtTokenAddress, config.variableDebtTokenAddress, ZERO_ADDRESS, ZERO_ADDRESS @@ -481,6 +526,7 @@ describe("ReserveConfiguration", async () => { .initReserve( config.xTokenAddress, ZERO_ADDRESS, + config.stableDebtTokenAddress, config.variableDebtTokenAddress, ZERO_ADDRESS, ZERO_ADDRESS @@ -670,6 +716,9 @@ describe("ReserveConfiguration", async () => { const xTokenImp = await new PToken__factory(await getFirstSigner()).deploy( pool.address ); + const stableDebtTokenImp = await new StableDebtToken__factory( + deployer.signer + ).deploy(pool.address); const variableDebtTokenImp = await new VariableDebtToken__factory( deployer.signer ).deploy(pool.address); @@ -688,7 +737,7 @@ describe("ReserveConfiguration", async () => { ); const mockRateStrategy = await new MockReserveInterestRateStrategy__factory( await getFirstSigner() - ).deploy(addressesProvider.address, 0, 0, 0, 0); + ).deploy(addressesProvider.address, 0, 0, 0, 0, 0, 0); const mockAuctionStrategy = await deployReserveAuctionStrategy("test", [ auctionStrategyExp.maxPriceMultiplier, auctionStrategyExp.minExpPriceMultiplier, @@ -701,6 +750,7 @@ describe("ReserveConfiguration", async () => { // Init the reserve const initInputParams: { xTokenImpl: string; + stableDebtTokenImpl: string; variableDebtTokenImpl: string; underlyingAssetDecimals: BigNumberish; interestRateStrategyAddress: string; @@ -720,6 +770,7 @@ describe("ReserveConfiguration", async () => { }[] = [ { xTokenImpl: xTokenImp.address, + stableDebtTokenImpl: stableDebtTokenImp.address, variableDebtTokenImpl: variableDebtTokenImp.address, underlyingAssetDecimals: 18, interestRateStrategyAddress: mockRateStrategy.address, @@ -760,6 +811,9 @@ const getReserveParams = async ( const mockToken = await new MintableERC20__factory( await getFirstSigner() ).deploy("MOCK", "MOCK", "18"); + const stableDebtTokenImplementation = await new StableDebtToken__factory( + await getFirstSigner() + ).deploy(pool.address); const variableDebtTokenImplementation = await new VariableDebtToken__factory( await getFirstSigner() ).deploy(pool.address); @@ -768,12 +822,13 @@ const getReserveParams = async ( ).deploy(pool.address); const mockRateStrategy = await new MockReserveInterestRateStrategy__factory( await getFirstSigner() - ).deploy(addressesProvider.address, 0, 0, 0, 0); + ).deploy(addressesProvider.address, 0, 0, 0, 0, 0, 0); // Init the reserve const initInputParams = [ { xTokenImpl: xTokenImplementation.address, + stableDebtTokenImpl: stableDebtTokenImplementation.address, variableDebtTokenImpl: variableDebtTokenImplementation.address, assetType: 0, underlyingAssetDecimals: 18, @@ -786,6 +841,8 @@ const getReserveParams = async ( xTokenSymbol: "PMOCK", variableDebtTokenName: "VMOCK", variableDebtTokenSymbol: "VMOCK", + stableDebtTokenName: "SMOCK", + stableDebtTokenSymbol: "SMOCK", params: "0x10", }, ]; diff --git a/test/_pool_configurator.spec.ts b/test/_pool_configurator.spec.ts index 4dad80c3d..e0bc438de 100644 --- a/test/_pool_configurator.spec.ts +++ b/test/_pool_configurator.spec.ts @@ -28,6 +28,7 @@ import { MockReserveInterestRateStrategy__factory, ProtocolDataProvider, PToken__factory, + StableDebtToken__factory, VariableDebtToken__factory, } from "../types"; import {TestEnv} from "./helpers/make-suite"; @@ -174,6 +175,9 @@ describe("PoolConfigurator: Common", () => { const mockToken = await new MintableERC20__factory( await getFirstSigner() ).deploy("MOCK", "MOCK", "18"); + const stableDebtTokenImplementation = await new StableDebtToken__factory( + await getFirstSigner() + ).deploy(pool.address); const variableDebtTokenImplementation = await new VariableDebtToken__factory(await getFirstSigner()).deploy( pool.address @@ -183,7 +187,7 @@ describe("PoolConfigurator: Common", () => { ).deploy(pool.address); const mockRateStrategy = await new MockReserveInterestRateStrategy__factory( await getFirstSigner() - ).deploy(addressesProvider.address, 0, 0, 0, 0); + ).deploy(addressesProvider.address, 0, 0, 0, 0, 0, 0); const mockAuctionStrategy = await deployReserveAuctionStrategy( eContractid.DefaultReserveAuctionStrategy, [ @@ -200,6 +204,7 @@ describe("PoolConfigurator: Common", () => { const initInputParams = [ { xTokenImpl: xTokenImplementation.address, + stableDebtTokenImpl: stableDebtTokenImplementation.address, variableDebtTokenImpl: variableDebtTokenImplementation.address, assetType: 0, underlyingAssetDecimals: 18, @@ -212,6 +217,8 @@ describe("PoolConfigurator: Common", () => { xTokenSymbol: "PMOCK", variableDebtTokenName: "VMOCK", variableDebtTokenSymbol: "VMOCK", + stableDebtTokenName: "SMOCK", + stableDebtTokenSymbol: "SMOCK", params: "0x10", }, ]; @@ -382,6 +389,11 @@ describe("PoolConfigurator: Common", () => { const {configurator, protocolDataProvider, weth} = await loadFixture( testEnvFixture ); + + await waitForTx( + await configurator.setReserveStableRateBorrowing(weth.address, false) + ); + expect(await configurator.setReserveBorrowing(weth.address, false)) .to.emit(configurator, "ReserveBorrowing") .withArgs(weth.address, false); @@ -395,6 +407,11 @@ describe("PoolConfigurator: Common", () => { it("TC-poolConfigurator-setReserveBorrowing-02: Deactivates the ETH reserve for borrowing via risk admin", async () => { const {configurator, protocolDataProvider, weth, riskAdmin} = await loadFixture(testEnvFixture); + + await waitForTx( + await configurator.setReserveStableRateBorrowing(weth.address, false) + ); + expect( await configurator .connect(riskAdmin.signer) @@ -1176,6 +1193,7 @@ describe("PoolConfigurator: Modifiers", () => { { xTokenImpl: randomAddress, assetType: 0, + stableDebtTokenImpl: randomAddress, variableDebtTokenImpl: randomAddress, underlyingAssetDecimals: randomNumber, interestRateStrategyAddress: randomAddress, @@ -1188,6 +1206,8 @@ describe("PoolConfigurator: Modifiers", () => { xTokenSymbol: "MOCK", variableDebtTokenName: "MOCK", variableDebtTokenSymbol: "MOCK", + stableDebtTokenName: "MOCK", + stableDebtTokenSymbol: "MOCK", params: "0x10", }, ]; @@ -1465,6 +1485,9 @@ describe("PoolConfigurator: Reserve Without Incentives Controller", () => { "18" ); + const stableDebtTokenImplementation = await new StableDebtToken__factory( + await getFirstSigner() + ).deploy(pool.address); const variableDebtTokenImplementation = await new VariableDebtToken__factory(await getFirstSigner()).deploy( pool.address @@ -1500,6 +1523,7 @@ describe("PoolConfigurator: Reserve Without Incentives Controller", () => { // Init the reserve const initInputParams: { xTokenImpl: string; + stableDebtTokenImpl: string; variableDebtTokenImpl: string; underlyingAssetDecimals: BigNumberish; interestRateStrategyAddress: string; @@ -1512,10 +1536,13 @@ describe("PoolConfigurator: Reserve Without Incentives Controller", () => { xTokenSymbol: string; variableDebtTokenName: string; variableDebtTokenSymbol: string; + stableDebtTokenName: string; + stableDebtTokenSymbol: string; params: string; }[] = [ { xTokenImpl: xTokenImplementation.address, + stableDebtTokenImpl: stableDebtTokenImplementation.address, variableDebtTokenImpl: variableDebtTokenImplementation.address, underlyingAssetDecimals: 18, interestRateStrategyAddress: interestRateStrategyAddress, @@ -1528,6 +1555,8 @@ describe("PoolConfigurator: Reserve Without Incentives Controller", () => { xTokenSymbol: "PMOCK", variableDebtTokenName: "VMOCK", variableDebtTokenSymbol: "VMOCK", + stableDebtTokenName: "SMOCK", + stableDebtTokenSymbol: "SMOCK", params: "0x10", }, ]; diff --git a/test/_pool_initialization.spec.ts b/test/_pool_initialization.spec.ts index c116a0255..df2570760 100644 --- a/test/_pool_initialization.spec.ts +++ b/test/_pool_initialization.spec.ts @@ -56,6 +56,7 @@ describe("Pool: Initialization", () => { .initReserve( dai.address, config.xTokenAddress, + config.stableDebtTokenAddress, config.variableDebtTokenAddress, ZERO_ADDRESS, ZERO_ADDRESS @@ -85,6 +86,7 @@ describe("Pool: Initialization", () => { ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, + ZERO_ADDRESS, ZERO_ADDRESS ) ).to.be.revertedWith(NOT_CONTRACT); diff --git a/test/helpers/uniswapv3-helper.ts b/test/helpers/uniswapv3-helper.ts index 5cee6b731..8f9d15432 100644 --- a/test/helpers/uniswapv3-helper.ts +++ b/test/helpers/uniswapv3-helper.ts @@ -11,7 +11,7 @@ import {IUniswapV3Pool__factory} from "../../types"; import {VERBOSE} from "../../helpers/hardhat-constants"; export function almostEqual(value0: BigNumberish, value1: BigNumberish) { - const maxDiff = BigNumber.from(value0.toString()).div("1000").abs(); + const maxDiff = BigNumber.from(value0.toString()).div("1000").mul(2).abs(); const abs = BigNumber.from(value0.toString()).sub(value1.toString()).abs(); if (!abs.lte(maxDiff) && VERBOSE) { console.log("---------value0=" + value0 + ", --------value1=" + value1); diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts new file mode 100644 index 000000000..59d316470 --- /dev/null +++ b/test/pool_instant_withdraw.spec.ts @@ -0,0 +1,117 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { TestEnv } from "./helpers/make-suite"; +import { testEnvFixture } from "./helpers/setup-env"; +import { mintAndValidate, supplyAndValidate } from "./helpers/validated-steps"; +import { advanceTimeAndBlock, waitForTx } from "../helpers/misc-utils"; +import { parseEther } from "ethers/lib/utils"; +import { LoanVault, MockedInstantWithdrawNFT } from "../types"; +import { deployMockedInstantWithdrawNFT } from "../helpers/contracts-deployments"; +import { getLoanVault } from "../helpers/contracts-getters"; +import { MAX_UINT_AMOUNT } from "../helpers/constants"; + +describe("Pool Instant Withdraw Test", () => { + let testEnv: TestEnv; + let instantWithdrawNFT: MockedInstantWithdrawNFT; + let loanVault: LoanVault; + + const fixture = async () => { + testEnv = await loadFixture(testEnvFixture); + const { + poolAdmin, + pool, + weth, + users: [user1, user2, user3], + } = testEnv; + + instantWithdrawNFT = await deployMockedInstantWithdrawNFT(); + loanVault = await getLoanVault(); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .addBorrowableAssets(instantWithdrawNFT.address, [weth.address]) + ); + + await supplyAndValidate(weth, "100", user1, true); + + await waitForTx( + await instantWithdrawNFT + .connect(user2.signer) + ["mint(address)"](user2.address) + ); + await waitForTx( + await instantWithdrawNFT + .connect(user2.signer) + .setApprovalForAll(pool.address, true) + ); + + await mintAndValidate(weth, "100", user3); + await waitForTx( + await weth.connect(user3.signer).approve(pool.address, MAX_UINT_AMOUNT) + ); + + await user1.signer.sendTransaction({ + to: loanVault.address, + value: parseEther("10"), + }); + + return testEnv; + }; + + it("term loan can be bought by other user", async () => { + const { + users: [, user2, user3], + weth, + pool, + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user2.signer) + .createLoan( + instantWithdrawNFT.address, + 0, + weth.address, + 0 + ) + ); + + expect(await weth.balanceOf(user2.address)).to.be.gte(parseEther("1")); + expect(await instantWithdrawNFT.ownerOf("0")).to.be.eq(loanVault.address); + + await waitForTx( + await pool.connect(user3.signer).swapLoanCollateral(0, user3.address) + ); + + expect(await instantWithdrawNFT.ownerOf("0")).to.be.eq(user3.address); + }); + + it("term loan can be settled", async () => { + const { + users: [, user2, user3], + weth, + pool, + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user2.signer) + .createLoan( + instantWithdrawNFT.address, + 0, + weth.address, + 0 + ) + ); + + expect(await weth.balanceOf(user2.address)).to.be.gte(parseEther("1")); + expect(await instantWithdrawNFT.ownerOf("0")).to.be.eq(loanVault.address); + + await advanceTimeAndBlock(parseInt("86400")); + + await waitForTx(await pool.connect(user3.signer).settleTermLoan(0)); + + expect(await instantWithdrawNFT.balanceOf(loanVault.address)).to.be.eq(0); + }); +}); diff --git a/test/xtoken_atoken_stable_debt_token.spec.ts b/test/xtoken_atoken_stable_debt_token.spec.ts new file mode 100644 index 000000000..abc592783 --- /dev/null +++ b/test/xtoken_atoken_stable_debt_token.spec.ts @@ -0,0 +1,194 @@ +import {expect} from "chai"; +import {BigNumber, utils} from "ethers"; +import {ProtocolErrors} from "../helpers/types"; +import {RAY} from "../helpers/constants"; +import { + DRE, + increaseTime, + setAutomine, + waitForTx, + impersonateAccountsHardhat, +} from "../helpers/misc-utils"; +import { + ATokenStableDebtToken__factory, + StableDebtToken__factory, +} from "../types"; +import {topUpNonPayableWithEther} from "./helpers/utils/funds"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {TestEnv} from "./helpers/make-suite"; +import {almostEqual} from "./helpers/uniswapv3-helper"; +import {parseEther} from "ethers/lib/utils"; + +describe("AToken Stable Debt Token Test", () => { + let testEnv: TestEnv; + const {CALLER_MUST_BE_POOL} = ProtocolErrors; + const fixture = async () => { + testEnv = await loadFixture(testEnvFixture); + const {deployer, pool} = testEnv; + + await topUpNonPayableWithEther( + deployer.signer, + [pool.address], + utils.parseEther("1") + ); + await impersonateAccountsHardhat([pool.address]); + + return testEnv; + }; + + it("Tries to mint not being the Pool (revert expected)", async () => { + const {deployer, aWETH, protocolDataProvider} = await loadFixture(fixture); + const aWETHStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(aWETH.address) + ).stableDebtTokenAddress; + const stableDebtContract = ATokenStableDebtToken__factory.connect( + aWETHStableDebtTokenAddress, + deployer.signer + ); + await expect( + stableDebtContract.mint(deployer.address, deployer.address, "1", "1") + ).to.be.revertedWith(CALLER_MUST_BE_POOL); + }); + it("Tries to burn not being the Pool (revert expected)", async () => { + const {deployer, aWETH, protocolDataProvider} = await loadFixture(fixture); + const aWETHStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(aWETH.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + aWETHStableDebtTokenAddress, + deployer.signer + ); + const name = await stableDebtContract.name(); + expect(name).to.be.equal("ParaSpace Stable Debt Token aWETH"); + await expect( + stableDebtContract.burn(deployer.address, "1") + ).to.be.revertedWith(CALLER_MUST_BE_POOL); + }); + + it("Mint and Burn as expected", async () => { + const rate1 = BigNumber.from(10).pow(26); + const rate2 = BigNumber.from(10).pow(26).mul(2); + const amount1 = parseEther("1"); + const amount2 = parseEther("1"); + const { + deployer, + pool, + aWETH, + protocolDataProvider, + users: [user0, user1], + } = await loadFixture(fixture); + + const poolSigner = await DRE.ethers.getSigner(pool.address); + const config = await protocolDataProvider.getReserveTokensAddresses( + aWETH.address + ); + const stableDebt = ATokenStableDebtToken__factory.connect( + config.stableDebtTokenAddress, + deployer.signer + ); + await setAutomine(false); + await stableDebt + .connect(poolSigner) + .mint(user0.address, user0.address, amount1, rate1); + await setAutomine(true); + await waitForTx( + await stableDebt + .connect(poolSigner) + .mint(user1.address, user1.address, amount2, rate2) + ); + + let supplyData = await stableDebt.getSupplyData(); + almostEqual(supplyData[0], parseEther("1").mul(2)); + almostEqual(supplyData[1], parseEther("1").mul(2)); + almostEqual(supplyData[2], BigNumber.from(10).pow(25).mul(15)); + let user0Balance = await stableDebt.balanceOf(user0.address); + let user1Balance = await stableDebt.balanceOf(user1.address); + almostEqual(user0Balance, parseEther("1")); + almostEqual(user1Balance, parseEther("1")); + + await increaseTime(60 * 60 * 24 * 365); + supplyData = await stableDebt.getSupplyData(); + almostEqual(supplyData[0], parseEther("1").mul(2)); + almostEqual(supplyData[1], BigNumber.from("2323618618389325423")); + almostEqual(supplyData[2], BigNumber.from(10).pow(25).mul(15)); + user0Balance = await stableDebt.balanceOf(user0.address); + user1Balance = await stableDebt.balanceOf(user1.address); + almostEqual(user0Balance, BigNumber.from("1105162046325274579")); + almostEqual(user1Balance, BigNumber.from("1221332933560973813")); + almostEqual(user0Balance.add(user1Balance), supplyData[1]); + + await aWETH.setIncomeIndex(BigNumber.from(10).pow(27).mul(2)); + supplyData = await stableDebt.getSupplyData(); + almostEqual(supplyData[0], parseEther("1").mul(4)); + almostEqual(supplyData[1], BigNumber.from("2323618618389325423").mul(2)); + almostEqual(supplyData[2], BigNumber.from(10).pow(25).mul(15)); + user0Balance = await stableDebt.balanceOf(user0.address); + user1Balance = await stableDebt.balanceOf(user1.address); + almostEqual(user0Balance, BigNumber.from("1105162046325274579").mul(2)); + almostEqual(user1Balance, BigNumber.from("1221332933560973813").mul(2)); + + //burn user0's debt + await waitForTx( + await stableDebt.connect(poolSigner).burn(user0.address, user0Balance) + ); + user0Balance = await stableDebt.balanceOf(user0.address); + expect(user0Balance).to.be.lte(parseEther("0.0001")); + + //burn user1's debt + await waitForTx( + await stableDebt.connect(poolSigner).burn(user1.address, user1Balance) + ); + supplyData = await stableDebt.getSupplyData(); + expect(supplyData[0]).to.be.eq(0); + expect(supplyData[1]).to.be.eq(0); + expect(supplyData[2]).to.be.eq(0); + user0Balance = await stableDebt.balanceOf(user0.address); + user1Balance = await stableDebt.balanceOf(user1.address); + expect(user0Balance).to.be.lte(parseEther("0.0001")); + expect(user1Balance).to.be.lte(parseEther("0.0001")); + }); + + it("Burn stable debt tokens such that `secondTerm >= firstTerm`", async () => { + // To enter the case where secondTerm >= firstTerm, we also need previousSupply <= amount. + // The easiest way is to use two users, such that for user 2 his stableRate > average stableRate. + // In practice to enter the case we can perform the following actions + // user 1 borrow 2 wei at rate = 10**27 + // user 2 borrow 1 wei rate = 10**30 + // progress time by a year, to accrue significant debt. + // then let user 2 withdraw sufficient funds such that secondTerm (userStableRate * burnAmount) >= averageRate * supply + // if we do not have user 1 deposit as well, we will have issues getting past previousSupply <= amount, as amount > supply for secondTerm to be > firstTerm. + const rateGuess1 = BigNumber.from(RAY); + const rateGuess2 = BigNumber.from(10).pow(30); + const amount1 = BigNumber.from(2); + const amount2 = BigNumber.from(1); + const {deployer, pool, aWETH, protocolDataProvider, users} = + await loadFixture(fixture); + + const poolSigner = await DRE.ethers.getSigner(pool.address); + const config = await protocolDataProvider.getReserveTokensAddresses( + aWETH.address + ); + const stableDebt = StableDebtToken__factory.connect( + config.stableDebtTokenAddress, + deployer.signer + ); + // Next two txs should be mined in the same block + await setAutomine(false); + await stableDebt + .connect(poolSigner) + .mint(users[0].address, users[0].address, amount1, rateGuess1); + await stableDebt + .connect(poolSigner) + .mint(users[1].address, users[1].address, amount2, rateGuess2); + + await setAutomine(true); + await increaseTime(60 * 60 * 24 * 365); + const totalSupplyAfterTime = BigNumber.from(18798191); + await waitForTx( + await stableDebt + .connect(poolSigner) + .burn(users[1].address, totalSupplyAfterTime.sub(1)) + ); + }); +}); diff --git a/test/xtoken_stable_debt_token.spec.ts b/test/xtoken_stable_debt_token.spec.ts new file mode 100644 index 000000000..8171cbd31 --- /dev/null +++ b/test/xtoken_stable_debt_token.spec.ts @@ -0,0 +1,285 @@ +import {expect} from "chai"; +import {BigNumber, utils} from "ethers"; +import {ProtocolErrors} from "../helpers/types"; +import {MAX_UINT_AMOUNT, RAY, ZERO_ADDRESS} from "../helpers/constants"; +import { + DRE, + increaseTime, + setAutomine, + impersonateAccountsHardhat, +} from "../helpers/misc-utils"; +import {StableDebtToken__factory} from "../types"; +import {topUpNonPayableWithEther} from "./helpers/utils/funds"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; + +describe("Stable Debt Token Test", () => { + const {CALLER_MUST_BE_POOL, CALLER_NOT_POOL_ADMIN} = ProtocolErrors; + it("Check initialization", async () => { + const {pool, weth, protocolDataProvider, users} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + users[0].signer + ); + expect(await stableDebtContract.UNDERLYING_ASSET_ADDRESS()).to.be.eq( + weth.address + ); + expect(await stableDebtContract.POOL()).to.be.eq(pool.address); + expect(await stableDebtContract.getIncentivesController()).to.not.be.eq( + ZERO_ADDRESS + ); + const totSupplyAndRateBefore = + await stableDebtContract.getTotalSupplyAndAvgRate(); + expect(totSupplyAndRateBefore[0].toString()).to.be.eq("0"); + expect(totSupplyAndRateBefore[1].toString()).to.be.eq("0"); + // Need to create some debt to do this good + await weth + .connect(users[0].signer) + ["mint(uint256)"](await convertToCurrencyDecimals(weth.address, "1000")); + await weth.connect(users[0].signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(users[0].signer) + .supply( + weth.address, + await convertToCurrencyDecimals(weth.address, "1000"), + users[0].address, + 0 + ); + await weth + .connect(users[1].signer) + ["mint(uint256)"](utils.parseEther("10")); + await weth.connect(users[1].signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(users[1].signer) + .supply(weth.address, utils.parseEther("10"), users[1].address, 0); + await pool + .connect(users[1].signer) + .borrow( + weth.address, + await convertToCurrencyDecimals(weth.address, "1"), + 0, + users[1].address + ); + const totSupplyAndRateAfter = + await stableDebtContract.getTotalSupplyAndAvgRate(); + //borrow by variable + expect(totSupplyAndRateAfter[0]).to.be.eq(0); + expect(totSupplyAndRateAfter[1]).to.be.eq(0); + }); + it("Tries to mint not being the Pool (revert expected)", async () => { + const {deployer, weth, protocolDataProvider} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + deployer.signer + ); + await expect( + stableDebtContract.mint(deployer.address, deployer.address, "1", "1") + ).to.be.revertedWith(CALLER_MUST_BE_POOL); + }); + it("Tries to burn not being the Pool (revert expected)", async () => { + const {deployer, weth, protocolDataProvider} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + deployer.signer + ); + const name = await stableDebtContract.name(); + expect(name).to.be.equal("ParaSpace Stable Debt Token WETH"); + await expect( + stableDebtContract.burn(deployer.address, "1") + ).to.be.revertedWith(CALLER_MUST_BE_POOL); + }); + it("Tries to transfer debt tokens (revert expected)", async () => { + const {users, weth, protocolDataProvider} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + users[0].signer + ); + await expect( + stableDebtContract + .connect(users[0].signer) + .transfer(users[1].address, 500) + ).to.be.revertedWith(ProtocolErrors.OPERATION_NOT_SUPPORTED); + }); + + it("Tries to approve debt tokens (revert expected)", async () => { + const {users, weth, protocolDataProvider} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + users[0].signer + ); + await expect( + stableDebtContract.connect(users[0].signer).approve(users[1].address, 500) + ).to.be.revertedWith(ProtocolErrors.OPERATION_NOT_SUPPORTED); + await expect( + stableDebtContract.allowance(users[0].address, users[1].address) + ).to.be.revertedWith(ProtocolErrors.OPERATION_NOT_SUPPORTED); + }); + + it("Tries to increase allowance of debt tokens (revert expected)", async () => { + const {users, weth, protocolDataProvider} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + users[0].signer + ); + await expect( + stableDebtContract + .connect(users[0].signer) + .increaseAllowance(users[1].address, 500) + ).to.be.revertedWith(ProtocolErrors.OPERATION_NOT_SUPPORTED); + }); + + it("Tries to decrease allowance of debt tokens (revert expected)", async () => { + const {users, weth, protocolDataProvider} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + users[0].signer + ); + await expect( + stableDebtContract + .connect(users[0].signer) + .decreaseAllowance(users[1].address, 500) + ).to.be.revertedWith(ProtocolErrors.OPERATION_NOT_SUPPORTED); + }); + + it("Tries to transferFrom (revert expected)", async () => { + const {users, weth, protocolDataProvider} = await loadFixture( + testEnvFixture + ); + const daiStableDebtTokenAddress = ( + await protocolDataProvider.getReserveTokensAddresses(weth.address) + ).stableDebtTokenAddress; + const stableDebtContract = StableDebtToken__factory.connect( + daiStableDebtTokenAddress, + users[0].signer + ); + await expect( + stableDebtContract + .connect(users[0].signer) + .transferFrom(users[0].address, users[1].address, 500) + ).to.be.revertedWith(ProtocolErrors.OPERATION_NOT_SUPPORTED); + }); + + it("Burn stable debt tokens such that `secondTerm >= firstTerm`", async () => { + // To enter the case where secondTerm >= firstTerm, we also need previousSupply <= amount. + // The easiest way is to use two users, such that for user 2 his stableRate > average stableRate. + // In practice to enter the case we can perform the following actions + // user 1 borrow 2 wei at rate = 10**27 + // user 2 borrow 1 wei rate = 10**30 + // progress time by a year, to accrue significant debt. + // then let user 2 withdraw sufficient funds such that secondTerm (userStableRate * burnAmount) >= averageRate * supply + // if we do not have user 1 deposit as well, we will have issues getting past previousSupply <= amount, as amount > supply for secondTerm to be > firstTerm. + const rateGuess1 = BigNumber.from(RAY); + const rateGuess2 = BigNumber.from(10).pow(30); + const amount1 = BigNumber.from(2); + const amount2 = BigNumber.from(1); + const {deployer, pool, dai, protocolDataProvider, users} = + await loadFixture(testEnvFixture); + // Impersonate the Pool + await topUpNonPayableWithEther( + deployer.signer, + [pool.address], + utils.parseEther("1") + ); + await impersonateAccountsHardhat([pool.address]); + const poolSigner = await DRE.ethers.getSigner(pool.address); + const config = await protocolDataProvider.getReserveTokensAddresses( + dai.address + ); + const stableDebt = StableDebtToken__factory.connect( + config.stableDebtTokenAddress, + deployer.signer + ); + // Next two txs should be mined in the same block + await setAutomine(false); + await stableDebt + .connect(poolSigner) + .mint(users[0].address, users[0].address, amount1, rateGuess1); + await stableDebt + .connect(poolSigner) + .mint(users[1].address, users[1].address, amount2, rateGuess2); + await setAutomine(true); + await increaseTime(60 * 60 * 24 * 365); + const totalSupplyAfterTime = BigNumber.from(18798191); + await stableDebt + .connect(poolSigner) + .burn(users[1].address, totalSupplyAfterTime.sub(1)); + }); + + it("setIncentivesController() ", async () => { + const {weth, protocolDataProvider, poolAdmin} = await loadFixture( + testEnvFixture + ); + const config = await protocolDataProvider.getReserveTokensAddresses( + weth.address + ); + const stableDebt = StableDebtToken__factory.connect( + config.stableDebtTokenAddress, + poolAdmin.signer + ); + expect(await stableDebt.getIncentivesController()).to.not.be.eq( + ZERO_ADDRESS + ); + expect( + await stableDebt + .connect(poolAdmin.signer) + .setIncentivesController(ZERO_ADDRESS) + ); + expect(await stableDebt.getIncentivesController()).to.be.eq(ZERO_ADDRESS); + }); + it("setIncentivesController() from not pool admin (revert expected)", async () => { + const { + weth, + protocolDataProvider, + users: [user], + } = await loadFixture(testEnvFixture); + const config = await protocolDataProvider.getReserveTokensAddresses( + weth.address + ); + const stableDebt = StableDebtToken__factory.connect( + config.stableDebtTokenAddress, + user.signer + ); + expect(await stableDebt.getIncentivesController()).to.not.be.eq( + ZERO_ADDRESS + ); + await expect( + stableDebt.connect(user.signer).setIncentivesController(ZERO_ADDRESS) + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + }); +}); From d9b8166c2874993373652311dc16e9892566ba43 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Fri, 10 Mar 2023 17:40:57 +0800 Subject: [PATCH 02/25] chore: use ERC1155 for collateral NFT --- contracts/interfaces/IInstantNFTOracle.sol | 10 ++-- contracts/interfaces/IInstantWithdrawNFT.sol | 2 +- contracts/interfaces/ILoanVault.sol | 4 +- contracts/interfaces/IPoolInstantWithdraw.sol | 4 ++ contracts/misc/LoanVault.sol | 52 +++++++++++++------ contracts/mocks/MockedETHNFTOracle.sol | 3 +- .../mocks/tokens/MockInstantWithdrawNFT.sol | 20 ------- .../mocks/tokens/MockedInstantWithdrawNFT.sol | 13 +++++ .../protocol/libraries/types/DataTypes.sol | 4 +- .../protocol/pool/PoolInstantWithdraw.sol | 50 ++++++++++++------ contracts/ui/WETHGateway.sol | 3 ++ contracts/ui/interfaces/IWETHGateway.sol | 1 + helpers/contracts-deployments.ts | 2 +- test/pool_instant_withdraw.spec.ts | 28 ++++++---- 14 files changed, 123 insertions(+), 73 deletions(-) delete mode 100644 contracts/mocks/tokens/MockInstantWithdrawNFT.sol create mode 100644 contracts/mocks/tokens/MockedInstantWithdrawNFT.sol diff --git a/contracts/interfaces/IInstantNFTOracle.sol b/contracts/interfaces/IInstantNFTOracle.sol index d6054b08d..ea07e4fa5 100644 --- a/contracts/interfaces/IInstantNFTOracle.sol +++ b/contracts/interfaces/IInstantNFTOracle.sol @@ -2,13 +2,15 @@ pragma solidity 0.8.10; interface IInstantNFTOracle { - function getPresentValueAndDiscountRate(uint256 tokenId, uint256 borrowRate) - external - view - returns (uint256, uint256); + function getPresentValueAndDiscountRate( + uint256 tokenId, + uint256 amount, + uint256 borrowRate + ) external view returns (uint256, uint256); function getPresentValueByDiscountRate( uint256 tokenId, + uint256 amount, uint256 discountRate ) external view returns (uint256); diff --git a/contracts/interfaces/IInstantWithdrawNFT.sol b/contracts/interfaces/IInstantWithdrawNFT.sol index 54850f413..5ae563a77 100644 --- a/contracts/interfaces/IInstantWithdrawNFT.sol +++ b/contracts/interfaces/IInstantWithdrawNFT.sol @@ -7,5 +7,5 @@ pragma solidity 0.8.10; * @notice Defines the basic interface for an InstantWithdrawNFT contract. **/ interface IInstantWithdrawNFT { - function burn(uint256 tokenId) external; + function burn(uint256 tokenId, uint256 amount) external; } diff --git a/contracts/interfaces/ILoanVault.sol b/contracts/interfaces/ILoanVault.sol index a38f96373..636f5c05e 100644 --- a/contracts/interfaces/ILoanVault.sol +++ b/contracts/interfaces/ILoanVault.sol @@ -10,12 +10,14 @@ interface ILoanVault { function transferCollateral( address collateralAsset, uint256 collateralTokenId, + uint256 collateralAmount, address to ) external; function settleCollateral( address collateralAsset, - uint256 collateralTokenId + uint256 collateralTokenId, + uint256 collateralAmount ) external; function swapETHToDerivativeAsset(address asset, uint256 amount) external; diff --git a/contracts/interfaces/IPoolInstantWithdraw.sol b/contracts/interfaces/IPoolInstantWithdraw.sol index f6a380207..db0409d0d 100644 --- a/contracts/interfaces/IPoolInstantWithdraw.sol +++ b/contracts/interfaces/IPoolInstantWithdraw.sol @@ -21,6 +21,7 @@ interface IPoolInstantWithdraw { * @param loanId The Id of the loan * @param collateralAsset The collateral asset of the loan * @param collateralTokenId The collateral token Id of the loan + * @param collateralAmount The collateral token amount of the loan * @param borrowAsset The borrow asset token address of the loan * @param borrowAmount The borrow amount of the loan * @param discountRate The discount rate of the collateral asset when created the loan @@ -30,6 +31,7 @@ interface IPoolInstantWithdraw { uint256 indexed loanId, address collateralAsset, uint256 collateralTokenId, + uint256 collateralAmount, address borrowAsset, uint256 borrowAmount, uint256 discountRate @@ -135,12 +137,14 @@ interface IPoolInstantWithdraw { * @notice create a term loan with the specified collateral asset * @param collateralAsset The address of the collateral asset * @param collateralTokenId The token id of the collateral asset + * @param collateralAmount The collateral token amount of the loan * @param borrowAsset The address of the asset user wanted to borrow * @return the loan's borrow amount **/ function createLoan( address collateralAsset, uint256 collateralTokenId, + uint256 collateralAmount, address borrowAsset, uint16 referralCode ) external returns (uint256); diff --git a/contracts/misc/LoanVault.sol b/contracts/misc/LoanVault.sol index dae7e9576..b388152e8 100644 --- a/contracts/misc/LoanVault.sol +++ b/contracts/misc/LoanVault.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.10; import "../dependencies/openzeppelin/upgradeability/Initializable.sol"; import "../dependencies/openzeppelin/upgradeability/OwnableUpgradeable.sol"; import {IERC20} from "../dependencies/openzeppelin/contracts/IERC20.sol"; -import {IERC721} from "../dependencies/openzeppelin/contracts/IERC721.sol"; +import {IERC1155} from "../dependencies/openzeppelin/contracts/IERC1155.sol"; import {SafeERC20} from "../dependencies/openzeppelin/contracts/SafeERC20.sol"; import {IInstantWithdrawNFT} from "../interfaces/IInstantWithdrawNFT.sol"; import {IPool} from "../interfaces/IPool.sol"; @@ -34,15 +34,19 @@ contract LoanVault is Initializable, OwnableUpgradeable { ); /** - * @dev Emitted during rescueERC721() + * @dev Emitted during RescueERC1155() * @param token The address of the token * @param to The address of the recipient * @param ids The ids of the tokens being rescued + * @param amounts The amount of NFTs being rescued for a specific id. + * @param data The data of the tokens that is being rescued. Usually this is 0. **/ - event RescueERC721( + event RescueERC1155( address indexed token, address indexed to, - uint256[] ids + uint256[] ids, + uint256[] amounts, + bytes data ); address private immutable lendingPool; @@ -119,20 +123,27 @@ contract LoanVault is Initializable, OwnableUpgradeable { function transferCollateral( address collateralAsset, uint256 collateralTokenId, + uint256 collateralAmount, address to ) external onlyPool { - IERC721(collateralAsset).safeTransferFrom( + IERC1155(collateralAsset).safeTransferFrom( address(this), to, - collateralTokenId + collateralTokenId, + collateralAmount, + "" ); } function settleCollateral( address collateralAsset, - uint256 collateralTokenId + uint256 collateralTokenId, + uint256 collateralAmount ) external onlyPool { - IInstantWithdrawNFT(collateralAsset).burn(collateralTokenId); + IInstantWithdrawNFT(collateralAsset).burn( + collateralTokenId, + collateralAmount + ); } function swapETHToDerivativeAsset(address asset, uint256 amount) @@ -167,13 +178,14 @@ contract LoanVault is Initializable, OwnableUpgradeable { receive() external payable {} - function onERC721Received( + function onERC1155Received( address, address, uint256, + uint256, bytes memory - ) external pure returns (bytes4) { - return this.onERC721Received.selector; + ) public pure returns (bytes4) { + return this.onERC1155Received.selector; } function rescueERC20( @@ -185,14 +197,20 @@ contract LoanVault is Initializable, OwnableUpgradeable { emit RescueERC20(token, to, amount); } - function rescueERC721( + function rescueERC1155( address token, address to, - uint256[] calldata ids + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data ) external onlyOwner { - for (uint256 i = 0; i < ids.length; i++) { - IERC721(token).safeTransferFrom(address(this), to, ids[i]); - } - emit RescueERC721(token, to, ids); + IERC1155(token).safeBatchTransferFrom( + address(this), + to, + ids, + amounts, + data + ); + emit RescueERC1155(token, to, ids, amounts, data); } } diff --git a/contracts/mocks/MockedETHNFTOracle.sol b/contracts/mocks/MockedETHNFTOracle.sol index c7bbc5752..d6d64a0ad 100644 --- a/contracts/mocks/MockedETHNFTOracle.sol +++ b/contracts/mocks/MockedETHNFTOracle.sol @@ -11,7 +11,7 @@ contract MockedETHNFTOracle is IInstantNFTOracle { endTime = block.timestamp + 86400; } - function getPresentValueAndDiscountRate(uint256, uint256) + function getPresentValueAndDiscountRate(uint256, uint256, uint256) external view returns (uint256, uint256) { @@ -19,6 +19,7 @@ contract MockedETHNFTOracle is IInstantNFTOracle { } function getPresentValueByDiscountRate( + uint256, uint256, uint256 ) external view returns (uint256) { diff --git a/contracts/mocks/tokens/MockInstantWithdrawNFT.sol b/contracts/mocks/tokens/MockInstantWithdrawNFT.sol deleted file mode 100644 index b4dc2793f..000000000 --- a/contracts/mocks/tokens/MockInstantWithdrawNFT.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.10; - -import "../../interfaces/IInstantWithdrawNFT.sol"; -import "./MintableERC721.sol"; - -contract MockedInstantWithdrawNFT is MintableERC721, IInstantWithdrawNFT { - constructor( - string memory name, - string memory symbol, - string memory baseTokenURI - ) MintableERC721(name, symbol, baseTokenURI) { - } - - function burn(uint256 tokenId) external { - _burn(tokenId); - } - - receive() external payable {} -} diff --git a/contracts/mocks/tokens/MockedInstantWithdrawNFT.sol b/contracts/mocks/tokens/MockedInstantWithdrawNFT.sol new file mode 100644 index 000000000..9b5b02fa2 --- /dev/null +++ b/contracts/mocks/tokens/MockedInstantWithdrawNFT.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.10; + +import "../../interfaces/IInstantWithdrawNFT.sol"; +import "./MintableERC1155.sol"; + +contract MockedInstantWithdrawNFT is MintableERC1155, IInstantWithdrawNFT { + function burn(uint256 tokenId, uint256 amount) external { + _burn(msg.sender, tokenId, amount); + } + + receive() external payable {} +} diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index ccf709f0a..9ad7ccaf9 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -416,14 +416,14 @@ library DataTypes { LoanState state; //loan start timestamp uint40 startTime; - //loan end timestamp - uint40 endTime; //address of borrower address borrower; //address of collateral asset token address collateralAsset; //the token id or collateral amount of collateral token uint64 collateralTokenId; + //the amount of collateral token + uint64 collateralAmount; //address of borrow asset address borrowAsset; //amount of borrow asset diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index 572b9369d..224313988 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -7,7 +7,7 @@ import {DataTypes} from "../libraries/types/DataTypes.sol"; import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; import {IERC20WithPermit} from "../../interfaces/IERC20WithPermit.sol"; import {IERC20Detailed} from "../../dependencies/openzeppelin/contracts/IERC20Detailed.sol"; -import {IERC721} from "../../dependencies/openzeppelin/contracts/IERC721.sol"; +import {IERC1155} from "../../dependencies/openzeppelin/contracts/IERC1155.sol"; import {IPoolAddressesProvider} from "../../interfaces/IPoolAddressesProvider.sol"; import {IPriceOracleGetter} from "../../interfaces/IPriceOracleGetter.sol"; import {IPoolInstantWithdraw} from "../../interfaces/IPoolInstantWithdraw.sol"; @@ -217,6 +217,7 @@ contract PoolInstantWithdraw is uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) .getPresentValueByDiscountRate( loan.collateralTokenId, + loan.collateralAmount, loan.discountRate ); @@ -245,6 +246,7 @@ contract PoolInstantWithdraw is function createLoan( address collateralAsset, uint256 collateralTokenId, + uint256 collateralAmount, address borrowAsset, uint16 referralCode ) external nonReentrant returns (uint256) { @@ -265,6 +267,7 @@ contract PoolInstantWithdraw is (presentValue, discountRate) = IInstantNFTOracle(WITHDRAW_ORACLE) .getPresentValueAndDiscountRate( collateralTokenId, + collateralAmount, reserve.currentStableBorrowRate ); @@ -286,10 +289,12 @@ contract PoolInstantWithdraw is // handle asset { // transfer collateralAsset to reserveAddress - IERC721(collateralAsset).safeTransferFrom( + IERC1155(collateralAsset).safeTransferFrom( msg.sender, VAULT_CONTRACT, - collateralTokenId + collateralTokenId, + collateralAmount, + "" ); // mint debt token for reserveAddress and transfer borrow asset to borrower @@ -319,12 +324,10 @@ contract PoolInstantWithdraw is loanId: loanId, state: DataTypes.LoanState.Active, startTime: uint40(block.timestamp), - endTime: uint40( - IInstantNFTOracle(WITHDRAW_ORACLE).getEndTime(collateralTokenId) - ), borrower: msg.sender, collateralAsset: collateralAsset, collateralTokenId: collateralTokenId.toUint64(), + collateralAmount: collateralAmount.toUint64(), borrowAsset: borrowAsset, borrowAmount: borrowAmount, discountRate: discountRate @@ -345,6 +348,7 @@ contract PoolInstantWithdraw is loanId, collateralAsset, collateralTokenId, + collateralAmount, borrowAsset, borrowAmount, discountRate @@ -369,18 +373,24 @@ contract PoolInstantWithdraw is address collateralAsset = loan.collateralAsset; uint256 collateralTokenId = uint256(loan.collateralTokenId); + uint256 collateralAmount = uint256(loan.collateralAmount); address borrowAsset = loan.borrowAsset; - //here oracle need to guarantee presentValue > debtValue. - uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) - .getPresentValueByDiscountRate( - collateralTokenId, - loan.discountRate + uint256 presentValueInBorrowAsset; + //calculate borrow asset amount needed + { + //here oracle need to guarantee presentValue > debtValue. + uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) + .getPresentValueByDiscountRate( + collateralTokenId, + collateralAmount, + loan.discountRate + ); + //repayableBorrowAssetAmount + presentValueInBorrowAsset = _calculatePresentValueInBorrowAsset( + borrowAsset, + presentValue ); - //repayableBorrowAssetAmount - uint256 presentValueInBorrowAsset = _calculatePresentValueInBorrowAsset( - borrowAsset, - presentValue - ); + } // update borrow asset state DataTypes.ReserveCache memory reserveCache; @@ -414,6 +424,7 @@ contract PoolInstantWithdraw is ILoanVault(VAULT_CONTRACT).transferCollateral( collateralAsset, collateralTokenId, + collateralAmount, receiver ); @@ -447,6 +458,7 @@ contract PoolInstantWithdraw is address collateralAsset = loan.collateralAsset; uint256 collateralTokenId = uint256(loan.collateralTokenId); + uint256 collateralAmount = uint256(loan.collateralAmount); address borrowAsset = loan.borrowAsset; // update borrow asset state @@ -456,7 +468,11 @@ contract PoolInstantWithdraw is reserve.updateState(reserveCache); ILoanVault LoanVault = ILoanVault(VAULT_CONTRACT); - LoanVault.settleCollateral(collateralAsset, collateralTokenId); + LoanVault.settleCollateral( + collateralAsset, + collateralTokenId, + collateralAmount + ); // repay borrow asset debt and update interest rate // rename to loanTotalDebt. diff --git a/contracts/ui/WETHGateway.sol b/contracts/ui/WETHGateway.sol index f6b609a1d..4d85a28b8 100644 --- a/contracts/ui/WETHGateway.sol +++ b/contracts/ui/WETHGateway.sol @@ -180,10 +180,12 @@ contract WETHGateway is ReentrancyGuard, IWETHGateway, OwnableUpgradeable { * @notice create a ETH term loan with the specified collateral asset * @param collateralAsset The address of the collateral asset * @param collateralTokenId The token id of the collateral asset + * @param collateralAmount The collateral token amount of the loan **/ function createLoan( address collateralAsset, uint256 collateralTokenId, + uint256 collateralAmount, uint16 referralCode ) external override nonReentrant { IERC721(collateralAsset).safeTransferFrom( @@ -195,6 +197,7 @@ contract WETHGateway is ReentrancyGuard, IWETHGateway, OwnableUpgradeable { uint256 ethAmount = IPool(pool).createLoan( collateralAsset, collateralTokenId, + collateralAmount, weth, referralCode ); diff --git a/contracts/ui/interfaces/IWETHGateway.sol b/contracts/ui/interfaces/IWETHGateway.sol index cbb4b3283..da6f5874d 100644 --- a/contracts/ui/interfaces/IWETHGateway.sol +++ b/contracts/ui/interfaces/IWETHGateway.sol @@ -28,6 +28,7 @@ interface IWETHGateway { function createLoan( address collateralAsset, uint256 collateralTokenId, + uint256 collateralAmount, uint16 referralCode ) external; diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 3b5ab20fc..0b59c0d89 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -2840,6 +2840,6 @@ export const deployMockedInstantWithdrawNFT = async (verify?: boolean) => withSaveAndVerify( new MockedInstantWithdrawNFT__factory(await getFirstSigner()), eContractid.MockedInstantWithdrawNFT, - ["MockETHNFT", "MockETHNFT", ""], + [], verify ) as Promise; diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts index 59d316470..70e6bb2fa 100644 --- a/test/pool_instant_withdraw.spec.ts +++ b/test/pool_instant_withdraw.spec.ts @@ -14,6 +14,8 @@ describe("Pool Instant Withdraw Test", () => { let testEnv: TestEnv; let instantWithdrawNFT: MockedInstantWithdrawNFT; let loanVault: LoanVault; + const tokenID = 1; + const tokenAmount = 10000; const fixture = async () => { testEnv = await loadFixture(testEnvFixture); @@ -36,9 +38,7 @@ describe("Pool Instant Withdraw Test", () => { await supplyAndValidate(weth, "100", user1, true); await waitForTx( - await instantWithdrawNFT - .connect(user2.signer) - ["mint(address)"](user2.address) + await instantWithdrawNFT.connect(user2.signer).mint(tokenID, tokenAmount) ); await waitForTx( await instantWithdrawNFT @@ -71,20 +71,25 @@ describe("Pool Instant Withdraw Test", () => { .connect(user2.signer) .createLoan( instantWithdrawNFT.address, - 0, + tokenID, + tokenAmount, weth.address, 0 ) ); expect(await weth.balanceOf(user2.address)).to.be.gte(parseEther("1")); - expect(await instantWithdrawNFT.ownerOf("0")).to.be.eq(loanVault.address); + expect( + await instantWithdrawNFT.balanceOf(loanVault.address, tokenID) + ).to.be.eq(tokenAmount); await waitForTx( await pool.connect(user3.signer).swapLoanCollateral(0, user3.address) ); - expect(await instantWithdrawNFT.ownerOf("0")).to.be.eq(user3.address); + expect(await instantWithdrawNFT.balanceOf(user3.address, tokenID)).to.be.eq( + tokenAmount + ); }); it("term loan can be settled", async () => { @@ -99,19 +104,24 @@ describe("Pool Instant Withdraw Test", () => { .connect(user2.signer) .createLoan( instantWithdrawNFT.address, - 0, + tokenID, + tokenAmount, weth.address, 0 ) ); expect(await weth.balanceOf(user2.address)).to.be.gte(parseEther("1")); - expect(await instantWithdrawNFT.ownerOf("0")).to.be.eq(loanVault.address); + expect( + await instantWithdrawNFT.balanceOf(loanVault.address, tokenID) + ).to.be.eq(tokenAmount); await advanceTimeAndBlock(parseInt("86400")); await waitForTx(await pool.connect(user3.signer).settleTermLoan(0)); - expect(await instantWithdrawNFT.balanceOf(loanVault.address)).to.be.eq(0); + expect( + await instantWithdrawNFT.balanceOf(loanVault.address, tokenID) + ).to.be.eq(0); }); }); From da4867dba796b8476204b6702a17039e03cea402 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Sat, 11 Mar 2023 11:56:16 +0800 Subject: [PATCH 03/25] chore: add utilization check --- .../libraries/logic/ValidationLogic.sol | 15 ++++++++++++- .../DefaultReserveInterestRateStrategy.sol | 4 +--- .../protocol/pool/PoolInstantWithdraw.sol | 1 + test/pool_instant_withdraw.spec.ts | 22 +++++++++---------- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index ae4cfd08b..1ff282282 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -297,14 +297,27 @@ library ValidationLogic { function validateInstantWithdrawBorrow( DataTypes.ReserveCache memory reserveCache, + address reverve, uint256 amount - ) internal pure { + ) internal view { ValidateBorrowLocalVars memory vars; validateBorrowAsset(reserveCache, amount, vars); require( vars.stableRateBorrowingEnabled, Errors.STABLE_BORROWING_NOT_ENABLED ); + + uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + uint256 totalDebt = totalVariableDebt + + reserveCache.nextTotalStableDebt; + uint256 availableLiquidity = IToken(reverve).balanceOf( + reserveCache.xTokenAddress + ); + uint256 availableLiquidityPlusDebt = availableLiquidity + totalDebt; + uint256 usageRatio = totalDebt.rayDiv(availableLiquidityPlusDebt); + require(usageRatio <= 0.8e27, Errors.USAGE_RATIO_TOO_HIGH); } /** diff --git a/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol b/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol index 969c8782e..e5c12c187 100644 --- a/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol +++ b/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol @@ -243,9 +243,7 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { vars.borrowUsageRatio = vars.totalDebt.rayDiv( vars.availableLiquidityPlusDebt ); - vars.supplyUsageRatio = vars.totalDebt.rayDiv( - vars.availableLiquidityPlusDebt - ); + vars.supplyUsageRatio = vars.borrowUsageRatio; } if (vars.borrowUsageRatio > OPTIMAL_USAGE_RATIO) { diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index 224313988..a0cd4d208 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -283,6 +283,7 @@ contract PoolInstantWithdraw is // validate borrow asset can be borrowed from lending pool ValidationLogic.validateInstantWithdrawBorrow( reserveCache, + borrowAsset, borrowAmount ); diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts index 70e6bb2fa..59767a383 100644 --- a/test/pool_instant_withdraw.spec.ts +++ b/test/pool_instant_withdraw.spec.ts @@ -1,14 +1,14 @@ -import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { expect } from "chai"; -import { TestEnv } from "./helpers/make-suite"; -import { testEnvFixture } from "./helpers/setup-env"; -import { mintAndValidate, supplyAndValidate } from "./helpers/validated-steps"; -import { advanceTimeAndBlock, waitForTx } from "../helpers/misc-utils"; -import { parseEther } from "ethers/lib/utils"; -import { LoanVault, MockedInstantWithdrawNFT } from "../types"; -import { deployMockedInstantWithdrawNFT } from "../helpers/contracts-deployments"; -import { getLoanVault } from "../helpers/contracts-getters"; -import { MAX_UINT_AMOUNT } from "../helpers/constants"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {TestEnv} from "./helpers/make-suite"; +import {testEnvFixture} from "./helpers/setup-env"; +import {mintAndValidate, supplyAndValidate} from "./helpers/validated-steps"; +import {advanceTimeAndBlock, waitForTx} from "../helpers/misc-utils"; +import {parseEther} from "ethers/lib/utils"; +import {LoanVault, MockedInstantWithdrawNFT} from "../types"; +import {deployMockedInstantWithdrawNFT} from "../helpers/contracts-deployments"; +import {getLoanVault} from "../helpers/contracts-getters"; +import {MAX_UINT_AMOUNT} from "../helpers/constants"; describe("Pool Instant Withdraw Test", () => { let testEnv: TestEnv; From 0ca4da702a72491fcf164ba049a1e158575604bf Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Mon, 13 Mar 2023 13:37:13 +0800 Subject: [PATCH 04/25] chore: fix some issue and add more test case --- contracts/interfaces/IPoolInstantWithdraw.sol | 6 + .../libraries/logic/ValidationLogic.sol | 9 +- .../protocol/libraries/types/DataTypes.sol | 8 +- .../protocol/pool/PoolInstantWithdraw.sol | 22 +- helpers/types.ts | 4 + test/pool_instant_withdraw.spec.ts | 195 +++++++++++++++++- 6 files changed, 227 insertions(+), 17 deletions(-) diff --git a/contracts/interfaces/IPoolInstantWithdraw.sol b/contracts/interfaces/IPoolInstantWithdraw.sol index db0409d0d..93d758a7d 100644 --- a/contracts/interfaces/IPoolInstantWithdraw.sol +++ b/contracts/interfaces/IPoolInstantWithdraw.sol @@ -94,6 +94,12 @@ interface IPoolInstantWithdraw { **/ function setLoanCreationFeeRate(uint256 feeRate) external; + /** + * @notice get fee rate for creating loan + * @return fee rate for creating loan + **/ + function getLoanCreationFeeRate() external view returns (uint256); + /** * @notice get borrowable asset list for the specified collateral asset * @param collateralAsset The address of the collateral asset diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 1ff282282..c4e87a882 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -297,7 +297,7 @@ library ValidationLogic { function validateInstantWithdrawBorrow( DataTypes.ReserveCache memory reserveCache, - address reverve, + address reserve, uint256 amount ) internal view { ValidateBorrowLocalVars memory vars; @@ -311,10 +311,11 @@ library ValidationLogic { reserveCache.nextVariableBorrowIndex ); uint256 totalDebt = totalVariableDebt + - reserveCache.nextTotalStableDebt; - uint256 availableLiquidity = IToken(reverve).balanceOf( + reserveCache.nextTotalStableDebt + + amount; + uint256 availableLiquidity = IToken(reserve).balanceOf( reserveCache.xTokenAddress - ); + ) - amount; uint256 availableLiquidityPlusDebt = availableLiquidity + totalDebt; uint256 usageRatio = totalDebt.rayDiv(availableLiquidityPlusDebt); require(usageRatio <= 0.8e27, Errors.USAGE_RATIO_TOO_HIGH); diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index 9ad7ccaf9..4fdcec2a7 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -25,16 +25,12 @@ library DataTypes { uint128 variableBorrowIndex; //the current variable borrow rate. Expressed in ray uint128 currentVariableBorrowRate; - //the current stable borrow rate. Expressed in ray - uint128 currentStableBorrowRate; //timestamp of last update uint40 lastUpdateTimestamp; //the id of the reserve. Represents the position in the list of the active reserves uint16 id; //xToken address address xTokenAddress; - //stableDebtToken address - address stableDebtTokenAddress; //variableDebtToken address address variableDebtTokenAddress; //address of the interest rate strategy @@ -43,6 +39,10 @@ library DataTypes { address auctionStrategyAddress; //the current treasury balance, scaled uint128 accruedToTreasury; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; + //stableDebtToken address + address stableDebtTokenAddress; } struct ReserveConfigurationMap { diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index a0cd4d208..a0e42621b 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -167,6 +167,17 @@ contract PoolInstantWithdraw is } } + function getLoanCreationFeeRate() + external + view + virtual + override + returns (uint256) + { + DataTypes.PoolStorage storage ps = poolStorage(); + return ps._loanCreationFeeRate; + } + /// @inheritdoc IPoolInstantWithdraw function getBorrowableAssets(address collateralAsset) external @@ -261,7 +272,7 @@ contract PoolInstantWithdraw is uint256 presentValue; uint256 discountRate; uint256 borrowAmount; - // calculate borrow amount + // calculate amount can be borrowed { // fetch present value and discount rate from Oracle (presentValue, discountRate) = IInstantNFTOracle(WITHDRAW_ORACLE) @@ -377,16 +388,14 @@ contract PoolInstantWithdraw is uint256 collateralAmount = uint256(loan.collateralAmount); address borrowAsset = loan.borrowAsset; uint256 presentValueInBorrowAsset; - //calculate borrow asset amount needed + // calculate amount for borrow asset with current present value { - //here oracle need to guarantee presentValue > debtValue. uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) .getPresentValueByDiscountRate( collateralTokenId, collateralAmount, loan.discountRate ); - //repayableBorrowAssetAmount presentValueInBorrowAsset = _calculatePresentValueInBorrowAsset( borrowAsset, presentValue @@ -429,7 +438,7 @@ contract PoolInstantWithdraw is receiver ); - // update loan + // update loan state loan.state = DataTypes.LoanState.Repaid; emit Repay( @@ -476,7 +485,6 @@ contract PoolInstantWithdraw is ); // repay borrow asset debt and update interest rate - // rename to loanTotalDebt. uint256 loanDebt = _calculateLoanStableDebt( loan.borrowAmount, loan.discountRate, @@ -499,7 +507,7 @@ contract PoolInstantWithdraw is ); reserve.updateInterestRates(reserveCache, borrowAsset, 0, 0); - // update loan + // update loan state loan.state = DataTypes.LoanState.Settled; emit Repay( diff --git a/helpers/types.ts b/helpers/types.ts index b84de5b82..45bc6b078 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -398,6 +398,10 @@ export enum ProtocolErrors { EMEGENCY_DISABLE_CALL = "emergency disable call", MAKER_SAME_AS_TAKER = "132", + INVALID_LOAN_STATE = "133", // invalid term loan status + INVALID_BORROW_ASSET = "134", // invalid borrow asset for collateral + INVALID_PRESENT_VALUE = "135", // invalid present value + USAGE_RATIO_TOO_HIGH = "136", // usage ratio too high after borrow } export type tEthereumAddress = string; diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts index 59767a383..195b772ec 100644 --- a/test/pool_instant_withdraw.spec.ts +++ b/test/pool_instant_withdraw.spec.ts @@ -9,6 +9,7 @@ import {LoanVault, MockedInstantWithdrawNFT} from "../types"; import {deployMockedInstantWithdrawNFT} from "../helpers/contracts-deployments"; import {getLoanVault} from "../helpers/contracts-getters"; import {MAX_UINT_AMOUNT} from "../helpers/constants"; +import {ProtocolErrors} from "../helpers/types"; describe("Pool Instant Withdraw Test", () => { let testEnv: TestEnv; @@ -59,7 +60,7 @@ describe("Pool Instant Withdraw Test", () => { return testEnv; }; - it("term loan can be bought by other user", async () => { + it("active term loan can be swaped by other user", async () => { const { users: [, user2, user3], weth, @@ -92,7 +93,7 @@ describe("Pool Instant Withdraw Test", () => { ); }); - it("term loan can be settled", async () => { + it("active term loan can be settled", async () => { const { users: [, user2, user3], weth, @@ -124,4 +125,194 @@ describe("Pool Instant Withdraw Test", () => { await instantWithdrawNFT.balanceOf(loanVault.address, tokenID) ).to.be.eq(0); }); + + it("cannot create term loan when borrow asset usage ratio too high", async () => { + const { + users: [user1, user2], + weth, + pool, + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .withdraw(weth.address, parseEther("98.8"), user1.address) + ); + + await expect( + pool + .connect(user2.signer) + .createLoan( + instantWithdrawNFT.address, + tokenID, + tokenAmount, + weth.address, + 0 + ) + ).to.be.revertedWith(ProtocolErrors.USAGE_RATIO_TOO_HIGH); + }); + + it("settled term loan can not be swaped", async () => { + const { + users: [, user2, user3], + weth, + pool, + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user2.signer) + .createLoan( + instantWithdrawNFT.address, + tokenID, + tokenAmount, + weth.address, + 0 + ) + ); + + await advanceTimeAndBlock(parseInt("86400")); + + await waitForTx(await pool.connect(user3.signer).settleTermLoan(0)); + + await expect( + pool.connect(user3.signer).swapLoanCollateral(0, user3.address) + ).to.be.revertedWith(ProtocolErrors.INVALID_LOAN_STATE); + }); + + it("swaped term loan can not be settled", async () => { + const { + users: [, user2, user3], + weth, + pool, + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user2.signer) + .createLoan( + instantWithdrawNFT.address, + tokenID, + tokenAmount, + weth.address, + 0 + ) + ); + + await waitForTx( + await pool.connect(user3.signer).swapLoanCollateral(0, user3.address) + ); + + await expect( + pool.connect(user3.signer).settleTermLoan(0) + ).to.be.revertedWith(ProtocolErrors.INVALID_LOAN_STATE); + }); + + it("can not create loan with unsupported borrow asset", async () => { + const { + users: [, user2], + usdt, + pool, + } = await loadFixture(fixture); + + await expect( + pool + .connect(user2.signer) + .createLoan( + instantWithdrawNFT.address, + tokenID, + tokenAmount, + usdt.address, + 0 + ) + ).to.be.revertedWith(ProtocolErrors.INVALID_BORROW_ASSET); + }); + + it("only admin or asset listing can add or remove support borrow asset", async () => { + const { + users: [, user2], + usdt, + pool, + poolAdmin, + } = await loadFixture(fixture); + + await expect( + pool + .connect(user2.signer) + .addBorrowableAssets(instantWithdrawNFT.address, [usdt.address]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_ASSET_LISTING_OR_POOL_ADMIN); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .addBorrowableAssets(instantWithdrawNFT.address, [usdt.address]) + ); + + let supportedBorrowAsset = await pool.getBorrowableAssets( + instantWithdrawNFT.address + ); + expect(supportedBorrowAsset.length).to.be.eq(2); + expect(supportedBorrowAsset[1]).to.be.eq(usdt.address); + + await expect( + pool + .connect(user2.signer) + .removeBorrowableAssets(instantWithdrawNFT.address, [usdt.address]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_ASSET_LISTING_OR_POOL_ADMIN); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .removeBorrowableAssets(instantWithdrawNFT.address, [usdt.address]) + ); + supportedBorrowAsset = await pool.getBorrowableAssets( + instantWithdrawNFT.address + ); + expect(supportedBorrowAsset.length).to.be.eq(1); + }); + + it("only admin or asset listing can set loan creation fee rate", async () => { + const { + users: [, user2], + pool, + poolAdmin, + } = await loadFixture(fixture); + + await expect( + pool.connect(user2.signer).setLoanCreationFeeRate(300) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_ASSET_LISTING_OR_POOL_ADMIN); + + await waitForTx( + await pool.connect(poolAdmin.signer).setLoanCreationFeeRate(300) + ); + + expect(await pool.getLoanCreationFeeRate()).to.be.eq(300); + }); + + it("user will borrow less if set loan creation fee rate", async () => { + const { + users: [, user2], + weth, + pool, + poolAdmin, + } = await loadFixture(fixture); + + await waitForTx( + await pool.connect(poolAdmin.signer).setLoanCreationFeeRate(1000) + ); + + await waitForTx( + await pool + .connect(user2.signer) + .createLoan( + instantWithdrawNFT.address, + tokenID, + tokenAmount, + weth.address, + 0 + ) + ); + + expect(await weth.balanceOf(user2.address)).to.be.lt(parseEther("1")); + }); }); From c251722ab5d3aaca576da574f945bd60ed19e1a2 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Mon, 13 Mar 2023 14:30:44 +0800 Subject: [PATCH 05/25] chore: fix typo --- contracts/protocol/libraries/logic/ValidationLogic.sol | 9 ++++++++- test/pool_instant_withdraw.spec.ts | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index c4e87a882..9c58dee36 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -55,6 +55,10 @@ library ValidationLogic { */ uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; + // Max usage ratio for instant withdraw borrowing + // A value of 0.8e27 results in 80% + uint256 public constant INSTANT_WITHDRAW_USAGE_RATIO_THRESHOLD = 0.8e27; + /** * @notice Validates a supply action. * @param reserveCache The cached data of the reserve @@ -318,7 +322,10 @@ library ValidationLogic { ) - amount; uint256 availableLiquidityPlusDebt = availableLiquidity + totalDebt; uint256 usageRatio = totalDebt.rayDiv(availableLiquidityPlusDebt); - require(usageRatio <= 0.8e27, Errors.USAGE_RATIO_TOO_HIGH); + require( + usageRatio <= INSTANT_WITHDRAW_USAGE_RATIO_THRESHOLD, + Errors.USAGE_RATIO_TOO_HIGH + ); } /** diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts index 195b772ec..70c38faaa 100644 --- a/test/pool_instant_withdraw.spec.ts +++ b/test/pool_instant_withdraw.spec.ts @@ -60,7 +60,7 @@ describe("Pool Instant Withdraw Test", () => { return testEnv; }; - it("active term loan can be swaped by other user", async () => { + it("active term loan can be swapped by other user", async () => { const { users: [, user2, user3], weth, @@ -152,7 +152,7 @@ describe("Pool Instant Withdraw Test", () => { ).to.be.revertedWith(ProtocolErrors.USAGE_RATIO_TOO_HIGH); }); - it("settled term loan can not be swaped", async () => { + it("settled term loan can not be swapped", async () => { const { users: [, user2, user3], weth, @@ -180,7 +180,7 @@ describe("Pool Instant Withdraw Test", () => { ).to.be.revertedWith(ProtocolErrors.INVALID_LOAN_STATE); }); - it("swaped term loan can not be settled", async () => { + it("swapped term loan can not be settled", async () => { const { users: [, user2, user3], weth, From 122bbba8b831fa2c4a32d7951531def1e15b18af Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Mon, 13 Mar 2023 16:08:02 +0800 Subject: [PATCH 06/25] chore: merge Oracle interface and withdraw NFT interface --- contracts/interfaces/IInstantNFTOracle.sol | 18 --------------- contracts/interfaces/IInstantWithdrawNFT.sol | 23 ++++++++++++++----- contracts/misc/LoanVault.sol | 1 + ...NFTOracle.sol => MockedETHWithdrawNFT.sol} | 9 ++++---- .../mocks/tokens/MockedInstantWithdrawNFT.sol | 13 ----------- .../protocol/pool/PoolInstantWithdraw.sol | 16 ++++--------- helpers/contracts-deployments.ts | 22 +++++------------- helpers/types.ts | 8 ++----- scripts/deployments/steps/06_pool.ts | 6 +---- test/pool_instant_withdraw.spec.ts | 8 +++---- 10 files changed, 41 insertions(+), 83 deletions(-) delete mode 100644 contracts/interfaces/IInstantNFTOracle.sol rename contracts/mocks/{MockedETHNFTOracle.sol => MockedETHWithdrawNFT.sol} (72%) delete mode 100644 contracts/mocks/tokens/MockedInstantWithdrawNFT.sol diff --git a/contracts/interfaces/IInstantNFTOracle.sol b/contracts/interfaces/IInstantNFTOracle.sol deleted file mode 100644 index ea07e4fa5..000000000 --- a/contracts/interfaces/IInstantNFTOracle.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.10; - -interface IInstantNFTOracle { - function getPresentValueAndDiscountRate( - uint256 tokenId, - uint256 amount, - uint256 borrowRate - ) external view returns (uint256, uint256); - - function getPresentValueByDiscountRate( - uint256 tokenId, - uint256 amount, - uint256 discountRate - ) external view returns (uint256); - - function getEndTime(uint256 tokenId) external view returns (uint256); -} diff --git a/contracts/interfaces/IInstantWithdrawNFT.sol b/contracts/interfaces/IInstantWithdrawNFT.sol index 5ae563a77..a72b1e026 100644 --- a/contracts/interfaces/IInstantWithdrawNFT.sol +++ b/contracts/interfaces/IInstantWithdrawNFT.sol @@ -1,11 +1,22 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.10; -/** - * @title IInstantWithdrawNFT - * - * @notice Defines the basic interface for an InstantWithdrawNFT contract. - **/ interface IInstantWithdrawNFT { - function burn(uint256 tokenId, uint256 amount) external; + function getPresentValueAndDiscountRate( + uint256 tokenId, + uint256 amount, + uint256 borrowRate + ) external view returns (uint256, uint256); + + function getPresentValueByDiscountRate( + uint256 tokenId, + uint256 amount, + uint256 discountRate + ) external view returns (uint256); + + function burn( + uint256 tokenId, + address recipient, + uint256 amount + ) external; } diff --git a/contracts/misc/LoanVault.sol b/contracts/misc/LoanVault.sol index b388152e8..223ac1f39 100644 --- a/contracts/misc/LoanVault.sol +++ b/contracts/misc/LoanVault.sol @@ -142,6 +142,7 @@ contract LoanVault is Initializable, OwnableUpgradeable { ) external onlyPool { IInstantWithdrawNFT(collateralAsset).burn( collateralTokenId, + address(this), collateralAmount ); } diff --git a/contracts/mocks/MockedETHNFTOracle.sol b/contracts/mocks/MockedETHWithdrawNFT.sol similarity index 72% rename from contracts/mocks/MockedETHNFTOracle.sol rename to contracts/mocks/MockedETHWithdrawNFT.sol index d6d64a0ad..f7bd0a4a1 100644 --- a/contracts/mocks/MockedETHNFTOracle.sol +++ b/contracts/mocks/MockedETHWithdrawNFT.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; -import "../interfaces/IInstantNFTOracle.sol"; +import "./tokens/MintableERC1155.sol"; +import "../interfaces/IInstantWithdrawNFT.sol"; -contract MockedETHNFTOracle is IInstantNFTOracle { +contract MockedETHWithdrawNFT is MintableERC1155, IInstantWithdrawNFT { uint256 internal startTime; uint256 internal endTime; constructor() { @@ -26,8 +27,8 @@ contract MockedETHNFTOracle is IInstantNFTOracle { return _getPresentValue(); } - function getEndTime(uint256) external view returns (uint256) { - return endTime; + function burn(uint256 tokenId, address, uint256 amount) external { + _burn(msg.sender, tokenId, amount); } function _getPresentValue() internal view returns(uint256) { diff --git a/contracts/mocks/tokens/MockedInstantWithdrawNFT.sol b/contracts/mocks/tokens/MockedInstantWithdrawNFT.sol deleted file mode 100644 index 9b5b02fa2..000000000 --- a/contracts/mocks/tokens/MockedInstantWithdrawNFT.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.10; - -import "../../interfaces/IInstantWithdrawNFT.sol"; -import "./MintableERC1155.sol"; - -contract MockedInstantWithdrawNFT is MintableERC1155, IInstantWithdrawNFT { - function burn(uint256 tokenId, uint256 amount) external { - _burn(msg.sender, tokenId, amount); - } - - receive() external payable {} -} diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index a0e42621b..0a34cc9c9 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -11,7 +11,7 @@ import {IERC1155} from "../../dependencies/openzeppelin/contracts/IERC1155.sol"; import {IPoolAddressesProvider} from "../../interfaces/IPoolAddressesProvider.sol"; import {IPriceOracleGetter} from "../../interfaces/IPriceOracleGetter.sol"; import {IPoolInstantWithdraw} from "../../interfaces/IPoolInstantWithdraw.sol"; -import {IInstantNFTOracle} from "../../interfaces/IInstantNFTOracle.sol"; +import {IInstantWithdrawNFT} from "../../interfaces/IInstantWithdrawNFT.sol"; import {IACLManager} from "../../interfaces/IACLManager.sol"; import {IReserveInterestRateStrategy} from "../../interfaces/IReserveInterestRateStrategy.sol"; import {IStableDebtToken} from "../../interfaces/IStableDebtToken.sol"; @@ -56,7 +56,6 @@ contract PoolInstantWithdraw is IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; address internal immutable VAULT_CONTRACT; - address internal immutable WITHDRAW_ORACLE; uint256 internal constant POOL_REVISION = 145; // See `IPoolCore` for descriptions @@ -100,14 +99,9 @@ contract PoolInstantWithdraw is * @dev Constructor. * @param provider The address of the PoolAddressesProvider contract */ - constructor( - IPoolAddressesProvider provider, - address vault, - address withdrawOracle - ) { + constructor(IPoolAddressesProvider provider, address vault) { ADDRESSES_PROVIDER = provider; VAULT_CONTRACT = vault; - WITHDRAW_ORACLE = withdrawOracle; } function getRevision() internal pure virtual override returns (uint256) { @@ -225,7 +219,7 @@ contract PoolInstantWithdraw is Errors.INVALID_LOAN_STATE ); - uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) + uint256 presentValue = IInstantWithdrawNFT(loan.collateralAsset) .getPresentValueByDiscountRate( loan.collateralTokenId, loan.collateralAmount, @@ -275,7 +269,7 @@ contract PoolInstantWithdraw is // calculate amount can be borrowed { // fetch present value and discount rate from Oracle - (presentValue, discountRate) = IInstantNFTOracle(WITHDRAW_ORACLE) + (presentValue, discountRate) = IInstantWithdrawNFT(collateralAsset) .getPresentValueAndDiscountRate( collateralTokenId, collateralAmount, @@ -390,7 +384,7 @@ contract PoolInstantWithdraw is uint256 presentValueInBorrowAsset; // calculate amount for borrow asset with current present value { - uint256 presentValue = IInstantNFTOracle(WITHDRAW_ORACLE) + uint256 presentValue = IInstantWithdrawNFT(loan.collateralAsset) .getPresentValueByDiscountRate( collateralTokenId, collateralAmount, diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 3de03a6da..06d159327 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -253,10 +253,8 @@ import { PoolInstantWithdraw__factory, LoanVault__factory, LoanVault, - MockedETHNFTOracle, - MockedETHNFTOracle__factory, - MockedInstantWithdrawNFT__factory, - MockedInstantWithdrawNFT, + MockedETHWithdrawNFT__factory, + MockedETHWithdrawNFT, ATokenStableDebtToken, ATokenStableDebtToken__factory, } from "../types"; @@ -2836,18 +2834,10 @@ export const deployLoanVault = async ( return proxyInstance as LoanVault; }; -export const deployMockETHNFTOracle = async (verify?: boolean) => +export const deployMockedETHWithdrawNFT = async (verify?: boolean) => withSaveAndVerify( - new MockedETHNFTOracle__factory(await getFirstSigner()), - eContractid.MockETHNFTOracle, + new MockedETHWithdrawNFT__factory(await getFirstSigner()), + eContractid.MockedETHWithdrawNFT, [], verify - ) as Promise; - -export const deployMockedInstantWithdrawNFT = async (verify?: boolean) => - withSaveAndVerify( - new MockedInstantWithdrawNFT__factory(await getFirstSigner()), - eContractid.MockedInstantWithdrawNFT, - [], - verify - ) as Promise; + ) as Promise; diff --git a/helpers/types.ts b/helpers/types.ts index 45bc6b078..6949ff899 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -11,10 +11,7 @@ import {NTokenMAYCLibraryAddresses} from "../types/factories/protocol/tokenizati import {NTokenMoonBirdsLibraryAddresses} from "../types/factories/protocol/tokenization/NTokenMoonBirds__factory"; import {NTokenUniswapV3LibraryAddresses} from "../types/factories/protocol/tokenization/NTokenUniswapV3__factory"; import {NTokenLibraryAddresses} from "../types/factories/protocol/tokenization/NToken__factory"; -import { - deployLoanVaultImpl, - deployMockETHNFTOracle, -} from "./contracts-deployments"; +import {deployLoanVaultImpl} from "./contracts-deployments"; export enum AssetType { ERC20 = 0, @@ -259,8 +256,7 @@ export enum eContractid { ParaProxyInterfacesImpl = "ParaProxyInterfacesImpl", MockedDelegateRegistry = "MockedDelegateRegistry", MockMultiAssetAirdropProject = "MockMultiAssetAirdropProject", - MockETHNFTOracle = "MockETHNFTOracle", - MockedInstantWithdrawNFT = "MockedInstantWithdrawNFT", + MockedETHWithdrawNFT = "MockedETHWithdrawNFT", LoanVault = "LoanVault", LoanVaultImpl = "LoanVaultImpl", ParaSpaceAirdrop = "ParaSpaceAirdrop", diff --git a/scripts/deployments/steps/06_pool.ts b/scripts/deployments/steps/06_pool.ts index 4b4e95c79..f67ed89d2 100644 --- a/scripts/deployments/steps/06_pool.ts +++ b/scripts/deployments/steps/06_pool.ts @@ -1,7 +1,6 @@ import {ZERO_ADDRESS} from "../../../helpers/constants"; import { deployLoanVault, - deployMockETHNFTOracle, deployPoolComponents, } from "../../../helpers/contracts-deployments"; import { @@ -128,14 +127,11 @@ export const step_06 = async (verify = false) => { const loanVaultAddress = (await getContractAddressInDb(eContractid.LoanVault)) || (await deployLoanVault(poolAddress, verify)).address; - const nFTOracleAddress = - (await getContractAddressInDb(eContractid.MockETHNFTOracle)) || - (await deployMockETHNFTOracle(verify)).address; // create PoolETHWithdraw here instead of in deployPoolComponents since LoanVault have a dependency for Pool address const poolInstantWithdraw = (await withSaveAndVerify( new PoolInstantWithdraw__factory(await getFirstSigner()), eContractid.PoolETHWithdrawImpl, - [addressesProvider.address, loanVaultAddress, nFTOracleAddress], + [addressesProvider.address, loanVaultAddress], verify, false, undefined, diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts index 70c38faaa..6df1acf98 100644 --- a/test/pool_instant_withdraw.spec.ts +++ b/test/pool_instant_withdraw.spec.ts @@ -5,15 +5,15 @@ import {testEnvFixture} from "./helpers/setup-env"; import {mintAndValidate, supplyAndValidate} from "./helpers/validated-steps"; import {advanceTimeAndBlock, waitForTx} from "../helpers/misc-utils"; import {parseEther} from "ethers/lib/utils"; -import {LoanVault, MockedInstantWithdrawNFT} from "../types"; -import {deployMockedInstantWithdrawNFT} from "../helpers/contracts-deployments"; +import {LoanVault, MockedETHWithdrawNFT} from "../types"; +import {deployMockedETHWithdrawNFT} from "../helpers/contracts-deployments"; import {getLoanVault} from "../helpers/contracts-getters"; import {MAX_UINT_AMOUNT} from "../helpers/constants"; import {ProtocolErrors} from "../helpers/types"; describe("Pool Instant Withdraw Test", () => { let testEnv: TestEnv; - let instantWithdrawNFT: MockedInstantWithdrawNFT; + let instantWithdrawNFT: MockedETHWithdrawNFT; let loanVault: LoanVault; const tokenID = 1; const tokenAmount = 10000; @@ -27,7 +27,7 @@ describe("Pool Instant Withdraw Test", () => { users: [user1, user2, user3], } = testEnv; - instantWithdrawNFT = await deployMockedInstantWithdrawNFT(); + instantWithdrawNFT = await deployMockedETHWithdrawNFT(); loanVault = await getLoanVault(); await waitForTx( From 3bb1016bea38b50a81d41935212ad88957ce079c Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Wed, 15 Mar 2023 09:48:46 +0800 Subject: [PATCH 07/25] chore: deployment script(wETH, awETH, stETH, wstETH) --- Makefile | 4 + contracts/interfaces/IPoolConfigurator.sol | 8 + contracts/interfaces/IPoolParameters.sol | 11 + contracts/misc/LoanVault.sol | 22 +- .../protocol/libraries/helpers/Errors.sol | 1 + .../libraries/logic/ConfiguratorLogic.sol | 24 ++ .../types/ConfiguratorInputTypes.sol | 10 + contracts/protocol/pool/PoolConfigurator.sol | 7 + contracts/protocol/pool/PoolParameters.sol | 23 ++ .../tokenization/StETHStableDebtToken.sol | 28 +++ helpers/contracts-deployments.ts | 25 +- helpers/init-helpers.ts | 8 + helpers/types.ts | 1 + market-config/reservesConfigs.ts | 4 +- scripts/dev/12-update-eth-instant-withdraw.ts | 221 ++++++++++++++++++ scripts/upgrade/index.ts | 107 ++++++++- 16 files changed, 490 insertions(+), 14 deletions(-) create mode 100644 contracts/protocol/tokenization/StETHStableDebtToken.sol create mode 100644 scripts/dev/12-update-eth-instant-withdraw.ts diff --git a/Makefile b/Makefile index 43298f9ab..e9b2150a1 100644 --- a/Makefile +++ b/Makefile @@ -420,6 +420,10 @@ send-eth: set-traits-multipliers: make SCRIPT_PATH=./scripts/dev/11.set-traits-multipliers.ts run +.PHONY: upgrade-eth-instant-withdraw +upgrade-eth-instant-withdraw: + make SCRIPT_PATH=./scripts/dev/12-update-eth-instant-withdraw.ts run + .PHONY: transfer-tokens transfer-tokens: make SCRIPT_PATH=./scripts/dev/2.transfer-tokens.ts run diff --git a/contracts/interfaces/IPoolConfigurator.sol b/contracts/interfaces/IPoolConfigurator.sol index 719a73ff8..eef67987d 100644 --- a/contracts/interfaces/IPoolConfigurator.sol +++ b/contracts/interfaces/IPoolConfigurator.sol @@ -368,4 +368,12 @@ interface IPoolConfigurator { * @param siloed The new siloed borrowing state */ function setSiloedBorrowing(address asset, bool siloed) external; + + /** + * @notice config stable debt token address after asset init + * @param input The stable debt token config parameters + */ + function configReserveStableDebtTokenAddress( + ConfiguratorInputTypes.ConfigStableDebtTokenInput calldata input + ) external; } diff --git a/contracts/interfaces/IPoolParameters.sol b/contracts/interfaces/IPoolParameters.sol index e0dea2d96..df4dd79b4 100644 --- a/contracts/interfaces/IPoolParameters.sol +++ b/contracts/interfaces/IPoolParameters.sol @@ -59,6 +59,17 @@ interface IPoolParameters { **/ function dropReserve(address asset) external; + /** + * @notice Updates the address of the stable debt token contract + * @dev Only callable by the PoolConfigurator contract + * @param asset The address of the underlying asset of the reserve + * @param stableDebtTokenAddress The address of the stable debt token contract + **/ + function setReserveStableDebtTokenAddress( + address asset, + address stableDebtTokenAddress + ) external; + /** * @notice Updates the address of the interest rate strategy contract * @dev Only callable by the PoolConfigurator contract diff --git a/contracts/misc/LoanVault.sol b/contracts/misc/LoanVault.sol index 223ac1f39..8d70ea137 100644 --- a/contracts/misc/LoanVault.sol +++ b/contracts/misc/LoanVault.sol @@ -53,14 +53,14 @@ contract LoanVault is Initializable, OwnableUpgradeable { address private immutable wETH; address private immutable aETH; IPool private immutable aavePool; + address private immutable stETH; + address private immutable wstETH; /* address private immutable astETH; address private immutable awstETH; address private immutable bendETH; address private immutable bendPool; address private immutable cETH; - address private immutable stETH; - address private immutable wstETH; */ /** @@ -74,12 +74,12 @@ contract LoanVault is Initializable, OwnableUpgradeable { constructor( address _lendingPool, address _wETH, - address _aETH + address _aETH, + address _wstETH ) /* address _bendETH, address _cETH, - address _wstETH, address _astETH, address _awstETH */ @@ -88,13 +88,13 @@ contract LoanVault is Initializable, OwnableUpgradeable { wETH = _wETH; aETH = _aETH; aavePool = IAToken(_aETH).POOL(); + wstETH = _wstETH; + stETH = IWstETH(_wstETH).stETH(); /* astETH = _astETH; awstETH = _awstETH; bendETH = _bendETH; cETH = _cETH; - wstETH = _wstETH; - stETH = IWstETH(_wstETH).stETH(); bendPool = address(IAToken(_bendETH).POOL()); */ } @@ -104,12 +104,13 @@ contract LoanVault is Initializable, OwnableUpgradeable { _unlimitedApproveToLendingPool(wETH); _unlimitedApproveToLendingPool(aETH); + _unlimitedApproveToLendingPool(stETH); + _unlimitedApproveToLendingPool(wstETH); /* _unlimitedApproveToLendingPool(astETH); _unlimitedApproveToLendingPool(awstETH); _unlimitedApproveToLendingPool(bendETH); _unlimitedApproveToLendingPool(cETH); - _unlimitedApproveToLendingPool(stETH); */ } @@ -156,6 +157,11 @@ contract LoanVault is Initializable, OwnableUpgradeable { } else if (asset == aETH) { IWETH(wETH).deposit{value: amount}(); aavePool.supply(wETH, amount, address(this), 0); + } else if (asset == stETH) { + ILido(stETH).submit{value: amount}(address(0)); + } else if (asset == wstETH) { + ILido(stETH).submit{value: amount}(address(0)); + IWstETH(wstETH).wrap(amount); /* } else if (asset == astETH) { ILido(stETH).submit{value: amount}(address(0)); @@ -169,8 +175,6 @@ contract LoanVault is Initializable, OwnableUpgradeable { IBendPool(bendPool).deposit(wETH, amount, address(this), 0); } else if (asset == cETH) { ICEther(cETH).mint{value: amount}(); - } else if (asset == stETH) { - ILido(stETH).submit{value: amount}(address(0)); */ } else { revert("not support asset"); diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 8a44fdc64..7a7bf1b1d 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -129,4 +129,5 @@ library Errors { string public constant INVALID_BORROW_ASSET = "134"; // invalid borrow asset for collateral string public constant INVALID_PRESENT_VALUE = "135"; // invalid present value string public constant USAGE_RATIO_TOO_HIGH = "136"; // usage ratio too high after borrow + string public constant STABLE_DEBT_TOKEN_ALREADY_SET = "137"; // stable debt token already set } diff --git a/contracts/protocol/libraries/logic/ConfiguratorLogic.sol b/contracts/protocol/libraries/logic/ConfiguratorLogic.sol index 8d32110e7..46aa609c4 100644 --- a/contracts/protocol/libraries/logic/ConfiguratorLogic.sol +++ b/contracts/protocol/libraries/logic/ConfiguratorLogic.sol @@ -148,6 +148,30 @@ library ConfiguratorLogic { ); } + function executeInitStableDebtToken( + IPool pool, + ConfiguratorInputTypes.ConfigStableDebtTokenInput calldata input + ) public { + address stableDebtTokenProxyAddress = _initTokenWithProxy( + input.stableDebtTokenImpl, + abi.encodeWithSelector( + IInitializableDebtToken.initialize.selector, + pool, + input.underlyingAsset, + input.incentivesController, + input.underlyingAssetDecimals, + input.stableDebtTokenName, + input.stableDebtTokenSymbol, + input.params + ) + ); + + pool.setReserveStableDebtTokenAddress( + input.underlyingAsset, + stableDebtTokenProxyAddress + ); + } + /** * @notice Updates the xToken implementation and initializes it * @dev Emits the `XTokenUpgraded` event diff --git a/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol b/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol index cd0dd3de4..018f4675c 100644 --- a/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol +++ b/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol @@ -51,4 +51,14 @@ library ConfiguratorInputTypes { address implementation; bytes params; } + + struct ConfigStableDebtTokenInput { + address stableDebtTokenImpl; + address underlyingAsset; + address incentivesController; + uint8 underlyingAssetDecimals; + string stableDebtTokenName; + string stableDebtTokenSymbol; + bytes params; + } } diff --git a/contracts/protocol/pool/PoolConfigurator.sol b/contracts/protocol/pool/PoolConfigurator.sol index 357e04b9c..d4c587478 100644 --- a/contracts/protocol/pool/PoolConfigurator.sol +++ b/contracts/protocol/pool/PoolConfigurator.sol @@ -355,6 +355,13 @@ contract PoolConfigurator is VersionedInitializable, IPoolConfigurator { ); } + /// @inheritdoc IPoolConfigurator + function configReserveStableDebtTokenAddress( + ConfiguratorInputTypes.ConfigStableDebtTokenInput calldata input + ) external override onlyRiskOrPoolAdmins { + ConfiguratorLogic.executeInitStableDebtToken(_pool, input); + } + /// @inheritdoc IPoolConfigurator function setReserveAuctionStrategyAddress( address asset, diff --git a/contracts/protocol/pool/PoolParameters.sol b/contracts/protocol/pool/PoolParameters.sol index 764a77a6d..b8514252b 100644 --- a/contracts/protocol/pool/PoolParameters.sol +++ b/contracts/protocol/pool/PoolParameters.sol @@ -150,6 +150,29 @@ contract PoolParameters is PoolLogic.executeDropReserve(ps._reserves, ps._reservesList, asset); } + /// @inheritdoc IPoolParameters + function setReserveStableDebtTokenAddress( + address asset, + address stableDebtTokenAddress + ) external virtual override onlyPoolConfigurator { + DataTypes.PoolStorage storage ps = poolStorage(); + DataTypes.ReserveData storage reserve = ps._reserves[asset]; + + require(asset != address(0), Errors.ZERO_ADDRESS_NOT_VALID); + require( + reserve.id != 0 || ps._reservesList[0] == asset, + Errors.ASSET_NOT_LISTED + ); + require( + reserve.stableDebtTokenAddress == address(0), + Errors.STABLE_DEBT_TOKEN_ALREADY_SET + ); + reserve.stableDebtTokenAddress = stableDebtTokenAddress; + + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateInterestRates(reserveCache, asset, 0, 0); + } + /// @inheritdoc IPoolParameters function setReserveInterestRateStrategyAddress( address asset, diff --git a/contracts/protocol/tokenization/StETHStableDebtToken.sol b/contracts/protocol/tokenization/StETHStableDebtToken.sol new file mode 100644 index 000000000..119514d72 --- /dev/null +++ b/contracts/protocol/tokenization/StETHStableDebtToken.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IPool} from "../../interfaces/IPool.sol"; +import {ILido} from "../../interfaces/ILido.sol"; +import {RebaseStableDebtToken} from "./RebaseStableDebtToken.sol"; +import {WadRayMath} from "../libraries/math/WadRayMath.sol"; + +/** + * @title stETH Stable Rebasing Debt Token + * + * @notice Implementation of the interest bearing token for the ParaSpace protocol + */ +contract StETHStableDebtToken is RebaseStableDebtToken { + constructor(IPool pool) RebaseStableDebtToken(pool) { + //intentionally empty + } + + /** + * @return Current rebasing index of stETH in RAY + **/ + function lastRebasingIndex() internal view override returns (uint256) { + // Returns amount of stETH corresponding to 10**27 stETH shares. + // The 10**27 is picked to provide the same precision as the ParaSpace + // liquidity index, which is in RAY (10**27). + return ILido(_underlyingAsset).getPooledEthByShares(WadRayMath.RAY); + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 06d159327..bb7f4cb3c 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -257,6 +257,8 @@ import { MockedETHWithdrawNFT, ATokenStableDebtToken, ATokenStableDebtToken__factory, + StETHStableDebtToken__factory, + StETHStableDebtToken, } from "../types"; import {MockContract} from "ethereum-waffle"; import { @@ -536,6 +538,10 @@ export const getPoolSignaturesFromDb = async () => { eContractid.PoolApeStakingImpl ); + const poolWithdrawSelectors = await getFunctionSignaturesFromDb( + eContractid.PoolETHWithdrawImpl + ); + const poolParaProxyInterfacesSelectors = await getFunctionSignaturesFromDb( eContractid.ParaProxyInterfacesImpl ); @@ -545,6 +551,7 @@ export const getPoolSignaturesFromDb = async () => { poolParametersSelectors, poolMarketplaceSelectors, poolApeStakingSelectors, + poolWithdrawSelectors, poolParaProxyInterfacesSelectors, }; }; @@ -2761,6 +2768,17 @@ export const deployATokenStableDebtToken = async ( verify ) as Promise; +export const deployStETHStableDebtToken = async ( + poolAddress: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new StETHStableDebtToken__factory(await getFirstSigner()), + eContractid.StETHStableDebtToken, + [poolAddress], + verify + ) as Promise; + export const deployMockStableDebtToken = async ( args: [ tEthereumAddress, @@ -2797,7 +2815,12 @@ export const deployLoanVaultImpl = async ( verify?: boolean ) => { const allTokens = await getAllTokens(); - const args = [poolAddress, allTokens.WETH.address, allTokens.aWETH.address]; + const args = [ + poolAddress, + allTokens.WETH.address, + allTokens.aWETH.address, + allTokens.wstETH.address, + ]; return withSaveAndVerify( new LoanVault__factory(await getFirstSigner()), diff --git a/helpers/init-helpers.ts b/helpers/init-helpers.ts index d89a31b85..c320675f5 100644 --- a/helpers/init-helpers.ts +++ b/helpers/init-helpers.ts @@ -48,6 +48,7 @@ import { deployAutoYieldApe, deployGenericStableDebtToken, deployATokenStableDebtToken, + deployStETHStableDebtToken, } from "./contracts-deployments"; import {ZERO_ADDRESS} from "./constants"; @@ -132,6 +133,7 @@ export const initReservesByHelper = async ( let stETHVariableDebtTokenImplementationAddress = ""; let aTokenVariableDebtTokenImplementationAddress = ""; let aTokenStableDebtTokenImplementationAddress = ""; + let stETHStableDebtTokenImplementationAddress = ""; let PsApeVariableDebtTokenImplementationAddress = ""; let nTokenBAKCImplementationAddress = ""; @@ -360,6 +362,12 @@ export const initReservesByHelper = async ( ).address; } variableDebtTokenToUse = stETHVariableDebtTokenImplementationAddress; + if (!stETHStableDebtTokenImplementationAddress) { + stETHStableDebtTokenImplementationAddress = ( + await deployStETHStableDebtToken(pool.address, verify) + ).address; + } + stableDebtTokenToUse = stETHStableDebtTokenImplementationAddress; } else if (reserveSymbol === ERC20TokenContractId.aWETH) { if (!pTokenATokenImplementationAddress) { pTokenATokenImplementationAddress = ( diff --git a/helpers/types.ts b/helpers/types.ts index 6949ff899..c73a9af41 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -231,6 +231,7 @@ export enum eContractid { ApeCoinStaking = "ApeCoinStaking", ATokenDebtToken = "ATokenDebtToken", ATokenStableDebtToken = "ATokenStableDebtToken", + StETHStableDebtToken = "StETHStableDebtToken", StETHDebtToken = "StETHDebtToken", CApeDebtToken = "CApeDebtToken", ApeStakingLogic = "ApeStakingLogic", diff --git a/market-config/reservesConfigs.ts b/market-config/reservesConfigs.ts index f3019c608..439b6ac37 100644 --- a/market-config/reservesConfigs.ts +++ b/market-config/reservesConfigs.ts @@ -427,7 +427,7 @@ export const strategySTETH: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, - stableBorrowRateEnabled: false, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenStETHImpl, reserveFactor: "1000", @@ -443,7 +443,7 @@ export const strategyWSTETH: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, - stableBorrowRateEnabled: false, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", diff --git a/scripts/dev/12-update-eth-instant-withdraw.ts b/scripts/dev/12-update-eth-instant-withdraw.ts new file mode 100644 index 000000000..2b6525782 --- /dev/null +++ b/scripts/dev/12-update-eth-instant-withdraw.ts @@ -0,0 +1,221 @@ +import {BigNumberish} from "ethers"; +import rawBRE from "hardhat"; +import { + getPoolAddressesProvider, + getPoolConfiguratorProxy, + getPoolProxy, +} from "../../helpers/contracts-getters"; +import {GLOBAL_OVERRIDES} from "../../helpers/hardhat-constants"; +import {DRE, waitForTx} from "../../helpers/misc-utils"; +import {DRY_RUN} from "../../helpers/hardhat-constants"; +import { + dryRunEncodedData, + getEthersSigners, +} from "../../helpers/contracts-helpers"; +import {getParaSpaceConfig} from "../../helpers/misc-utils"; +import {tEthereumAddress} from "../../helpers/types"; +import { + deployATokenStableDebtToken, + deployGenericStableDebtToken, + deployReserveInterestRateStrategy, + deployStETHStableDebtToken, +} from "../../helpers/contracts-deployments"; +import {resetPool} from "../upgrade"; +import {ONE_ADDRESS} from "../../helpers/constants"; +import {upgradeConfigurator} from "../upgrade/configurator"; + +const updateETHInstantWithdraw = async () => { + console.time("update eth instant withdraw"); + + console.log("DRE.network.name:", DRE.network.name); + + const addressesProvider = await getPoolAddressesProvider(); + const poolConfiguratorProxy = await getPoolConfiguratorProxy( + await addressesProvider.getPoolConfigurator() + ); + const pool = await getPoolProxy(); + + //1. pause pool + if (DRY_RUN) { + const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( + "setPoolPause", + [true] + ); + await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); + } else { + await waitForTx( + await poolConfiguratorProxy.setPoolPause(true, GLOBAL_OVERRIDES) + ); + } + + //2. upgrade pool configurator + await upgradeConfigurator(false); + + //3, update pool + await resetPool(false); + + //4, update asset interest rate strategy + const config = getParaSpaceConfig(); + const reservesParams = config.ReservesConfig; + const assetAddresses = await pool.getReservesList(); + const MintableERC20 = await DRE.ethers.getContractFactory("MintableERC20"); + const strategyAddresses: Record = {}; + const assetAddressMap: Record = {}; + + for (const assetAddress of assetAddresses) { + if (assetAddress === ONE_ADDRESS) { + console.log("skip for sApe, continue"); + continue; + } + + const token = await MintableERC20.attach(assetAddress); + const signers = await getEthersSigners(); + const tokenSymbol = await token.connect(signers[2]).symbol(); + console.log("tokenSymbol:", tokenSymbol); + if (!reservesParams[tokenSymbol]) { + console.log("token has no params, continue"); + continue; + } + const reserveIRStrategy = reservesParams[tokenSymbol].strategy; + assetAddressMap[tokenSymbol] = assetAddress; + + //4.1 deploy interest rate strategy + if (!strategyAddresses[reserveIRStrategy.name]) { + strategyAddresses[reserveIRStrategy.name] = ( + await deployReserveInterestRateStrategy( + reserveIRStrategy.name, + [ + addressesProvider.address, + reserveIRStrategy.optimalUsageRatio, + reserveIRStrategy.baseVariableBorrowRate, + reserveIRStrategy.variableRateSlope1, + reserveIRStrategy.variableRateSlope2, + reserveIRStrategy.stableRateSlope1, + reserveIRStrategy.stableRateSlope2, + reserveIRStrategy.baseStableRateOffset, + reserveIRStrategy.stableRateExcessOffset, + reserveIRStrategy.optimalStableToTotalDebtRatio, + ], + false + ) + ).address; + } + + //4.2 update interest rate strategy + if (DRY_RUN) { + const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( + "setReserveInterestRateStrategyAddress", + [assetAddress, strategyAddresses[reserveIRStrategy.name]] + ); + await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); + } else { + await waitForTx( + await poolConfiguratorProxy.setReserveInterestRateStrategyAddress( + assetAddress, + strategyAddresses[reserveIRStrategy.name], + GLOBAL_OVERRIDES + ) + ); + } + } + + //5 enable stable borrow for erc20 that needed + const stableBorrowAsset = ["WETH", "aWETH", "stETH", "wstETH"]; + + //5.1 deploy stable debt token implementation + const genericStableDebtTokenImplementationAddress = ( + await deployGenericStableDebtToken(pool.address, false) + ).address; + const aTokenStableDebtTokenImplementationAddress = ( + await deployATokenStableDebtToken(pool.address, false) + ).address; + const stETHStableDebtTokenImplementationAddress = ( + await deployStETHStableDebtToken(pool.address, false) + ).address; + + for (const symbol of stableBorrowAsset) { + let stableDebtTokenImplementationAddress = + genericStableDebtTokenImplementationAddress; + if (symbol === "aWETH") { + stableDebtTokenImplementationAddress = + aTokenStableDebtTokenImplementationAddress; + } else if (symbol === "stETH") { + stableDebtTokenImplementationAddress = + stETHStableDebtTokenImplementationAddress; + } + + const configStableDebtTokenInput: { + stableDebtTokenImpl: string; + underlyingAsset: string; + incentivesController: string; + underlyingAssetDecimals: BigNumberish; + stableDebtTokenName: string; + stableDebtTokenSymbol: string; + params: string; + } = { + stableDebtTokenImpl: stableDebtTokenImplementationAddress, + underlyingAsset: assetAddressMap[symbol], + incentivesController: config.IncentivesController, + underlyingAssetDecimals: reservesParams[symbol].reserveDecimals, + stableDebtTokenName: `${config.StableDebtTokenNamePrefix} ${config.SymbolPrefix}${symbol}`, + stableDebtTokenSymbol: `sDebt${config.SymbolPrefix}${symbol}`, + params: "0x10", + }; + + //5.2 deploy and set stable debt token proxy for asset and enable stable borrowing + if (DRY_RUN) { + let encodedData = poolConfiguratorProxy.interface.encodeFunctionData( + "configReserveStableDebtTokenAddress", + [configStableDebtTokenInput] + ); + await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); + + encodedData = poolConfiguratorProxy.interface.encodeFunctionData( + "setReserveStableRateBorrowing", + [assetAddressMap[symbol], true] + ); + await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); + } else { + await waitForTx( + await poolConfiguratorProxy.configReserveStableDebtTokenAddress( + configStableDebtTokenInput, + GLOBAL_OVERRIDES + ) + ); + await waitForTx( + await poolConfiguratorProxy.setReserveStableRateBorrowing( + assetAddressMap[symbol], + true, + GLOBAL_OVERRIDES + ) + ); + } + } + + //6. unpause pool + if (DRY_RUN) { + const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( + "setPoolPause", + [false] + ); + await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); + } else { + await waitForTx( + await poolConfiguratorProxy.setPoolPause(false, GLOBAL_OVERRIDES) + ); + } + + console.timeEnd("update eth instant withdraw"); +}; + +async function main() { + await rawBRE.run("set-DRE"); + await updateETHInstantWithdraw(); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/upgrade/index.ts b/scripts/upgrade/index.ts index 581ea4740..2b1a5eb63 100644 --- a/scripts/upgrade/index.ts +++ b/scripts/upgrade/index.ts @@ -1,9 +1,11 @@ import {waitForTx} from "../../helpers/misc-utils"; import { + deployLoanVault, deployPoolComponents, getPoolSignaturesFromDb, } from "../../helpers/contracts-deployments"; import { + getFirstSigner, getPoolAddressesProvider, getPoolProxy, } from "../../helpers/contracts-getters"; @@ -12,8 +14,18 @@ import {ZERO_ADDRESS} from "../../helpers/constants"; import {upgradePToken} from "./ptoken"; import {upgradeNToken} from "./ntoken"; import {DRY_RUN, GLOBAL_OVERRIDES} from "../../helpers/hardhat-constants"; -import {IParaProxy} from "../../types"; -import {dryRunEncodedData} from "../../helpers/contracts-helpers"; +import { + IParaProxy, + PoolInstantWithdraw, + PoolInstantWithdraw__factory, +} from "../../types"; +import { + dryRunEncodedData, + getContractAddressInDb, + getFunctionSignatures, + withSaveAndVerify, +} from "../../helpers/contracts-helpers"; +import {eContractid} from "../../helpers/types"; dotenv.config(); @@ -69,13 +81,30 @@ export const resetPool = async (verify = false) => { poolParametersSelectors: newPoolParametersSelectors, poolMarketplaceSelectors: newPoolMarketplaceSelectors, poolApeStakingSelectors: newPoolApeStakingSelectors, + poolInstantWithdrawSelectors: newPoolInstantWithdrawSelectors, } = await deployPoolComponents(addressesProvider.address, verify); + + const poolAddress = await addressesProvider.getPool(); + const loanVaultAddress = + (await getContractAddressInDb(eContractid.LoanVault)) || + (await deployLoanVault(poolAddress, verify)).address; + const poolInstantWithdraw = (await withSaveAndVerify( + new PoolInstantWithdraw__factory(await getFirstSigner()), + eContractid.PoolETHWithdrawImpl, + [addressesProvider.address, loanVaultAddress], + verify, + false, + undefined, + getFunctionSignatures(PoolInstantWithdraw__factory.abi) + )) as PoolInstantWithdraw; + console.timeEnd("deploy PoolComponent"); const implementations = [ [poolCore.address, newPoolCoreSelectors], [poolMarketplace.address, newPoolMarketplaceSelectors], [poolParameters.address, newPoolParametersSelectors], + [poolInstantWithdraw.address, newPoolInstantWithdrawSelectors], ] as [string, string[]][]; if (poolApeStaking) { implementations.push([poolApeStaking.address, newPoolApeStakingSelectors]); @@ -85,6 +114,7 @@ export const resetPool = async (verify = false) => { coreProxyImplementation, marketplaceProxyImplementation, parametersProxyImplementation, + instantWithdrawProxyImplementation, apeStakingProxyImplementation, ] = implementations.map(([implAddress, newSelectors]) => { const proxyImplementation: IParaProxy.ProxyImplementationStruct[] = []; @@ -102,6 +132,10 @@ export const resetPool = async (verify = false) => { "marketplaceProxyImplementation:", marketplaceProxyImplementation ); + console.log( + "instantWithdrawProxyImplementation:", + instantWithdrawProxyImplementation + ); console.log("apeStakingProxyImplementation:", apeStakingProxyImplementation); console.time("upgrade PoolCore"); @@ -167,6 +201,27 @@ export const resetPool = async (verify = false) => { } console.timeEnd("upgrade PoolMarketplace"); + console.time("upgrade PoolInstantWithdraw"); + if (instantWithdrawProxyImplementation) { + if (DRY_RUN) { + const encodedData = addressesProvider.interface.encodeFunctionData( + "updatePoolImpl", + [instantWithdrawProxyImplementation, ZERO_ADDRESS, "0x"] + ); + await dryRunEncodedData(addressesProvider.address, encodedData); + } else { + await waitForTx( + await addressesProvider.updatePoolImpl( + instantWithdrawProxyImplementation, + ZERO_ADDRESS, + "0x", + GLOBAL_OVERRIDES + ) + ); + } + } + console.timeEnd("upgrade PoolInstantWithdraw"); + console.time("upgrade PoolApeStaking"); if (apeStakingProxyImplementation) { if (DRY_RUN) { @@ -197,6 +252,7 @@ export const upgradePool = async (verify = false) => { poolParametersSelectors: oldPoolParametersSelectors, poolMarketplaceSelectors: oldPoolMarketplaceSelectors, poolApeStakingSelectors: oldPoolApeStakingSelectors, + poolWithdrawSelectors: oldPoolWithdrawSelectors, poolParaProxyInterfacesSelectors: oldPoolParaProxyInterfacesSelectors, } = await getPoolSignaturesFromDb(); @@ -210,8 +266,24 @@ export const upgradePool = async (verify = false) => { poolParametersSelectors: newPoolParametersSelectors, poolMarketplaceSelectors: newPoolMarketplaceSelectors, poolApeStakingSelectors: newPoolApeStakingSelectors, + poolInstantWithdrawSelectors: newPoolInstantWithdrawSelectors, poolParaProxyInterfacesSelectors: newPoolParaProxyInterfacesSelectors, } = await deployPoolComponents(addressesProvider.address, verify); + + const poolAddress = await addressesProvider.getPool(); + const loanVaultAddress = + (await getContractAddressInDb(eContractid.LoanVault)) || + (await deployLoanVault(poolAddress, verify)).address; + const poolInstantWithdraw = (await withSaveAndVerify( + new PoolInstantWithdraw__factory(await getFirstSigner()), + eContractid.PoolETHWithdrawImpl, + [addressesProvider.address, loanVaultAddress], + verify, + false, + undefined, + getFunctionSignatures(PoolInstantWithdraw__factory.abi) + )) as PoolInstantWithdraw; + console.timeEnd("deploy PoolComponent"); const implementations = [ @@ -226,6 +298,11 @@ export const upgradePool = async (verify = false) => { newPoolParametersSelectors, oldPoolParametersSelectors, ], + [ + poolInstantWithdraw.address, + newPoolInstantWithdrawSelectors, + oldPoolWithdrawSelectors, + ], [ poolParaProxyInterfaces.address, newPoolParaProxyInterfacesSelectors, @@ -245,6 +322,7 @@ export const upgradePool = async (verify = false) => { coreProxyImplementation, marketplaceProxyImplementation, parametersProxyImplementation, + instantWithdrawProxyImplementation, interfacesProxyImplementation, apeStakingProxyImplementation, ] = implementations.map(([implAddress, newSelectors, oldSelectors]) => { @@ -278,6 +356,10 @@ export const upgradePool = async (verify = false) => { "marketplaceProxyImplementation:", marketplaceProxyImplementation ); + console.log( + "instantWithdrawProxyImplementation:", + instantWithdrawProxyImplementation + ); console.log("apeStakingProxyImplementation:", apeStakingProxyImplementation); console.log("interfacesProxyImplementation:", interfacesProxyImplementation); @@ -344,6 +426,27 @@ export const upgradePool = async (verify = false) => { } console.timeEnd("upgrade PoolMarketplace"); + console.time("upgrade PoolInstantWithdraw"); + if (instantWithdrawProxyImplementation) { + if (DRY_RUN) { + const encodedData = addressesProvider.interface.encodeFunctionData( + "updatePoolImpl", + [instantWithdrawProxyImplementation, ZERO_ADDRESS, "0x"] + ); + await dryRunEncodedData(addressesProvider.address, encodedData); + } else { + await waitForTx( + await addressesProvider.updatePoolImpl( + instantWithdrawProxyImplementation, + ZERO_ADDRESS, + "0x", + GLOBAL_OVERRIDES + ) + ); + } + } + console.timeEnd("upgrade PoolInstantWithdraw"); + console.time("upgrade PoolApeStaking"); if (apeStakingProxyImplementation) { if (DRY_RUN) { From aab411fb1f427d5fb925fae363734422f342e749 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Wed, 15 Mar 2023 14:32:50 +0800 Subject: [PATCH 08/25] chore: add multicall to support complex operation --- contracts/interfaces/IPoolInstantWithdraw.sol | 9 +++++ .../protocol/pool/PoolInstantWithdraw.sol | 13 +++++++ test/pool_instant_withdraw.spec.ts | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/contracts/interfaces/IPoolInstantWithdraw.sol b/contracts/interfaces/IPoolInstantWithdraw.sol index 93d758a7d..4d4db8414 100644 --- a/contracts/interfaces/IPoolInstantWithdraw.sol +++ b/contracts/interfaces/IPoolInstantWithdraw.sol @@ -169,4 +169,13 @@ interface IPoolInstantWithdraw { * @param loanId The id for the specified loan **/ function settleTermLoan(uint256 loanId) external; + + /** + * @notice Call multiple functions in the current contract and return the data from all of them if they all succeed + * @param data The encoded function data for each of the calls to make to this contract + * @return results The results from each of the calls passed in via data + **/ + function multicall(bytes[] calldata data) + external + returns (bytes[] memory results); } diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index 0a34cc9c9..d0a3c80d8 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -108,6 +108,19 @@ contract PoolInstantWithdraw is return POOL_REVISION; } + /// @inheritdoc IPoolInstantWithdraw + function multicall(bytes[] calldata data) + external + virtual + returns (bytes[] memory results) + { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + results[i] = Address.functionDelegateCall(address(this), data[i]); + } + return results; + } + /// @inheritdoc IPoolInstantWithdraw function addBorrowableAssets( address collateralAsset, diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts index 6df1acf98..ff9c3295a 100644 --- a/test/pool_instant_withdraw.spec.ts +++ b/test/pool_instant_withdraw.spec.ts @@ -60,6 +60,43 @@ describe("Pool Instant Withdraw Test", () => { return testEnv; }; + it("user can create two loan with 1 transaction with multicall", async () => { + const { + users: [, user2], + weth, + pool, + } = await loadFixture(fixture); + + await waitForTx( + await instantWithdrawNFT.connect(user2.signer).mint(2, tokenAmount) + ); + + const tx0 = pool.interface.encodeFunctionData("createLoan", [ + instantWithdrawNFT.address, + 1, + tokenAmount, + weth.address, + 0, + ]); + const tx1 = pool.interface.encodeFunctionData("createLoan", [ + instantWithdrawNFT.address, + 2, + tokenAmount, + weth.address, + 0, + ]); + + await waitForTx(await pool.connect(user2.signer).multicall([tx0, tx1])); + + expect(await weth.balanceOf(user2.address)).to.be.gte(parseEther("2")); + expect(await instantWithdrawNFT.balanceOf(loanVault.address, 1)).to.be.eq( + tokenAmount + ); + expect(await instantWithdrawNFT.balanceOf(loanVault.address, 2)).to.be.eq( + tokenAmount + ); + }); + it("active term loan can be swapped by other user", async () => { const { users: [, user2, user3], From 83ba1aeb65103548f8a2aa925c788869257b33f4 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Wed, 15 Mar 2023 18:29:28 +0800 Subject: [PATCH 09/25] chore: fix deploy --- contracts/protocol/pool/PoolParameters.sol | 1 + contracts/ui/UiPoolDataProvider.sol | 18 ++++--- contracts/ui/WETHGateway.sol | 13 ----- contracts/ui/interfaces/IWETHGateway.sol | 4 -- scripts/dev/12-update-eth-instant-withdraw.ts | 51 +++++++++++++++---- 5 files changed, 53 insertions(+), 34 deletions(-) diff --git a/contracts/protocol/pool/PoolParameters.sol b/contracts/protocol/pool/PoolParameters.sol index b8514252b..372561c71 100644 --- a/contracts/protocol/pool/PoolParameters.sol +++ b/contracts/protocol/pool/PoolParameters.sol @@ -170,6 +170,7 @@ contract PoolParameters is reserve.stableDebtTokenAddress = stableDebtTokenAddress; DataTypes.ReserveCache memory reserveCache = reserve.cache(); + reserve.updateState(reserveCache); reserve.updateInterestRates(reserveCache, asset, 0, 0); } diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index 9739a1ce0..0233a5661 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -28,7 +28,6 @@ import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; import {IUniswapV3OracleWrapper} from "../interfaces/IUniswapV3OracleWrapper.sol"; import {UinswapV3PositionData} from "../interfaces/IUniswapV3PositionInfoProvider.sol"; import {Helpers} from "../protocol/libraries/helpers/Helpers.sol"; -import "hardhat/console.sol"; contract UiPoolDataProvider is IUiPoolDataProvider { using WadRayMath for uint256; @@ -138,13 +137,16 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.underlyingAsset ); - ( - reserveData.totalPrincipalStableDebt, - , - reserveData.averageStableRate, - reserveData.stableDebtLastUpdateTimestamp - ) = IStableDebtToken(reserveData.stableDebtTokenAddress) - .getSupplyData(); + if (reserveData.stableDebtTokenAddress != address(0)) { + ( + reserveData.totalPrincipalStableDebt, + , + reserveData.averageStableRate, + reserveData.stableDebtLastUpdateTimestamp + ) = IStableDebtToken(reserveData.stableDebtTokenAddress) + .getSupplyData(); + } + reserveData.totalScaledVariableDebt = IVariableDebtToken( reserveData.variableDebtTokenAddress ).scaledTotalSupply(); diff --git a/contracts/ui/WETHGateway.sol b/contracts/ui/WETHGateway.sol index 4d85a28b8..d06ea3e50 100644 --- a/contracts/ui/WETHGateway.sol +++ b/contracts/ui/WETHGateway.sol @@ -205,19 +205,6 @@ contract WETHGateway is ReentrancyGuard, IWETHGateway, OwnableUpgradeable { _safeTransferETH(msg.sender, ethAmount); } - /** - * @notice swap a term loan collateral with ETH, - * the amount user need to pay is calculated by the present value of the collateral - * @param loanId The id for the specified loan - * @param receiver The address to receive the collateral asset - **/ - function swapLoanCollateral(uint256 loanId, address receiver) - external - payable - override - nonReentrant - {} - function onERC721Received( address, address, diff --git a/contracts/ui/interfaces/IWETHGateway.sol b/contracts/ui/interfaces/IWETHGateway.sol index da6f5874d..16b1ad6cd 100644 --- a/contracts/ui/interfaces/IWETHGateway.sol +++ b/contracts/ui/interfaces/IWETHGateway.sol @@ -31,8 +31,4 @@ interface IWETHGateway { uint256 collateralAmount, uint16 referralCode ) external; - - function swapLoanCollateral(uint256 loanId, address receiver) - external - payable; } diff --git a/scripts/dev/12-update-eth-instant-withdraw.ts b/scripts/dev/12-update-eth-instant-withdraw.ts index 2b6525782..19c7908f2 100644 --- a/scripts/dev/12-update-eth-instant-withdraw.ts +++ b/scripts/dev/12-update-eth-instant-withdraw.ts @@ -17,6 +17,7 @@ import {tEthereumAddress} from "../../helpers/types"; import { deployATokenStableDebtToken, deployGenericStableDebtToken, + deployProtocolDataProvider, deployReserveInterestRateStrategy, deployStETHStableDebtToken, } from "../../helpers/contracts-deployments"; @@ -63,20 +64,23 @@ const updateETHInstantWithdraw = async () => { const assetAddressMap: Record = {}; for (const assetAddress of assetAddresses) { + let tokenSymbol; if (assetAddress === ONE_ADDRESS) { - console.log("skip for sApe, continue"); - continue; + tokenSymbol = "sAPE"; + } else { + const token = await MintableERC20.attach(assetAddress); + const signers = await getEthersSigners(); + tokenSymbol = await token.connect(signers[2]).symbol(); } - - const token = await MintableERC20.attach(assetAddress); - const signers = await getEthersSigners(); - const tokenSymbol = await token.connect(signers[2]).symbol(); console.log("tokenSymbol:", tokenSymbol); + console.log("assetAddress:", assetAddress); + + let reserveIRStrategy; if (!reservesParams[tokenSymbol]) { - console.log("token has no params, continue"); - continue; + reserveIRStrategy = reservesParams["sAPE"].strategy; + } else { + reserveIRStrategy = reservesParams[tokenSymbol].strategy; } - const reserveIRStrategy = reservesParams[tokenSymbol].strategy; assetAddressMap[tokenSymbol] = assetAddress; //4.1 deploy interest rate strategy @@ -205,6 +209,35 @@ const updateETHInstantWithdraw = async () => { ); } + // 7. upgrade protocol data provider + console.log("upgrade protocol data provider"); + const protocolDataProvider = await deployProtocolDataProvider( + addressesProvider.address, + false + ); + await addressesProvider.setProtocolDataProvider( + protocolDataProvider.address, + GLOBAL_OVERRIDES + ); + console.log("upgrade protocol data provider done..."); + + console.log("start add borrowing asset"); + + // 8. add borrowing asset + await waitForTx( + await pool.addBorrowableAssets( + "0xff3E5AD98a99d17044Cff0cB7D442682898E77aF", + [ + assetAddressMap["WETH"], + assetAddressMap["aWETH"], + assetAddressMap["stETH"], + assetAddressMap["wstETH"], + ], + GLOBAL_OVERRIDES + ) + ); + console.log("add borrowing asset done....."); + console.timeEnd("update eth instant withdraw"); }; From ecfdee64bd83d99c39c834be55a79ee318b20559 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Thu, 16 Mar 2023 13:39:46 +0800 Subject: [PATCH 10/25] chore: deploy stable debt token address for all asset --- Makefile | 2 +- .../tokenization/CApeStableDebtToken.sol | 25 +++++ contracts/ui/UiPoolDataProvider.sol | 16 ++-- helpers/contracts-deployments.ts | 13 +++ helpers/types.ts | 1 + ...w.ts => 12.update-eth-instant-withdraw.ts} | 92 +++++++++---------- 6 files changed, 89 insertions(+), 60 deletions(-) create mode 100644 contracts/protocol/tokenization/CApeStableDebtToken.sol rename scripts/dev/{12-update-eth-instant-withdraw.ts => 12.update-eth-instant-withdraw.ts} (86%) diff --git a/Makefile b/Makefile index e9b2150a1..b5e824cfc 100644 --- a/Makefile +++ b/Makefile @@ -422,7 +422,7 @@ set-traits-multipliers: .PHONY: upgrade-eth-instant-withdraw upgrade-eth-instant-withdraw: - make SCRIPT_PATH=./scripts/dev/12-update-eth-instant-withdraw.ts run + make SCRIPT_PATH=./scripts/dev/12.update-eth-instant-withdraw.ts run .PHONY: transfer-tokens transfer-tokens: diff --git a/contracts/protocol/tokenization/CApeStableDebtToken.sol b/contracts/protocol/tokenization/CApeStableDebtToken.sol new file mode 100644 index 000000000..2d709c87d --- /dev/null +++ b/contracts/protocol/tokenization/CApeStableDebtToken.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IPool} from "../../interfaces/IPool.sol"; +import {ICApe} from "../../interfaces/ICApe.sol"; +import {RebaseStableDebtToken} from "./RebaseStableDebtToken.sol"; +import {WadRayMath} from "../libraries/math/WadRayMath.sol"; + +/** + * @title stETH Rebasing Debt Token + * + * @notice Implementation of the interest bearing token for the ParaSpace protocol + */ +contract CApeStableDebtToken is RebaseStableDebtToken { + constructor(IPool pool) RebaseStableDebtToken(pool) { + //intentionally empty + } + + /** + * @return Current rebasing index of PsAPE in RAY + **/ + function lastRebasingIndex() internal view override returns (uint256) { + return ICApe(_underlyingAsset).getPooledApeByShares(WadRayMath.RAY); + } +} diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index 0233a5661..3f56dcd77 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -137,15 +137,13 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.underlyingAsset ); - if (reserveData.stableDebtTokenAddress != address(0)) { - ( - reserveData.totalPrincipalStableDebt, - , - reserveData.averageStableRate, - reserveData.stableDebtLastUpdateTimestamp - ) = IStableDebtToken(reserveData.stableDebtTokenAddress) - .getSupplyData(); - } + ( + reserveData.totalPrincipalStableDebt, + , + reserveData.averageStableRate, + reserveData.stableDebtLastUpdateTimestamp + ) = IStableDebtToken(reserveData.stableDebtTokenAddress) + .getSupplyData(); reserveData.totalScaledVariableDebt = IVariableDebtToken( reserveData.variableDebtTokenAddress diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index bb7f4cb3c..44dd4ff34 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -259,6 +259,8 @@ import { ATokenStableDebtToken__factory, StETHStableDebtToken__factory, StETHStableDebtToken, + CApeStableDebtToken__factory, + CApeStableDebtToken, } from "../types"; import {MockContract} from "ethereum-waffle"; import { @@ -2452,6 +2454,17 @@ export const deployCApeDebtToken = async ( verify ) as Promise; +export const deployCApeStableDebtToken = async ( + poolAddress: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new CApeStableDebtToken__factory(await getFirstSigner()), + eContractid.CApeStableDebtToken, + [poolAddress], + verify + ) as Promise; + export const deployPYieldToken = async ( poolAddress: tEthereumAddress, verify?: boolean diff --git a/helpers/types.ts b/helpers/types.ts index c73a9af41..50261d4fb 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -234,6 +234,7 @@ export enum eContractid { StETHStableDebtToken = "StETHStableDebtToken", StETHDebtToken = "StETHDebtToken", CApeDebtToken = "CApeDebtToken", + CApeStableDebtToken = "CApeStableDebtToken", ApeStakingLogic = "ApeStakingLogic", MintableERC721Logic = "MintableERC721Logic", MerkleVerifier = "MerkleVerifier", diff --git a/scripts/dev/12-update-eth-instant-withdraw.ts b/scripts/dev/12.update-eth-instant-withdraw.ts similarity index 86% rename from scripts/dev/12-update-eth-instant-withdraw.ts rename to scripts/dev/12.update-eth-instant-withdraw.ts index 19c7908f2..80c7eba78 100644 --- a/scripts/dev/12-update-eth-instant-withdraw.ts +++ b/scripts/dev/12.update-eth-instant-withdraw.ts @@ -16,6 +16,7 @@ import {getParaSpaceConfig} from "../../helpers/misc-utils"; import {tEthereumAddress} from "../../helpers/types"; import { deployATokenStableDebtToken, + deployCApeStableDebtToken, deployGenericStableDebtToken, deployProtocolDataProvider, deployReserveInterestRateStrategy, @@ -55,7 +56,7 @@ const updateETHInstantWithdraw = async () => { //3, update pool await resetPool(false); - //4, update asset interest rate strategy + //4, update asset interest rate strategy and set stable debt token address const config = getParaSpaceConfig(); const reservesParams = config.ReservesConfig; const assetAddresses = await pool.getReservesList(); @@ -63,6 +64,20 @@ const updateETHInstantWithdraw = async () => { const strategyAddresses: Record = {}; const assetAddressMap: Record = {}; + //4.1 deploy stable debt token implementation + const genericStableDebtTokenImplementationAddress = ( + await deployGenericStableDebtToken(pool.address, false) + ).address; + const aTokenStableDebtTokenImplementationAddress = ( + await deployATokenStableDebtToken(pool.address, false) + ).address; + const stETHStableDebtTokenImplementationAddress = ( + await deployStETHStableDebtToken(pool.address, false) + ).address; + const cApeStableDebtTokenImplementationAddress = ( + await deployCApeStableDebtToken(pool.address, false) + ).address; + for (const assetAddress of assetAddresses) { let tokenSymbol; if (assetAddress === ONE_ADDRESS) { @@ -83,7 +98,7 @@ const updateETHInstantWithdraw = async () => { } assetAddressMap[tokenSymbol] = assetAddress; - //4.1 deploy interest rate strategy + //4.2 deploy interest rate strategy if (!strategyAddresses[reserveIRStrategy.name]) { strategyAddresses[reserveIRStrategy.name] = ( await deployReserveInterestRateStrategy( @@ -105,7 +120,7 @@ const updateETHInstantWithdraw = async () => { ).address; } - //4.2 update interest rate strategy + //4.3 update interest rate strategy if (DRY_RUN) { const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( "setReserveInterestRateStrategyAddress", @@ -121,31 +136,19 @@ const updateETHInstantWithdraw = async () => { ) ); } - } - - //5 enable stable borrow for erc20 that needed - const stableBorrowAsset = ["WETH", "aWETH", "stETH", "wstETH"]; - - //5.1 deploy stable debt token implementation - const genericStableDebtTokenImplementationAddress = ( - await deployGenericStableDebtToken(pool.address, false) - ).address; - const aTokenStableDebtTokenImplementationAddress = ( - await deployATokenStableDebtToken(pool.address, false) - ).address; - const stETHStableDebtTokenImplementationAddress = ( - await deployStETHStableDebtToken(pool.address, false) - ).address; - for (const symbol of stableBorrowAsset) { + //4.4 update stable debt token address let stableDebtTokenImplementationAddress = genericStableDebtTokenImplementationAddress; - if (symbol === "aWETH") { + if (tokenSymbol === "aWETH") { stableDebtTokenImplementationAddress = aTokenStableDebtTokenImplementationAddress; - } else if (symbol === "stETH") { + } else if (tokenSymbol === "stETH") { stableDebtTokenImplementationAddress = stETHStableDebtTokenImplementationAddress; + } else if (tokenSymbol === "cAPE") { + stableDebtTokenImplementationAddress = + cApeStableDebtTokenImplementationAddress; } const configStableDebtTokenInput: { @@ -158,27 +161,20 @@ const updateETHInstantWithdraw = async () => { params: string; } = { stableDebtTokenImpl: stableDebtTokenImplementationAddress, - underlyingAsset: assetAddressMap[symbol], + underlyingAsset: assetAddress, incentivesController: config.IncentivesController, - underlyingAssetDecimals: reservesParams[symbol].reserveDecimals, - stableDebtTokenName: `${config.StableDebtTokenNamePrefix} ${config.SymbolPrefix}${symbol}`, - stableDebtTokenSymbol: `sDebt${config.SymbolPrefix}${symbol}`, + underlyingAssetDecimals: reservesParams[tokenSymbol].reserveDecimals, + stableDebtTokenName: `${config.StableDebtTokenNamePrefix} ${config.SymbolPrefix}${tokenSymbol}`, + stableDebtTokenSymbol: `sDebt${config.SymbolPrefix}${tokenSymbol}`, params: "0x10", }; - //5.2 deploy and set stable debt token proxy for asset and enable stable borrowing if (DRY_RUN) { - let encodedData = poolConfiguratorProxy.interface.encodeFunctionData( + const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( "configReserveStableDebtTokenAddress", [configStableDebtTokenInput] ); await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); - - encodedData = poolConfiguratorProxy.interface.encodeFunctionData( - "setReserveStableRateBorrowing", - [assetAddressMap[symbol], true] - ); - await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); } else { await waitForTx( await poolConfiguratorProxy.configReserveStableDebtTokenAddress( @@ -186,6 +182,19 @@ const updateETHInstantWithdraw = async () => { GLOBAL_OVERRIDES ) ); + } + } + + //5 enable stable borrow for erc20 that needed + const stableBorrowAsset = ["WETH", "aWETH", "stETH", "wstETH"]; + for (const symbol of stableBorrowAsset) { + if (DRY_RUN) { + const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( + "setReserveStableRateBorrowing", + [assetAddressMap[symbol], true] + ); + await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); + } else { await waitForTx( await poolConfiguratorProxy.setReserveStableRateBorrowing( assetAddressMap[symbol], @@ -221,23 +230,6 @@ const updateETHInstantWithdraw = async () => { ); console.log("upgrade protocol data provider done..."); - console.log("start add borrowing asset"); - - // 8. add borrowing asset - await waitForTx( - await pool.addBorrowableAssets( - "0xff3E5AD98a99d17044Cff0cB7D442682898E77aF", - [ - assetAddressMap["WETH"], - assetAddressMap["aWETH"], - assetAddressMap["stETH"], - assetAddressMap["wstETH"], - ], - GLOBAL_OVERRIDES - ) - ); - console.log("add borrowing asset done....."); - console.timeEnd("update eth instant withdraw"); }; From 8bf3282ed0fabaf9383c3559a3f0819f6c44167d Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 21 Mar 2023 10:15:55 +0800 Subject: [PATCH 11/25] chore: protect goerli paraproxyinterfaces Signed-off-by: GopherJ --- scripts/upgrade/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/upgrade/index.ts b/scripts/upgrade/index.ts index 2b1a5eb63..42bc63322 100644 --- a/scripts/upgrade/index.ts +++ b/scripts/upgrade/index.ts @@ -43,7 +43,9 @@ export const resetPool = async (verify = false) => { console.time("reset pool"); for (const facet of facets.filter( - (x) => x.implAddress !== "0x0874eBaad20aE4a6F1623a3bf6f914355B7258dB" // ParaProxyInterfaces + (x) => + x.implAddress !== "0x0874eBaad20aE4a6F1623a3bf6f914355B7258dB" && + x.implAddress !== "0xC85d346eB17B37b93B30a37603Ef9550Ab18aC83" // ParaProxyInterfaces )) { const implementation = [ { From 3cf9d614a9b3ccf0614c2efde24f59a04eb3cd99 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 21 Mar 2023 11:34:26 +0800 Subject: [PATCH 12/25] chore: support mnemonic in anvil fork Signed-off-by: GopherJ --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index b5e824cfc..91ff19f8e 100644 --- a/Makefile +++ b/Makefile @@ -593,9 +593,12 @@ anvil: anvil \ $(if $(FORK),--fork-url https://eth-$(FORK).alchemyapi.io/v2/$(ALCHEMY_KEY) --no-rate-limit,) \ $(if $(FORK),--chain-id 522,--chain-id 31337) \ + $(if $(DEPLOYER_MNEMONIC),--mnemonic "$(DEPLOYER_MNEMONIC)",) \ --tracing \ --host 0.0.0.0 \ + --block-time 1 \ --state-interval 60 \ + --timeout 300000 \ --dump-state state.json \ $(if $(wildcard state.json),--load-state state.json,) \ --disable-block-gas-limit \ From cc06130811bf9dd1210d0f5d99257d36916c59ae Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 21 Mar 2023 11:34:35 +0800 Subject: [PATCH 13/25] fix: warning Signed-off-by: GopherJ --- scripts/upgrade/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/upgrade/index.ts b/scripts/upgrade/index.ts index 42bc63322..3e5a402c5 100644 --- a/scripts/upgrade/index.ts +++ b/scripts/upgrade/index.ts @@ -41,7 +41,6 @@ export const resetPool = async (verify = false) => { const pool = await getPoolProxy(); const facets = await pool.facets(); - console.time("reset pool"); for (const facet of facets.filter( (x) => x.implAddress !== "0x0874eBaad20aE4a6F1623a3bf6f914355B7258dB" && @@ -71,7 +70,6 @@ export const resetPool = async (verify = false) => { ); } } - console.timeEnd("reset pool"); console.time("deploy PoolComponent"); const { From 15722c3de7f26f8c05d08818a67c6405278c583c Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 21 Mar 2023 13:36:36 +0800 Subject: [PATCH 14/25] chore: bump hardhat Signed-off-by: GopherJ --- hardhat.config.ts | 1 + package.json | 2 +- yarn.lock | 23 ++++++++++++++++------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index dd6c7f655..880604e0e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -147,6 +147,7 @@ const hardhatConfig: HardhatUserConfig = { gasPrice: "auto", gas: "auto", allowUnlimitedContractSize: true, + timeout: 100000000000, }, parallel: { url: NETWORKS_RPC_URL[eEthereumNetwork.parallel], diff --git a/package.json b/package.json index e6d926afe..5f8f5aeee 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "ethers": "5.7.2", "ethers-multisend": "^2.4.0", "evm-bn": "^1.1.2", - "hardhat": "^2.12.0", + "hardhat": "^2.13.0", "hardhat-contract-sizer": "^2.0.3", "hardhat-deploy": "^0.11.4", "hardhat-gas-reporter": "^1.0.9", diff --git a/yarn.lock b/yarn.lock index 271b01988..c8547e634 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1400,7 +1400,7 @@ __metadata: ethers: 5.7.2 ethers-multisend: ^2.4.0 evm-bn: ^1.1.2 - hardhat: ^2.12.0 + hardhat: ^2.13.0 hardhat-contract-sizer: ^2.0.3 hardhat-deploy: ^0.11.4 hardhat-gas-reporter: ^1.0.9 @@ -8232,9 +8232,9 @@ __metadata: languageName: node linkType: hard -"hardhat@npm:^2.12.0": - version: 2.12.0 - resolution: "hardhat@npm:2.12.0" +"hardhat@npm:^2.13.0": + version: 2.13.0 + resolution: "hardhat@npm:2.13.0" dependencies: "@ethersproject/abi": ^5.1.2 "@metamask/eth-sig-util": ^4.0.0 @@ -8283,7 +8283,7 @@ __metadata: source-map-support: ^0.5.13 stacktrace-parser: ^0.1.10 tsort: 0.0.1 - undici: ^5.4.0 + undici: ^5.14.0 uuid: ^8.3.2 ws: ^7.4.6 peerDependencies: @@ -8295,8 +8295,8 @@ __metadata: typescript: optional: true bin: - hardhat: internal/cli/cli.js - checksum: 28ae9e7d6cf8e66167a94efbabd5ac4c086f0c05a77b190162906f1dd46b4971eb2eb94412b1eaee5626ab2ec6431a94feae15348b10ba10c1a4b7efcef789ec + hardhat: internal/cli/bootstrap.js + checksum: a69813120e5e8f05cefb9ae9c7ff2e2fd06aa8802baf6bb81778810b3ea879d6cb31f04c5719ebe6c0289cc2bd72d1e1f00d65afcf1a049ed3130c35025046e1 languageName: node linkType: hard @@ -14890,6 +14890,15 @@ __metadata: languageName: node linkType: hard +"undici@npm:^5.14.0": + version: 5.21.0 + resolution: "undici@npm:5.21.0" + dependencies: + busboy: ^1.6.0 + checksum: 013d5fd503b631d607942c511c2ab3f3fa78ebcab302acab998b43176b4815503ec15ed9752c5a47918b3bff8a0137768001d3eb57625b2bb6f6d30d8a794d6c + languageName: node + linkType: hard + "undici@npm:^5.4.0": version: 5.11.0 resolution: "undici@npm:5.11.0" From d90d8eac3be3684a693345cc9523983f4048a421 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 21 Mar 2023 13:59:17 +0800 Subject: [PATCH 15/25] chore: bump version Signed-off-by: GopherJ --- contracts/protocol/pool/PoolApeStaking.sol | 2 +- contracts/protocol/pool/PoolConfigurator.sol | 2 +- contracts/protocol/pool/PoolCore.sol | 2 +- contracts/protocol/pool/PoolInstantWithdraw.sol | 2 +- contracts/protocol/pool/PoolMarketplace.sol | 2 +- contracts/protocol/pool/PoolParameters.sol | 2 +- contracts/protocol/tokenization/NToken.sol | 2 +- contracts/protocol/tokenization/PToken.sol | 2 +- contracts/protocol/tokenization/StableDebtToken.sol | 2 +- contracts/protocol/tokenization/VariableDebtToken.sol | 2 +- package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/protocol/pool/PoolApeStaking.sol b/contracts/protocol/pool/PoolApeStaking.sol index 553b87485..4293417e8 100644 --- a/contracts/protocol/pool/PoolApeStaking.sol +++ b/contracts/protocol/pool/PoolApeStaking.sol @@ -43,7 +43,7 @@ contract PoolApeStaking is IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; IAutoCompoundApe internal immutable APE_COMPOUND; IERC20 internal immutable APE_COIN; - uint256 internal constant POOL_REVISION = 145; + uint256 internal constant POOL_REVISION = 146; IERC20 internal immutable USDC; ISwapRouter internal immutable SWAP_ROUTER; diff --git a/contracts/protocol/pool/PoolConfigurator.sol b/contracts/protocol/pool/PoolConfigurator.sol index d4c587478..eb70d40e3 100644 --- a/contracts/protocol/pool/PoolConfigurator.sol +++ b/contracts/protocol/pool/PoolConfigurator.sol @@ -66,7 +66,7 @@ contract PoolConfigurator is VersionedInitializable, IPoolConfigurator { _; } - uint256 public constant CONFIGURATOR_REVISION = 145; + uint256 public constant CONFIGURATOR_REVISION = 146; /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { diff --git a/contracts/protocol/pool/PoolCore.sol b/contracts/protocol/pool/PoolCore.sol index 4a68c3c21..208f5ad8c 100644 --- a/contracts/protocol/pool/PoolCore.sol +++ b/contracts/protocol/pool/PoolCore.sol @@ -50,7 +50,7 @@ contract PoolCore is { using ReserveLogic for DataTypes.ReserveData; - uint256 public constant POOL_REVISION = 145; + uint256 public constant POOL_REVISION = 146; IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; function getRevision() internal pure virtual override returns (uint256) { diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index d0a3c80d8..1ffea2496 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -56,7 +56,7 @@ contract PoolInstantWithdraw is IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; address internal immutable VAULT_CONTRACT; - uint256 internal constant POOL_REVISION = 145; + uint256 internal constant POOL_REVISION = 146; // See `IPoolCore` for descriptions event Borrow( diff --git a/contracts/protocol/pool/PoolMarketplace.sol b/contracts/protocol/pool/PoolMarketplace.sol index fb885c2fa..5410c2e07 100644 --- a/contracts/protocol/pool/PoolMarketplace.sol +++ b/contracts/protocol/pool/PoolMarketplace.sol @@ -53,7 +53,7 @@ contract PoolMarketplace is using SafeERC20 for IERC20; IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; - uint256 internal constant POOL_REVISION = 145; + uint256 internal constant POOL_REVISION = 146; /** * @dev Constructor. diff --git a/contracts/protocol/pool/PoolParameters.sol b/contracts/protocol/pool/PoolParameters.sol index 372561c71..f07657c0a 100644 --- a/contracts/protocol/pool/PoolParameters.sol +++ b/contracts/protocol/pool/PoolParameters.sol @@ -47,7 +47,7 @@ contract PoolParameters is using ReserveLogic for DataTypes.ReserveData; IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; - uint256 internal constant POOL_REVISION = 145; + uint256 internal constant POOL_REVISION = 146; uint256 internal constant MAX_AUCTION_HEALTH_FACTOR = 3e18; uint256 internal constant MIN_AUCTION_HEALTH_FACTOR = 1e18; diff --git a/contracts/protocol/tokenization/NToken.sol b/contracts/protocol/tokenization/NToken.sol index 557c5d112..2347925f2 100644 --- a/contracts/protocol/tokenization/NToken.sol +++ b/contracts/protocol/tokenization/NToken.sol @@ -27,7 +27,7 @@ import {XTokenType} from "../../interfaces/IXTokenType.sol"; contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { using SafeERC20 for IERC20; - uint256 public constant NTOKEN_REVISION = 145; + uint256 public constant NTOKEN_REVISION = 146; /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { diff --git a/contracts/protocol/tokenization/PToken.sol b/contracts/protocol/tokenization/PToken.sol index 0300c21e2..816b5cadf 100644 --- a/contracts/protocol/tokenization/PToken.sol +++ b/contracts/protocol/tokenization/PToken.sol @@ -36,7 +36,7 @@ contract PToken is "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); - uint256 public constant PTOKEN_REVISION = 145; + uint256 public constant PTOKEN_REVISION = 146; address internal _treasury; address internal _underlyingAsset; diff --git a/contracts/protocol/tokenization/StableDebtToken.sol b/contracts/protocol/tokenization/StableDebtToken.sol index 2475ef53e..578be76b0 100644 --- a/contracts/protocol/tokenization/StableDebtToken.sol +++ b/contracts/protocol/tokenization/StableDebtToken.sol @@ -26,7 +26,7 @@ contract StableDebtToken is DebtTokenBase, IncentivizedERC20, IStableDebtToken { using WadRayMath for uint256; using SafeCast for uint256; - uint256 public constant DEBT_TOKEN_REVISION = 145; + uint256 public constant DEBT_TOKEN_REVISION = 146; // Map of users address and the timestamp of their last update (userAddress => lastUpdateTimestamp) mapping(address => uint40) internal _timestamps; diff --git a/contracts/protocol/tokenization/VariableDebtToken.sol b/contracts/protocol/tokenization/VariableDebtToken.sol index 2b78b50c2..112dcc6f4 100644 --- a/contracts/protocol/tokenization/VariableDebtToken.sol +++ b/contracts/protocol/tokenization/VariableDebtToken.sol @@ -29,7 +29,7 @@ contract VariableDebtToken is using WadRayMath for uint256; using SafeCast for uint256; - uint256 public constant DEBT_TOKEN_REVISION = 145; + uint256 public constant DEBT_TOKEN_REVISION = 146; uint256[50] private __gap; /** diff --git a/package.json b/package.json index 5f8f5aeee..9510b5389 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paraspace/core-v1", - "version": "1.4.5", + "version": "1.4.6", "description": "ParaSpace Protocol V1 core smart contracts", "files": [ "contracts", From 8f033e74c8c5e7aab5d4d7f6a5c92e08ef853819 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 21 Mar 2023 17:57:11 +0800 Subject: [PATCH 16/25] fix: add missing stableBorrowing flag Signed-off-by: GopherJ --- market-config/reservesConfigs.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/market-config/reservesConfigs.ts b/market-config/reservesConfigs.ts index c00f5e967..a62db98f5 100644 --- a/market-config/reservesConfigs.ts +++ b/market-config/reservesConfigs.ts @@ -89,6 +89,7 @@ export const strategyFRAX: IReserveParams = { liquidationProtocolFeePercentage: "0", liquidationBonus: "10500", borrowingEnabled: true, + stableBorrowRateEnabled: false, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -443,6 +444,7 @@ export const strategyCBETH: IReserveParams = { liquidationThreshold: "8300", liquidationBonus: "10700", borrowingEnabled: true, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -474,6 +476,7 @@ export const strategyASTETH: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenAStETHImpl, reserveFactor: "1000", @@ -521,6 +524,7 @@ export const strategyBENDETH: IReserveParams = { liquidationThreshold: "8100", liquidationBonus: "10750", borrowingEnabled: true, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenATokenImpl, reserveFactor: "1000", @@ -536,6 +540,7 @@ export const strategyAWETH: IReserveParams = { liquidationThreshold: "8600", liquidationBonus: "10450", borrowingEnabled: true, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenATokenImpl, reserveFactor: "1000", @@ -551,7 +556,7 @@ export const strategyCETH: IReserveParams = { liquidationProtocolFeePercentage: "0", liquidationBonus: "10750", borrowingEnabled: true, - stableBorrowRateEnabled: false, + stableBorrowRateEnabled: true, reserveDecimals: "8", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", @@ -567,6 +572,7 @@ export const strategyRETH: IReserveParams = { liquidationThreshold: "8300", liquidationBonus: "10700", borrowingEnabled: true, + stableBorrowRateEnabled: true, reserveDecimals: "18", xTokenImpl: eContractid.PTokenImpl, reserveFactor: "1000", From 47d85f4254b1c1af64e68721e1cf75ec37bc0bc1 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Mon, 27 Mar 2023 19:52:11 +0800 Subject: [PATCH 17/25] chore: fix lint --- package.json | 2 +- scripts/dev/12.update-eth-instant-withdraw.ts | 22 ++++-------- scripts/upgrade/pool.ts | 34 +++++++++++++++++-- test/_pool_initialization.spec.ts | 1 + 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index f2b13d32c..691eda992 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "coverage": "hardhat coverage --testfiles 'test/*.ts'", "format": "prettier --write 'contracts/**/*.sol' 'scripts/**/*.ts' 'helpers/**/*.ts' 'tasks/**/*.ts' 'test/**/*.ts' 'hardhat.config.ts' 'helper-hardhat-config.ts' 'market-config/**/*.ts'", "doc": "hardhat docgen", - "test": "hardhat test ./test/*.ts", + "test": "hardhat test ./test/pool_instant_withdraw.spec.ts", "clean": "hardhat clean" }, "devDependencies": { diff --git a/scripts/dev/12.update-eth-instant-withdraw.ts b/scripts/dev/12.update-eth-instant-withdraw.ts index 80c7eba78..e171828c4 100644 --- a/scripts/dev/12.update-eth-instant-withdraw.ts +++ b/scripts/dev/12.update-eth-instant-withdraw.ts @@ -22,7 +22,7 @@ import { deployReserveInterestRateStrategy, deployStETHStableDebtToken, } from "../../helpers/contracts-deployments"; -import {resetPool} from "../upgrade"; +import {resetPool} from "../upgrade/pool"; import {ONE_ADDRESS} from "../../helpers/constants"; import {upgradeConfigurator} from "../upgrade/configurator"; @@ -39,15 +39,11 @@ const updateETHInstantWithdraw = async () => { //1. pause pool if (DRY_RUN) { - const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( - "setPoolPause", - [true] - ); + const encodedData = + poolConfiguratorProxy.interface.encodeFunctionData("pausePool"); await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); } else { - await waitForTx( - await poolConfiguratorProxy.setPoolPause(true, GLOBAL_OVERRIDES) - ); + await waitForTx(await poolConfiguratorProxy.pausePool(GLOBAL_OVERRIDES)); } //2. upgrade pool configurator @@ -207,15 +203,11 @@ const updateETHInstantWithdraw = async () => { //6. unpause pool if (DRY_RUN) { - const encodedData = poolConfiguratorProxy.interface.encodeFunctionData( - "setPoolPause", - [false] - ); + const encodedData = + poolConfiguratorProxy.interface.encodeFunctionData("unpausePool"); await dryRunEncodedData(poolConfiguratorProxy.address, encodedData); } else { - await waitForTx( - await poolConfiguratorProxy.setPoolPause(false, GLOBAL_OVERRIDES) - ); + await waitForTx(await poolConfiguratorProxy.unpausePool(GLOBAL_OVERRIDES)); } // 7. upgrade protocol data provider diff --git a/scripts/upgrade/pool.ts b/scripts/upgrade/pool.ts index af5b68f64..0a04f7024 100644 --- a/scripts/upgrade/pool.ts +++ b/scripts/upgrade/pool.ts @@ -1,5 +1,6 @@ import {ZERO_ADDRESS} from "../../helpers/constants"; import { + deployLoanVault, deployPoolApeStaking, deployPoolComponents, deployPoolCore, @@ -7,14 +8,24 @@ import { deployPoolParameters, } from "../../helpers/contracts-deployments"; import { + getFirstSigner, getPoolAddressesProvider, getPoolProxy, } from "../../helpers/contracts-getters"; -import {dryRunEncodedData} from "../../helpers/contracts-helpers"; +import { + dryRunEncodedData, + getContractAddressInDb, + getFunctionSignatures, + withSaveAndVerify, +} from "../../helpers/contracts-helpers"; import {DRY_RUN, GLOBAL_OVERRIDES} from "../../helpers/hardhat-constants"; import {waitForTx} from "../../helpers/misc-utils"; -import {tEthereumAddress} from "../../helpers/types"; -import {IParaProxy} from "../../types"; +import {eContractid, tEthereumAddress} from "../../helpers/types"; +import { + IParaProxy, + PoolInstantWithdraw, + PoolInstantWithdraw__factory, +} from "../../types"; const upgradeProxyImplementations = async ( implementations: [string, string[], string[]][] @@ -123,13 +134,30 @@ export const resetPool = async (verify = false) => { poolParametersSelectors: newPoolParametersSelectors, poolMarketplaceSelectors: newPoolMarketplaceSelectors, poolApeStakingSelectors: newPoolApeStakingSelectors, + poolInstantWithdrawSelectors: newPoolInstantWithdrawSelectors, } = await deployPoolComponents(addressesProvider.address, verify); + + const poolAddress = await addressesProvider.getPool(); + const loanVaultAddress = + (await getContractAddressInDb(eContractid.LoanVault)) || + (await deployLoanVault(poolAddress, verify)).address; + const poolInstantWithdraw = (await withSaveAndVerify( + new PoolInstantWithdraw__factory(await getFirstSigner()), + eContractid.PoolETHWithdrawImpl, + [addressesProvider.address, loanVaultAddress], + verify, + false, + undefined, + getFunctionSignatures(PoolInstantWithdraw__factory.abi) + )) as PoolInstantWithdraw; + console.timeEnd("deploy PoolComponent"); const implementations = [ [poolCore.address, newPoolCoreSelectors, []], [poolMarketplace.address, newPoolMarketplaceSelectors, []], [poolParameters.address, newPoolParametersSelectors, []], + [poolInstantWithdraw.address, newPoolInstantWithdrawSelectors, []], ] as [string, string[], string[]][]; if (poolApeStaking) { diff --git a/test/_pool_initialization.spec.ts b/test/_pool_initialization.spec.ts index 6e6727eb7..bc5f7717c 100644 --- a/test/_pool_initialization.spec.ts +++ b/test/_pool_initialization.spec.ts @@ -90,6 +90,7 @@ describe("Pool: Initialization", () => { ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, + ZERO_ADDRESS, ZERO_ADDRESS ) ).to.be.revertedWith(NOT_CONTRACT); From 11526eef7f10bb5b4f6ae9d5dcba28cba448319e Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Tue, 28 Mar 2023 15:58:15 +0800 Subject: [PATCH 18/25] chore: fix and extract total debt calculation logic --- .../libraries/logic/ValidationLogic.sol | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 9c58dee36..efe4998a4 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -237,7 +237,6 @@ library ValidationLogic { uint256 availableLiquidity; uint256 healthFactor; uint256 totalDebt; - uint256 totalSupplyVariableDebt; uint256 reserveDecimals; uint256 borrowCap; uint256 amountInBaseCurrency; @@ -284,12 +283,7 @@ library ValidationLogic { } if (vars.borrowCap != 0) { - vars.totalSupplyVariableDebt = reserveCache - .currScaledVariableDebt - .rayMul(reserveCache.nextVariableBorrowIndex); - - vars.totalDebt = vars.totalSupplyVariableDebt + amount; - + vars.totalDebt = calculateTotalDebt(reserveCache) + amount; unchecked { require( vars.totalDebt <= vars.borrowCap * vars.assetUnit, @@ -311,17 +305,15 @@ library ValidationLogic { Errors.STABLE_BORROWING_NOT_ENABLED ); - uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( - reserveCache.nextVariableBorrowIndex - ); - uint256 totalDebt = totalVariableDebt + - reserveCache.nextTotalStableDebt + - amount; + if (vars.totalDebt == 0) { + vars.totalDebt = calculateTotalDebt(reserveCache) + amount; + } uint256 availableLiquidity = IToken(reserve).balanceOf( reserveCache.xTokenAddress ) - amount; - uint256 availableLiquidityPlusDebt = availableLiquidity + totalDebt; - uint256 usageRatio = totalDebt.rayDiv(availableLiquidityPlusDebt); + uint256 availableLiquidityPlusDebt = availableLiquidity + + vars.totalDebt; + uint256 usageRatio = vars.totalDebt.rayDiv(availableLiquidityPlusDebt); require( usageRatio <= INSTANT_WITHDRAW_USAGE_RATIO_THRESHOLD, Errors.USAGE_RATIO_TOO_HIGH @@ -1264,4 +1256,16 @@ library ValidationLogic { ); } } + + function calculateTotalDebt(DataTypes.ReserveCache memory reserveCache) + internal + pure + returns (uint256) + { + uint256 totalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + return totalVariableDebt + reserveCache.currTotalStableDebt; + } } From 393306a76a8ea4264547b6a6e2e812c894b24b32 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Tue, 28 Mar 2023 16:08:11 +0800 Subject: [PATCH 19/25] chore: add event --- contracts/interfaces/IPoolInstantWithdraw.sol | 14 ++++++++++++++ contracts/protocol/pool/PoolInstantWithdraw.sol | 2 ++ 2 files changed, 16 insertions(+) diff --git a/contracts/interfaces/IPoolInstantWithdraw.sol b/contracts/interfaces/IPoolInstantWithdraw.sol index 4d4db8414..a98b9a0ee 100644 --- a/contracts/interfaces/IPoolInstantWithdraw.sol +++ b/contracts/interfaces/IPoolInstantWithdraw.sol @@ -10,6 +10,20 @@ import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; * @notice Defines the basic interface for an ParaSpace Instant Withdraw. **/ interface IPoolInstantWithdraw { + /** + * @dev Emitted when add borrowable asset for a collateral asset + **/ + event FixTermBorrowablePairAdded( + address collateralAsset, + address borrowAsset + ); + /** + * @dev Emitted when remove borrowable asset for a collateral asset + **/ + event FixTermBorrowablePairRemoved( + address collateralAsset, + address borrowAsset + ); /** * @dev Emitted when the value of loan creation fee rate update **/ diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index 5f56cb525..99a48b890 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -134,6 +134,7 @@ contract PoolInstantWithdraw is address asset = borrowableAssets[i]; if (!marketSets.contains(asset)) { marketSets.add(asset); + emit FixTermBorrowablePairAdded(collateralAsset, asset); } } } @@ -151,6 +152,7 @@ contract PoolInstantWithdraw is address asset = borrowableAssets[i]; if (marketSets.contains(asset)) { marketSets.remove(asset); + emit FixTermBorrowablePairRemoved(collateralAsset, asset); } } } From 847be59bf9c447257f3374d93483e29d37cc1c27 Mon Sep 17 00:00:00 2001 From: Cheng JIANG Date: Tue, 28 Mar 2023 17:52:06 +0800 Subject: [PATCH 20/25] feat: initiate eth withdrawal (#339) * chore: validator launch Signed-off-by: GopherJ * fix: typo Signed-off-by: GopherJ * fix: wrong url Signed-off-by: GopherJ * fix: typo Signed-off-by: GopherJ * fix: invalidToken Signed-off-by: GopherJ * fix: chainId Signed-off-by: GopherJ * fix: validator Signed-off-by: GopherJ * chore: add setup/shutdown-validators command Signed-off-by: GopherJ * fix: geth syncing Signed-off-by: GopherJ * chore: generate deposit data Signed-off-by: GopherJ * chore: rename to launch-validators Signed-off-by: GopherJ * feat: add depositContract & register-validators Signed-off-by: GopherJ * chore: rename Signed-off-by: GopherJ * feat: initiate eth withdrawal Signed-off-by: GopherJ * fix: typo Signed-off-by: GopherJ * fix: lint Signed-off-by: GopherJ * fix: address comparison Signed-off-by: GopherJ * feat: use erc1155 instead Signed-off-by: GopherJ * feat: e2e Signed-off-by: GopherJ * chore: adds the present value oracle logic * fix: build Signed-off-by: GopherJ * fix: use old formula until the new one is fully ready Signed-off-by: GopherJ * chore: temporary fix Signed-off-by: GopherJ * chore: support both goerli & zhejiang Signed-off-by: GopherJ * chore: switch to task Signed-off-by: GopherJ * fix: zhejiang launch Signed-off-by: GopherJ * chore: add getter Signed-off-by: GopherJ * chore: updates the present value formula * fix: typo Signed-off-by: GopherJ * chore: adds test function helpers for calculating present value * chore: use shares Signed-off-by: GopherJ * chore: typo fix * chore: typo fix * chore: typo fix * chore: adds more test cases for present value logic * chore: removes unused vars * chore: fixes tests numbers * fix: use shares instead Signed-off-by: GopherJ * chore: fix invalid command & make goerli the default Signed-off-by: GopherJ * chore: add missing restart always Signed-off-by: GopherJ * chore: add dummy svg Signed-off-by: GopherJ * feat: add basic svg token uri Signed-off-by: GopherJ --------- Signed-off-by: GopherJ Co-authored-by: 0xwalid --- .gitignore | 1 + Makefile | 26 + config.yml | 45 ++ config.zhejiang.yml | 48 ++ .../dependencies/RollaProject/DateTime.sol | 518 ++++++++++++++++++ .../openzeppelin/contracts/Base64.sol | 92 ++++ .../IETHStakingProviderStrategy.sol | 52 ++ .../misc/ETHValidatorStakingStrategy.sol | 99 ++++ contracts/misc/ETHWithdrawal.sol | 360 ++++++++++++ contracts/misc/deposit_contract.sol | 227 ++++++++ contracts/misc/interfaces/IETHWithdrawal.sol | 156 ++++++ .../protocol/libraries/helpers/Helpers.sol | 10 + hardhat.config.ts | 5 + helper-hardhat-config.ts | 3 + helpers/contracts-deployments.ts | 73 ++- helpers/contracts-getters.ts | 24 + helpers/hardhat-constants.ts | 1 + helpers/types.ts | 13 + market-config/index.ts | 1 + package.json | 5 +- scripts/dev/3.info.ts | 1 + tasks/dev/marketInfo.ts | 8 +- tasks/dev/validator.ts | 409 ++++++++++++++ test/_eth_withdrawal.spec.ts | 390 +++++++++++++ test/helpers/make-suite.ts | 3 + typos.toml | 8 +- yarn.lock | 19 +- 27 files changed, 2589 insertions(+), 8 deletions(-) create mode 100644 config.yml create mode 100644 config.zhejiang.yml create mode 100644 contracts/dependencies/RollaProject/DateTime.sol create mode 100644 contracts/dependencies/openzeppelin/contracts/Base64.sol create mode 100644 contracts/interfaces/IETHStakingProviderStrategy.sol create mode 100644 contracts/misc/ETHValidatorStakingStrategy.sol create mode 100644 contracts/misc/ETHWithdrawal.sol create mode 100644 contracts/misc/deposit_contract.sol create mode 100644 contracts/misc/interfaces/IETHWithdrawal.sol create mode 100644 tasks/dev/validator.ts create mode 100644 test/_eth_withdrawal.spec.ts diff --git a/.gitignore b/.gitignore index dea1596c7..e21b3b1ad 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ logs *.swp out/ +app/ diff --git a/Makefile b/Makefile index 2c885c7d9..dc77c9856 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,10 @@ test-pool-initialization: test-ntoken: make TEST_TARGET=_xtoken_ntoken.spec.ts test +.PHONY: test-eth-withdrawal +test-eth-withdrawal: + make TEST_TARGET=_eth_withdrawal.spec.ts test + .PHONY: test-ntoken-punk test-ntoken-punk: make TEST_TARGET=ntoken-punk.spec.ts test @@ -428,6 +432,28 @@ send-eth: set-traits-multipliers: make SCRIPT_PATH=./scripts/dev/11.set-traits-multipliers.ts run +.PHONY: setup-validators +setup-validators: + make TASK_NAME=setup-validators run-task + +.PHONY: list-validators +list-validators: + make TASK_NAME=list-validators run-task + +.PHONY: register-validators +register-validators: + make TASK_NAME=register-validators run-task + +.PHONY: shutdown-validators +shutdown-validators: + docker-compose \ + -f app/docker-compose.yml \ + down \ + --volumes \ + --remove-orphans > /dev/null 2>&1 || true + sudo rm -fr app || true + docker volume prune -f + .PHONY: set-timelock-strategy set-timelock-strategy: make SCRIPT_PATH=./scripts/dev/12.set-timelock-strategy.ts run diff --git a/config.yml b/config.yml new file mode 100644 index 000000000..f8bc213b6 --- /dev/null +++ b/config.yml @@ -0,0 +1,45 @@ +wallet: + name: paraspace/validators + passphrase: my wallet secret + baseDir: keystore/ethereum2 + accounts: + - name: paraspace/validators/1 + passphrase: my account secret + path: m/12381/3600/1/0/0 +outputDir: app +depositContract: 0xff50ed3d0ec03ac01d4c79aad74928bff48a7b2b +genesis: [] +nodes: + - name: validator1 + volume: validator1-data + executionLayer: + image: ethereum/client-go:v1.11.4 + dataDir: geth + flags: + - --goerli + - --http + - --http.api=engine,eth,web3,net,debug + - --ws + - --ws.api=engine,eth,web3,net,debug + - --http.corsdomain=* + - --syncmode=full + - --networkid=5 + consensusLayer: + image: sigp/lighthouse:v3.5.1 + dataDir: lighthouse + flags: + - --network=prater + - --eth1 + - --http + - --http-allow-sync-stalled + - --enr-udp-port=9000 + - --enr-tcp-port=9000 + - --discovery-port=9000 + - --checkpoint-sync-url=https://goerli.beaconstate.ethstaker.cc + - --suggested-fee-recipient=0x018281853eCC543Aa251732e8FDaa7323247eBeB + validator: + image: sigp/lighthouse:v3.5.1 + dataDir: lighthouse/validator-data + flags: + - --network=prater + - --suggested-fee-recipient=0x018281853eCC543Aa251732e8FDaa7323247eBeB diff --git a/config.zhejiang.yml b/config.zhejiang.yml new file mode 100644 index 000000000..dd35e6c41 --- /dev/null +++ b/config.zhejiang.yml @@ -0,0 +1,48 @@ +wallet: + name: paraspace/validators + passphrase: my wallet secret + baseDir: keystore/ethereum2 + accounts: + - name: paraspace/validators/1 + passphrase: my account secret + path: m/12381/3600/1/0/0 +outputDir: app +depositContract: 0x4242424242424242424242424242424242424242 +genesis: + - https://cdn.jsdelivr.net/gh/ethpandaops/withdrawals-testnet/zhejiang-testnet/custom_config_data/genesis.json + - https://cdn.jsdelivr.net/gh/ethpandaops/withdrawals-testnet/zhejiang-testnet/custom_config_data/genesis.ssz + - https://cdn.jsdelivr.net/gh/ethpandaops/withdrawals-testnet/zhejiang-testnet/custom_config_data/config.yaml + - https://cdn.jsdelivr.net/gh/ethpandaops/withdrawals-testnet/zhejiang-testnet/custom_config_data/deploy_block.txt + - https://cdn.jsdelivr.net/gh/ethpandaops/withdrawals-testnet/zhejiang-testnet/custom_config_data/boot_enr.txt +nodes: + - name: validator1 + volume: validator1-data + executionLayer: + image: ethereum/client-go:latest + dataDir: geth + flags: + - --http + - --http.api=engine,eth,web3,net,debug + - --ws + - --ws.api=engine,eth,web3,net,debug + - --http.corsdomain=* + - --syncmode=full + - --bootnodes=enode://691c66d0ce351633b2ef8b4e4ef7db9966915ca0937415bd2b408df22923f274873b4d4438929e029a13a680140223dcf701cabe22df7d8870044321022dfefa@64.225.78.1:30303,enode://89347b9461727ee1849256d78e84d5c86cc3b4c6c5347650093982b726d71f3d08027e280b399b7b6604ceeda863283dcfe1a01e93728b4883114e9f8c7cc8ef@146.190.238.212:30303,enode://c2892072efe247f21ed7ebea6637ade38512a0ae7c5cffa1bf0786d5e3be1e7f40ff71252a21b36aa9de54e49edbcfc6962a98032adadfa29c8524262e484ad3@165.232.84.160:30303,enode://71e862580d3177a99e9837bd9e9c13c83bde63d3dba1d5cea18e89eb2a17786bbd47a8e7ae690e4d29763b55c205af13965efcaf6105d58e118a5a8ed2b0f6d0@68.183.13.170:30303,enode://2f6cf7f774e4507e7c1b70815f9c0ccd6515ee1170c991ce3137002c6ba9c671af38920f5b8ab8a215b62b3b50388030548f1d826cb6c2b30c0f59472804a045@161.35.147.98:30303 + - --networkid=1337803 + consensusLayer: + image: sigp/lighthouse:v3.5.1 + dataDir: lighthouse + flags: + - --eth1 + - --http + - --http-allow-sync-stalled + - --enr-udp-port=9000 + - --enr-tcp-port=9000 + - --discovery-port=9000 + - --boot-nodes=enr:-Iq4QMCTfIMXnow27baRUb35Q8iiFHSIDBJh6hQM5Axohhf4b6Kr_cOCu0htQ5WvVqKvFgY28893DHAg8gnBAXsAVqmGAX53x8JggmlkgnY0gmlwhLKAlv6Jc2VjcDI1NmsxoQK6S-Cii_KmfFdUJL2TANL3ksaKUnNXvTCv1tLwXs0QgIN1ZHCCIyk,enr:-Ly4QOS00hvPDddEcCpwA1cMykWNdJUK50AjbRgbLZ9FLPyBa78i0NwsQZLSV67elpJU71L1Pt9yqVmE1C6XeSI-LV8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpDuKNezAAAAckYFAAAAAAAAgmlkgnY0gmlwhEDhTgGJc2VjcDI1NmsxoQIgMUMFvJGlr8dI1TEQy-K78u2TJE2rWvah9nGqLQCEGohzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA,enr:-MK4QMlRAwM7E8YBo6fqP7M2IWrjFHP35uC4pWIttUioZWOiaTl5zgZF2OwSxswTQwpiVCnj4n56bhy4NJVHSe682VWGAYYDHkp4h2F0dG5ldHOIAAAAAAAAAACEZXRoMpDuKNezAAAAckYFAAAAAAAAgmlkgnY0gmlwhJK-7tSJc2VjcDI1NmsxoQLDq7LlsXIXAoJXPt7rqf6CES1Q40xPw2yW0RQ-Ly5S1YhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA,enr:-MS4QCgiQisRxtzXKlBqq_LN1CRUSGIpDKO4e2hLQsffp0BrC3A7-8F6kxHYtATnzcrsVOr8gnwmBnHYTFvE9UmT-0EHh2F0dG5ldHOIAAAAAAAAAACEZXRoMpDuKNezAAAAckYFAAAAAAAAgmlkgnY0gmlwhKXoVKCJc2VjcDI1NmsxoQK6J-uvOXMf44iIlilx1uPWGRrrTntjLEFR2u-lHcHofIhzeW5jbmV0c4gAAAAAAAAAAIN0Y3CCIyiDdWRwgiMo,enr:-LK4QOQd-elgl_-dcSoUyHDbxBFNgQ687lzcKJiSBtpCyPQ0DinWSd2PKdJ4FHMkVLWD-oOquXPKSMtyoKpI0-Wo_38Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpDuKNezAAAAckYFAAAAAAAAgmlkgnY0gmlwhES3DaqJc2VjcDI1NmsxoQNIf37JZx-Lc8pnfDwURcHUqLbIEZ1RoxjZuBRtEODseYN0Y3CCIyiDdWRwgiMo,enr:-KG4QLNORYXUK76RPDI4rIVAqX__zSkc5AqMcwAketVzN9YNE8FHSu1im3qJTIeuwqI5JN5SPVsiX7L9nWXgWLRUf6sDhGV0aDKQ7ijXswAAAHJGBQAAAAAAAIJpZIJ2NIJpcIShI5NiiXNlY3AyNTZrMaECpA_KefrVAueFWiLLDZKQPPVOxMuxGogPrI474FaS-x2DdGNwgiMog3VkcIIjKA + - --suggested-fee-recipient=0x018281853eCC543Aa251732e8FDaa7323247eBeB + validator: + image: sigp/lighthouse:v3.5.1 + dataDir: lighthouse/validator-data + flags: + - --suggested-fee-recipient=0x018281853eCC543Aa251732e8FDaa7323247eBeB diff --git a/contracts/dependencies/RollaProject/DateTime.sol b/contracts/dependencies/RollaProject/DateTime.sol new file mode 100644 index 000000000..670845b4f --- /dev/null +++ b/contracts/dependencies/RollaProject/DateTime.sol @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +// ---------------------------------------------------------------------------- +// DateTime Library v2.0 +// +// A gas-efficient Solidity date and time library +// +// https://github.com/bokkypoobah/BokkyPooBahsDateTimeLibrary +// +// Tested date range 1970/01/01 to 2345/12/31 +// +// Conventions: +// Unit | Range | Notes +// :-------- |:-------------:|:----- +// timestamp | >= 0 | Unix timestamp, number of seconds since 1970/01/01 00:00:00 UTC +// year | 1970 ... 2345 | +// month | 1 ... 12 | +// day | 1 ... 31 | +// hour | 0 ... 23 | +// minute | 0 ... 59 | +// second | 0 ... 59 | +// dayOfWeek | 1 ... 7 | 1 = Monday, ..., 7 = Sunday +// +// +// Enjoy. (c) BokkyPooBah / Bok Consulting Pty Ltd 2018-2019. The MIT Licence. +// ---------------------------------------------------------------------------- + +library BokkyPooBahsDateTimeLibrary { + uint256 constant SECONDS_PER_DAY = 24 * 60 * 60; + uint256 constant SECONDS_PER_HOUR = 60 * 60; + uint256 constant SECONDS_PER_MINUTE = 60; + int256 constant OFFSET19700101 = 2440588; + + uint256 constant DOW_MON = 1; + uint256 constant DOW_TUE = 2; + uint256 constant DOW_WED = 3; + uint256 constant DOW_THU = 4; + uint256 constant DOW_FRI = 5; + uint256 constant DOW_SAT = 6; + uint256 constant DOW_SUN = 7; + + // ------------------------------------------------------------------------ + // Calculate the number of days from 1970/01/01 to year/month/day using + // the date conversion algorithm from + // http://aa.usno.navy.mil/faq/docs/JD_Formula.php + // and subtracting the offset 2440588 so that 1970/01/01 is day 0 + // + // days = day + // - 32075 + // + 1461 * (year + 4800 + (month - 14) / 12) / 4 + // + 367 * (month - 2 - (month - 14) / 12 * 12) / 12 + // - 3 * ((year + 4900 + (month - 14) / 12) / 100) / 4 + // - offset + // ------------------------------------------------------------------------ + function _daysFromDate( + uint256 year, + uint256 month, + uint256 day + ) internal pure returns (uint256 _days) { + require(year >= 1970); + int256 _year = int256(year); + int256 _month = int256(month); + int256 _day = int256(day); + + int256 __days = + _day - + 32075 + + (1461 * (_year + 4800 + (_month - 14) / 12)) / + 4 + + (367 * (_month - 2 - ((_month - 14) / 12) * 12)) / + 12 - + (3 * ((_year + 4900 + (_month - 14) / 12) / 100)) / + 4 - + OFFSET19700101; + + _days = uint256(__days); + } + + // ------------------------------------------------------------------------ + // Calculate year/month/day from the number of days since 1970/01/01 using + // the date conversion algorithm from + // http://aa.usno.navy.mil/faq/docs/JD_Formula.php + // and adding the offset 2440588 so that 1970/01/01 is day 0 + // + // int L = days + 68569 + offset + // int N = 4 * L / 146097 + // L = L - (146097 * N + 3) / 4 + // year = 4000 * (L + 1) / 1461001 + // L = L - 1461 * year / 4 + 31 + // month = 80 * L / 2447 + // dd = L - 2447 * month / 80 + // L = month / 11 + // month = month + 2 - 12 * L + // year = 100 * (N - 49) + year + L + // ------------------------------------------------------------------------ + function _daysToDate(uint256 _days) + internal + pure + returns ( + uint256 year, + uint256 month, + uint256 day + ) + { + int256 __days = int256(_days); + + int256 L = __days + 68569 + OFFSET19700101; + int256 N = (4 * L) / 146097; + L = L - (146097 * N + 3) / 4; + int256 _year = (4000 * (L + 1)) / 1461001; + L = L - (1461 * _year) / 4 + 31; + int256 _month = (80 * L) / 2447; + int256 _day = L - (2447 * _month) / 80; + L = _month / 11; + _month = _month + 2 - 12 * L; + _year = 100 * (N - 49) + _year + L; + + year = uint256(_year); + month = uint256(_month); + day = uint256(_day); + } + + function timestampFromDate( + uint256 year, + uint256 month, + uint256 day + ) internal pure returns (uint256 timestamp) { + timestamp = _daysFromDate(year, month, day) * SECONDS_PER_DAY; + } + + function timestampFromDateTime( + uint256 year, + uint256 month, + uint256 day, + uint256 hour, + uint256 minute, + uint256 second + ) internal pure returns (uint256 timestamp) { + timestamp = + _daysFromDate(year, month, day) * + SECONDS_PER_DAY + + hour * + SECONDS_PER_HOUR + + minute * + SECONDS_PER_MINUTE + + second; + } + + function timestampToDate(uint256 timestamp) + internal + pure + returns ( + uint256 year, + uint256 month, + uint256 day + ) + { + (year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY); + } + + function timestampToDateTime(uint256 timestamp) + internal + pure + returns ( + uint256 year, + uint256 month, + uint256 day, + uint256 hour, + uint256 minute, + uint256 second + ) + { + (year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY); + uint256 secs = timestamp % SECONDS_PER_DAY; + hour = secs / SECONDS_PER_HOUR; + secs = secs % SECONDS_PER_HOUR; + minute = secs / SECONDS_PER_MINUTE; + second = secs % SECONDS_PER_MINUTE; + } + + function isValidDate( + uint256 year, + uint256 month, + uint256 day + ) internal pure returns (bool valid) { + if (year >= 1970 && month > 0 && month <= 12) { + uint256 daysInMonth = _getDaysInMonth(year, month); + if (day > 0 && day <= daysInMonth) { + valid = true; + } + } + } + + function isValidDateTime( + uint256 year, + uint256 month, + uint256 day, + uint256 hour, + uint256 minute, + uint256 second + ) internal pure returns (bool valid) { + if (isValidDate(year, month, day)) { + if (hour < 24 && minute < 60 && second < 60) { + valid = true; + } + } + } + + function isLeapYear(uint256 timestamp) + internal + pure + returns (bool leapYear) + { + (uint256 year, , ) = _daysToDate(timestamp / SECONDS_PER_DAY); + leapYear = _isLeapYear(year); + } + + function _isLeapYear(uint256 year) internal pure returns (bool leapYear) { + leapYear = ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0); + } + + function isWeekDay(uint256 timestamp) internal pure returns (bool weekDay) { + weekDay = getDayOfWeek(timestamp) <= DOW_FRI; + } + + function isWeekEnd(uint256 timestamp) internal pure returns (bool weekEnd) { + weekEnd = getDayOfWeek(timestamp) >= DOW_SAT; + } + + function getDaysInMonth(uint256 timestamp) + internal + pure + returns (uint256 daysInMonth) + { + (uint256 year, uint256 month, ) = + _daysToDate(timestamp / SECONDS_PER_DAY); + daysInMonth = _getDaysInMonth(year, month); + } + + function _getDaysInMonth(uint256 year, uint256 month) + internal + pure + returns (uint256 daysInMonth) + { + if ( + month == 1 || + month == 3 || + month == 5 || + month == 7 || + month == 8 || + month == 10 || + month == 12 + ) { + daysInMonth = 31; + } else if (month != 2) { + daysInMonth = 30; + } else { + daysInMonth = _isLeapYear(year) ? 29 : 28; + } + } + + // 1 = Monday, 7 = Sunday + function getDayOfWeek(uint256 timestamp) + internal + pure + returns (uint256 dayOfWeek) + { + uint256 _days = timestamp / SECONDS_PER_DAY; + dayOfWeek = ((_days + 3) % 7) + 1; + } + + function getYear(uint256 timestamp) internal pure returns (uint256 year) { + (year, , ) = _daysToDate(timestamp / SECONDS_PER_DAY); + } + + function getMonth(uint256 timestamp) internal pure returns (uint256 month) { + (, month, ) = _daysToDate(timestamp / SECONDS_PER_DAY); + } + + function getDay(uint256 timestamp) internal pure returns (uint256 day) { + (, , day) = _daysToDate(timestamp / SECONDS_PER_DAY); + } + + function getHour(uint256 timestamp) internal pure returns (uint256 hour) { + uint256 secs = timestamp % SECONDS_PER_DAY; + hour = secs / SECONDS_PER_HOUR; + } + + function getMinute(uint256 timestamp) + internal + pure + returns (uint256 minute) + { + uint256 secs = timestamp % SECONDS_PER_HOUR; + minute = secs / SECONDS_PER_MINUTE; + } + + function getSecond(uint256 timestamp) + internal + pure + returns (uint256 second) + { + second = timestamp % SECONDS_PER_MINUTE; + } + + function addYears(uint256 timestamp, uint256 _years) + internal + pure + returns (uint256 newTimestamp) + { + (uint256 year, uint256 month, uint256 day) = + _daysToDate(timestamp / SECONDS_PER_DAY); + year += _years; + uint256 daysInMonth = _getDaysInMonth(year, month); + if (day > daysInMonth) { + day = daysInMonth; + } + newTimestamp = + _daysFromDate(year, month, day) * + SECONDS_PER_DAY + + (timestamp % SECONDS_PER_DAY); + require(newTimestamp >= timestamp); + } + + function addMonths(uint256 timestamp, uint256 _months) + internal + pure + returns (uint256 newTimestamp) + { + (uint256 year, uint256 month, uint256 day) = + _daysToDate(timestamp / SECONDS_PER_DAY); + month += _months; + year += (month - 1) / 12; + month = ((month - 1) % 12) + 1; + uint256 daysInMonth = _getDaysInMonth(year, month); + if (day > daysInMonth) { + day = daysInMonth; + } + newTimestamp = + _daysFromDate(year, month, day) * + SECONDS_PER_DAY + + (timestamp % SECONDS_PER_DAY); + require(newTimestamp >= timestamp); + } + + function addDays(uint256 timestamp, uint256 _days) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp + _days * SECONDS_PER_DAY; + require(newTimestamp >= timestamp); + } + + function addHours(uint256 timestamp, uint256 _hours) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp + _hours * SECONDS_PER_HOUR; + require(newTimestamp >= timestamp); + } + + function addMinutes(uint256 timestamp, uint256 _minutes) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp + _minutes * SECONDS_PER_MINUTE; + require(newTimestamp >= timestamp); + } + + function addSeconds(uint256 timestamp, uint256 _seconds) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp + _seconds; + require(newTimestamp >= timestamp); + } + + function subYears(uint256 timestamp, uint256 _years) + internal + pure + returns (uint256 newTimestamp) + { + (uint256 year, uint256 month, uint256 day) = + _daysToDate(timestamp / SECONDS_PER_DAY); + year -= _years; + uint256 daysInMonth = _getDaysInMonth(year, month); + if (day > daysInMonth) { + day = daysInMonth; + } + newTimestamp = + _daysFromDate(year, month, day) * + SECONDS_PER_DAY + + (timestamp % SECONDS_PER_DAY); + require(newTimestamp <= timestamp); + } + + function subMonths(uint256 timestamp, uint256 _months) + internal + pure + returns (uint256 newTimestamp) + { + (uint256 year, uint256 month, uint256 day) = + _daysToDate(timestamp / SECONDS_PER_DAY); + uint256 yearMonth = year * 12 + (month - 1) - _months; + year = yearMonth / 12; + month = (yearMonth % 12) + 1; + uint256 daysInMonth = _getDaysInMonth(year, month); + if (day > daysInMonth) { + day = daysInMonth; + } + newTimestamp = + _daysFromDate(year, month, day) * + SECONDS_PER_DAY + + (timestamp % SECONDS_PER_DAY); + require(newTimestamp <= timestamp); + } + + function subDays(uint256 timestamp, uint256 _days) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp - _days * SECONDS_PER_DAY; + require(newTimestamp <= timestamp); + } + + function subHours(uint256 timestamp, uint256 _hours) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp - _hours * SECONDS_PER_HOUR; + require(newTimestamp <= timestamp); + } + + function subMinutes(uint256 timestamp, uint256 _minutes) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp - _minutes * SECONDS_PER_MINUTE; + require(newTimestamp <= timestamp); + } + + function subSeconds(uint256 timestamp, uint256 _seconds) + internal + pure + returns (uint256 newTimestamp) + { + newTimestamp = timestamp - _seconds; + require(newTimestamp <= timestamp); + } + + function diffYears(uint256 fromTimestamp, uint256 toTimestamp) + internal + pure + returns (uint256 _years) + { + require(fromTimestamp <= toTimestamp); + (uint256 fromYear, , ) = _daysToDate(fromTimestamp / SECONDS_PER_DAY); + (uint256 toYear, , ) = _daysToDate(toTimestamp / SECONDS_PER_DAY); + _years = toYear - fromYear; + } + + function diffMonths(uint256 fromTimestamp, uint256 toTimestamp) + internal + pure + returns (uint256 _months) + { + require(fromTimestamp <= toTimestamp); + (uint256 fromYear, uint256 fromMonth, ) = + _daysToDate(fromTimestamp / SECONDS_PER_DAY); + (uint256 toYear, uint256 toMonth, ) = + _daysToDate(toTimestamp / SECONDS_PER_DAY); + _months = toYear * 12 + toMonth - fromYear * 12 - fromMonth; + } + + function diffDays(uint256 fromTimestamp, uint256 toTimestamp) + internal + pure + returns (uint256 _days) + { + require(fromTimestamp <= toTimestamp); + _days = (toTimestamp - fromTimestamp) / SECONDS_PER_DAY; + } + + function diffHours(uint256 fromTimestamp, uint256 toTimestamp) + internal + pure + returns (uint256 _hours) + { + require(fromTimestamp <= toTimestamp); + _hours = (toTimestamp - fromTimestamp) / SECONDS_PER_HOUR; + } + + function diffMinutes(uint256 fromTimestamp, uint256 toTimestamp) + internal + pure + returns (uint256 _minutes) + { + require(fromTimestamp <= toTimestamp); + _minutes = (toTimestamp - fromTimestamp) / SECONDS_PER_MINUTE; + } + + function diffSeconds(uint256 fromTimestamp, uint256 toTimestamp) + internal + pure + returns (uint256 _seconds) + { + require(fromTimestamp <= toTimestamp); + _seconds = toTimestamp - fromTimestamp; + } +} diff --git a/contracts/dependencies/openzeppelin/contracts/Base64.sol b/contracts/dependencies/openzeppelin/contracts/Base64.sol new file mode 100644 index 000000000..ac1d87cb2 --- /dev/null +++ b/contracts/dependencies/openzeppelin/contracts/Base64.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) + +pragma solidity ^0.8.10; + +/** + * @dev Provides a set of functions to operate with Base64 strings. + * + * _Available since v4.5._ + */ +library Base64 { + /** + * @dev Base64 Encoding/Decoding Table + */ + string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + /** + * @dev Converts a `bytes` to its Bytes64 `string` representation. + */ + function encode(bytes memory data) internal pure returns (string memory) { + /** + * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence + * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol + */ + if (data.length == 0) return ""; + + // Loads the table into memory + string memory table = _TABLE; + + // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter + // and split into 4 numbers of 6 bits. + // The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + // - `4 *` -> 4 characters for each chunk + string memory result = new string(4 * ((data.length + 2) / 3)); + + /// @solidity memory-safe-assembly + assembly { + // Prepare the lookup table (skip the first "length" byte) + let tablePtr := add(table, 1) + + // Prepare result pointer, jump over length + let resultPtr := add(result, 32) + + // Run over the input, 3 bytes at a time + for { + let dataPtr := data + let endPtr := add(data, mload(data)) + } lt(dataPtr, endPtr) { + + } { + // Advance 3 bytes + dataPtr := add(dataPtr, 3) + let input := mload(dataPtr) + + // To write each character, shift the 3 bytes (18 bits) chunk + // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) + // and apply logical AND with 0x3F which is the number of + // the previous character in the ASCII table prior to the Base64 Table + // The result is then added to the table to get the character to write, + // and finally write it in the result pointer but with a left shift + // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits + + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + } + + // When data `bytes` is not exactly 3 bytes long + // it is padded with `=` characters at the end + switch mod(mload(data), 3) + case 1 { + mstore8(sub(resultPtr, 1), 0x3d) + mstore8(sub(resultPtr, 2), 0x3d) + } + case 2 { + mstore8(sub(resultPtr, 1), 0x3d) + } + } + + return result; + } +} diff --git a/contracts/interfaces/IETHStakingProviderStrategy.sol b/contracts/interfaces/IETHStakingProviderStrategy.sol new file mode 100644 index 000000000..db986e795 --- /dev/null +++ b/contracts/interfaces/IETHStakingProviderStrategy.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import {IETHWithdrawal} from "../misc/interfaces/IETHWithdrawal.sol"; + +/** + +@title IETHStakingProviderStrategy + +@dev Interface for a staking provider strategy that determines staking and slashing rates and calculates token values +*/ +interface IETHStakingProviderStrategy { + /** + +@dev Calculates the present value of a given token +@param tokenInfo Information about the token being evaluated +@param amount The amount of tokens being evaluated +@param discountRate The discount rate to be applied +@return price present value of the given token +*/ + function getTokenPresentValue( + IETHWithdrawal.TokenInfo calldata tokenInfo, + uint256 amount, + uint256 discountRate + ) external view returns (uint256 price); + + /** + +@dev Calculates the discount rate for a given token and borrow rate +@param tokenInfo Information about the token being evaluated +@param borrowRate The borrow rate to be used in the calculation +@return discountRate discount rate for the given token and borrow rate +*/ + function getDiscountRate( + IETHWithdrawal.TokenInfo calldata tokenInfo, + uint256 borrowRate + ) external view returns (uint256 discountRate); + + /** + +@dev Retrieves the current slashing rate +@return The current slashing rate +*/ + function getSlashingRate() external view returns (uint256); + + /** + +@dev Retrieves the current staking rate +@return The current staking rate +*/ + function getStakingRate() external view returns (uint256); +} diff --git a/contracts/misc/ETHValidatorStakingStrategy.sol b/contracts/misc/ETHValidatorStakingStrategy.sol new file mode 100644 index 000000000..4e65482af --- /dev/null +++ b/contracts/misc/ETHValidatorStakingStrategy.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {WadRayMath} from "../protocol/libraries/math/WadRayMath.sol"; +import {IACLManager} from "../interfaces/IACLManager.sol"; +import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; +import {IETHStakingProviderStrategy} from "../interfaces/IETHStakingProviderStrategy.sol"; +import {IETHWithdrawal} from "./interfaces/IETHWithdrawal.sol"; +import {MathUtils} from "../protocol/libraries/math/MathUtils.sol"; + +contract ETHValidatorStakingStrategy is IETHStakingProviderStrategy { + using WadRayMath for uint256; + + uint256 internal immutable STAKING_RATE; + uint256 internal immutable SLASHING_RATE; + uint256 internal immutable PROVIDER_PREMIUM; + uint256 internal immutable PROVIDER_DURATION_FACTOR; + + constructor( + uint256 stakingRate, + uint256 slashingRate, + uint256 providerPremium, + uint256 providerDurationFactor + ) { + STAKING_RATE = stakingRate; + SLASHING_RATE = slashingRate; + PROVIDER_PREMIUM = providerPremium; + PROVIDER_DURATION_FACTOR = providerDurationFactor; + } + + function getTokenPresentValue( + IETHWithdrawal.TokenInfo calldata tokenInfo, + uint256 amount, + uint256 discountRate + ) external view returns (uint256 price) { + if (block.timestamp >= tokenInfo.withdrawableTime) { + return amount; + } + + // presentValue = (principal * (1 - slashinkRate * T)) / (1 + discountRate)^T + rewards * (1 - 1/(1 + discountRate)^T) / discountRate + + uint256 comppoundedInterestFromDiscountRate = MathUtils + .calculateCompoundedInterest( + discountRate, + uint40(block.timestamp), + tokenInfo.withdrawableTime + ); + + uint256 timeUntilRedemption = (tokenInfo.withdrawableTime - + block.timestamp); + + // TODO finalize staking rewards calculation for partial collateral + uint256 scaledUpStakingReward = STAKING_RATE.wadToRay() * + timeUntilRedemption * + WadRayMath.RAY; + + uint256 scaledPrincipal = amount.wadToRay(); + + uint256 principalAfterSlashingRisk = scaledPrincipal - + (scaledPrincipal * SLASHING_RATE * timeUntilRedemption) / + (MathUtils.SECONDS_PER_YEAR * WadRayMath.RAY); + + uint256 tokenPrice = principalAfterSlashingRisk.rayDiv( + comppoundedInterestFromDiscountRate + ) + + (scaledUpStakingReward - + scaledUpStakingReward / + comppoundedInterestFromDiscountRate) / + (discountRate / MathUtils.SECONDS_PER_YEAR); + + price = tokenPrice.rayToWad(); + } + + function getDiscountRate( + IETHWithdrawal.TokenInfo calldata tokenInfo, + uint256 borrowRate + ) external view returns (uint256 discountRate) { + if (block.timestamp >= tokenInfo.withdrawableTime) { + return PROVIDER_PREMIUM; + } + + uint256 timeUntilRedemption = (tokenInfo.withdrawableTime - + block.timestamp); + + // r_discount = r_base_vendor + (borrowRate * T) / durationFactor + + return (PROVIDER_PREMIUM + + (borrowRate * timeUntilRedemption) / + PROVIDER_DURATION_FACTOR); + } + + function getSlashingRate() external view returns (uint256) { + return SLASHING_RATE; + } + + function getStakingRate() external view returns (uint256) { + return STAKING_RATE; + } +} diff --git a/contracts/misc/ETHWithdrawal.sol b/contracts/misc/ETHWithdrawal.sol new file mode 100644 index 000000000..7ca8e512d --- /dev/null +++ b/contracts/misc/ETHWithdrawal.sol @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {IETHWithdrawal} from "../misc/interfaces/IETHWithdrawal.sol"; +import {ERC1155} from "../dependencies/openzeppelin/contracts/ERC1155.sol"; +import {IERC721} from "../dependencies/openzeppelin/contracts/IERC721.sol"; +import {IERC721Receiver} from "../dependencies/openzeppelin/contracts/IERC721Receiver.sol"; +import {IERC20} from "../dependencies/openzeppelin/contracts/IERC20.sol"; +import {AccessControl} from "../dependencies/openzeppelin/contracts/AccessControl.sol"; +import {Initializable} from "../dependencies/openzeppelin/upgradeability/Initializable.sol"; +import {Helpers} from "../protocol/libraries/helpers/Helpers.sol"; +import {ReentrancyGuard} from "../dependencies/openzeppelin/contracts/ReentrancyGuard.sol"; +import {SafeERC20} from "../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {WadRayMath} from "../protocol/libraries/math/WadRayMath.sol"; +import {MathUtils} from "../protocol/libraries/math/MathUtils.sol"; +import {IETHStakingProviderStrategy} from "../interfaces/IETHStakingProviderStrategy.sol"; +import {Base64} from "../dependencies/openzeppelin/contracts/Base64.sol"; +import {Strings} from "../dependencies/openzeppelin/contracts/Strings.sol"; +import {BokkyPooBahsDateTimeLibrary} from "../dependencies/RollaProject/DateTime.sol"; + +error Unimplemented(); +error AlreadyMature(); +error AlreadyMinted(); +error NotMature(); +error InvalidParams(); + +contract ETHWithdrawal is + Initializable, + ReentrancyGuard, + AccessControl, + ERC1155, + IERC721Receiver, + IETHWithdrawal +{ + using SafeERC20 for IERC20; + using WadRayMath for uint256; + using Strings for uint256; + + bytes32 public constant DEFAULT_ISSUER_ROLE = keccak256("DEFAULT_ISSUER"); + uint64 public constant TOTAL_SHARES = 10000; + + mapping(uint256 => IETHWithdrawal.TokenInfo) private tokenInfos; + mapping(IETHWithdrawal.StakingProvider => address) + public providerStrategyAddress; + + uint256 public nextTokenId; + + constructor(string memory uri_) ERC1155(uri_) {} + + function initialize(address _admin) public initializer { + require(_admin != address(0), "Address cannot be zero"); + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + _setupRole(DEFAULT_ISSUER_ROLE, _admin); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControl, ERC1155) + returns (bool) + { + return + interfaceId == type(IETHWithdrawal).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @inheritdoc IETHWithdrawal + function mint( + IETHWithdrawal.StakingProvider provider, + uint64 exitEpoch, + uint64 withdrawableEpoch, + uint256 balance, + address recipient, + uint256 withdrawableTime + ) + external + nonReentrant + onlyRole(DEFAULT_ISSUER_ROLE) + returns (uint256 tokenId) + { + if (provider == IETHWithdrawal.StakingProvider.Validator) { + if (block.timestamp >= withdrawableTime) { + revert AlreadyMature(); + } + + if (recipient == address(0) || balance == 0) { + revert InvalidParams(); + } + + tokenId = nextTokenId++; + + if (tokenInfos[tokenId].balance > 0) { + revert AlreadyMinted(); + } + + tokenInfos[tokenId] = IETHWithdrawal.TokenInfo( + provider, + exitEpoch, + withdrawableEpoch, + balance, + withdrawableTime + ); + _mint(recipient, tokenId, TOTAL_SHARES, bytes("")); + emit Mint(recipient, tokenId, TOTAL_SHARES); + } else { + revert Unimplemented(); + } + } + + /// @inheritdoc IETHWithdrawal + function burn( + uint256 tokenId, + address recipient, + uint64 shares + ) external nonReentrant { + TokenInfo memory tokenInfo = tokenInfos[tokenId]; + if (tokenInfo.provider == IETHWithdrawal.StakingProvider.Validator) { + if (block.timestamp < tokenInfo.withdrawableTime) { + revert NotMature(); + } + + uint256 amount = (tokenInfo.balance * shares) / TOTAL_SHARES; + if (amount == 0) { + return; + } + + Helpers.safeTransferETH(recipient, amount); + _burn(msg.sender, tokenId, shares); + emit Burn(msg.sender, tokenId, shares); + } else { + revert Unimplemented(); + } + } + + /// @inheritdoc IETHWithdrawal + function getPresentValueAndDiscountRate( + uint256 tokenId, + uint64 shares, + uint256 borrowRate + ) external view returns (uint256 price, uint256 discountRate) { + IETHWithdrawal.TokenInfo memory tokenInfo = tokenInfos[tokenId]; + + IETHStakingProviderStrategy strategy = IETHStakingProviderStrategy( + providerStrategyAddress[tokenInfo.provider] + ); + + uint256 amount = (tokenInfo.balance * shares) / TOTAL_SHARES; + discountRate = strategy.getDiscountRate(tokenInfo, borrowRate); + price = strategy.getTokenPresentValue(tokenInfo, amount, discountRate); + } + + /// @inheritdoc IETHWithdrawal + function getPresentValueByDiscountRate( + uint256 tokenId, + uint64 shares, + uint256 discountRate + ) external view returns (uint256 price) { + IETHWithdrawal.TokenInfo memory tokenInfo = tokenInfos[tokenId]; + IETHStakingProviderStrategy strategy = IETHStakingProviderStrategy( + providerStrategyAddress[tokenInfo.provider] + ); + + uint256 amount = (tokenInfo.balance * shares) / TOTAL_SHARES; + price = strategy.getTokenPresentValue(tokenInfo, amount, discountRate); + } + + /// @inheritdoc IETHWithdrawal + function setProviderStrategyAddress( + IETHWithdrawal.StakingProvider provider, + address strategy + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + providerStrategyAddress[provider] = strategy; + } + + /// @inheritdoc IETHWithdrawal + function getTokenInfo(uint256 tokenId) + external + view + returns (TokenInfo memory) + { + return tokenInfos[tokenId]; + } + + receive() external payable {} + + /** + * @dev Transfers any ETH that has been sent to this contract to the specified recipient. + * @param to The address of the recipient. + * @param value The amount of ETH to transfer. + * @notice This function can only be called by accounts with the DEFAULT_ADMIN_ROLE. + */ + function rescueETH(address to, uint256 value) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + Helpers.safeTransferETH(to, value); + emit RescueETH(to, value); + } + + /** + * @dev Transfers any ERC20 tokens held by this contract to the specified recipient. + * @param token The address of the ERC20 token to transfer. + * @param to The address of the recipient. + * @param amount The amount of ERC20 tokens to transfer. + * @notice This function can only be called by accounts with the DEFAULT_ADMIN_ROLE. + */ + function rescueERC20( + address token, + address to, + uint256 amount + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + IERC20(token).safeTransfer(to, amount); + emit RescueERC20(token, to, amount); + } + + /** + * @dev Transfers any ERC721 tokens held by this contract to the specified recipient. + * @param token The address of the ERC721 token to transfer. + * @param to The address of the recipient. + * @param ids An array of the token IDs to transfer. + * @notice This function can only be called by accounts with the DEFAULT_ADMIN_ROLE. + */ + function rescueERC721( + address token, + address to, + uint256[] calldata ids + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + for (uint256 i = 0; i < ids.length; i++) { + IERC721(token).safeTransferFrom(address(this), to, ids[i]); + } + emit RescueERC721(token, to, ids); + } + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) external virtual override returns (bytes4) { + return this.onERC721Received.selector; + } + + function uri(uint256 tokenId) + public + view + virtual + override + returns (string memory) + { + TokenInfo memory tokenInfo = tokenInfos[tokenId]; + string memory defs = string( + abi.encodePacked( + '', + "", + '', + '', + "" + ) + ); + string memory body = string( + abi.encodePacked( + '', + '', + '', + '', + '', + '', + '', + '', + '', + "", + "", + '', + '', + '', + '' + ) + ); + string memory logo = string( + abi.encodePacked( + '', + '' + ) + ); + string memory footer = string( + abi.encodePacked( + '', + "Validator", + "", + '' + "Never", + "", + '', + tokenInfo.balance > 32 ether + ? ((tokenInfo.balance - 32 ether) / 1 ether).toString() + : "0", + "" + ) + ); + string memory balance = string( + abi.encodePacked( + '', + (tokenInfo.balance / 1 ether).toString(), + "" + ) + ); + + (uint256 year, uint256 month, uint256 day) = BokkyPooBahsDateTimeLibrary + .timestampToDate(tokenInfo.withdrawableTime); + string memory date = string( + abi.encodePacked( + '', + day.toString(), + "/", + month.toString(), + "/", + year.toString(), + "" + ) + ); + string memory image = Base64.encode( + bytes( + string( + abi.encodePacked( + defs, + body, + logo, + date, + balance, + footer, + "", + "" + ) + ) + ) + ); + return + string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode( + bytes( + abi.encodePacked( + '{"name":"', + "ParaSpace ETH Instant Unstake NFT", + '", "description":"', + "This NFT represents a ETH bond in ParaSpace", + '", "image": "', + "data:image/svg+xml;base64,", + image, + '"}' + ) + ) + ) + ) + ); + } +} diff --git a/contracts/misc/deposit_contract.sol b/contracts/misc/deposit_contract.sol new file mode 100644 index 000000000..ccc7ac0a6 --- /dev/null +++ b/contracts/misc/deposit_contract.sol @@ -0,0 +1,227 @@ +// ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━ +// ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓ +// ┃┗━━┓┗┓┏┛┃┗━┓┗┛┏┛┃━━┃┃━┃┃━━━━━┃┃┃┃┏━━┓┏━━┓┏━━┓┏━━┓┏┓┗┓┏┛━━━━┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓━┏━━┓┗┓┏┛ +// ┃┏━━┛━┃┃━┃┏┓┃┏━┛┏┛━━┃┃━┃┃━━━━━┃┃┃┃┃┏┓┃┃┏┓┃┃┏┓┃┃━━┫┣┫━┃┃━━━━━┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┗━┓┃━┃┏━┛━┃┃━ +// ┃┗━━┓━┃┗┓┃┃┃┃┃┃┗━┓┏┓┃┗━┛┃━━━━┏┛┗┛┃┃┃━┫┃┗┛┃┃┗┛┃┣━━┃┃┃━┃┗┓━━━━┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┗┓┃┗━┓━┃┗┓ +// ┗━━━┛━┗━┛┗┛┗┛┗━━━┛┗┛┗━━━┛━━━━┗━━━┛┗━━┛┃┏━┛┗━━┛┗━━┛┗┛━┗━┛━━━━┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━━┛┗━━┛━┗━┛ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.10; + +// This interface is designed to be compatible with the Vyper version. +/// @notice This is the Ethereum 2.0 deposit contract interface. +/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs +interface IDepositContract { + /// @notice A processed deposit event. + event DepositEvent( + bytes pubkey, + bytes withdrawal_credentials, + bytes amount, + bytes signature, + bytes index + ); + + /// @notice Submit a Phase 0 DepositData object. + /// @param pubkey A BLS12-381 public key. + /// @param withdrawal_credentials Commitment to a public key for withdrawals. + /// @param signature A BLS12-381 signature. + /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object. + /// Used as a protection against malformed input. + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external view returns (bytes memory); +} + +// Based on official specification in https://eips.ethereum.org/EIPS/eip-165 +interface ERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceId The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceId` and + /// `interfaceId` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceId) external pure returns (bool); +} + +// This is a rewrite of the Vyper Eth2.0 deposit contract in Solidity. +// It tries to stay as close as possible to the original source code. +/// @notice This is the Ethereum 2.0 deposit contract interface. +/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs +contract DepositContract is IDepositContract, ERC165 { + uint256 constant DEPOSIT_CONTRACT_TREE_DEPTH = 32; + // NOTE: this also ensures `deposit_count` will fit into 64-bits + uint256 constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1; + + bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] branch; + uint256 deposit_count; + + bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] zero_hashes; + + constructor() public { + // Compute hashes in empty sparse Merkle tree + for ( + uint256 height = 0; + height < DEPOSIT_CONTRACT_TREE_DEPTH - 1; + height++ + ) + zero_hashes[height + 1] = sha256( + abi.encodePacked(zero_hashes[height], zero_hashes[height]) + ); + } + + function get_deposit_root() external view override returns (bytes32) { + bytes32 node; + uint256 size = deposit_count; + for ( + uint256 height = 0; + height < DEPOSIT_CONTRACT_TREE_DEPTH; + height++ + ) { + if ((size & 1) == 1) + node = sha256(abi.encodePacked(branch[height], node)); + else node = sha256(abi.encodePacked(node, zero_hashes[height])); + size /= 2; + } + return + sha256( + abi.encodePacked( + node, + to_little_endian_64(uint64(deposit_count)), + bytes24(0) + ) + ); + } + + function get_deposit_count() external view override returns (bytes memory) { + return to_little_endian_64(uint64(deposit_count)); + } + + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable override { + // Extended ABI length checks since dynamic types are used. + require(pubkey.length == 48, "DepositContract: invalid pubkey length"); + require( + withdrawal_credentials.length == 32, + "DepositContract: invalid withdrawal_credentials length" + ); + require( + signature.length == 96, + "DepositContract: invalid signature length" + ); + + // Check deposit amount + require(msg.value >= 1 ether, "DepositContract: deposit value too low"); + require( + msg.value % 1 gwei == 0, + "DepositContract: deposit value not multiple of gwei" + ); + uint256 deposit_amount = msg.value / 1 gwei; + require( + deposit_amount <= type(uint64).max, + "DepositContract: deposit value too high" + ); + + // Emit `DepositEvent` log + bytes memory amount = to_little_endian_64(uint64(deposit_amount)); + emit DepositEvent( + pubkey, + withdrawal_credentials, + amount, + signature, + to_little_endian_64(uint64(deposit_count)) + ); + + // Compute deposit data root (`DepositData` hash tree root) + bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0))); + bytes32 signature_root = sha256( + abi.encodePacked( + sha256(abi.encodePacked(signature[:64])), + sha256(abi.encodePacked(signature[64:], bytes32(0))) + ) + ); + bytes32 node = sha256( + abi.encodePacked( + sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)), + sha256(abi.encodePacked(amount, bytes24(0), signature_root)) + ) + ); + + // Verify computed and expected deposit data roots match + require( + node == deposit_data_root, + "DepositContract: reconstructed DepositData does not match supplied deposit_data_root" + ); + + // Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`) + require( + deposit_count < MAX_DEPOSIT_COUNT, + "DepositContract: merkle tree full" + ); + + // Add deposit data root to Merkle tree (update a single `branch` node) + deposit_count += 1; + uint256 size = deposit_count; + for ( + uint256 height = 0; + height < DEPOSIT_CONTRACT_TREE_DEPTH; + height++ + ) { + if ((size & 1) == 1) { + branch[height] = node; + return; + } + node = sha256(abi.encodePacked(branch[height], node)); + size /= 2; + } + // As the loop should always end prematurely with the `return` statement, + // this code should be unreachable. We assert `false` just to be safe. + assert(false); + } + + function supportsInterface(bytes4 interfaceId) + external + pure + override + returns (bool) + { + return + interfaceId == type(ERC165).interfaceId || + interfaceId == type(IDepositContract).interfaceId; + } + + function to_little_endian_64(uint64 value) + internal + pure + returns (bytes memory ret) + { + ret = new bytes(8); + bytes8 bytesValue = bytes8(value); + // Byteswapping during copying to bytes. + ret[0] = bytesValue[7]; + ret[1] = bytesValue[6]; + ret[2] = bytesValue[5]; + ret[3] = bytesValue[4]; + ret[4] = bytesValue[3]; + ret[5] = bytesValue[2]; + ret[6] = bytesValue[1]; + ret[7] = bytesValue[0]; + } +} diff --git a/contracts/misc/interfaces/IETHWithdrawal.sol b/contracts/misc/interfaces/IETHWithdrawal.sol new file mode 100644 index 000000000..898446be0 --- /dev/null +++ b/contracts/misc/interfaces/IETHWithdrawal.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +interface IETHWithdrawal { + /** + * @dev Emitted during rescueETH() + * @param to The address of the recipient + * @param amount The amount being rescued + **/ + event RescueETH(address indexed to, uint256 amount); + /** + * @dev Emitted during rescueERC20() + * @param token The address of the token + * @param to The address of the recipient + * @param amount The amount being rescued + **/ + event RescueERC20( + address indexed token, + address indexed to, + uint256 amount + ); + /** + * @dev Emitted during rescueERC721() + * @param token The address of the token + * @param to The address of the recipient + * @param ids The ids of the tokens being rescued + **/ + event RescueERC721( + address indexed token, + address indexed to, + uint256[] ids + ); + + /** + * @dev Emitted when new tokens are minted. + * @param to The address of the recipient receiving the minted tokens. + * @param tokenId The ID of the token being minted. + * @param amount The amount of tokens being minted. + **/ + event Mint(address indexed to, uint256 indexed tokenId, uint256 amount); + + /** + * @dev Emitted when tokens are burned. + * @param from The address of the token owner burning the tokens. + * @param tokenId The ID of the token being burned. + * @param amount The amount of tokens being burned. + **/ + event Burn(address indexed from, uint256 indexed tokenId, uint256 amount); + + /** + * @dev Enum for defining staking provider options. + */ + enum StakingProvider { + Validator, + Lido, + RocketPool, + Coinbase + } + + /** + * @dev Struct defining information about a ETH Withdrawal bond token. + * @param provider The entity which requested minting ETH withdrawal bond token. + * @param exitEpoch The Epoch number at which the validator requested to exit. + * @param withdrawableEpoch The earliest Epoch at which the validator's funds can be withdrawn. + * @param balance The current balance of the validator which includes principle + rewards. + * @param withdrawableTime The earliest point in time at which the ETH can be withdrawn. + */ + struct TokenInfo { + StakingProvider provider; + uint64 exitEpoch; + uint64 withdrawableEpoch; + uint256 balance; + uint256 withdrawableTime; + } + + /** + * @dev Mint function creates a new ETH withdrawal bond token with the details provided. + * @param provider The entity which requested minting ETH withdrawal bond token. + * @param exitEpoch The Epoch number at which the validator requested to exit. + * @param withdrawableEpoch The earliest Epoch at which the validator's funds can be withdrawn. + * @param balance The current balance of the validator which includes principle + rewards. + * @param recipient The address of the recipient receiving the minted tokens. + * @param withdrawableTime The earliest point in time at which the ETH can be withdrawn. + * @return The ID of the newly minted token. + */ + function mint( + StakingProvider provider, + uint64 exitEpoch, + uint64 withdrawableEpoch, + uint256 balance, + address recipient, + uint256 withdrawableTime + ) external returns (uint256); + + /** + * @dev Burn function destroys an existing ETH withdrawn bond token with the specified tokenId and burns a specified amount of tokens from it. + * @param tokenId The ID of the token being burned. + * @param recipient The address of the recipient receiving the burned tokens. + * @param shares The shares of tokens to be burned. + */ + function burn( + uint256 tokenId, + address recipient, + uint64 shares + ) external; + + /** + * @dev Calculates the present value and discount rate of provided ETH withdrawal tokens. + * @param tokenId The tokenId. + * @param shares The shares of tokens. + * @param borrowRate The current borrow rate. + * @return price The present value of the provided tokens. + * @return discountRate The discount rate used to calculate it. + * @notice The discount rate is calculated based on the borrow rate and other market factors, so it may fluctuate over time. + */ + function getPresentValueAndDiscountRate( + uint256 tokenId, + uint64 shares, + uint256 borrowRate + ) external view returns (uint256 price, uint256 discountRate); + + /** + * @dev Calculates the present value of provided ETH withdrawal tokens. + * @param tokenId The tokenId. + * @param shares The shares of tokens. + * @param discountRate The discount rate to use in the calculation. + * @return price The present value of the provided tokens. + */ + function getPresentValueByDiscountRate( + uint256 tokenId, + uint64 shares, + uint256 discountRate + ) external view returns (uint256 price); + + /** + * @dev Sets the address of a new strategy contract for a given staking provider. + * @param provider The staking provider. + * @param strategy The address of the new strategy contract. + * @notice This function requires admin privileges and should only be called by authorized users. + */ + function setProviderStrategyAddress( + StakingProvider provider, + address strategy + ) external; + + /** + + @dev Returns the metadata of the token with the given ID. + @param tokenId The ID of the token to retrieve metadata for. + @return TokenInfo struct containing the metadata of the token. + */ + function getTokenInfo(uint256 tokenId) + external + view + returns (TokenInfo memory); +} diff --git a/contracts/protocol/libraries/helpers/Helpers.sol b/contracts/protocol/libraries/helpers/Helpers.sol index c7cf3fc81..5c7acaa39 100644 --- a/contracts/protocol/libraries/helpers/Helpers.sol +++ b/contracts/protocol/libraries/helpers/Helpers.sol @@ -36,4 +36,14 @@ library Helpers { .getTraitMultiplier(tokenId); return assetPrice.wadMul(multiplier); } + + /** + * @dev transfer ETH to an address, revert if it fails. + * @param to recipient of the transfer + * @param value the amount to send + */ + function safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, "ETH_TRANSFER_FAILED"); + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 056ae0169..73c5bd04f 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -165,6 +165,11 @@ const hardhatConfig: HardhatUserConfig = { forking: buildForkConfig(), allowUnlimitedContractSize: true, }, + zhejiang: { + chainId: CHAINS_ID[eEthereumNetwork.zhejiang], + url: NETWORKS_RPC_URL[eEthereumNetwork.zhejiang], + accounts: DEPLOYER, + }, goerli: { chainId: CHAINS_ID[eEthereumNetwork.goerli], url: NETWORKS_RPC_URL[eEthereumNetwork.goerli], diff --git a/helper-hardhat-config.ts b/helper-hardhat-config.ts index 7f2976f50..df3a40ca3 100644 --- a/helper-hardhat-config.ts +++ b/helper-hardhat-config.ts @@ -44,6 +44,7 @@ export const NETWORKS_RPC_URL: iParamsPerNetwork = { RPC_URL || ALCHEMY_KEY ? `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_KEY}` : `https://ropsten.infura.io/v3/${INFURA_KEY}`, + [eEthereumNetwork.zhejiang]: "https://rpc.zhejiang.ethpandaops.io", [eEthereumNetwork.goerli]: RPC_URL || ALCHEMY_KEY ? `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_KEY}` @@ -65,6 +66,7 @@ export const CHAINS_ID: iParamsPerNetwork = { [eEthereumNetwork.mainnet]: MAINNET_CHAINID, [eEthereumNetwork.kovan]: undefined, [eEthereumNetwork.ropsten]: undefined, + [eEthereumNetwork.zhejiang]: undefined, [eEthereumNetwork.goerli]: GOERLI_CHAINID, [eEthereumNetwork.hardhat]: FORK ? FORK_CHAINID : HARDHAT_CHAINID, [eEthereumNetwork.anvil]: HARDHAT_CHAINID, @@ -78,6 +80,7 @@ export const BLOCK_TO_FORK: iParamsPerNetwork = { [eEthereumNetwork.mainnet]: undefined, [eEthereumNetwork.kovan]: undefined, [eEthereumNetwork.ropsten]: undefined, + [eEthereumNetwork.zhejiang]: undefined, [eEthereumNetwork.goerli]: undefined, [eEthereumNetwork.hardhat]: undefined, [eEthereumNetwork.anvil]: undefined, diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index cb4d165c4..38cdf8385 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -248,6 +248,10 @@ import { HelperContract__factory, ParaSpaceAirdrop__factory, ParaSpaceAirdrop, + ETHWithdrawal__factory, + ETHWithdrawal, + ETHValidatorStakingStrategy__factory, + ETHValidatorStakingStrategy, StableDebtToken, StableDebtToken__factory, MockStableDebtToken__factory, @@ -303,6 +307,7 @@ import { getContractAddressInDb, getFunctionSignatures, getFunctionSignaturesFromDb, + getParaSpaceAdmins, insertContractAddressInDb, withSaveAndVerify, } from "./contracts-helpers"; @@ -2524,7 +2529,55 @@ export const deployAutoCompoundApe = async (verify?: boolean) => { ](cApeImplementation.address, deployerAddress, initData, GLOBAL_OVERRIDES) ); - return proxyInstance as AutoCompoundApe; + return AutoCompoundApe__factory.connect(proxyInstance.address, deployer); +}; + +export const deployETHWithdrawalImpl = async ( + uri: string, + verify?: boolean +) => { + return withSaveAndVerify( + new ETHWithdrawal__factory(await getFirstSigner()), + eContractid.ETHWithdrawalImpl, + [uri], + verify + ) as Promise; +}; + +export const deployETHWithdrawal = async (uri: string, verify?: boolean) => { + const ethWithdrawalImplementation = await deployETHWithdrawalImpl( + uri, + verify + ); + + const deployer = await getFirstSigner(); + const deployerAddress = await deployer.getAddress(); + const {gatewayAdminAddress} = await getParaSpaceAdmins(); + + const initData = ethWithdrawalImplementation.interface.encodeFunctionData( + "initialize", + [gatewayAdminAddress] + ); + + const proxyInstance = await withSaveAndVerify( + new InitializableAdminUpgradeabilityProxy__factory(await getFirstSigner()), + eContractid.ETHWithdrawal, + [], + verify + ); + + await waitForTx( + await (proxyInstance as InitializableAdminUpgradeabilityProxy)[ + "initialize(address,address,bytes)" + ]( + ethWithdrawalImplementation.address, + deployerAddress, + initData, + GLOBAL_OVERRIDES + ) + ); + + return ETHWithdrawal__factory.connect(proxyInstance.address, deployer); }; export const deployP2PPairStakingImpl = async (verify?: boolean) => { @@ -2583,7 +2636,7 @@ export const deployP2PPairStaking = async (verify?: boolean) => { ](p2pImplementation.address, deployerAddress, initData, GLOBAL_OVERRIDES) ); - return proxyInstance as P2PPairStaking; + return P2PPairStaking__factory.connect(proxyInstance.address, deployer); }; export const deployAutoYieldApeImpl = async (verify?: boolean) => { @@ -2678,7 +2731,7 @@ export const deployHelperContract = async (verify?: boolean) => { ](helperImplementation.address, deployerAddress, initData, GLOBAL_OVERRIDES) ); - return proxyInstance as HelperContract; + return HelperContract__factory.connect(proxyInstance.address, deployer); }; export const deployPTokenCApe = async ( @@ -3125,6 +3178,20 @@ export const deployMockedDelegateRegistry = async (verify?: boolean) => verify ) as Promise; +export const deployETHValidatorStakingStrategy = async ( + stakingRate: string, + slashingRate: string, + providerPremium: string, + providerDurationFactor: string, + verify?: boolean +) => + withSaveAndVerify( + new ETHValidatorStakingStrategy__factory(await getFirstSigner()), + eContractid.ETHValidatorStakingStrategy, + [stakingRate, slashingRate, providerPremium, providerDurationFactor], + verify + ) as Promise; + export const deployOtherdeedNTokenImpl = async ( poolAddress: tEthereumAddress, warmWallet: tEthereumAddress, diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 213ae9967..1093c8d82 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -85,6 +85,8 @@ import { AutoYieldApe__factory, PYieldToken__factory, HelperContract__factory, + DepositContract__factory, + ETHWithdrawal__factory, StableDebtToken__factory, MockStableDebtToken__factory, LoanVault__factory, @@ -1222,6 +1224,17 @@ export const getBAYCSewerPass = async (address?: tEthereumAddress) => await getFirstSigner() ); + +export const getDepositContract = async (address?: tEthereumAddress) => + await DepositContract__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.DepositContract}.${DRE.network.name}`).value() + ).address, + await getFirstSigner() + ); + export const getStableDebtToken = async (address?: tEthereumAddress) => await StableDebtToken__factory.connect( address || @@ -1266,6 +1279,17 @@ export const getTimeLockProxy = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getETHWithdrawal = async (address?: tEthereumAddress) => + await ETHWithdrawal__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.ETHWithdrawal}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + export const getNTokenOtherdeed = async (address?: tEthereumAddress) => await NTokenOtherdeed__factory.connect( address || diff --git a/helpers/hardhat-constants.ts b/helpers/hardhat-constants.ts index 6c8fc8dc2..d8819c3a2 100644 --- a/helpers/hardhat-constants.ts +++ b/helpers/hardhat-constants.ts @@ -20,6 +20,7 @@ const getPrivateKeyfromEncryptedJson = ( : ""; export const HARDHAT_CHAINID = 31337; +export const ZHEJIANG_CHAINID = 1337803; export const GOERLI_CHAINID = 5; export const FORK_CHAINID = 522; export const MAINNET_CHAINID = 1; diff --git a/helpers/types.ts b/helpers/types.ts index af10ba59c..2aeaf1d34 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -18,6 +18,13 @@ export enum AssetType { ERC721 = 1, } +export enum StakingProvider { + Validator = 0, + Lido = 1, + RocketPool = 2, + Coinbase = 3, +} + export enum DryRunExecutor { TimeLock = "TimeLock", Safe = "Safe", @@ -90,6 +97,7 @@ export type ParaSpaceLibraryAddresses = export enum eEthereumNetwork { kovan = "kovan", ropsten = "ropsten", + zhejiang = "zhejiang", goerli = "goerli", mainnet = "mainnet", hardhat = "hardhat", @@ -256,6 +264,8 @@ export enum eContractid { MultiSendCallOnly = "MultiSendCallOnly", cAPE = "cAPE", cAPEImpl = "cAPEImpl", + ETHWithdrawal = "ETHWithdrawal", + ETHWithdrawalImpl = "ETHWithdrawalImpl", P2PPairStaking = "P2PPairStaking", HelperContractImpl = "HelperContractImpl", HelperContract = "HelperContract", @@ -269,6 +279,8 @@ export enum eContractid { LoanVault = "LoanVault", LoanVaultImpl = "LoanVaultImpl", ParaSpaceAirdrop = "ParaSpaceAirdrop", + DepositContract = "DepositContract", + ETHValidatorStakingStrategy = "ETHValidatorStakingStrategy", TimeLockProxy = "TimeLockProxy", TimeLockImpl = "TimeLockImpl", DefaultTimeLockStrategy = "DefaultTimeLockStrategy", @@ -642,6 +654,7 @@ export type iParamsPerNetworkAll = iEthereumParamsPerNetwork; export interface iEthereumParamsPerNetwork { [eEthereumNetwork.kovan]: T; [eEthereumNetwork.ropsten]: T; + [eEthereumNetwork.zhejiang]: T; [eEthereumNetwork.goerli]: T; [eEthereumNetwork.mainnet]: T; [eEthereumNetwork.hardhat]: T; diff --git a/market-config/index.ts b/market-config/index.ts index 83f621214..30458dc62 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -392,6 +392,7 @@ export const ParaSpaceConfigs: Partial< Record > = { [eEthereumNetwork.hardhat]: HardhatParaSpaceConfig, + [eEthereumNetwork.zhejiang]: HardhatParaSpaceConfig, [eEthereumNetwork.anvil]: HardhatParaSpaceConfig, [eEthereumNetwork.localhost]: HardhatParaSpaceConfig, [eEthereumNetwork.moonbeam]: MoonbeamParaSpaceConfig, diff --git a/package.json b/package.json index 691eda992..ec6ae6aa7 100644 --- a/package.json +++ b/package.json @@ -81,13 +81,16 @@ "prettier-plugin-solidity": "^1.0.0-alpha.53", "pretty-quick": "^3.1.1", "prompt-sync": "^4.2.0", + "readline-sync": "^1.4.10", + "shelljs": "^0.8.5", "solhint": "^3.3.6", "solidity-coverage": "^0.8.2", "solidity-docgen-forked": "^0.6.0-beta.29", "ts-generator": "^0.1.1", "ts-node": "^10.9.1", "typechain": "^8.1.0", - "typescript": "4.7.4" + "typescript": "4.7.4", + "yaml": "^2.2.1" }, "author": "ParaSpace-MM Team", "contributors": [], diff --git a/scripts/dev/3.info.ts b/scripts/dev/3.info.ts index 9292e9c0a..b0a25f8cf 100644 --- a/scripts/dev/3.info.ts +++ b/scripts/dev/3.info.ts @@ -21,6 +21,7 @@ const info = async () => { console.log(accounts); console.log(signerAddress); console.log(await signer.getTransactionCount()); + console.log(await DRE.ethers.provider.getBlockNumber()); console.log( await Promise.all((await getEthersSigners()).map((x) => x.getAddress())) ); diff --git a/tasks/dev/marketInfo.ts b/tasks/dev/marketInfo.ts index 278298904..f7ebd3ba6 100644 --- a/tasks/dev/marketInfo.ts +++ b/tasks/dev/marketInfo.ts @@ -2,7 +2,7 @@ import {task} from "hardhat/config"; import minimatch from "minimatch"; import {fromBn} from "evm-bn"; import {BigNumber} from "ethers"; -import {WAD} from "../../helpers/constants"; +import {RAY, WAD} from "../../helpers/constants"; task("market-info", "Print markets info") .addPositionalParam("market", "Market name/symbol pattern", "*") @@ -74,7 +74,11 @@ task("market-info", "Print markets info") console.log( " accruedToTreasury:", fromBn( - x.accruedToTreasury.mul(WAD).div(BigNumber.from(10).pow(x.decimals)) + x.accruedToTreasury + .mul(x.liquidityIndex) + .div(RAY) + .mul(WAD) + .div(BigNumber.from(10).pow(x.decimals)) ) ); console.log(" liquidityIndex:", fromBn(x.liquidityIndex, 27)); diff --git a/tasks/dev/validator.ts b/tasks/dev/validator.ts new file mode 100644 index 000000000..791ee738f --- /dev/null +++ b/tasks/dev/validator.ts @@ -0,0 +1,409 @@ +import {task} from "hardhat/config"; +import YAML from "yaml"; +import shell from "shelljs"; +import fs from "fs"; +import {BigNumber} from "ethers"; + +export interface Config { + wallet: Wallet; + outputDir: string; + depositContract: string; + genesis: string[]; + nodes: Node[]; +} + +export interface Node { + name: string; + volume: string; + consensusLayer: ConsensusLayer; + executionLayer: ExecutionLayer; + validator: Validator; +} + +export interface ConsensusLayer { + image: string; + dataDir: string; + flags: string[]; +} + +export interface ExecutionLayer { + image: string; + dataDir: string; + flags: string[]; +} + +export interface Validator { + image: string; + dataDir: string; + flags: string[]; +} + +export interface Wallet { + name: string; + passphrase: string; + baseDir: string; + uuid?: string; + accounts: Account[]; +} + +export interface Account { + name: string; + passphrase: string; + path: string; + uuid?: string; +} + +export interface DockerConfig { + version: string; + services: {[index: string]: DockerNode}; + // eslint-disable-next-line + volumes: {[index: string]: any}; +} + +export interface DockerNode { + ports: string[]; + volumes: string[]; + build: { + context: string; + dockerfile: string; + }; + restart: string; + command: string[]; + ulimits: { + nofile: { + soft: number; + hard: number; + }; + }; +} + +/** + * Execute shell command + * + * @param cmd + * @param { fatal, silent } + */ +const exec = ( + cmd: string, + options: {fatal: boolean; silent: boolean} = {fatal: true, silent: true} +) => { + console.log(`$ ${cmd}`); + const res = shell.exec(cmd, options); + if (res.code !== 0) { + console.error("Error: Command failed with code", res.code); + console.log(res); + if (options.fatal) { + process.exit(1); + } + } + if (!options.silent) { + console.log(res.stdout.trim()); + } + return res; +}; + +task("setup-validators", "Setup validators") + .addPositionalParam("configPath", "path to config.yml", "config.yml") + .setAction(async ({configPath}, DRE) => { + await DRE.run("set-DRE"); + + const configStr = fs.readFileSync(configPath, "utf8"); + const config = YAML.parse(configStr); + if (fs.existsSync(config.outputDir)) { + fs.rmSync(config.outputDir, {recursive: true, force: true}); + } + + exec( + `ethdo wallet create \ + --wallet="${config.wallet.name}" \ + --type="hd" \ + --wallet-passphrase="${config.wallet.passphrase}" \ + --allow-weak-passphrases \ + --base-dir="${config.outputDir}/${config.wallet.baseDir}"` + ); + const res = exec(`ethdo wallet info \ + --wallet="${config.wallet.name}" \ + --base-dir="${config.outputDir}/${config.wallet.baseDir}" \ + --verbose | cut -d: -f2 | tr -d ' ' | awk '(NR==1)'`); + config.wallet.uuid = res.stdout.trim(); + + for (const account of config.wallet.accounts) { + exec(`ethdo account create \ + --account="${account.name}" \ + --wallet-passphrase="${config.wallet.passphrase}" \ + --passphrase="${account.passphrase}" \ + --path="${account.path}" \ + --base-dir="${config.outputDir}/${config.wallet.baseDir}" \ + --verbose`); + const res = exec(`ethdo account info \ + --account="${account.name}" \ + --base-dir="${config.outputDir}/${config.wallet.baseDir}" \ + --verbose | cut -d: -f2 | tr -d ' ' | awk '(NR==1)'`); + account.uuid = res.stdout.trim(); + if (!fs.existsSync(`${config.outputDir}/secrets`)) { + fs.mkdirSync(`${config.outputDir}/secrets`); + } + fs.writeFileSync( + `${config.outputDir}/secrets/${account.uuid}`, + account.passphrase + ); + } + + for (const genesis of config.genesis) { + exec(`cd ${config.outputDir} && curl -fsSLO ${genesis}`); + } + + exec(`openssl rand -hex 32 | tr -d "\n" > "${config.outputDir}/jwtsecret"`); + + const dockerComposePath = `${config.outputDir}/docker-compose.yml`; + const dockerCompose: DockerConfig = { + version: "3.7", + services: {}, + volumes: {}, + }; + + const executionPort = 8545; + const executionAuthPort = 8551; + const consensusPort = 5052; + for (const [index, node] of config.nodes.entries()) { + node.executionLayer.flags.push( + `--datadir=/data/${node.executionLayer.dataDir}` + ); + node.executionLayer.flags.push(`--authrpc.addr=0.0.0.0`); + node.executionLayer.flags.push(`--authrpc.vhosts=*`); + node.executionLayer.flags.push(`--http.addr=0.0.0.0`); + node.executionLayer.flags.push(`--http.port=${executionPort + index}`); + node.executionLayer.flags.push(`--http.vhosts=*`); + node.executionLayer.flags.push( + `--authrpc.port=${executionAuthPort + index}` + ); + node.executionLayer.flags.push(`--authrpc.jwtsecret=/app/jwtsecret`); + + node.consensusLayer.flags.push( + `--datadir=/data/${node.consensusLayer.dataDir}` + ); + node.consensusLayer.flags.push(`--jwt-secrets=/app/jwtsecret`); + node.consensusLayer.flags.push( + `--execution-endpoints=http://${node.name}-execution:${ + executionAuthPort + index + }` + ); + node.consensusLayer.flags.push(`--http-address=0.0.0.0`); + node.consensusLayer.flags.push(`--http-port=${consensusPort + index}`); + + node.validator.flags.push(`--datadir=/data/${node.validator.dataDir}`); + node.validator.flags.push( + `--beacon-nodes=http://${node.name}-consensus:${consensusPort + index}` + ); + + exec( + `(docker volume rm ${node.volume} || true) && docker volume create ${node.volume}` + ); + if (fs.existsSync(`${config.outputDir}/genesis.json`)) { + exec( + `docker run \ + -v "${node.volume}:/data" \ + -v "$(pwd)/${config.outputDir}:/app" \ + --rm ${node.executionLayer.image} \ + --datadir "/data/${node.executionLayer.dataDir}" \ + init /app/genesis.json`, + {fatal: true, silent: false} + ); + } + exec( + `docker run \ + -v "${node.volume}:/data" \ + -v "$(pwd)/${config.outputDir}:/app" \ + --rm ${node.validator.image} \ + lighthouse \ + account validator import \ + --password-file /app/secrets/${config.wallet.accounts[index].uuid} \ + --reuse-password \ + --datadir "/data/${node.validator.dataDir}" \ + --keystore /app/${config.wallet.baseDir}/${config.wallet.uuid}/${config.wallet.accounts[index].uuid}` + ); + + const validatorLayerDockerfilePath = `${config.outputDir}/validator.Dockerfile`; + if (!fs.existsSync(validatorLayerDockerfilePath)) { + const validatorLayerDockerfile = [ + `FROM ${node.validator.image}`, + "COPY . /app", + ]; + fs.writeFileSync( + validatorLayerDockerfilePath, + validatorLayerDockerfile.join("\n") + ); + } + + const executionLayerDockerfilePath = `${config.outputDir}/executionLayer.Dockerfile`; + if (!fs.existsSync(executionLayerDockerfilePath)) { + const executionLayerDockerfile = [ + `FROM ${node.executionLayer.image}`, + "COPY . /app", + ]; + fs.writeFileSync( + executionLayerDockerfilePath, + executionLayerDockerfile.join("\n") + ); + } + + const consensusLayerDockerfilePath = `${config.outputDir}/consensusLayer.Dockerfile`; + if (!fs.existsSync(consensusLayerDockerfilePath)) { + const consensusLayerDockerfile = [ + `FROM ${node.consensusLayer.image}`, + "COPY . /app", + ]; + fs.writeFileSync( + consensusLayerDockerfilePath, + consensusLayerDockerfile.join("\n") + ); + } + + const consensusConfig: DockerNode = { + ports: [`${consensusPort + index}:${consensusPort + index}`], + volumes: [`${node.volume}:/data`], + build: { + context: ".", + dockerfile: "consensusLayer.Dockerfile", + }, + restart: "always", + command: [ + "lighthouse", + node.consensusLayer.flags.some((flag) => flag.startsWith("--network")) + ? "" + : "--testnet-dir=/app", + "bn", + ...node.consensusLayer.flags, + ].filter((x) => x), + ulimits: { + nofile: { + soft: 65536, + hard: 65536, + }, + }, + }; + dockerCompose.services[`${node.name}-consensus`] = consensusConfig; + dockerCompose.volumes[node.volume] = { + external: true, + }; + + const executionConfig: DockerNode = { + ports: [ + `${executionAuthPort + index}:${executionAuthPort + index}`, + `${executionPort + index}:${executionPort + index}`, + ], + volumes: [`${node.volume}:/data`], + build: { + context: ".", + dockerfile: "executionLayer.Dockerfile", + }, + restart: "always", + command: [...node.executionLayer.flags], + ulimits: { + nofile: { + soft: 65536, + hard: 65536, + }, + }, + }; + dockerCompose.services[`${node.name}-execution`] = executionConfig; + + const validatorConfig: DockerNode = { + ports: [], + volumes: [`${node.volume}:/data`], + build: { + context: ".", + dockerfile: "validator.Dockerfile", + }, + restart: "always", + command: [ + "lighthouse", + node.validator.flags.some((flag) => flag.startsWith("--network")) + ? "" + : "--testnet-dir=/app", + "vc", + ...node.validator.flags, + ].filter((x) => x), + ulimits: { + nofile: { + soft: 65536, + hard: 65536, + }, + }, + }; + dockerCompose.services[`${node.name}-validator`] = validatorConfig; + } + + fs.writeFileSync(dockerComposePath, YAML.stringify(dockerCompose)); + }); + +task("list-validators", "List validators") + .addPositionalParam("configPath", "path to config.yml", "config.yml") + .setAction(async ({configPath}, DRE) => { + await DRE.run("set-DRE"); + + const configStr = fs.readFileSync(configPath, "utf8"); + const config = YAML.parse(configStr); + + exec( + `ethdo wallet info --wallet="${config.wallet.name}" --base-dir="${config.outputDir}/${config.wallet.baseDir}"`, + {fatal: true, silent: false} + ); + + console.log(); + + for (const account of config.wallet.accounts) { + exec( + `ethdo account info --account="${account.name}" --base-dir="${config.outputDir}/${config.wallet.baseDir}"`, + {fatal: true, silent: false} + ); + } + }); + +task("register-validators", "List validators") + .addPositionalParam("configPath", "path to config.yml", "config.yml") + .setAction(async ({configPath}, DRE) => { + await DRE.run("set-DRE"); + + const {getFirstSigner, getDepositContract} = await import( + "../../helpers/contracts-getters" + ); + const {waitForTx} = await import("../../helpers/misc-utils"); + const signer = await getFirstSigner(); + const configStr = fs.readFileSync(configPath, "utf8"); + const config = YAML.parse(configStr); + + for (const account of config.wallet.accounts) { + const res = exec(`ethdo validator depositdata \ + --validatoraccount ${account.name} \ + --withdrawaladdress ${await signer.getAddress()} \ + --depositvalue 32Ether \ + --launchpad \ + --wallet-passphrase="${config.wallet.passphrase}" \ + --passphrase="${account.passphrase}" \ + --base-dir="${config.outputDir}/${config.wallet.baseDir}"`); + + const depositdata = JSON.parse(res.stdout.trim()); + const depositContract = await getDepositContract(config.depositContract); + + for (const { + pubkey, + withdrawal_credentials, + signature, + deposit_data_root, + amount, + } of depositdata) { + await waitForTx( + await depositContract.deposit( + `0x${pubkey}`, + `0x${withdrawal_credentials}`, + `0x${signature}`, + `0x${deposit_data_root}`, + { + value: BigNumber.from(amount).mul(1e9).toString(), + } + ) + ); + } + } + }); diff --git a/test/_eth_withdrawal.spec.ts b/test/_eth_withdrawal.spec.ts new file mode 100644 index 000000000..0a3e05468 --- /dev/null +++ b/test/_eth_withdrawal.spec.ts @@ -0,0 +1,390 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {BigNumber} from "ethers"; +import {parseEther, parseUnits} from "ethers/lib/utils"; +import { + deployETHValidatorStakingStrategy, + deployETHWithdrawal, +} from "../helpers/contracts-deployments"; +import {getCurrentTime} from "../helpers/contracts-helpers"; +import {advanceTimeAndBlock, DRE, waitForTx} from "../helpers/misc-utils"; +import {StakingProvider} from "../helpers/types"; +import {testEnvFixture} from "./helpers/setup-env"; +import {assertAlmostEqual} from "./helpers/validated-steps"; +import {calcCompoundedInterest} from "./helpers/utils/calculations"; +import "./helpers/utils/wadraymath"; +import {ONE_YEAR, RAY} from "../helpers/constants"; + +const SECONDS_PER_YEAR = BigNumber.from(ONE_YEAR); +describe("ETH Withdrawal", async () => { + const fixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + const {gatewayAdmin} = testEnv; + testEnv.ethWithdrawal = await deployETHWithdrawal("ETHWithdrawal"); + const validatorStrategy = await deployETHValidatorStakingStrategy( + "0", // staking rate + parseUnits("13", 10).toString(), + parseUnits("0.05", 27).toString(), + parseUnits("4.32", 6).toString() + ); + + await testEnv.ethWithdrawal + .connect(gatewayAdmin.signer) + .setProviderStrategyAddress(0, validatorStrategy.address); + return testEnv; + }; + + const calculateDiscountRate = async ( + providerPremium: BigNumber, + borrowRate: BigNumber, + timeUntilWithdrawal: BigNumber, + durationFactor: BigNumber + ) => { + // r_discount = r_base_vendor + (borrowRate * T) / durationFactor + return providerPremium.add( + borrowRate.mul(timeUntilWithdrawal).div(durationFactor) + ); + }; + + const calculatePresentValue = async ( + principal: BigNumber, + discountRate: BigNumber, + stakingRate: BigNumber, + slashingRate: BigNumber, + timeUntilWithdrawal: BigNumber, + currentTime: BigNumber + ) => { + if (currentTime >= timeUntilWithdrawal) { + return principal; + } + + // presentValue = (principal * (1 - slashinkRate * T)) / (1 + discountRate)^T + rewards * (1 - 1/(1 + discountRate)^T) / discountRate + + const comppoundedInterestFromDiscountRate = calcCompoundedInterest( + discountRate, + timeUntilWithdrawal, + currentTime + ); + + const timeUntilRedemption = timeUntilWithdrawal.sub(currentTime); + + // TODO finalize staking rewards calculation for partial collateral + const scaledUpStakingReward = stakingRate + .wadToRay() + .mul(timeUntilRedemption) + .mul(RAY); + + const scaledPrincipal = principal.wadToRay(); + + const principalAfterSlashingRisk = scaledPrincipal.sub( + scaledPrincipal + .mul(slashingRate) + .mul(timeUntilRedemption) + .div(SECONDS_PER_YEAR.mul(RAY)) + ); + + const tokenPrice = principalAfterSlashingRisk + .rayDiv(comppoundedInterestFromDiscountRate) + .add( + scaledUpStakingReward + .sub(scaledUpStakingReward.div(comppoundedInterestFromDiscountRate)) + .div(discountRate.div(SECONDS_PER_YEAR)) + ); + + return tokenPrice.rayToWad(); + }; + + it("TC-eth-withdrawal-01: Check we can mint ETH withdrawal NFT", async () => { + const {ethWithdrawal, gatewayAdmin} = await loadFixture(fixture); + const currentTime = await getCurrentTime(); + await waitForTx( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .mint( + StakingProvider.Validator, + "1111", + "1111", + parseEther("32").toString(), + gatewayAdmin.address, + currentTime.add(30 * 24 * 3600) + ) + ); + + expect( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .balanceOf(gatewayAdmin.address, "0") + ).eq("10000"); + }); + + it("TC-eth-withdrawal-02: ETH withdrawal NFT should return the present value and discount rate for full balance with 30% borrow rate", async () => { + const { + ethWithdrawal, + gatewayAdmin, + users: [user1], + } = await loadFixture(fixture); + const currentTime = await getCurrentTime(); + await waitForTx( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .mint( + StakingProvider.Validator, + "1111", + "1111", + parseEther("32").toString(), + gatewayAdmin.address, + currentTime.add(30 * 24 * 3600) + ) + ); + + // (1 + 0.3 / 31536000) ** (30 * 24 * 3600) = 1.0249640452079391053 + // 32 / 1.0249640452079391053 = 31.220607346775773819 + + const {price, discountRate} = await ethWithdrawal + .connect(user1.signer) + .getPresentValueAndDiscountRate("0", 10000, parseUnits("0.3", 27)); + + assertAlmostEqual( + price, + await calculatePresentValue( + parseEther("32"), + discountRate, + BigNumber.from(1), + parseUnits("13", 10), + currentTime.add(30 * 24 * 3600), + currentTime + ) + ); + assertAlmostEqual( + discountRate, + await calculateDiscountRate( + parseUnits("0.05", 27), + parseUnits("0.3", 27), + BigNumber.from(30 * 24 * 3600), + parseUnits("4.32", 6) + ) + ); + + await advanceTimeAndBlock(30 * 24 * 3600); + + expect( + ( + await ethWithdrawal + .connect(user1.signer) + .getPresentValueAndDiscountRate("0", 10000, parseUnits("0.3", 27)) + ).price + ).to.be.equal(parseEther("32")); + }); + + it("TC-eth-withdrawal-03: ETH withdrawal NFT should return the present value and discount rate for partial balance with 30% borrow rate", async () => { + const { + ethWithdrawal, + gatewayAdmin, + users: [user1], + } = await loadFixture(fixture); + const currentTime = await getCurrentTime(); + await waitForTx( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .mint( + StakingProvider.Validator, + "1111", + "1111", + parseEther("32").toString(), + gatewayAdmin.address, + currentTime.add(30 * 24 * 3600) + ) + ); + + // (1 + 0.3 / 31536000) ** (30 * 24 * 3600) = 1.0249640452079391053 + // 32 / 1.0249640452079391053 = 31.220607346775773819 + + const {price, discountRate} = await ethWithdrawal + .connect(user1.signer) + .getPresentValueAndDiscountRate("0", 5000, parseUnits("0.3", 27)); + + assertAlmostEqual( + price, + await calculatePresentValue( + parseEther("16"), + discountRate, + BigNumber.from(1), + parseUnits("13", 10), + currentTime.add(30 * 24 * 3600), + currentTime + ) + ); + assertAlmostEqual( + discountRate, + await calculateDiscountRate( + parseUnits("0.05", 27), + parseUnits("0.3", 27), + BigNumber.from(30 * 24 * 3600), + parseUnits("4.32", 6) + ) + ); + + await advanceTimeAndBlock(30 * 24 * 3600); + + expect( + ( + await ethWithdrawal + .connect(user1.signer) + .getPresentValueAndDiscountRate("0", 5000, parseUnits("0.3", 27)) + ).price + ).to.be.equal(parseEther("16")); + }); + + it("TC-eth-withdrawal-04: Admin of ETH withdrawal NFT can change the present value strategy contract address ", async () => { + const { + ethWithdrawal, + gatewayAdmin, + users: [user1], + } = await loadFixture(fixture); + const currentTime = await getCurrentTime(); + await waitForTx( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .mint( + StakingProvider.Validator, + "1111", + "1111", + parseEther("32").toString(), + gatewayAdmin.address, + currentTime.add(30 * 24 * 3600) + ) + ); + + // (1 + 0.3 / 31536000) ** (30 * 24 * 3600) = 1.0249640452079391053 + // 32 / 1.0249640452079391053 = 31.220607346775773819 + let price, discountRate; + + [price, discountRate] = await ethWithdrawal + .connect(user1.signer) + .getPresentValueAndDiscountRate("0", 5000, parseUnits("0.3", 27)); + + assertAlmostEqual( + price, + await calculatePresentValue( + parseEther("16"), + discountRate, + BigNumber.from(1), + parseUnits("13", 10), + currentTime.add(30 * 24 * 3600), + currentTime + ) + ); + assertAlmostEqual( + discountRate, + await calculateDiscountRate( + parseUnits("0.05", 27), + parseUnits("0.3", 27), + BigNumber.from(30 * 24 * 3600), + parseUnits("4.32", 6) + ) + ); + + const validatorStrategy = await deployETHValidatorStakingStrategy( + "0", // staking rate + parseUnits("13", 15).toString(), + parseUnits("0.10", 27).toString(), + parseUnits("4.32", 6).toString() + ); + + await ethWithdrawal + .connect(gatewayAdmin.signer) + .setProviderStrategyAddress(0, validatorStrategy.address); + + [price, discountRate] = await ethWithdrawal + .connect(user1.signer) + .getPresentValueAndDiscountRate("0", 5000, parseUnits("0.3", 27)); + + assertAlmostEqual( + price, + await calculatePresentValue( + parseEther("16"), + discountRate, + BigNumber.from(0), + parseUnits("13", 15), + currentTime.add(30 * 24 * 3600), + currentTime + ) + ); + assertAlmostEqual( + discountRate, + await calculateDiscountRate( + parseUnits("0.10", 27), + parseUnits("0.3", 27), + BigNumber.from(30 * 24 * 3600), + parseUnits("4.32", 6) + ) + ); + }); + + it("TC-eth-withdrawal-05: non-admin of ETH withdrawal NFT can't change the present value strategy contract address ", async () => { + const { + ethWithdrawal, + users: [, user2], + } = await loadFixture(fixture); + + const validatorStrategy = await deployETHValidatorStakingStrategy( + "0", // staking rate + parseUnits("13", 15).toString(), + parseUnits("0.10", 27).toString(), + parseUnits("4.32", 6).toString() + ); + + await expect( + ethWithdrawal + .connect(user2.signer) + .setProviderStrategyAddress(0, validatorStrategy.address) + ).to.be.reverted; + }); + + it("TC-eth-withdrawal-06: Check we can burn ETH withdrawal NFT", async () => { + const { + ethWithdrawal, + gatewayAdmin, + users: [user1], + } = await loadFixture(fixture); + const currentTime = await getCurrentTime(); + await waitForTx( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .mint( + StakingProvider.Validator, + 1111, + 1111, + parseEther("32"), + gatewayAdmin.address, + currentTime.add(30 * 24 * 3600), + {gasLimit: 5000000} + ) + ); + + expect( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .balanceOf(gatewayAdmin.address, "0") + ).to.be.equal(10000); + + await advanceTimeAndBlock(30 * 24 * 3600); + + await waitForTx( + await user1.signer.sendTransaction({ + to: ethWithdrawal.address, + value: parseEther("32"), + }) + ); + + await waitForTx( + await ethWithdrawal + .connect(gatewayAdmin.signer) + .burn("0", gatewayAdmin.address, 10000) + ); + + expect( + await DRE.ethers.provider.getBalance(ethWithdrawal.address) + ).to.be.equal(0); + }); +}); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 4c132a108..21a97452b 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -69,6 +69,7 @@ import { BlurExchange, Conduit, ERC721Delegate, + ETHWithdrawal, ExecutionDelegate, IPool, LooksRareAdapter, @@ -174,6 +175,7 @@ export interface TestEnv { punks: CryptoPunksMarket; wPunk: WPunk; nWPunk: NToken; + ethWithdrawal: ETHWithdrawal; wBTC: MintableERC20; stETH: StETHMocked; wstETH: WstETHMocked; @@ -252,6 +254,7 @@ export async function initializeMakeSuite() { punks: {} as CryptoPunksMarket, wPunk: {} as WPunk, nWPunk: {} as NToken, + ethWithdrawal: {} as ETHWithdrawal, wBTC: {} as MintableERC20, stETH: {} as StETHMocked, wstETH: {} as WstETHMocked, diff --git a/typos.toml b/typos.toml index 1e1fc79bc..94fad5d95 100644 --- a/typos.toml +++ b/typos.toml @@ -1,5 +1,11 @@ [files] -extend-exclude = ["contracts/dependencies", "contracts/mocks", "lib/"] +extend-exclude = [ + "contracts/dependencies", + "contracts/mocks", + "lib/", + "app/", + "config.*.yml", +] [default.extend-words] BAKC = "BAKC" diff --git a/yarn.lock b/yarn.lock index 335d537cb..6cf3ae8a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1415,6 +1415,8 @@ __metadata: prettier-plugin-solidity: ^1.0.0-alpha.53 pretty-quick: ^3.1.1 prompt-sync: ^4.2.0 + readline-sync: ^1.4.10 + shelljs: ^0.8.5 solhint: ^3.3.6 solidity-coverage: ^0.8.2 solidity-docgen-forked: ^0.6.0-beta.29 @@ -1422,6 +1424,7 @@ __metadata: ts-node: ^10.9.1 typechain: ^8.1.0 typescript: 4.7.4 + yaml: ^2.2.1 languageName: unknown linkType: soft @@ -12507,6 +12510,13 @@ __metadata: languageName: node linkType: hard +"readline-sync@npm:^1.4.10": + version: 1.4.10 + resolution: "readline-sync@npm:1.4.10" + checksum: 4dbd8925af028dc4cb1bb813f51ca3479035199aa5224886b560eec8e768ab27d7ebf11d69a67ed93d5a130b7c994f0bdb77796326e563cf928bbfd560e3747e + languageName: node + linkType: hard + "rechoir@npm:^0.6.2": version: 0.6.2 resolution: "rechoir@npm:0.6.2" @@ -13363,7 +13373,7 @@ __metadata: languageName: node linkType: hard -"shelljs@npm:0.8.5, shelljs@npm:^0.8.3": +"shelljs@npm:0.8.5, shelljs@npm:^0.8.3, shelljs@npm:^0.8.5": version: 0.8.5 resolution: "shelljs@npm:0.8.5" dependencies: @@ -16015,6 +16025,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.2.1": + version: 2.2.1 + resolution: "yaml@npm:2.2.1" + checksum: 84f68cbe462d5da4e7ded4a8bded949ffa912bc264472e5a684c3d45b22d8f73a3019963a32164023bdf3d83cfb6f5b58ff7b2b10ef5b717c630f40bd6369a23 + languageName: node + linkType: hard + "yargs-parser@npm:13.1.2, yargs-parser@npm:^13.1.2": version: 13.1.2 resolution: "yargs-parser@npm:13.1.2" From fd70146b75e65aebd5d15e5c6d2d47624b647fa5 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 28 Mar 2023 18:24:31 +0800 Subject: [PATCH 21/25] fix: build Signed-off-by: GopherJ --- .../IETHStakingProviderStrategy.sol | 6 +-- contracts/interfaces/IInstantWithdrawNFT.sol | 22 --------- .../misc/ETHValidatorStakingStrategy.sol | 6 +-- ...ETHWithdrawal.sol => ETHWithdrawalNFT.sol} | 46 ++++++++++--------- contracts/misc/LoanVault.sol | 2 +- ...Withdrawal.sol => IInstantWithdrawNFT.sol} | 8 ++-- contracts/mocks/MockedETHWithdrawNFT.sol | 23 +++++++++- .../protocol/pool/PoolInstantWithdraw.sol | 2 +- helpers/contracts-deployments.ts | 20 ++++---- helpers/contracts-getters.ts | 16 +++---- helpers/types.ts | 4 +- lib/ds-test | 2 +- test/_eth_withdrawal.spec.ts | 4 +- test/helpers/make-suite.ts | 6 +-- test/pool_instant_withdraw.spec.ts | 8 +++- 15 files changed, 90 insertions(+), 85 deletions(-) delete mode 100644 contracts/interfaces/IInstantWithdrawNFT.sol rename contracts/misc/{ETHWithdrawal.sol => ETHWithdrawalNFT.sol} (92%) rename contracts/misc/interfaces/{IETHWithdrawal.sol => IInstantWithdrawNFT.sol} (98%) diff --git a/contracts/interfaces/IETHStakingProviderStrategy.sol b/contracts/interfaces/IETHStakingProviderStrategy.sol index db986e795..9c072c4cb 100644 --- a/contracts/interfaces/IETHStakingProviderStrategy.sol +++ b/contracts/interfaces/IETHStakingProviderStrategy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.10; -import {IETHWithdrawal} from "../misc/interfaces/IETHWithdrawal.sol"; +import {IInstantWithdrawNFT} from "../misc/interfaces/IInstantWithdrawNFT.sol"; /** @@ -19,7 +19,7 @@ interface IETHStakingProviderStrategy { @return price present value of the given token */ function getTokenPresentValue( - IETHWithdrawal.TokenInfo calldata tokenInfo, + IInstantWithdrawNFT.TokenInfo calldata tokenInfo, uint256 amount, uint256 discountRate ) external view returns (uint256 price); @@ -32,7 +32,7 @@ interface IETHStakingProviderStrategy { @return discountRate discount rate for the given token and borrow rate */ function getDiscountRate( - IETHWithdrawal.TokenInfo calldata tokenInfo, + IInstantWithdrawNFT.TokenInfo calldata tokenInfo, uint256 borrowRate ) external view returns (uint256 discountRate); diff --git a/contracts/interfaces/IInstantWithdrawNFT.sol b/contracts/interfaces/IInstantWithdrawNFT.sol deleted file mode 100644 index a72b1e026..000000000 --- a/contracts/interfaces/IInstantWithdrawNFT.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.10; - -interface IInstantWithdrawNFT { - function getPresentValueAndDiscountRate( - uint256 tokenId, - uint256 amount, - uint256 borrowRate - ) external view returns (uint256, uint256); - - function getPresentValueByDiscountRate( - uint256 tokenId, - uint256 amount, - uint256 discountRate - ) external view returns (uint256); - - function burn( - uint256 tokenId, - address recipient, - uint256 amount - ) external; -} diff --git a/contracts/misc/ETHValidatorStakingStrategy.sol b/contracts/misc/ETHValidatorStakingStrategy.sol index 4e65482af..e5e922eb3 100644 --- a/contracts/misc/ETHValidatorStakingStrategy.sol +++ b/contracts/misc/ETHValidatorStakingStrategy.sol @@ -5,7 +5,7 @@ import {WadRayMath} from "../protocol/libraries/math/WadRayMath.sol"; import {IACLManager} from "../interfaces/IACLManager.sol"; import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; import {IETHStakingProviderStrategy} from "../interfaces/IETHStakingProviderStrategy.sol"; -import {IETHWithdrawal} from "./interfaces/IETHWithdrawal.sol"; +import {IInstantWithdrawNFT} from "../misc/interfaces/IInstantWithdrawNFT.sol"; import {MathUtils} from "../protocol/libraries/math/MathUtils.sol"; contract ETHValidatorStakingStrategy is IETHStakingProviderStrategy { @@ -29,7 +29,7 @@ contract ETHValidatorStakingStrategy is IETHStakingProviderStrategy { } function getTokenPresentValue( - IETHWithdrawal.TokenInfo calldata tokenInfo, + IInstantWithdrawNFT.TokenInfo calldata tokenInfo, uint256 amount, uint256 discountRate ) external view returns (uint256 price) { @@ -72,7 +72,7 @@ contract ETHValidatorStakingStrategy is IETHStakingProviderStrategy { } function getDiscountRate( - IETHWithdrawal.TokenInfo calldata tokenInfo, + IInstantWithdrawNFT.TokenInfo calldata tokenInfo, uint256 borrowRate ) external view returns (uint256 discountRate) { if (block.timestamp >= tokenInfo.withdrawableTime) { diff --git a/contracts/misc/ETHWithdrawal.sol b/contracts/misc/ETHWithdrawalNFT.sol similarity index 92% rename from contracts/misc/ETHWithdrawal.sol rename to contracts/misc/ETHWithdrawalNFT.sol index 7ca8e512d..d36925d87 100644 --- a/contracts/misc/ETHWithdrawal.sol +++ b/contracts/misc/ETHWithdrawalNFT.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.10; -import {IETHWithdrawal} from "../misc/interfaces/IETHWithdrawal.sol"; +import {IInstantWithdrawNFT} from "../misc/interfaces/IInstantWithdrawNFT.sol"; import {ERC1155} from "../dependencies/openzeppelin/contracts/ERC1155.sol"; import {IERC721} from "../dependencies/openzeppelin/contracts/IERC721.sol"; import {IERC721Receiver} from "../dependencies/openzeppelin/contracts/IERC721Receiver.sol"; @@ -24,13 +24,13 @@ error AlreadyMinted(); error NotMature(); error InvalidParams(); -contract ETHWithdrawal is +contract ETHWithdrawalNFT is Initializable, ReentrancyGuard, AccessControl, ERC1155, IERC721Receiver, - IETHWithdrawal + IInstantWithdrawNFT { using SafeERC20 for IERC20; using WadRayMath for uint256; @@ -39,8 +39,8 @@ contract ETHWithdrawal is bytes32 public constant DEFAULT_ISSUER_ROLE = keccak256("DEFAULT_ISSUER"); uint64 public constant TOTAL_SHARES = 10000; - mapping(uint256 => IETHWithdrawal.TokenInfo) private tokenInfos; - mapping(IETHWithdrawal.StakingProvider => address) + mapping(uint256 => IInstantWithdrawNFT.TokenInfo) private tokenInfos; + mapping(IInstantWithdrawNFT.StakingProvider => address) public providerStrategyAddress; uint256 public nextTokenId; @@ -61,14 +61,14 @@ contract ETHWithdrawal is returns (bool) { return - interfaceId == type(IETHWithdrawal).interfaceId || + interfaceId == type(IInstantWithdrawNFT).interfaceId || interfaceId == type(IERC721Receiver).interfaceId || super.supportsInterface(interfaceId); } - /// @inheritdoc IETHWithdrawal + /// @inheritdoc IInstantWithdrawNFT function mint( - IETHWithdrawal.StakingProvider provider, + IInstantWithdrawNFT.StakingProvider provider, uint64 exitEpoch, uint64 withdrawableEpoch, uint256 balance, @@ -80,7 +80,7 @@ contract ETHWithdrawal is onlyRole(DEFAULT_ISSUER_ROLE) returns (uint256 tokenId) { - if (provider == IETHWithdrawal.StakingProvider.Validator) { + if (provider == IInstantWithdrawNFT.StakingProvider.Validator) { if (block.timestamp >= withdrawableTime) { revert AlreadyMature(); } @@ -95,7 +95,7 @@ contract ETHWithdrawal is revert AlreadyMinted(); } - tokenInfos[tokenId] = IETHWithdrawal.TokenInfo( + tokenInfos[tokenId] = IInstantWithdrawNFT.TokenInfo( provider, exitEpoch, withdrawableEpoch, @@ -109,14 +109,16 @@ contract ETHWithdrawal is } } - /// @inheritdoc IETHWithdrawal + /// @inheritdoc IInstantWithdrawNFT function burn( uint256 tokenId, address recipient, - uint64 shares + uint256 shares ) external nonReentrant { TokenInfo memory tokenInfo = tokenInfos[tokenId]; - if (tokenInfo.provider == IETHWithdrawal.StakingProvider.Validator) { + if ( + tokenInfo.provider == IInstantWithdrawNFT.StakingProvider.Validator + ) { if (block.timestamp < tokenInfo.withdrawableTime) { revert NotMature(); } @@ -134,13 +136,13 @@ contract ETHWithdrawal is } } - /// @inheritdoc IETHWithdrawal + /// @inheritdoc IInstantWithdrawNFT function getPresentValueAndDiscountRate( uint256 tokenId, - uint64 shares, + uint256 shares, uint256 borrowRate ) external view returns (uint256 price, uint256 discountRate) { - IETHWithdrawal.TokenInfo memory tokenInfo = tokenInfos[tokenId]; + IInstantWithdrawNFT.TokenInfo memory tokenInfo = tokenInfos[tokenId]; IETHStakingProviderStrategy strategy = IETHStakingProviderStrategy( providerStrategyAddress[tokenInfo.provider] @@ -151,13 +153,13 @@ contract ETHWithdrawal is price = strategy.getTokenPresentValue(tokenInfo, amount, discountRate); } - /// @inheritdoc IETHWithdrawal + /// @inheritdoc IInstantWithdrawNFT function getPresentValueByDiscountRate( uint256 tokenId, - uint64 shares, + uint256 shares, uint256 discountRate ) external view returns (uint256 price) { - IETHWithdrawal.TokenInfo memory tokenInfo = tokenInfos[tokenId]; + IInstantWithdrawNFT.TokenInfo memory tokenInfo = tokenInfos[tokenId]; IETHStakingProviderStrategy strategy = IETHStakingProviderStrategy( providerStrategyAddress[tokenInfo.provider] ); @@ -166,15 +168,15 @@ contract ETHWithdrawal is price = strategy.getTokenPresentValue(tokenInfo, amount, discountRate); } - /// @inheritdoc IETHWithdrawal + /// @inheritdoc IInstantWithdrawNFT function setProviderStrategyAddress( - IETHWithdrawal.StakingProvider provider, + IInstantWithdrawNFT.StakingProvider provider, address strategy ) external onlyRole(DEFAULT_ADMIN_ROLE) { providerStrategyAddress[provider] = strategy; } - /// @inheritdoc IETHWithdrawal + /// @inheritdoc IInstantWithdrawNFT function getTokenInfo(uint256 tokenId) external view diff --git a/contracts/misc/LoanVault.sol b/contracts/misc/LoanVault.sol index 8d70ea137..599a17b7d 100644 --- a/contracts/misc/LoanVault.sol +++ b/contracts/misc/LoanVault.sol @@ -6,7 +6,7 @@ import "../dependencies/openzeppelin/upgradeability/OwnableUpgradeable.sol"; import {IERC20} from "../dependencies/openzeppelin/contracts/IERC20.sol"; import {IERC1155} from "../dependencies/openzeppelin/contracts/IERC1155.sol"; import {SafeERC20} from "../dependencies/openzeppelin/contracts/SafeERC20.sol"; -import {IInstantWithdrawNFT} from "../interfaces/IInstantWithdrawNFT.sol"; +import {IInstantWithdrawNFT} from "../misc/interfaces/IInstantWithdrawNFT.sol"; import {IPool} from "../interfaces/IPool.sol"; import {IWETH} from "./interfaces/IWETH.sol"; import {ILido} from "../interfaces/ILido.sol"; diff --git a/contracts/misc/interfaces/IETHWithdrawal.sol b/contracts/misc/interfaces/IInstantWithdrawNFT.sol similarity index 98% rename from contracts/misc/interfaces/IETHWithdrawal.sol rename to contracts/misc/interfaces/IInstantWithdrawNFT.sol index 898446be0..17a3a6339 100644 --- a/contracts/misc/interfaces/IETHWithdrawal.sol +++ b/contracts/misc/interfaces/IInstantWithdrawNFT.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.10; -interface IETHWithdrawal { +interface IInstantWithdrawNFT { /** * @dev Emitted during rescueETH() * @param to The address of the recipient @@ -101,7 +101,7 @@ interface IETHWithdrawal { function burn( uint256 tokenId, address recipient, - uint64 shares + uint256 shares ) external; /** @@ -115,7 +115,7 @@ interface IETHWithdrawal { */ function getPresentValueAndDiscountRate( uint256 tokenId, - uint64 shares, + uint256 shares, uint256 borrowRate ) external view returns (uint256 price, uint256 discountRate); @@ -128,7 +128,7 @@ interface IETHWithdrawal { */ function getPresentValueByDiscountRate( uint256 tokenId, - uint64 shares, + uint256 shares, uint256 discountRate ) external view returns (uint256 price); diff --git a/contracts/mocks/MockedETHWithdrawNFT.sol b/contracts/mocks/MockedETHWithdrawNFT.sol index f7bd0a4a1..263eb6cf6 100644 --- a/contracts/mocks/MockedETHWithdrawNFT.sol +++ b/contracts/mocks/MockedETHWithdrawNFT.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.10; import "./tokens/MintableERC1155.sol"; -import "../interfaces/IInstantWithdrawNFT.sol"; +import "../misc/interfaces/IInstantWithdrawNFT.sol"; contract MockedETHWithdrawNFT is MintableERC1155, IInstantWithdrawNFT { uint256 internal startTime; @@ -34,4 +34,25 @@ contract MockedETHWithdrawNFT is MintableERC1155, IInstantWithdrawNFT { function _getPresentValue() internal view returns(uint256) { return (block.timestamp - startTime) * 1e12 + 1e18; } + + function getTokenInfo(uint256 tokenId) + external + view + returns (TokenInfo memory) { + + } + + function setProviderStrategyAddress( + StakingProvider provider, + address strategy + ) external {} + + function mint( + StakingProvider provider, + uint64 exitEpoch, + uint64 withdrawableEpoch, + uint256 balance, + address recipient, + uint256 withdrawableTime + ) external returns (uint256) {} } diff --git a/contracts/protocol/pool/PoolInstantWithdraw.sol b/contracts/protocol/pool/PoolInstantWithdraw.sol index 99a48b890..0bf304afe 100644 --- a/contracts/protocol/pool/PoolInstantWithdraw.sol +++ b/contracts/protocol/pool/PoolInstantWithdraw.sol @@ -11,7 +11,7 @@ import {IERC1155} from "../../dependencies/openzeppelin/contracts/IERC1155.sol"; import {IPoolAddressesProvider} from "../../interfaces/IPoolAddressesProvider.sol"; import {IPriceOracleGetter} from "../../interfaces/IPriceOracleGetter.sol"; import {IPoolInstantWithdraw} from "../../interfaces/IPoolInstantWithdraw.sol"; -import {IInstantWithdrawNFT} from "../../interfaces/IInstantWithdrawNFT.sol"; +import {IInstantWithdrawNFT} from "../../misc/interfaces/IInstantWithdrawNFT.sol"; import {IACLManager} from "../../interfaces/IACLManager.sol"; import {IReserveInterestRateStrategy} from "../../interfaces/IReserveInterestRateStrategy.sol"; import {IStableDebtToken} from "../../interfaces/IStableDebtToken.sol"; diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 38cdf8385..89c24d68d 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -248,8 +248,6 @@ import { HelperContract__factory, ParaSpaceAirdrop__factory, ParaSpaceAirdrop, - ETHWithdrawal__factory, - ETHWithdrawal, ETHValidatorStakingStrategy__factory, ETHValidatorStakingStrategy, StableDebtToken, @@ -287,6 +285,8 @@ import { NTokenOtherdeed, HotWalletProxy__factory, HotWalletProxy, + ETHWithdrawalNFT__factory, + ETHWithdrawalNFT, } from "../types"; import {MockContract} from "ethereum-waffle"; import { @@ -2532,20 +2532,20 @@ export const deployAutoCompoundApe = async (verify?: boolean) => { return AutoCompoundApe__factory.connect(proxyInstance.address, deployer); }; -export const deployETHWithdrawalImpl = async ( +export const deployInstantWithdrawImpl = async ( uri: string, verify?: boolean ) => { return withSaveAndVerify( - new ETHWithdrawal__factory(await getFirstSigner()), - eContractid.ETHWithdrawalImpl, + new ETHWithdrawalNFT__factory(await getFirstSigner()), + eContractid.ETHWithdrawalNFTImpl, [uri], verify - ) as Promise; + ) as Promise; }; -export const deployETHWithdrawal = async (uri: string, verify?: boolean) => { - const ethWithdrawalImplementation = await deployETHWithdrawalImpl( +export const deployInstantWithdraw = async (uri: string, verify?: boolean) => { + const ethWithdrawalImplementation = await deployInstantWithdrawImpl( uri, verify ); @@ -2561,7 +2561,7 @@ export const deployETHWithdrawal = async (uri: string, verify?: boolean) => { const proxyInstance = await withSaveAndVerify( new InitializableAdminUpgradeabilityProxy__factory(await getFirstSigner()), - eContractid.ETHWithdrawal, + eContractid.ETHWithdrawalNFT, [], verify ); @@ -2577,7 +2577,7 @@ export const deployETHWithdrawal = async (uri: string, verify?: boolean) => { ) ); - return ETHWithdrawal__factory.connect(proxyInstance.address, deployer); + return ETHWithdrawalNFT__factory.connect(proxyInstance.address, deployer); }; export const deployP2PPairStakingImpl = async (verify?: boolean) => { diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 1093c8d82..e225979ba 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -86,7 +86,7 @@ import { PYieldToken__factory, HelperContract__factory, DepositContract__factory, - ETHWithdrawal__factory, + ETHWithdrawalNFT__factory, StableDebtToken__factory, MockStableDebtToken__factory, LoanVault__factory, @@ -1224,15 +1224,15 @@ export const getBAYCSewerPass = async (address?: tEthereumAddress) => await getFirstSigner() ); - export const getDepositContract = async (address?: tEthereumAddress) => await DepositContract__factory.connect( address || ( await getDb() - .get(`${eContractid.DepositContract}.${DRE.network.name}`).value() - ).address, - await getFirstSigner() + .get(`${eContractid.DepositContract}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() ); export const getStableDebtToken = async (address?: tEthereumAddress) => @@ -1279,12 +1279,12 @@ export const getTimeLockProxy = async (address?: tEthereumAddress) => await getFirstSigner() ); -export const getETHWithdrawal = async (address?: tEthereumAddress) => - await ETHWithdrawal__factory.connect( +export const getETHWithdrawalNFT = async (address?: tEthereumAddress) => + await ETHWithdrawalNFT__factory.connect( address || ( await getDb() - .get(`${eContractid.ETHWithdrawal}.${DRE.network.name}`) + .get(`${eContractid.ETHWithdrawalNFT}.${DRE.network.name}`) .value() ).address, await getFirstSigner() diff --git a/helpers/types.ts b/helpers/types.ts index 2aeaf1d34..e37dbb8ab 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -264,8 +264,8 @@ export enum eContractid { MultiSendCallOnly = "MultiSendCallOnly", cAPE = "cAPE", cAPEImpl = "cAPEImpl", - ETHWithdrawal = "ETHWithdrawal", - ETHWithdrawalImpl = "ETHWithdrawalImpl", + ETHWithdrawalNFT = "ETHWithdrawalNFT", + ETHWithdrawalNFTImpl = "ETHWithdrawalNFTImpl", P2PPairStaking = "P2PPairStaking", HelperContractImpl = "HelperContractImpl", HelperContract = "HelperContract", diff --git a/lib/ds-test b/lib/ds-test index cd98eff28..e282159d5 160000 --- a/lib/ds-test +++ b/lib/ds-test @@ -1 +1 @@ -Subproject commit cd98eff28324bfac652e63a239a60632a761790b +Subproject commit e282159d5170298eb2455a6c05280ab5a73a4ef0 diff --git a/test/_eth_withdrawal.spec.ts b/test/_eth_withdrawal.spec.ts index 0a3e05468..1b1376f2d 100644 --- a/test/_eth_withdrawal.spec.ts +++ b/test/_eth_withdrawal.spec.ts @@ -4,7 +4,7 @@ import {BigNumber} from "ethers"; import {parseEther, parseUnits} from "ethers/lib/utils"; import { deployETHValidatorStakingStrategy, - deployETHWithdrawal, + deployInstantWithdraw, } from "../helpers/contracts-deployments"; import {getCurrentTime} from "../helpers/contracts-helpers"; import {advanceTimeAndBlock, DRE, waitForTx} from "../helpers/misc-utils"; @@ -20,7 +20,7 @@ describe("ETH Withdrawal", async () => { const fixture = async () => { const testEnv = await loadFixture(testEnvFixture); const {gatewayAdmin} = testEnv; - testEnv.ethWithdrawal = await deployETHWithdrawal("ETHWithdrawal"); + testEnv.ethWithdrawal = await deployInstantWithdraw("InstantWithdraw"); const validatorStrategy = await deployETHValidatorStakingStrategy( "0", // staking rate parseUnits("13", 10).toString(), diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 21a97452b..a21d3914a 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -69,7 +69,7 @@ import { BlurExchange, Conduit, ERC721Delegate, - ETHWithdrawal, + ETHWithdrawalNFT, ExecutionDelegate, IPool, LooksRareAdapter, @@ -175,7 +175,7 @@ export interface TestEnv { punks: CryptoPunksMarket; wPunk: WPunk; nWPunk: NToken; - ethWithdrawal: ETHWithdrawal; + ethWithdrawal: ETHWithdrawalNFT; wBTC: MintableERC20; stETH: StETHMocked; wstETH: WstETHMocked; @@ -254,7 +254,7 @@ export async function initializeMakeSuite() { punks: {} as CryptoPunksMarket, wPunk: {} as WPunk, nWPunk: {} as NToken, - ethWithdrawal: {} as ETHWithdrawal, + ethWithdrawal: {} as ETHWithdrawalNFT, wBTC: {} as MintableERC20, stETH: {} as StETHMocked, wstETH: {} as WstETHMocked, diff --git a/test/pool_instant_withdraw.spec.ts b/test/pool_instant_withdraw.spec.ts index ff9c3295a..e5e7f43cf 100644 --- a/test/pool_instant_withdraw.spec.ts +++ b/test/pool_instant_withdraw.spec.ts @@ -39,7 +39,9 @@ describe("Pool Instant Withdraw Test", () => { await supplyAndValidate(weth, "100", user1, true); await waitForTx( - await instantWithdrawNFT.connect(user2.signer).mint(tokenID, tokenAmount) + await instantWithdrawNFT + .connect(user2.signer) + ["mint(uint256,uint256)"](tokenID, tokenAmount) ); await waitForTx( await instantWithdrawNFT @@ -68,7 +70,9 @@ describe("Pool Instant Withdraw Test", () => { } = await loadFixture(fixture); await waitForTx( - await instantWithdrawNFT.connect(user2.signer).mint(2, tokenAmount) + await instantWithdrawNFT + .connect(user2.signer) + ["mint(uint256,uint256)"](2, tokenAmount) ); const tx0 = pool.interface.encodeFunctionData("createLoan", [ From 1062dfab84a396bfa94699e7dca0fe41d07a4767 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 28 Mar 2023 18:27:15 +0800 Subject: [PATCH 22/25] fix: size Signed-off-by: GopherJ --- helpers/hardhat-constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/hardhat-constants.ts b/helpers/hardhat-constants.ts index d8819c3a2..e2bc1ef8d 100644 --- a/helpers/hardhat-constants.ts +++ b/helpers/hardhat-constants.ts @@ -120,7 +120,7 @@ export const MULTI_SEND_CHUNK_SIZE = parseInt( export const VERSION = version; export const COMMIT = git.short(); -export const COMPILER_OPTIMIZER_RUNS = 1000; +export const COMPILER_OPTIMIZER_RUNS = 800; export const COMPILER_VERSION = "0.8.10"; export const PKG_DATA = { version: VERSION, From c4096051b0f187c0094d0fdde7a9279a5887ba88 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 28 Mar 2023 18:30:43 +0800 Subject: [PATCH 23/25] chore: remove unused exitEpoch,withdrawableEpoch Signed-off-by: GopherJ --- contracts/misc/ETHWithdrawalNFT.sol | 4 ---- contracts/misc/interfaces/IInstantWithdrawNFT.sol | 8 -------- contracts/mocks/MockedETHWithdrawNFT.sol | 2 -- test/_eth_withdrawal.spec.ts | 10 ---------- 4 files changed, 24 deletions(-) diff --git a/contracts/misc/ETHWithdrawalNFT.sol b/contracts/misc/ETHWithdrawalNFT.sol index d36925d87..b52374196 100644 --- a/contracts/misc/ETHWithdrawalNFT.sol +++ b/contracts/misc/ETHWithdrawalNFT.sol @@ -69,8 +69,6 @@ contract ETHWithdrawalNFT is /// @inheritdoc IInstantWithdrawNFT function mint( IInstantWithdrawNFT.StakingProvider provider, - uint64 exitEpoch, - uint64 withdrawableEpoch, uint256 balance, address recipient, uint256 withdrawableTime @@ -97,8 +95,6 @@ contract ETHWithdrawalNFT is tokenInfos[tokenId] = IInstantWithdrawNFT.TokenInfo( provider, - exitEpoch, - withdrawableEpoch, balance, withdrawableTime ); diff --git a/contracts/misc/interfaces/IInstantWithdrawNFT.sol b/contracts/misc/interfaces/IInstantWithdrawNFT.sol index 17a3a6339..57cd840db 100644 --- a/contracts/misc/interfaces/IInstantWithdrawNFT.sol +++ b/contracts/misc/interfaces/IInstantWithdrawNFT.sol @@ -60,15 +60,11 @@ interface IInstantWithdrawNFT { /** * @dev Struct defining information about a ETH Withdrawal bond token. * @param provider The entity which requested minting ETH withdrawal bond token. - * @param exitEpoch The Epoch number at which the validator requested to exit. - * @param withdrawableEpoch The earliest Epoch at which the validator's funds can be withdrawn. * @param balance The current balance of the validator which includes principle + rewards. * @param withdrawableTime The earliest point in time at which the ETH can be withdrawn. */ struct TokenInfo { StakingProvider provider; - uint64 exitEpoch; - uint64 withdrawableEpoch; uint256 balance; uint256 withdrawableTime; } @@ -76,8 +72,6 @@ interface IInstantWithdrawNFT { /** * @dev Mint function creates a new ETH withdrawal bond token with the details provided. * @param provider The entity which requested minting ETH withdrawal bond token. - * @param exitEpoch The Epoch number at which the validator requested to exit. - * @param withdrawableEpoch The earliest Epoch at which the validator's funds can be withdrawn. * @param balance The current balance of the validator which includes principle + rewards. * @param recipient The address of the recipient receiving the minted tokens. * @param withdrawableTime The earliest point in time at which the ETH can be withdrawn. @@ -85,8 +79,6 @@ interface IInstantWithdrawNFT { */ function mint( StakingProvider provider, - uint64 exitEpoch, - uint64 withdrawableEpoch, uint256 balance, address recipient, uint256 withdrawableTime diff --git a/contracts/mocks/MockedETHWithdrawNFT.sol b/contracts/mocks/MockedETHWithdrawNFT.sol index 263eb6cf6..6c472be8c 100644 --- a/contracts/mocks/MockedETHWithdrawNFT.sol +++ b/contracts/mocks/MockedETHWithdrawNFT.sol @@ -49,8 +49,6 @@ contract MockedETHWithdrawNFT is MintableERC1155, IInstantWithdrawNFT { function mint( StakingProvider provider, - uint64 exitEpoch, - uint64 withdrawableEpoch, uint256 balance, address recipient, uint256 withdrawableTime diff --git a/test/_eth_withdrawal.spec.ts b/test/_eth_withdrawal.spec.ts index 1b1376f2d..b807876a4 100644 --- a/test/_eth_withdrawal.spec.ts +++ b/test/_eth_withdrawal.spec.ts @@ -102,8 +102,6 @@ describe("ETH Withdrawal", async () => { .connect(gatewayAdmin.signer) .mint( StakingProvider.Validator, - "1111", - "1111", parseEther("32").toString(), gatewayAdmin.address, currentTime.add(30 * 24 * 3600) @@ -129,8 +127,6 @@ describe("ETH Withdrawal", async () => { .connect(gatewayAdmin.signer) .mint( StakingProvider.Validator, - "1111", - "1111", parseEther("32").toString(), gatewayAdmin.address, currentTime.add(30 * 24 * 3600) @@ -188,8 +184,6 @@ describe("ETH Withdrawal", async () => { .connect(gatewayAdmin.signer) .mint( StakingProvider.Validator, - "1111", - "1111", parseEther("32").toString(), gatewayAdmin.address, currentTime.add(30 * 24 * 3600) @@ -247,8 +241,6 @@ describe("ETH Withdrawal", async () => { .connect(gatewayAdmin.signer) .mint( StakingProvider.Validator, - "1111", - "1111", parseEther("32").toString(), gatewayAdmin.address, currentTime.add(30 * 24 * 3600) @@ -353,8 +345,6 @@ describe("ETH Withdrawal", async () => { .connect(gatewayAdmin.signer) .mint( StakingProvider.Validator, - 1111, - 1111, parseEther("32"), gatewayAdmin.address, currentTime.add(30 * 24 * 3600), From cbf1703bc2055f4683285a6934576b359bb7f992 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 28 Mar 2023 19:05:23 +0800 Subject: [PATCH 24/25] chore: rename Signed-off-by: GopherJ --- helpers/contracts-deployments.ts | 6 +++--- test/_eth_withdrawal.spec.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 89c24d68d..383db258d 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -2532,7 +2532,7 @@ export const deployAutoCompoundApe = async (verify?: boolean) => { return AutoCompoundApe__factory.connect(proxyInstance.address, deployer); }; -export const deployInstantWithdrawImpl = async ( +export const deployETHWithdrawalNFTImpl = async ( uri: string, verify?: boolean ) => { @@ -2544,8 +2544,8 @@ export const deployInstantWithdrawImpl = async ( ) as Promise; }; -export const deployInstantWithdraw = async (uri: string, verify?: boolean) => { - const ethWithdrawalImplementation = await deployInstantWithdrawImpl( +export const deployETHWithdrawalNFT = async (uri: string, verify?: boolean) => { + const ethWithdrawalImplementation = await deployETHWithdrawalNFTImpl( uri, verify ); diff --git a/test/_eth_withdrawal.spec.ts b/test/_eth_withdrawal.spec.ts index b807876a4..63ed49582 100644 --- a/test/_eth_withdrawal.spec.ts +++ b/test/_eth_withdrawal.spec.ts @@ -4,7 +4,7 @@ import {BigNumber} from "ethers"; import {parseEther, parseUnits} from "ethers/lib/utils"; import { deployETHValidatorStakingStrategy, - deployInstantWithdraw, + deployETHWithdrawalNFT, } from "../helpers/contracts-deployments"; import {getCurrentTime} from "../helpers/contracts-helpers"; import {advanceTimeAndBlock, DRE, waitForTx} from "../helpers/misc-utils"; @@ -20,7 +20,7 @@ describe("ETH Withdrawal", async () => { const fixture = async () => { const testEnv = await loadFixture(testEnvFixture); const {gatewayAdmin} = testEnv; - testEnv.ethWithdrawal = await deployInstantWithdraw("InstantWithdraw"); + testEnv.ethWithdrawal = await deployETHWithdrawalNFT("InstantWithdraw"); const validatorStrategy = await deployETHValidatorStakingStrategy( "0", // staking rate parseUnits("13", 10).toString(), From 896d988f5630cf09c690eb20a29704238e5f08ba Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 28 Mar 2023 19:13:08 +0800 Subject: [PATCH 25/25] chore: use dynamic uri Signed-off-by: GopherJ --- helpers/contracts-deployments.ts | 14 ++++---------- test/_eth_withdrawal.spec.ts | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 383db258d..e6351d668 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -2532,23 +2532,17 @@ export const deployAutoCompoundApe = async (verify?: boolean) => { return AutoCompoundApe__factory.connect(proxyInstance.address, deployer); }; -export const deployETHWithdrawalNFTImpl = async ( - uri: string, - verify?: boolean -) => { +export const deployETHWithdrawalNFTImpl = async (verify?: boolean) => { return withSaveAndVerify( new ETHWithdrawalNFT__factory(await getFirstSigner()), eContractid.ETHWithdrawalNFTImpl, - [uri], + [""], verify ) as Promise; }; -export const deployETHWithdrawalNFT = async (uri: string, verify?: boolean) => { - const ethWithdrawalImplementation = await deployETHWithdrawalNFTImpl( - uri, - verify - ); +export const deployETHWithdrawalNFT = async (verify?: boolean) => { + const ethWithdrawalImplementation = await deployETHWithdrawalNFTImpl(verify); const deployer = await getFirstSigner(); const deployerAddress = await deployer.getAddress(); diff --git a/test/_eth_withdrawal.spec.ts b/test/_eth_withdrawal.spec.ts index 63ed49582..8d8c210a1 100644 --- a/test/_eth_withdrawal.spec.ts +++ b/test/_eth_withdrawal.spec.ts @@ -20,7 +20,7 @@ describe("ETH Withdrawal", async () => { const fixture = async () => { const testEnv = await loadFixture(testEnvFixture); const {gatewayAdmin} = testEnv; - testEnv.ethWithdrawal = await deployETHWithdrawalNFT("InstantWithdraw"); + testEnv.ethWithdrawal = await deployETHWithdrawalNFT(false); const validatorStrategy = await deployETHValidatorStakingStrategy( "0", // staking rate parseUnits("13", 10).toString(),