diff --git a/contracts/peer-to-peer/interfaces/oracles/IGLPManager.sol b/contracts/peer-to-peer/interfaces/oracles/IGLPManager.sol new file mode 100644 index 00000000..8a17cbc8 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IGLPManager.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IGLPManager { + /** + * @notice gets price of GLP in USD with 30 decimals + * @param _maximize will pass true + * @return price of GLP in USD + */ + function getPrice(bool _maximize) external view returns (uint256); +} diff --git a/contracts/peer-to-peer/interfaces/oracles/IGTOKEN.sol b/contracts/peer-to-peer/interfaces/oracles/IGTOKEN.sol new file mode 100644 index 00000000..11cfeb18 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IGTOKEN.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IGTOKEN { + /** + * @notice gets amount of underlying token for a given amount of gToken + * @return amount of underlying token + */ + function shareToAssetsPrice() external view returns (uint256); +} diff --git a/contracts/peer-to-peer/oracles/custom/MysoArbitrumUsdOracle.sol b/contracts/peer-to-peer/oracles/custom/MysoArbitrumUsdOracle.sol new file mode 100644 index 00000000..72501906 --- /dev/null +++ b/contracts/peer-to-peer/oracles/custom/MysoArbitrumUsdOracle.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +//import {ChainlinkBase} from "../chainlink/ChainlinkBase.sol"; +import {ChainlinkArbitrumSequencerUSD} from "../chainlink/ChainlinkArbitrumSequencerUSD.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IMysoTokenManager} from "../../interfaces/oracles/IMysoTokenManager.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {LogExpMath} from "./utils/LogExpMath.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @dev supports oracles which are compatible with v2v3 or v3 interfaces + */ +contract MysoOracle is ChainlinkArbitrumSequencerUSD, Ownable { + struct PriceParams { + // maxPrice is in 8 decimals for chainlink consistency + uint96 maxPrice; + // k is in 18 decimals + // e.g. 8e17 is 0.8 in decimal + uint96 k; + // a and b are in terms of 1000 + // e.g. 1770 is 1.77 in decimal + uint32 a; + uint32 b; + } + // solhint-disable var-name-mixedcase + //address internal constant MYSO = 0x00000000000000000000000000000000DeaDBeef; // TODO: put in real myso address + address internal constant MYSO = 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; + address internal constant GDAI = 0xd85E038593d7A098614721EaE955EC2022B9B91B; + address internal constant GUSDC = + 0xd3443ee1e91aF28e5FB858Fbd0D72A63bA8046E0; + address internal constant GETH = 0x5977A9682D7AF81D347CFc338c61692163a2784C; + address internal constant GLP = 0x4277f8F2c384827B5273592FF7CeBd9f2C1ac258; + address internal constant ETH_USD_CHAINLINK = + 0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612; + address internal constant DAI_ETH_CHAINLINK = + 0xc5C8E77B397E531B8EC06BFb0048328B30E9eCfB; + address internal constant USDC_USD_CHAINLINK = + 0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3; + address internal constant GLP_MANAGER = + 0x3963FfC9dff443c2A94f21b129D429891E32ec18; + + uint256 internal constant MYSO_PRICE_TIME_LOCK = 5 minutes; + + address public mysoTokenManager; + + PriceParams public mysoPriceParams; + + event MysoTokenManagerUpdated(address newMysoTokenManager); + + error NoMyso(); + + /** + * @dev constructor for MysoOracle + * @param _tokenAddrs array of token addresses + * @param _oracleAddrs array of oracle addresses + * @param _owner owner of the contract + * @param _mysoTokenManager address of myso token manager contract + * @param _maxPrice max price in 8 decimals + * @param _k k in 18 decimals + * @param _a a in terms of 1000 + * @param _b b in terms of 1000 + */ + constructor( + address[] memory _tokenAddrs, + address[] memory _oracleAddrs, + address _owner, + address _mysoTokenManager, + uint96 _maxPrice, + uint96 _k, + uint32 _a, + uint32 _b + ) ChainlinkArbitrumSequencerUSD(_tokenAddrs, _oracleAddrs) Ownable() { + mysoTokenManager = _mysoTokenManager; + mysoPriceParams = PriceParams(_maxPrice, _k, _a, _b); + _transferOwnership(_owner); + } + + /** + * @dev updates myso token manager contract address + * @param _newMysoTokenManager new myso token manager contract address + */ + + function setMysoTokenManager( + address _newMysoTokenManager + ) external onlyOwner { + mysoTokenManager = _newMysoTokenManager; + emit MysoTokenManagerUpdated(_newMysoTokenManager); + } + + /** + * @dev updates myso price params + * @param _maxPrice max price in 8 decimals + * @param _k k in 18 decimals + * @param _a a in terms of 1000 + * @param _b b in terms of 1000 + */ + function setMysoPriceParams( + uint96 _maxPrice, + uint96 _k, + uint32 _a, + uint32 _b + ) external onlyOwner { + mysoPriceParams = PriceParams(_maxPrice, _k, _a, _b); + } + + function getPrice( + address collToken, + address loanToken + ) external view override returns (uint256 collTokenPriceInLoanToken) { + (uint256 priceOfCollToken, uint256 priceOfLoanToken) = getRawPrices( + collToken, + loanToken + ); + uint256 loanTokenDecimals = (loanToken == MYSO) + ? 18 + : IERC20Metadata(loanToken).decimals(); + collTokenPriceInLoanToken = + (priceOfCollToken * 10 ** loanTokenDecimals) / + priceOfLoanToken; + } + + function getRawPrices( + address collToken, + address loanToken + ) + public + view + override + returns (uint256 collTokenPriceRaw, uint256 loanTokenPriceRaw) + { + // must have at least one token is MYSO to use this oracle + if (collToken != MYSO && loanToken != MYSO) { + revert NoMyso(); + } + (collTokenPriceRaw, loanTokenPriceRaw) = ( + _getPriceOfToken(collToken), + _getPriceOfToken(loanToken) + ); + } + + function _getPriceOfToken( + address token + ) internal view virtual override returns (uint256 tokenPriceRaw) { + if (token == MYSO) { + tokenPriceRaw = _getMysoPriceInUsd(); + } else if (token == GDAI) { + tokenPriceRaw = _getGTOKENPriceInUsd(GDAI, DAI_ETH_CHAINLINK); + } else if (token == GUSDC) { + tokenPriceRaw = _getGTOKENPriceInUsd(GUSDC, USDC_USD_CHAINLINK); + } else if (token == GETH) { + tokenPriceRaw = _getGTOKENPriceInUsd(GETH, ETH_USD_CHAINLINK); + } else if (token == GLP) { + tokenPriceRaw = IGLPManager(GLP_MANAGER).getPrice(true) / 1e22; + } else { + tokenPriceRaw = super._getPriceOfToken(token); + } + } + + function _getMysoPriceInUsd() + internal + view + returns (uint256 mysoPriceInUsd) + { + uint256 _totalMysoLoanAmount = IMysoTokenManager(mysoTokenManager) + .totalMysoLoanAmount(); + PriceParams memory params = mysoPriceParams; + uint256 maxPrice = uint256(params.maxPrice); + uint256 k = uint256(params.k); + uint256 a = uint256(params.a); + uint256 b = uint256(params.b); + uint256 numerator = k * b; + uint256 denominator = uint256( + LogExpMath.exp( + int256(Math.mulDiv(_totalMysoLoanAmount, a, 1000000000)) + ) + ) + (2 * b - 1000) * 1e15; + mysoPriceInUsd = maxPrice - Math.mulDiv(numerator, 1e5, denominator); + } + + function _getGTOKENPriceInUsd( + address token, + address chainlinkOracle + ) internal view returns (uint256 gTokenPriceRaw) { + uint256 assetsPerGtoken = IGTOKEN(token).shareToAssetsPrice(); + uint256 assetPriceInUsd = _checkAndReturnLatestRoundData( + chainlinkOracle + ); + uint256 tokenPriceInUsd = _getPriceOfToken(token); + gTokenPriceRaw = Math.mulDiv( + tokenPriceInUsd * assetsPerGtoken, + 1e8, + assetPriceInUsd * 1e18 + ); + } +} diff --git a/contracts/test/TestnetTokenManagerArbitrumOracle.sol b/contracts/test/TestnetTokenManagerArbitrumOracle.sol new file mode 100644 index 00000000..586286dd --- /dev/null +++ b/contracts/test/TestnetTokenManagerArbitrumOracle.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.19; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {DataTypesPeerToPeer} from "../peer-to-peer/DataTypesPeerToPeer.sol"; +import {DataTypesPeerToPool} from "../peer-to-pool/DataTypesPeerToPool.sol"; +import {Errors} from "../Errors.sol"; +import {IMysoTokenManager} from "../interfaces/IMysoTokenManager.sol"; +import {ILenderVaultImpl} from "../peer-to-peer/interfaces/ILenderVaultImpl.sol"; + +contract TestnetTokenManagerArbitrumOracle is Ownable2Step, IMysoTokenManager { + using SafeERC20 for IERC20; + struct RewardInfo { + uint128 collThreshold; + uint128 mysoTokenMultiplier; + } + address internal constant MYSO_TOKEN = + 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; // Dai testnet stand-in arbitrum test + mapping(address => RewardInfo) public rewardInfos; + bool public mysoRewardsActive; + uint256 public totalMysoLoanAmount; + uint256 public minMysoStakingRequirement; + address public mysoIOOVault; + + event RewardInfoSet( + address indexed collToken, + uint128 collThreshold, + uint128 mysoTokenMultiplier + ); + event MysoRewardsToggled(bool active); + event MinMysoStakingRequirementSet(uint256 minStakingRequirement); + event IOOVaultSet(address indexed mysoIOOVault); + + // TODO: mapping oracleAddr -> vaultAddr -> tokenAddr -> loanAmount + flag for being turned tracked + // This will allow for other IOOs to use a custom oracle with auto-updating price for loan amount if desired + + constructor() { + minMysoStakingRequirement = 10_000 * 1e18; + _transferOwnership(msg.sender); + } + + function processP2PBorrow( + uint128[2] memory currProtocolFeeParams, + DataTypesPeerToPeer.BorrowTransferInstructions + calldata /*borrowInstructions*/, + DataTypesPeerToPeer.Loan calldata loan, + address lenderVault + ) external returns (uint128[2] memory applicableProtocolFeeParams) { + applicableProtocolFeeParams = currProtocolFeeParams; + if (loan.loanToken == MYSO_TOKEN && lenderVault == mysoIOOVault) { + totalMysoLoanAmount += loan.initLoanAmount; + } + } + + // solhint-disable no-empty-blocks + function processP2PCreateVault( + uint256 /*numRegisteredVaults*/, + address /*vaultCreator*/, + address /*newLenderVaultAddr*/ + ) external {} + + // solhint-disable no-empty-blocks + function processP2PCreateWrappedTokenForERC721s( + address /*tokenCreator*/, + DataTypesPeerToPeer.WrappedERC721TokenInfo[] + calldata /*tokensToBeWrapped*/, + bytes calldata /*mysoTokenManagerData*/ + ) external {} + + // solhint-disable no-empty-blocks + function processP2PCreateWrappedTokenForERC20s( + address /*tokenCreator*/, + DataTypesPeerToPeer.WrappedERC20TokenInfo[] + calldata /*tokensToBeWrapped*/, + bytes calldata /*mysoTokenManagerData*/ + ) external {} + + // solhint-disable no-empty-blocks + function processP2PoolDeposit( + address /*fundingPool*/, + address /*depositor*/, + uint256 /*depositAmount*/, + uint256 /*depositLockupDuration*/, + uint256 /*transferFee*/ + ) external {} + + // solhint-disable no-empty-blocks + function processP2PoolSubscribe( + address /*fundingPool*/, + address /*subscriber*/, + address /*loanProposal*/, + uint256 /*subscriptionAmount*/, + uint256 /*subscriptionLockupDuration*/, + uint256 /*totalSubscriptions*/, + DataTypesPeerToPool.LoanTerms calldata /*loanTerms*/ + ) external {} + + // solhint-disable no-empty-blocks + function processP2PoolLoanFinalization( + address /*loanProposal*/, + address /*fundingPool*/, + address /*arranger*/, + address /*borrower*/, + uint256 /*grossLoanAmount*/, + bytes calldata /*mysoTokenManagerData*/ + ) external {} + + // solhint-disable no-empty-blocks + function processP2PoolCreateLoanProposal( + address /*fundingPool*/, + address /*proposalCreator*/, + address /*collToken*/, + uint256 /*arrangerFee*/, + uint256 /*numLoanProposals*/ + ) external {} + + function withdraw(address token, address to, uint256 amount) external { + _checkOwner(); + SafeERC20.safeTransfer(IERC20(token), to, amount); + } + + function setRewardInfo( + address collToken, + uint128 collThreshold, + uint128 mysoTokenMultiplier + ) external { + _checkOwner(); + RewardInfo storage rewardInfo = rewardInfos[collToken]; + rewardInfo.collThreshold = collThreshold; + rewardInfo.mysoTokenMultiplier = mysoTokenMultiplier; + emit RewardInfoSet(collToken, collThreshold, mysoTokenMultiplier); + } + + function toggleMysoRewards() external { + _checkOwner(); + mysoRewardsActive = !mysoRewardsActive; + emit MysoRewardsToggled(mysoRewardsActive); + } + + function setMinMysoStakingRequirement( + uint256 _minMysoStakingRequirement + ) external { + _checkOwner(); + minMysoStakingRequirement = _minMysoStakingRequirement; + emit MinMysoStakingRequirementSet(_minMysoStakingRequirement); + } + + function setIOOVault(address _mysoIOOVault) external { + _checkOwner(); + mysoIOOVault = _mysoIOOVault; + emit IOOVaultSet(_mysoIOOVault); + } + + function transferOwnership(address _newOwnerProposal) public override { + _checkOwner(); + if ( + _newOwnerProposal == address(0) || + _newOwnerProposal == address(this) || + _newOwnerProposal == pendingOwner() || + _newOwnerProposal == owner() + ) { + revert Errors.InvalidNewOwnerProposal(); + } + super._transferOwnership(_newOwnerProposal); + } +}