diff --git a/contracts/interfaces/IETHStakingProviderStrategy.sol b/contracts/interfaces/IETHStakingProviderStrategy.sol new file mode 100644 index 000000000..abec020e2 --- /dev/null +++ b/contracts/interfaces/IETHStakingProviderStrategy.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +/** + * @title IETHStakingProviderStrategy + * + * @notice Interface for the calculation of current eth derivative parameters + */ +interface IETHStakingProviderStrategy { + struct TokenInfo { + StakingProvider provider; + uint64 exitEpoch; + uint64 withdrawableEpoch; + uint256 balance; + uint256 withdrawableTime; + } + + function getTokenPresentValue(TokenInfo tokenInfo, uint256 discountRate) + external + returns (uint256 price); + + function getDiscountRate(TokenInfo tokenInfo, uint256 borrowRate) + external + returns (uint256 discountRate); + + function getSlashingRisk(uint256 tokenId) + external + returns (uint256 slashingRisk); + + function getStakingRate(uint256 tokenId) + external + returns (uint256 stakingRate); +} diff --git a/contracts/misc/ETHUnstakePresentValue.sol b/contracts/misc/ETHUnstakePresentValue.sol new file mode 100644 index 000000000..38618b10e --- /dev/null +++ b/contracts/misc/ETHUnstakePresentValue.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: AGPL-3.0 +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"; + +contract ETHUnstakePresentValue { + enum StakingProvider { + Validator, + Lido, + RocketPool, + Coinbase + } + + struct ParaETH { + uint256 principal; + uint256 unstakeTime; + StakingProvider provider; + uint256 underlyingTokenId; // in case there's an NFT representing the withdraw + uint256 unclaimedRewards; + } + + struct ProviderConfiguration { + uint64 slashingRisk; + uint64 discountRate; + uint64 stakingRate; + } + + IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; + + mapping(uint256 => ProviderConfiguration) providerConfiguration; + + /** + * @dev Constructor. + * @param provider The address of the PoolAddressesProvider contract + */ + constructor(IPoolAddressesProvider provider) { + ADDRESSES_PROVIDER = provider; + } + + /** + * @dev Only pool admin can call functions marked by this modifier. + **/ + modifier onlyPoolAdmin() { + _onlyPoolAdmin(); + _; + } + + function _onlyPoolAdmin() internal view virtual { + require( + IACLManager(ADDRESSES_PROVIDER.getACLManager()).isPoolAdmin( + msg.sender + ), + Errors.CALLER_NOT_POOL_ADMIN + ); + } + + function getTokenPrice(uint256 tokenId) external returns (uint256 price) { + ParaETH tokenInfo = getTokenInfo(tokenId); + + if (tokenInfo.provider == StakingProvider.Lido) { + return calculateLidoValue(tokenInfo); + } else if (tokenInfo.provider == StakingProvider.Validator) { + return calculateValidatorValue(tokenInfo); + } else { + return calculateOtherLSDValue(tokenInfo); + } + } + + function calculateLidoValue(ParaETH tokenInfo) + internal + returns (uint256 price) + { + uint256 redemptionRate = getLidoRedemptionRate( + tokenInfo.underlyingTokenId + ); + (uint64 slashingRisk, uint64 discountRate, ) = getProviderConfiguration( + tokenInfo.provider + ); + uint256 unstakeTime = slashingRisk * + tokenInfo.unstakeTime - + block.timestamp; + // TODO convert unstakeTime to days + return + (tokenInfo.principal * + redemptionRate * + (WadRayMath.WAD - unstakeTime)) / + (WadRayMath.WAD + discountRate)**unstakeTime; + } + + function calculateOtherLSDValue(ParaETH tokenInfo) + internal + returns (uint256 price) + { + ( + uint64 slashingRisk, + uint64 discountRate, + uint64 stakingRate + ) = getProviderConfiguration(tokenInfo.provider); + uint256 unstakeTime = slashingRisk * + tokenInfo.unstakeTime - + block.timestamp; + + return + (tokenInfo.principal * + (WadRayMath.WAD - slashingRisk * unstakeTime)) / + (WadRayMath.WAD + discountRate)**unstakeTime; + } + + function calculateValidatorValue(ParaETH tokenInfo) + internal + returns (uint256 price) + { + ( + uint64 slashingRisk, + uint64 discountRate, + uint64 stakingRate + ) = getProviderConfiguration(tokenInfo.provider); + uint256 unstakeTime = slashingRisk * + tokenInfo.unstakeTime - + block.timestamp; + + return + (tokenInfo.principal * + (WadRayMath.WAD - slashingRisk * unstakeTime)) / + (WadRayMath.WAD + discountRate)**unstakeTime; + } + + function getProviderConfiguration(StakingProvider provider) + public + returns ( + uint64 slashingRisk, + uint64 discountRate, + uint64 stakingRate + ) + { + ProviderConfiguration memory configs = providerConfiguration[provider]; + + return ( + configs.slashingRisk, + configs.discountRate, + configs.stakingRate + ); + } + + function setProviderConfiguration( + StakingProvider provider, + ProviderConfiguration memory configs + ) external onlyPoolAdmin { + providerConfiguration[provider] = configs; + } + + function getLidoRedemptionRate(uint256 tokenId) internal returns (uint256) { + return WadRayMath.WAD; + } +} diff --git a/contracts/misc/ETHUnstakePresentValueV2.sol b/contracts/misc/ETHUnstakePresentValueV2.sol new file mode 100644 index 000000000..faa922143 --- /dev/null +++ b/contracts/misc/ETHUnstakePresentValueV2.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0 +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"; + +contract ETHUnstakePresentValue { + enum StakingProvider { + Validator, + Lido, + RocketPool, + Coinbase + } + + struct TokenInfo { + StakingProvider provider; + uint64 exitEpoch; + uint64 withdrawableEpoch; + uint256 balance; + uint256 withdrawableTime; + } + + struct ProviderConfiguration { + uint64 slashingRisk; + uint64 discountRate; + uint64 stakingRate; + } + + IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; + + mapping(uint256 => address) providerStrategyAddress; + + /** + * @dev Constructor. + * @param provider The address of the PoolAddressesProvider contract + */ + constructor(IPoolAddressesProvider provider) { + ADDRESSES_PROVIDER = provider; + } + + /** + * @dev Only pool admin can call functions marked by this modifier. + **/ + modifier onlyPoolAdmin() { + _onlyPoolAdmin(); + _; + } + + function _onlyPoolAdmin() internal view virtual { + require( + IACLManager(ADDRESSES_PROVIDER.getACLManager()).isPoolAdmin( + msg.sender + ), + Errors.CALLER_NOT_POOL_ADMIN + ); + } + + function getPresentValueAndDiscountRate(uint256 tokenId, uint256 borrowRate) + external + returns (uint256 price, uint256 discountRate) + { + TokenInfo tokenInfo = getTokenInfo(tokenId); + + IETHStakingProviderStrategy strategy = providerStrategyAddress[ + tokenInfo.provider + ]; + + discountRate = strategy.getDiscountRate(tokenInfo, borrowRate); + price = strategy.getTokenPresentValue(tokenInfo, discountRate); + } + + function getPresentValueByDiscountRate( + uint256 tokenId, + uint256 discountRate + ) external returns (uint256 price) { + price = strategy.getTokenPresentValue(tokenInfo, discountRate); + } + + function setProviderStrategyAddress( + StakingProvider provider, + address strategy + ) external onlyPoolAdmin { + providerConfiguration[provider] = strategy; + } +} diff --git a/contracts/misc/LidoStakingStrategy.sol b/contracts/misc/LidoStakingStrategy.sol new file mode 100644 index 000000000..e22cd00b7 --- /dev/null +++ b/contracts/misc/LidoStakingStrategy.sol @@ -0,0 +1,64 @@ +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 {PercentageMath} from "../protocol/libraries/math/PercentageMath.sol"; + +contract LidoStakingStrategy is IETHStakingProviderStrategy { + uint256 constant SLASHING_RISK = + PercentageMath.percentMul(WadRayMath.WAD, 50); + uint256 constant STAKING_RATE = + PercentageMath.percentMul(WadRayMath.WAD, 50); + uint256 constant DURATION_FACTOR = + PercentageMath.percentMul(WadRayMath.WAD, 10); + uint256 constant PROVIDER_PREMIUM = + PercentageMath.percentMul(WadRayMath.WAD, 15); + + function getTokenPresentValue(TokenInfo tokenInfo, uint256 discountRate) + external + returns (uint256 price) + { + uint256 redemptionRate = getLidoRedemptionRate( + tokenInfo.underlyingTokenId + ); + + uint256 unstakeTime = (SLASHING_RISK * + tokenInfo.unstakeTime - + block.timestamp) / 86400; + + return + (tokenInfo.principal * + redemptionRate * + (WadRayMath.WAD - SLASHING_RISK * unstakeTime)) / + (WadRayMath.WAD + discountRate)**unstakeTime; + } + + function getDiscountRate(TokenInfo tokenInfo, uint256 borrowRate) + external + returns (uint256 discountRate) + { + uint256 unstakeTime = (SLASHING_RISK * + tokenInfo.unstakeTime - + block.timestamp) / 86400; + + return borrowRate + borrowRate * DURATION_FACTOR * PROVIDER_PREMIUM; + } + + function getSlashingRisk(uint256 tokenId) + external + returns (uint256 slashingRisk) + { + return SLASHING_RISK; + } + + function getStakingRate(uint256 tokenId) + external + returns (uint256 stakingRate) + { + return STAKING_RATE; + } + + function getLidoRedemptionRate(uint256 tokenId) internal returns (uint256) { + return WadRayMath.WAD; + } +}