diff --git a/.solhint.json b/.solhint.json index 314dce00..e3df3b41 100644 --- a/.solhint.json +++ b/.solhint.json @@ -4,12 +4,12 @@ "rules": { "ordering": "error", "no-global-import": "error", - "state-visibility": "error", + "state-visibility": "warn", "imports-on-top": "error", "no-unused-vars": "error", "code-complexity": ["warn", 12], "compiler-version": ["error", "^0.8.19"], - "const-name-snakecase": "error", + "const-name-snakecase": "warn", "event-name-camelcase": "error", "constructor-syntax": "error", "func-name-mixedcase": "off", diff --git a/README.md b/README.md index b5934c27..57618d01 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The protocol supports two different models, each targeted at different use cases ## Quick Start ``` npm i -npx hardhat test +npx hardhat test --grep "IOO price correctly" ``` ## Contract Files diff --git a/contracts/peer-to-peer/interfaces/oracles/IANKRETH.sol b/contracts/peer-to-peer/interfaces/oracles/IANKRETH.sol new file mode 100644 index 00000000..b8025f32 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IANKRETH.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IANKRETH { + /** + * @notice gets amount of Eth for given ankrEth + * @param amount of ankrEth + * @return amount of eth + */ + function sharesToBonds(uint256 amount) external view returns (uint256); +} diff --git a/contracts/peer-to-peer/interfaces/oracles/IFXUSD.sol b/contracts/peer-to-peer/interfaces/oracles/IFXUSD.sol new file mode 100644 index 00000000..1649a4a4 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IFXUSD.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IFXUSD { + /** + * @notice gets USD NAV in 18 decimals + * @return NAV in USD + */ + function nav() external view returns (uint256); +} 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/interfaces/oracles/IMETH.sol b/contracts/peer-to-peer/interfaces/oracles/IMETH.sol new file mode 100644 index 00000000..e3043cea --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IMETH.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IMETH { + /** + * @notice gets amount of Eth for given mEth + * @param mETHAmount amount of mEth + * @return amount of stEth + */ + function mETHToETH(uint256 mETHAmount) external view returns (uint256); +} diff --git a/contracts/peer-to-peer/interfaces/oracles/IMysoTokenManager.sol b/contracts/peer-to-peer/interfaces/oracles/IMysoTokenManager.sol new file mode 100644 index 00000000..a9e0e552 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IMysoTokenManager.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IMysoTokenManager { + /** + * @notice gets Myso token loan amount from MysoTokenManager + * @return total Myso loan amount up until now + */ + function totalMysoLoanAmount() external view returns (uint256); +} diff --git a/contracts/peer-to-peer/interfaces/oracles/IRSWETH.sol b/contracts/peer-to-peer/interfaces/oracles/IRSWETH.sol new file mode 100644 index 00000000..f383ebe8 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IRSWETH.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IRSWETH { + /** + * @notice gets rswETH to ETH rate + * @return amount of ETH per rswETH + */ + function rswETHToETHRate() external view returns (uint256); +} diff --git a/contracts/peer-to-peer/interfaces/oracles/IWSTETH.sol b/contracts/peer-to-peer/interfaces/oracles/IWSTETH.sol new file mode 100644 index 00000000..2213234a --- /dev/null +++ b/contracts/peer-to-peer/interfaces/oracles/IWSTETH.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IWSTETH { + /** + * @notice gets amount of stEth for given wstEth + * @param _wstETHAmount amount of wstEth + * @return amount of stEth + */ + function getStETHByWstETH( + uint256 _wstETHAmount + ) external view returns (uint256); +} diff --git a/contracts/peer-to-peer/oracles/chainlink/ChainlinkBaseWithoutCheck.sol b/contracts/peer-to-peer/oracles/chainlink/ChainlinkBaseWithoutCheck.sol new file mode 100644 index 00000000..d5b1f150 --- /dev/null +++ b/contracts/peer-to-peer/oracles/chainlink/ChainlinkBaseWithoutCheck.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {AggregatorV3Interface} from "../../interfaces/oracles/chainlink/AggregatorV3Interface.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Constants} from "../../../Constants.sol"; +import {Errors} from "../../../Errors.sol"; +import {IOracle} from "../../interfaces/IOracle.sol"; + +/** + * @dev supports oracles which are compatible with v2v3 or v3 interfaces + */ +abstract contract ChainlinkBaseWithoutCheck is IOracle { + // solhint-disable var-name-mixedcase + uint256 public immutable BASE_CURRENCY_UNIT; + mapping(address => address) public oracleAddrs; + + constructor( + address[] memory _tokenAddrs, + address[] memory _oracleAddrs, + uint256 baseCurrencyUnit + ) { + uint256 tokenAddrsLength = _tokenAddrs.length; + if (tokenAddrsLength != _oracleAddrs.length) { + revert Errors.InvalidArrayLength(); + } + uint8 oracleDecimals; + uint256 version; + for (uint256 i; i < tokenAddrsLength; ) { + if (_tokenAddrs[i] == address(0) || _oracleAddrs[i] == address(0)) { + revert Errors.InvalidAddress(); + } + oracleDecimals = AggregatorV3Interface(_oracleAddrs[i]).decimals(); + if (10 ** oracleDecimals != baseCurrencyUnit) { + revert Errors.InvalidOracleDecimals(); + } + version = AggregatorV3Interface(_oracleAddrs[i]).version(); + if (version != 4) { + revert Errors.InvalidOracleVersion(); + } + oracleAddrs[_tokenAddrs[i]] = _oracleAddrs[i]; + unchecked { + ++i; + } + } + BASE_CURRENCY_UNIT = baseCurrencyUnit; + } + + function getPrice( + address collToken, + address loanToken + ) external view virtual returns (uint256 collTokenPriceInLoanToken) { + (uint256 priceOfCollToken, uint256 priceOfLoanToken) = getRawPrices( + collToken, + loanToken + ); + uint256 loanTokenDecimals = IERC20Metadata(loanToken).decimals(); + collTokenPriceInLoanToken = Math.mulDiv( + priceOfCollToken, + 10 ** loanTokenDecimals, + priceOfLoanToken + ); + } + + function getRawPrices( + address collToken, + address loanToken + ) + public + view + virtual + returns (uint256 collTokenPriceRaw, uint256 loanTokenPriceRaw) + { + (collTokenPriceRaw, loanTokenPriceRaw) = ( + _getPriceOfToken(collToken), + _getPriceOfToken(loanToken) + ); + } + + function _getPriceOfToken( + address token + ) internal view virtual returns (uint256 tokenPriceRaw) { + address oracleAddr = oracleAddrs[token]; + if (oracleAddr == address(0)) { + revert Errors.NoOracle(); + } + tokenPriceRaw = _checkAndReturnLatestRoundData(oracleAddr); + } + + function _checkAndReturnLatestRoundData( + address oracleAddr + ) internal view virtual returns (uint256 tokenPriceRaw) { + ( + uint80 roundId, + int256 answer, + , + uint256 updatedAt, + uint80 answeredInRound + ) = AggregatorV3Interface(oracleAddr).latestRoundData(); + if ( + roundId == 0 || + answeredInRound < roundId || + answer < 1 || + updatedAt > block.timestamp || + updatedAt + Constants.MAX_PRICE_UPDATE_TIMESTAMP_DIVERGENCE < + block.timestamp + ) { + revert Errors.InvalidOracleAnswer(); + } + tokenPriceRaw = uint256(answer); + } +} 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..e3c92a82 --- /dev/null +++ b/contracts/peer-to-peer/oracles/custom/MysoArbitrumUsdOracle.sol @@ -0,0 +1,195 @@ +// 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"; +import {IGLPManager} from "../../interfaces/oracles/IGLPManager.sol"; +import {IGTOKEN} from "../../interfaces/oracles/IGTOKEN.sol"; + +/** + * @dev supports oracles which are compatible with v2v3 or v3 interfaces + */ +contract MysoArbitrumUsdOracle 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 = 0x25bA1ED5DEEA9d8e8add565dA069Ed1eDA397C12; + 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 + ); + gTokenPriceRaw = Math.mulDiv(assetsPerGtoken, assetPriceInUsd, 1e18); + } +} diff --git a/contracts/peer-to-peer/oracles/custom/MysoBnbUsdOracle.sol b/contracts/peer-to-peer/oracles/custom/MysoBnbUsdOracle.sol new file mode 100644 index 00000000..717abd99 --- /dev/null +++ b/contracts/peer-to-peer/oracles/custom/MysoBnbUsdOracle.sol @@ -0,0 +1,163 @@ +// 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"; +import {IGLPManager} from "../../interfaces/oracles/IGLPManager.sol"; +import {IGTOKEN} from "../../interfaces/oracles/IGTOKEN.sol"; + +/** + * @dev supports oracles which are compatible with v2v3 or v3 interfaces + */ +contract MysoBnbUsdOracle is ChainlinkBase, 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 + + uint256 internal constant MYSO_PRICE_TIME_LOCK = 5 minutes; + + address internal constant MYSO = 0x85be1fAB94E9AB109ea339E164689281FAd3f0dF; + + 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 + ) ChainlinkBase(_tokenAddrs, _oracleAddrs, 1e8) 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 { + 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); + } +} diff --git a/contracts/peer-to-peer/oracles/custom/MysoMantleUsdOracle.sol b/contracts/peer-to-peer/oracles/custom/MysoMantleUsdOracle.sol new file mode 100644 index 00000000..f4738027 --- /dev/null +++ b/contracts/peer-to-peer/oracles/custom/MysoMantleUsdOracle.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ChainlinkBaseWithoutCheck} from "../chainlink/ChainlinkBaseWithoutCheck.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"; + +/** + * @dev supports oracles which are compatible with v2v3 or v3 interfaces + */ +contract MysoMantleUsdOracle is ChainlinkBaseWithoutCheck, 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 = 0x25bA1ED5DEEA9d8e8add565dA069Ed1eDA397C12; + address internal constant USDC = 0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9; + + 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 + ) ChainlinkBaseWithoutCheck(_tokenAddrs, _oracleAddrs, 1e8) 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 : 6; + 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 == USDC) { + tokenPriceRaw = 1e8; + } 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); + } +} diff --git a/contracts/peer-to-peer/oracles/custom/MysoOracle.sol b/contracts/peer-to-peer/oracles/custom/MysoOracle.sol new file mode 100644 index 00000000..f5febe3f --- /dev/null +++ b/contracts/peer-to-peer/oracles/custom/MysoOracle.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ChainlinkBase} from "../chainlink/ChainlinkBase.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IWSTETH} from "../../interfaces/oracles/IWSTETH.sol"; +import {IANKRETH} from "../../interfaces/oracles/IANKRETH.sol"; +import {IMETH} from "../../interfaces/oracles/IMETH.sol"; +import {IFXUSD} from "../../interfaces/oracles/IFXUSD.sol"; +import {IRSWETH} from "../../interfaces/oracles/IRSWETH.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 ChainlinkBase, 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 = 0x5fDe99e121F3aC02e7d6ACb081dB1f89c1e93C17; + address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address internal constant WSTETH = + 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + address internal constant METH = 0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa; + address internal constant FXUSD = + 0x085780639CC2cACd35E474e71f4d000e2405d8f6; + address internal constant RSWETH = + 0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0; + address internal constant RPL = 0xD33526068D116cE69F19A9ee46F0bd304F21A51f; + address internal constant CRVUSD = + 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E; + address internal constant ANKRETH = + 0xE95A203B1a91a908F9B9CE46459d101078c2c3cb; + address internal constant METH_STAKING_CONTRACT = + 0xe3cBd06D7dadB3F4e6557bAb7EdD924CD1489E8f; + uint256 internal constant MYSO_IOO_BASE_CURRENCY_UNIT = 1e18; // 18 decimals for ETH based oracles + address internal constant ETH_USD_CHAINLINK = + 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; + address internal constant STETH_ETH_CHAINLINK = + 0x86392dC19c0b719886221c78AB11eb8Cf5c52812; + address internal constant RPL_USD_CHAINLINK = + 0x4E155eD98aFE9034b7A5962f6C84c86d869daA9d; + address internal constant CRVUSD_USD_CHAINLINK = + 0xEEf0C605546958c1f899b6fB336C20671f9cD49F; + + 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 + ) + ChainlinkBase(_tokenAddrs, _oracleAddrs, MYSO_IOO_BASE_CURRENCY_UNIT) + 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 = _getMysoPriceInEth(); + } else if (token == WETH) { + tokenPriceRaw = 1e18; + } else if (token == WSTETH) { + tokenPriceRaw = _getWstEthPrice(); + } else if (token == METH) { + tokenPriceRaw = IMETH(METH_STAKING_CONTRACT).mETHToETH(1e18); + } else if (token == RPL) { + tokenPriceRaw = _getRPLPriceInEth(); + } else if (token == ANKRETH) { + tokenPriceRaw = IANKRETH(ANKRETH).sharesToBonds(1e18); + } else if (token == FXUSD) { + tokenPriceRaw = _getFXUSDPriceInEth(); + } else if (token == RSWETH) { + tokenPriceRaw = IRSWETH(RSWETH).rswETHToETHRate(); + } else if (token == CRVUSD) { + tokenPriceRaw = _getCRVUSDPriceInEth(); + } else { + tokenPriceRaw = super._getPriceOfToken(token); + } + } + + function _getWstEthPrice() internal view returns (uint256 wstEthPriceRaw) { + uint256 stEthAmountPerWstEth = IWSTETH(WSTETH).getStETHByWstETH(1e18); + uint256 stEthPriceInEth = _checkAndReturnLatestRoundData( + (STETH_ETH_CHAINLINK) + ); + wstEthPriceRaw = Math.mulDiv( + stEthPriceInEth, + stEthAmountPerWstEth, + 1e18 + ); + } + + function _getMysoPriceInEth() + internal + view + returns (uint256 mysoPriceInEth) + { + uint256 ethPriceInUsd = _checkAndReturnLatestRoundData( + ETH_USD_CHAINLINK + ); + uint256 _totalMysoLoanAmount = IMysoTokenManager(mysoTokenManager) + .totalMysoLoanAmount(); + uint256 mysoPriceInUsd = _getMysoPriceInUsd(_totalMysoLoanAmount); + mysoPriceInEth = Math.mulDiv(mysoPriceInUsd, 1e18, ethPriceInUsd); + } + + function _getMysoPriceInUsd( + uint256 totalMysoLoanAmount + ) internal view returns (uint256 mysoPriceInUsd) { + 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 _getRPLPriceInEth() internal view returns (uint256 rplPriceRaw) { + uint256 rplPriceInUSD = _checkAndReturnLatestRoundData( + (RPL_USD_CHAINLINK) + ); + uint256 ethPriceInUsd = _checkAndReturnLatestRoundData( + ETH_USD_CHAINLINK + ); + rplPriceRaw = Math.mulDiv(rplPriceInUSD, 1e18, ethPriceInUsd); + } + + function _getCRVUSDPriceInEth() + internal + view + returns (uint256 crvUsdPriceRaw) + { + uint256 crvUsdPriceInUSD = _checkAndReturnLatestRoundData( + (CRVUSD_USD_CHAINLINK) + ); + uint256 ethPriceInUsd = _checkAndReturnLatestRoundData( + ETH_USD_CHAINLINK + ); + crvUsdPriceRaw = Math.mulDiv(crvUsdPriceInUSD, 1e18, ethPriceInUsd); + } + + function _getFXUSDPriceInEth() + internal + view + returns (uint256 fxusdPriceRaw) + { + uint256 ethPriceInUsd = _checkAndReturnLatestRoundData( + ETH_USD_CHAINLINK + ); + uint256 fxusdPriceInUSD = IFXUSD(FXUSD).nav(); + fxusdPriceRaw = Math.mulDiv(fxusdPriceInUSD, 1e8, ethPriceInUsd); + } +} diff --git a/contracts/peer-to-peer/oracles/custom/utils/LogExpMath.sol b/contracts/peer-to-peer/oracles/custom/utils/LogExpMath.sol new file mode 100644 index 00000000..fdfd17b1 --- /dev/null +++ b/contracts/peer-to-peer/oracles/custom/utils/LogExpMath.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +library LogExpMath { + // All fixed point multiplications and divisions are inlined. This means we need to divide by ONE when multiplying + // two numbers, and multiply by ONE when dividing them. + + // All arguments and return values are 18 decimal fixed point numbers. + + int256 constant ONE_18 = 1e18; + + // Internally, intermediate values are computed with higher precision as 20 decimal fixed point numbers, and in the + // case of ln36, 36 decimals. + int256 constant ONE_20 = 1e20; + int256 constant ONE_36 = 1e36; + + // The domain of natural exponentiation is bound by the word size and number of decimals used. + // + // Because internally the result will be stored using 20 decimals, the largest possible result is + // (2^255 - 1) / 10^20, which makes the largest exponent ln((2^255 - 1) / 10^20) = 130.700829182905140221. + // The smallest possible result is 10^(-18), which makes largest negative argument + // ln(10^(-18)) = -41.446531673892822312. + // We use 130.0 and -41.0 to have some safety margin. + int256 constant MAX_NATURAL_EXPONENT = 130e18; + int256 constant MIN_NATURAL_EXPONENT = -41e18; + + // Bounds for ln_36's argument. Both ln(0.9) and ln(1.1) can be represented with 36 decimal places in a fixed point + // 256 bit integer. + int256 constant LN_36_LOWER_BOUND = ONE_18 - 1e17; + int256 constant LN_36_UPPER_BOUND = ONE_18 + 1e17; + + uint256 constant MILD_EXPONENT_BOUND = 2 ** 254 / uint256(ONE_20); + + // 18 decimal constants + int256 constant x0 = 128000000000000000000; // 2ˆ7 + int256 constant a0 = + 38877084059945950922200000000000000000000000000000000000; // eˆ(x0) (no decimals) + int256 constant x1 = 64000000000000000000; // 2ˆ6 + int256 constant a1 = 6235149080811616882910000000; // eˆ(x1) (no decimals) + + // 20 decimal constants + int256 constant x2 = 3200000000000000000000; // 2ˆ5 + int256 constant a2 = 7896296018268069516100000000000000; // eˆ(x2) + int256 constant x3 = 1600000000000000000000; // 2ˆ4 + int256 constant a3 = 888611052050787263676000000; // eˆ(x3) + int256 constant x4 = 800000000000000000000; // 2ˆ3 + int256 constant a4 = 298095798704172827474000; // eˆ(x4) + int256 constant x5 = 400000000000000000000; // 2ˆ2 + int256 constant a5 = 5459815003314423907810; // eˆ(x5) + int256 constant x6 = 200000000000000000000; // 2ˆ1 + int256 constant a6 = 738905609893065022723; // eˆ(x6) + int256 constant x7 = 100000000000000000000; // 2ˆ0 + int256 constant a7 = 271828182845904523536; // eˆ(x7) + int256 constant x8 = 50000000000000000000; // 2ˆ-1 + int256 constant a8 = 164872127070012814685; // eˆ(x8) + int256 constant x9 = 25000000000000000000; // 2ˆ-2 + int256 constant a9 = 128402541668774148407; // eˆ(x9) + int256 constant x10 = 12500000000000000000; // 2ˆ-3 + int256 constant a10 = 113314845306682631683; // eˆ(x10) + int256 constant x11 = 6250000000000000000; // 2ˆ-4 + int256 constant a11 = 106449445891785942956; // eˆ(x11) + + /** + * @dev Natural exponentiation (e^x) with signed 18 decimal fixed point exponent. + * + * Reverts if `x` is smaller than MIN_NATURAL_EXPONENT, or larger than `MAX_NATURAL_EXPONENT`. + */ + function exp(int256 x) internal pure returns (int256) { + require( + x >= MIN_NATURAL_EXPONENT && x <= MAX_NATURAL_EXPONENT, + "INVALID_EXPONENT" + ); + + if (x < 0) { + // We only handle positive exponents: e^(-x) is computed as 1 / e^x. We can safely make x positive since it + // fits in the signed 256 bit range (as it is larger than MIN_NATURAL_EXPONENT). + // Fixed point division requires multiplying by ONE_18. + return ((ONE_18 * ONE_18) / exp(-x)); + } + + // First, we use the fact that e^(x+y) = e^x * e^y to decompose x into a sum of powers of two, which we call x_n, + // where x_n == 2^(7 - n), and e^x_n = a_n has been precomputed. We choose the first x_n, x0, to equal 2^7 + // because all larger powers are larger than MAX_NATURAL_EXPONENT, and therefore not present in the + // decomposition. + // At the end of this process we will have the product of all e^x_n = a_n that apply, and the remainder of this + // decomposition, which will be lower than the smallest x_n. + // exp(x) = k_0 * a_0 * k_1 * a_1 * ... + k_n * a_n * exp(remainder), where each k_n equals either 0 or 1. + // We mutate x by subtracting x_n, making it the remainder of the decomposition. + + // The first two a_n (e^(2^7) and e^(2^6)) are too large if stored as 18 decimal numbers, and could cause + // intermediate overflows. Instead we store them as plain integers, with 0 decimals. + // Additionally, x0 + x1 is larger than MAX_NATURAL_EXPONENT, which means they will not both be present in the + // decomposition. + + // For each x_n, we test if that term is present in the decomposition (if x is larger than it), and if so deduct + // it and compute the accumulated product. + + int256 firstAN; + if (x >= x0) { + x -= x0; + firstAN = a0; + } else if (x >= x1) { + x -= x1; + firstAN = a1; + } else { + firstAN = 1; // One with no decimal places + } + + // We now transform x into a 20 decimal fixed point number, to have enhanced precision when computing the + // smaller terms. + x *= 100; + + // `product` is the accumulated product of all a_n (except a0 and a1), which starts at 20 decimal fixed point + // one. Recall that fixed point multiplication requires dividing by ONE_20. + int256 product = ONE_20; + + if (x >= x2) { + x -= x2; + product = (product * a2) / ONE_20; + } + if (x >= x3) { + x -= x3; + product = (product * a3) / ONE_20; + } + if (x >= x4) { + x -= x4; + product = (product * a4) / ONE_20; + } + if (x >= x5) { + x -= x5; + product = (product * a5) / ONE_20; + } + if (x >= x6) { + x -= x6; + product = (product * a6) / ONE_20; + } + if (x >= x7) { + x -= x7; + product = (product * a7) / ONE_20; + } + if (x >= x8) { + x -= x8; + product = (product * a8) / ONE_20; + } + if (x >= x9) { + x -= x9; + product = (product * a9) / ONE_20; + } + + // x10 and x11 are unnecessary here since we have high enough precision already. + + // Now we need to compute e^x, where x is small (in particular, it is smaller than x9). We use the Taylor series + // expansion for e^x: 1 + x + (x^2 / 2!) + (x^3 / 3!) + ... + (x^n / n!). + + int256 seriesSum = ONE_20; // The initial one in the sum, with 20 decimal places. + int256 term; // Each term in the sum, where the nth term is (x^n / n!). + + // The first term is simply x. + term = x; + seriesSum += term; + + // Each term (x^n / n!) equals the previous one times x, divided by n. Since x is a fixed point number, + // multiplying by it requires dividing by ONE_20, but dividing by the non-fixed point n values does not. + + term = ((term * x) / ONE_20) / 2; + seriesSum += term; + + term = ((term * x) / ONE_20) / 3; + seriesSum += term; + + term = ((term * x) / ONE_20) / 4; + seriesSum += term; + + term = ((term * x) / ONE_20) / 5; + seriesSum += term; + + term = ((term * x) / ONE_20) / 6; + seriesSum += term; + + term = ((term * x) / ONE_20) / 7; + seriesSum += term; + + term = ((term * x) / ONE_20) / 8; + seriesSum += term; + + term = ((term * x) / ONE_20) / 9; + seriesSum += term; + + term = ((term * x) / ONE_20) / 10; + seriesSum += term; + + term = ((term * x) / ONE_20) / 11; + seriesSum += term; + + term = ((term * x) / ONE_20) / 12; + seriesSum += term; + + // 12 Taylor terms are sufficient for 18 decimal precision. + + // We now have the first a_n (with no decimals), and the product of all other a_n present, and the Taylor + // approximation of the exponentiation of the remainder (both with 20 decimals). All that remains is to multiply + // all three (one 20 decimal fixed point multiplication, dividing by ONE_20, and one integer multiplication), + // and then drop two digits to return an 18 decimal value. + + return (((product * seriesSum) / ONE_20) * firstAN) / 100; + } +} diff --git a/contracts/test/MockStMysoToken.sol b/contracts/test/MockStMysoToken.sol new file mode 100644 index 00000000..14b51fcc --- /dev/null +++ b/contracts/test/MockStMysoToken.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Wrapper} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockStMysoToken is Ownable, ERC20Wrapper { + uint8 private _decimals; + mapping(address => uint256) public weight; + address public mysoToken; + + constructor( + string memory _name, + string memory _symbol, + IERC20 _mysoToken + ) ERC20(_name, _symbol) ERC20Wrapper(_mysoToken) Ownable() { + _decimals = ERC20(address(_mysoToken)).decimals(); + } + + function depositFor( + address account, + uint256 amount + ) public override returns (bool) { + weight[account] = amount; + return super.depositFor(account, amount); + } + + function withdrawTo( + address account, + uint256 amount + ) public override returns (bool) { + weight[account] -= amount; + return super.withdrawTo(account, amount); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } +} diff --git a/contracts/test/TestnetMysoOracle.sol b/contracts/test/TestnetMysoOracle.sol new file mode 100644 index 00000000..80466a3a --- /dev/null +++ b/contracts/test/TestnetMysoOracle.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IMysoTokenManager} from "../peer-to-peer/interfaces/oracles/IMysoTokenManager.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {LogExpMath} from "../peer-to-peer/oracles/custom/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 TestnetMysoOracle is 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 = 0x8fF1307ba7e5FDc3A411d259bAe641e2B1d897c4; // myso sepolia address + address internal constant WETH = 0x3564c06EfEe0FeB0af5EF59dc5440bC45A265dA5; + address internal constant USDC = 0x2296282e2e2a4158140E3b4B99855ADa4a06a4B8; + address internal constant USDT = 0xdc2E37A79Ee93906c4665e49f82C1b895dFc7092; + address internal constant DAI = 0x40aD85ab1aA2a5bA61a2a026cfe1F4fd859eA188; + + uint256 internal constant MYSO_PRICE_TIME_LOCK = 5 minutes; + + address public mysoTokenManager; + + PriceParams public mysoPriceParams; + + event MysoTokenManagerUpdated(address newMysoTokenManager); + + error NoMyso(); + error NoOracle(); + + /** + * @dev constructor for TestMysoOracle + * @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 _owner, + address _mysoTokenManager, + uint96 _maxPrice, + uint96 _k, + uint32 _a, + uint32 _b + ) 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 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 + 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 returns (uint256 tokenPriceRaw) { + if (token == MYSO) { + tokenPriceRaw = _getMysoPriceInEth(); + } else if (token == WETH) { + tokenPriceRaw = 1e18; + } else if (token == USDC || token == USDT || token == DAI) { + tokenPriceRaw = Math.mulDiv(1e8, 1e18, 3300 * 1e8); + } else { + revert NoOracle(); + } + } + + function _getMysoPriceInEth() + internal + view + returns (uint256 mysoPriceInEth) + { + uint256 ethPriceInUsd = 3300 * 1e8; + uint256 _totalMysoLoanAmount = IMysoTokenManager(mysoTokenManager) + .totalMysoLoanAmount(); + uint256 mysoPriceInUsd = _getMysoPriceInUsd(_totalMysoLoanAmount); + mysoPriceInEth = Math.mulDiv(mysoPriceInUsd, 1e18, ethPriceInUsd); + } + + function _getMysoPriceInUsd( + uint256 totalMysoLoanAmount + ) internal view returns (uint256 mysoPriceInUsd) { + 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); + } +} diff --git a/contracts/test/TestnetTokenManager.sol b/contracts/test/TestnetTokenManager.sol index 5d92f501..40e82ffc 100644 --- a/contracts/test/TestnetTokenManager.sol +++ b/contracts/test/TestnetTokenManager.sol @@ -2,27 +2,46 @@ pragma solidity 0.8.19; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +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 TestnetTokenManager is ERC20, Ownable2Step, IMysoTokenManager { - uint8 internal _decimals; - address internal _vaultCompartmentVictim; - address internal _vaultAddr; - uint256 internal _borrowerReward; - uint256 internal _lenderReward; - uint256 internal _vaultCreationReward; - uint256 internal constant MAX_SUPPLY = 100_000_000 ether; - - constructor() ERC20("TYSO", "TYSO") { - _decimals = 18; - _borrowerReward = 1 ether; - _lenderReward = 1 ether; - _vaultCreationReward = 1 ether; +contract TestnetTokenManager is Ownable2Step, IMysoTokenManager { + using SafeERC20 for IERC20; + struct RewardInfo { + uint128 collThreshold; + uint128 mysoTokenMultiplier; + } + address internal constant MYSO_TOKEN = + //0x8fF1307ba7e5FDc3A411d259bAe641e2B1d897c4; // sepolia dev-token + 0x6B175474E89094C44Da98b954EedeAC495271d0F; // Dai testnet stand-in + address internal constant STAKED_MYSO_TOKEN = + 0x0A6cBCB5Ac7Fc6B47f06c2cE3E828b6EEBf37B06; + 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); } @@ -34,19 +53,38 @@ contract TestnetTokenManager is ERC20, Ownable2Step, IMysoTokenManager { address lenderVault ) external returns (uint128[2] memory applicableProtocolFeeParams) { applicableProtocolFeeParams = currProtocolFeeParams; - if (totalSupply() + _borrowerReward + _lenderReward < MAX_SUPPLY) { - _mint(loan.borrower, _borrowerReward); - _mint(lenderVault, _lenderReward); + if (loan.loanToken == MYSO_TOKEN && lenderVault == mysoIOOVault) { + totalMysoLoanAmount += loan.initLoanAmount; + } + if ( + mysoRewardsActive && + IERC20(STAKED_MYSO_TOKEN).balanceOf(loan.borrower) > + minMysoStakingRequirement + ) { + RewardInfo memory rewardInfo = rewardInfos[loan.collToken]; + uint256 rewardAmount = loan.initCollAmount * + rewardInfo.mysoTokenMultiplier; + uint256 bal = IERC20(MYSO_TOKEN).balanceOf(address(this)); + rewardAmount = rewardAmount > bal ? bal : rewardAmount; + if ( + loan.initCollAmount > rewardInfo.collThreshold && + rewardAmount > 0 + ) { + SafeERC20.safeTransfer( + IERC20(MYSO_TOKEN), + ILenderVaultImpl(lenderVault).owner(), + rewardAmount + ); + } } } + // solhint-disable no-empty-blocks function processP2PCreateVault( uint256 /*numRegisteredVaults*/, address /*vaultCreator*/, - address newLenderVaultAddr - ) external { - _mint(newLenderVaultAddr, _vaultCreationReward); - } + address /*newLenderVaultAddr*/ + ) external {} // solhint-disable no-empty-blocks function processP2PCreateWrappedTokenForERC721s( @@ -103,15 +141,41 @@ contract TestnetTokenManager is ERC20, Ownable2Step, IMysoTokenManager { uint256 /*numLoanProposals*/ ) external {} - function setRewards( - uint256 borrowerReward, - uint256 lenderReward, - uint256 vaultCreationReward + 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(); - _borrowerReward = borrowerReward; - _lenderReward = lenderReward; - _vaultCreationReward = vaultCreationReward; + 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 { @@ -126,8 +190,4 @@ contract TestnetTokenManager is ERC20, Ownable2Step, IMysoTokenManager { } super._transferOwnership(_newOwnerProposal); } - - function decimals() public view override returns (uint8) { - return _decimals; - } } 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); + } +} diff --git a/contracts/tokenManager/MysoTokenManager.sol b/contracts/tokenManager/MysoTokenManager.sol new file mode 100644 index 00000000..ac8aedbc --- /dev/null +++ b/contracts/tokenManager/MysoTokenManager.sol @@ -0,0 +1,273 @@ +// 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 {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.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 {Helpers} from "../Helpers.sol"; +import {IMysoTokenManager} from "../interfaces/IMysoTokenManager.sol"; +import {ILenderVaultImpl} from "../peer-to-peer/interfaces/ILenderVaultImpl.sol"; +import {IStMysoToken} from "./interfaces/IStMysoToken.sol"; + +contract MysoTokenManager is Ownable2Step, IMysoTokenManager { + using SafeERC20 for IERC20; + using ECDSA for bytes32; + + struct RewardInfo { + uint128 collThreshold; + // Multiplier in units of BASE + uint128 mysoTokenMultiplier; + } + + uint256 public totalMysoLoanAmount; + uint256 public minMysoWeight; + + address public mysoIOOVault; + address public mysoToken; + address public stMysoToken; + address public signingAuthority; + mapping(address => RewardInfo) public rewardInfos; + mapping(bytes32 => bool) public alreadyClaimed; + event RewardInfoSet( + address indexed collToken, + uint128 collThreshold, + uint128 mysoTokenMultiplier + ); + event MysoTokenSet(address mysoToken); + event StMysoTokenSet(address stMysoToken); + event MinMysoWeightSet(uint256 minMysoWeight); + event IOOVaultSet(address mysoIOOVault); + event SignerSet(address signer); + event TotalMysoLoanAmountSet(uint256 totalMysoLoanAmount); + + error NotAllowed(); + + constructor( + address _mysoIOOVault, + address _mysoToken, + address _stMysoToken, + uint256 _minMysoWeight, + address _signer + ) { + mysoIOOVault = _mysoIOOVault; + mysoToken = _mysoToken; + stMysoToken = _stMysoToken; + minMysoWeight = _minMysoWeight; + signingAuthority = _signer; + _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; + address _mysoToken = mysoToken; + if (_mysoToken != address(0)) { + bool isMysoIoo = loan.loanToken == _mysoToken && + lenderVault == mysoIOOVault; + if (!_isAllowed(isMysoIoo, borrowInstructions, loan)) { + revert NotAllowed(); + } + if (isMysoIoo) { + totalMysoLoanAmount += loan.initLoanAmount; + } + address _stMysoToken = stMysoToken; + if ( + _stMysoToken != address(0) && + IStMysoToken(_stMysoToken).weight(loan.borrower) >= + minMysoWeight + ) { + RewardInfo memory rewardInfo = rewardInfos[loan.collToken]; + // @dev: multiplier in BASE cancels out with MYT decimals + uint256 rewardAmount = (loan.initCollAmount * + rewardInfo.mysoTokenMultiplier) / + (10 ** IERC20Metadata(loan.collToken).decimals()); + uint256 bal = IERC20(_mysoToken).balanceOf(address(this)); + rewardAmount = rewardAmount > bal ? bal : rewardAmount; + if ( + loan.initCollAmount >= rewardInfo.collThreshold && + rewardAmount > 0 + ) { + SafeERC20.safeTransfer( + IERC20(_mysoToken), + ILenderVaultImpl(lenderVault).owner(), + rewardAmount + ); + } + } + } + } + + // solhint-disable no-empty-blocks + function processP2PCreateVault( + uint256 /*numRegisteredVaults*/, + address /*vaultCreator*/, + address /*newLenderVaultAddr*/ + ) external virtual {} + + // solhint-disable no-empty-blocks + function processP2PCreateWrappedTokenForERC721s( + address /*tokenCreator*/, + DataTypesPeerToPeer.WrappedERC721TokenInfo[] + calldata /*tokensToBeWrapped*/, + bytes calldata /*mysoTokenManagerData*/ + ) external virtual {} + + // solhint-disable no-empty-blocks + function processP2PCreateWrappedTokenForERC20s( + address /*tokenCreator*/, + DataTypesPeerToPeer.WrappedERC20TokenInfo[] + calldata /*tokensToBeWrapped*/, + bytes calldata /*mysoTokenManagerData*/ + ) external virtual {} + + // solhint-disable no-empty-blocks + function processP2PoolDeposit( + address /*fundingPool*/, + address /*depositor*/, + uint256 /*depositAmount*/, + uint256 /*depositLockupDuration*/, + uint256 /*transferFee*/ + ) external virtual {} + + // solhint-disable no-empty-blocks + function processP2PoolSubscribe( + address /*fundingPool*/, + address /*subscriber*/, + address /*loanProposal*/, + uint256 /*subscriptionAmount*/, + uint256 /*subscriptionLockupDuration*/, + uint256 /*totalSubscriptions*/, + DataTypesPeerToPool.LoanTerms calldata /*loanTerms*/ + ) external virtual {} + + // solhint-disable no-empty-blocks + function processP2PoolLoanFinalization( + address /*loanProposal*/, + address /*fundingPool*/, + address /*arranger*/, + address /*borrower*/, + uint256 /*grossLoanAmount*/, + bytes calldata /*mysoTokenManagerData*/ + ) external virtual {} + + // solhint-disable no-empty-blocks + function processP2PoolCreateLoanProposal( + address /*fundingPool*/, + address /*proposalCreator*/, + address /*collToken*/, + uint256 /*arrangerFee*/, + uint256 /*numLoanProposals*/ + ) external virtual {} + + 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 setMinMysoWeight(uint256 _minMysoWeight) external { + _checkOwner(); + minMysoWeight = _minMysoWeight; + emit MinMysoWeightSet(_minMysoWeight); + } + + function setMysoToken(address _mysoToken) external { + _checkOwner(); + mysoToken = _mysoToken; + emit MysoTokenSet(_mysoToken); + } + + function setStMysoToken(address _stMysoToken) external { + _checkOwner(); + stMysoToken = _stMysoToken; + emit StMysoTokenSet(_stMysoToken); + } + + function setIOOVault(address _mysoIOOVault) external { + _checkOwner(); + mysoIOOVault = _mysoIOOVault; + emit IOOVaultSet(_mysoIOOVault); + } + + function setSigner(address _signer) external { + _checkOwner(); + signingAuthority = _signer; + emit SignerSet(_signer); + } + + function setTotalMysoLoanAmount(uint256 _totalMysoLoanAmount) external { + _checkOwner(); + totalMysoLoanAmount = _totalMysoLoanAmount; + emit TotalMysoLoanAmountSet(_totalMysoLoanAmount); + } + + function transferOwnership(address _newOwnerProposal) public override { + _checkOwner(); + if ( + _newOwnerProposal == address(0) || + _newOwnerProposal == address(this) || + _newOwnerProposal == pendingOwner() || + _newOwnerProposal == owner() + ) { + revert Errors.InvalidNewOwnerProposal(); + } + super._transferOwnership(_newOwnerProposal); + } + + function _isAllowed( + bool isMysoIoo, + DataTypesPeerToPeer.BorrowTransferInstructions + calldata borrowInstructions, + DataTypesPeerToPeer.Loan calldata loan + ) internal virtual returns (bool) { + if (isMysoIoo) { + address _signer = signingAuthority; + if (_signer == address(0)) { + return true; + } else { + (bytes memory compactSig, uint256 nonce) = abi.decode( + borrowInstructions.mysoTokenManagerData, + (bytes, uint256) + ); + bytes32 payloadHash = keccak256( + abi.encode(loan.borrower, nonce) + ); + if (alreadyClaimed[payloadHash]) { + return false; + } + (bytes32 r, bytes32 vs) = Helpers.splitSignature(compactSig); + bytes32 messageHash = ECDSA.toEthSignedMessageHash(payloadHash); + address recoveredSigner = messageHash.recover(r, vs); + if (recoveredSigner == _signer) { + alreadyClaimed[payloadHash] = true; + return true; + } + return false; + } + } else { + return true; + } + } +} diff --git a/contracts/tokenManager/MysoTokenManagerArbitrum.sol b/contracts/tokenManager/MysoTokenManagerArbitrum.sol new file mode 100644 index 00000000..8e180698 --- /dev/null +++ b/contracts/tokenManager/MysoTokenManagerArbitrum.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.19; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {MysoTokenManagerWithCaps} from "./MysoTokenManagerWithCaps.sol"; +import {DataTypesPeerToPeer} from "../peer-to-peer/DataTypesPeerToPeer.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MysoTokenManagerArbitrum is MysoTokenManagerWithCaps { + using SafeERC20 for IERC20Metadata; + + struct P2PoolRewardInfo { + address rewardToken; + uint256 rewardMultiplier; + } + mapping(address => mapping(uint256 => mapping(uint256 => P2PoolRewardInfo))) + public p2PoolRewardInfo; + + event P2PoolRewardInfoSet( + address indexed fundingPool, + uint256 depositLockupDuration, + uint256 index, + address rewardToken, + uint256 rewardMultiplier + ); + + error InvalidToAddress(); + error OufOfBounds(); + + constructor( + address _mysoIOOVault, + address _mysoToken, + address _stMysoToken, + uint256 _minMysoWeight, + address _signer, + CollatCapInfo[] memory _collatCaps + ) + MysoTokenManagerWithCaps( + _mysoIOOVault, + _mysoToken, + _stMysoToken, + _minMysoWeight, + _signer, + _collatCaps + ) + {} + + function processP2PoolDeposit( + address fundingPool, + address depositor, + uint256 depositAmount, + uint256 depositLockupDuration, + uint256 /*transferFee*/ + ) external virtual override { + ( + uint256[] memory rewards, + address[] memory tokens + ) = getP2PoolDepositRewards( + fundingPool, + depositAmount, + depositLockupDuration + ); + for (uint256 i = 0; i < 8; i++) { + if (rewards[i] == 0) { + break; + } + IERC20Metadata(tokens[i]).safeTransfer(depositor, rewards[i]); + } + } + + function setP2PoolRewardInfo( + address fundingPool, + uint256 depositLockupDuration, + uint256 index, + address rewardToken, + uint256 rewardMultiplier + ) external onlyOwner { + if (index >= 8) { + revert OufOfBounds(); + } + p2PoolRewardInfo[fundingPool][depositLockupDuration][ + index + ] = P2PoolRewardInfo({ + rewardToken: rewardToken, + rewardMultiplier: rewardMultiplier + }); + emit P2PoolRewardInfoSet( + fundingPool, + depositLockupDuration, + index, + rewardToken, + rewardMultiplier + ); + } + + function getP2PoolDepositRewards( + address fundingPool, + uint256 depositAmount, + uint256 depositLockupDuration + ) public view returns (uint256[] memory, address[] memory) { + uint256[] memory rewards = new uint256[](8); + address[] memory tokens = new address[](8); + + for (uint256 i = 0; i < 8; i++) { + P2PoolRewardInfo memory _p2PoolRewardInfo = p2PoolRewardInfo[ + fundingPool + ][depositLockupDuration][i]; + if ( + _p2PoolRewardInfo.rewardToken == address(0) || + _p2PoolRewardInfo.rewardMultiplier == 0 + ) { + break; + } + rewards[i] = + (_p2PoolRewardInfo.rewardMultiplier * depositAmount) / + 1e18; + tokens[i] = _p2PoolRewardInfo.rewardToken; + } + + return (rewards, tokens); + } +} diff --git a/contracts/tokenManager/MysoTokenManagerWithCaps.sol b/contracts/tokenManager/MysoTokenManagerWithCaps.sol new file mode 100644 index 00000000..4591ce25 --- /dev/null +++ b/contracts/tokenManager/MysoTokenManagerWithCaps.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.19; + +import {MysoTokenManager} from "./MysoTokenManager.sol"; +import {DataTypesPeerToPeer} from "../peer-to-peer/DataTypesPeerToPeer.sol"; + +contract MysoTokenManagerWithCaps is MysoTokenManager { + struct CollatCapInfo { + address token; + uint128 maxPledge; + } + + mapping(address => uint256) public collatCaps; + mapping(address => uint256) public totalPledged; + + event CollatCapsSet(CollatCapInfo[] collatCaps); + + constructor( + address _mysoIOOVault, + address _mysoToken, + address _stMysoToken, + uint256 _minMysoWeight, + address _signer, + CollatCapInfo[] memory _collatCaps + ) + MysoTokenManager( + _mysoIOOVault, + _mysoToken, + _stMysoToken, + _minMysoWeight, + _signer + ) + { + for (uint256 i = 0; i < _collatCaps.length; i++) { + collatCaps[_collatCaps[i].token] = _collatCaps[i].maxPledge; + } + } + + function setCollatCaps(CollatCapInfo[] calldata _collatCaps) external { + _checkOwner(); + for (uint256 i = 0; i < _collatCaps.length; i++) { + collatCaps[_collatCaps[i].token] = _collatCaps[i].maxPledge; + } + emit CollatCapsSet(_collatCaps); + } + + function _isAllowed( + bool isMysoIoo, + DataTypesPeerToPeer.BorrowTransferInstructions + calldata borrowInstructions, + DataTypesPeerToPeer.Loan calldata loan + ) internal override returns (bool isAllowed) { + isAllowed = super._isAllowed(isMysoIoo, borrowInstructions, loan); + if (!isAllowed) { + return false; + } + if (isMysoIoo) { + uint256 _newPledged = totalPledged[loan.collToken] + + loan.initCollAmount; + uint256 _collatCap = collatCaps[loan.collToken]; + // @dev: default 0 value means no cap + if (_collatCap == 0) { + return true; + } + isAllowed = _newPledged <= collatCaps[loan.collToken]; + if (isAllowed) { + totalPledged[loan.collToken] = _newPledged; + } + } else { + isAllowed = true; + } + } +} diff --git a/contracts/tokenManager/interfaces/IStMysoToken.sol b/contracts/tokenManager/interfaces/IStMysoToken.sol new file mode 100644 index 00000000..21b05fa0 --- /dev/null +++ b/contracts/tokenManager/interfaces/IStMysoToken.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +interface IStMysoToken { + function weight(address acc) external returns (uint256); +} diff --git a/hardhat.config.ts b/hardhat.config.ts index d44dea70..b64cde37 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -28,6 +28,28 @@ export const getRecentMainnetForkingConfig = () => { return { chainId: chainId, url: url, blockNumber: blockNumber } } +export const getMysoOracleMainnetForkingConfig = () => { + const INFURA_API_KEY = process.env.INFURA_API_KEY + if (INFURA_API_KEY === undefined) { + throw new Error('Invalid hardhat.config.ts! Need to set `INFURA_API_KEY`!') + } + const chainId = 1 + const url = `https://mainnet.infura.io/v3/${INFURA_API_KEY}` + const blockNumber = 19300000 // 2024-02-24 (9PM UTC) + return { chainId: chainId, url: url, blockNumber: blockNumber } +} + +export const getMysoOracleArbitrumForkingConfig = () => { + const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY + if (ALCHEMY_API_KEY === undefined) { + throw new Error('Invalid hardhat.config.ts! Need to set `ALCHEMY_API_KEY`!') + } + const chainId = 42161 + const url = `https://arb-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}` + const blockNumber = 196000000 // 2024-03-31 (4AM UTC) + return { chainId: chainId, url: url, blockNumber: blockNumber } +} + export const getArbitrumForkingConfig = () => { const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY if (ALCHEMY_API_KEY === undefined) { @@ -121,7 +143,22 @@ const config: HardhatUserConfig = { mantleMainnet: { chainId: 5000, url: `https://rpc.mantle.xyz/`, //`https://rpc.ankr.com/mantle/${process.env.ANKR_API_KEY}`, - accounts: [`0x${process.env.MANTLE_MAINNET_DEPLOYER_KEY}`] + accounts: [`0x${process.env.MANTLE_MAINNET_DEPLOYER_KEY}`, `0x${process.env.MANTLE_EARLY_ACCESS}`] + }, + mainnet: { + chainId: 1, + url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_MAINNET_API_KEY}`, + accounts: [`0x${process.env.ETHEREUM_MAINNET_DEPLOYER}`, `0x${process.env.ETHEREUM_MAINNET_EARLY_ACCESS}`] + }, + arbitrum: { + chainId: 42161, + url: `https://arb-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`, + accounts: [`0x${process.env.ARBITRUM_MAINNET_DEPLOYER_KEY}`, `0x${process.env.ETHEREUM_MAINNET_EARLY_ACCESS}`] + }, + bnb: { + chainId: 56, + url: `https://bsc.drpc.org`, //`https://bsc.meowrpc.com`, + accounts: [`0x${process.env.BNB_DEPLOYER_KEY}`, `0x${process.env.BNB_EARLY_ACCESS}`] } }, mocha: { @@ -145,6 +182,9 @@ const config: HardhatUserConfig = { }, gasReporter: { enabled: true + }, + etherscan: { + apiKey: process.env.ARBISCAN_API_KEY //process.env.ARBISCAN_API_KEY, BSCAN_API_KEY } } diff --git a/scripts/peer-to-peer/dao/deployMysoArbitrumOracle.ts b/scripts/peer-to-peer/dao/deployMysoArbitrumOracle.ts new file mode 100644 index 00000000..91277f0d --- /dev/null +++ b/scripts/peer-to-peer/dao/deployMysoArbitrumOracle.ts @@ -0,0 +1,62 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + + const usdc = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" + const usdt = "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" + const dai = "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + const usdcUsdOracle = "0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3" + const usdtUsdOracle = "0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7" + const daiUsdOracle = "0xc5C8E77B397E531B8EC06BFb0048328B30E9eCfB" + const tokenAddrs = [usdc, usdt, dai] + const oracleAddrs = [usdcUsdOracle, usdtUsdOracle, daiUsdOracle] + const owner = deployer.address + const mysoTokenManager = "0x5241Aebb80db39b1A54c12b2A7eb96C431F8C9f1" + // for curve params, see: https://www.desmos.com/calculator/fo7fpzfcaq + // NOTE: price is token price, NOT FDV (ie to make consistent with desmos plot shift by 2 decimals) + const maxPrice = ethers.utils.parseUnits("0.55", 8) + // NOTE: k is in token price, NOT FDV (ie to make consistent with desmos plot shift by 2 decimals) + const k = ethers.utils.parseUnits("0.66", 18) + const a = ethers.utils.parseUnits("8", 3) + const b = ethers.utils.parseUnits("0.8", 3) + + console.log(tokenAddrs) + console.log(oracleAddrs) + console.log(owner) + console.log(mysoTokenManager) + console.log(maxPrice) + console.log(k) + console.log(a) + console.log(b) + + const MysoArbitrumUsdOracle = await ethers.getContractFactory('MysoArbitrumUsdOracle') + const mysoArbitrumUsdOracle = await MysoArbitrumUsdOracle.connect(deployer).deploy(tokenAddrs, oracleAddrs, owner, mysoTokenManager, maxPrice, k, a, b) + await mysoArbitrumUsdOracle.deployed() + logger.log('mysoArbitrumUsdOracle deployed at:', mysoArbitrumUsdOracle.address) + + // npx hardhat verify --constructor-args .\scripts\peer-to-peer\dao\mysoArbitrumOracleArgs.js --network arbitrum 0x2da3354790f942E3A57c325220F10731a7887946 +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/dao/deployMysoArbitrumTokenManager.ts b/scripts/peer-to-peer/dao/deployMysoArbitrumTokenManager.ts new file mode 100644 index 00000000..bbbcff75 --- /dev/null +++ b/scripts/peer-to-peer/dao/deployMysoArbitrumTokenManager.ts @@ -0,0 +1,49 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer, earlyAccessSigner] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + + const mysoIOOVault = "0xC9d073458C4C2737f9A81940FA744E17F9F11eB0" + const mysoToken = "0x25bA1ED5DEEA9d8e8add565dA069Ed1eDA397C12" + const stMysoToken = ethers.constants.AddressZero + const minMysoWeight = ethers.constants.MaxUint256 + const signer = earlyAccessSigner.address + const collatCaps : any = [] + + console.log(mysoIOOVault) + console.log(mysoToken) + console.log(stMysoToken) + console.log(minMysoWeight) + console.log(signer) + console.log(collatCaps) + + const MysoTokenManagerWithCaps = await ethers.getContractFactory('MysoTokenManagerWithCaps') + const mysoTokenManagerWithCaps = await MysoTokenManagerWithCaps.connect(deployer).deploy(mysoIOOVault, mysoToken, stMysoToken, minMysoWeight, signer, collatCaps) + await mysoTokenManagerWithCaps.deployed() + logger.log('mysoTokenManagerWithCaps deployed at:', mysoTokenManagerWithCaps.address) + + // npx hardhat verify --constructor-args .\scripts\peer-to-peer\dao\mysoArbitrumTokenManagerArgs.js --network arbitrum 0x5241Aebb80db39b1A54c12b2A7eb96C431F8C9f1 +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/dao/deployMysoBnbOracle.ts b/scripts/peer-to-peer/dao/deployMysoBnbOracle.ts new file mode 100644 index 00000000..458be497 --- /dev/null +++ b/scripts/peer-to-peer/dao/deployMysoBnbOracle.ts @@ -0,0 +1,59 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + + const usdcBinancePegged = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" + const usdc = "0x8965349fb649A33a30cbFDa057D8eC2C48AbE2A2" + const usdcUsdOracle = "0x51597f405303C4377E36123cBc172b13269EA163" + const tokenAddrs = [usdcBinancePegged, usdc] + const oracleAddrs = [usdcUsdOracle, usdcUsdOracle] + const owner = deployer.address + const mysoTokenManager = "0x937EE6ddB61fE1d4576b34b7ab598004612A4cD8" + // for curve params, see: https://www.desmos.com/calculator/fo7fpzfcaq + // NOTE: price is token price, NOT FDV (ie to make consistent with desmos plot shift by 2 decimals) + const maxPrice = ethers.utils.parseUnits("0.55", 8) + // NOTE: k is in token price, NOT FDV (ie to make consistent with desmos plot shift by 2 decimals) + const k = ethers.utils.parseUnits("0.66", 18) + const a = ethers.utils.parseUnits("8", 3) + const b = ethers.utils.parseUnits("0.8", 3) + + console.log(tokenAddrs) + console.log(oracleAddrs) + console.log(owner) + console.log(mysoTokenManager) + console.log(maxPrice) + console.log(k) + console.log(a) + console.log(b) + + const MysoBnbUsdOracle = await ethers.getContractFactory('MysoBnbUsdOracle') + const mysoBnbUsdOracle = await MysoBnbUsdOracle.connect(deployer).deploy(tokenAddrs, oracleAddrs, owner, mysoTokenManager, maxPrice, k, a, b) + await mysoBnbUsdOracle.deployed() + logger.log('mysoBnbUsdOracle deployed at:', mysoBnbUsdOracle.address) + + // npx hardhat verify --constructor-args .\scripts\peer-to-peer\dao\mysoBnbTokenOracleArgs.js --network bnb 0xf5BfbdDaf0e0CD3551175b6296590BDc6362Fd69 +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/dao/deployMysoBnbTokenManager.ts b/scripts/peer-to-peer/dao/deployMysoBnbTokenManager.ts new file mode 100644 index 00000000..d1612940 --- /dev/null +++ b/scripts/peer-to-peer/dao/deployMysoBnbTokenManager.ts @@ -0,0 +1,49 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer, earlyAccessSigner] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + + const mysoIOOVault = "0x6b50BFf92863C1c70EfE0CA65602BAfE47a7577c" + const mysoToken = "0x85be1fab94e9ab109ea339e164689281fad3f0df" + const stMysoToken = ethers.constants.AddressZero + const minMysoWeight = ethers.constants.MaxUint256 + const signer = earlyAccessSigner.address + const collatCaps : any = [] + + console.log(mysoIOOVault) + console.log(mysoToken) + console.log(stMysoToken) + console.log(minMysoWeight) + console.log(signer) + console.log(collatCaps) + + const MysoTokenManagerWithCaps = await ethers.getContractFactory('MysoTokenManagerWithCaps') + const mysoTokenManagerWithCaps = await MysoTokenManagerWithCaps.connect(deployer).deploy(mysoIOOVault, mysoToken, stMysoToken, minMysoWeight, signer, collatCaps) + await mysoTokenManagerWithCaps.deployed() + logger.log('mysoTokenManagerWithCaps deployed at:', mysoTokenManagerWithCaps.address) + + // npx hardhat verify --constructor-args .\scripts\peer-to-peer\dao\mysoBnbTokenManagerArgs.js --network arbitrum 0x937EE6ddB61fE1d4576b34b7ab598004612A4cD8 +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/dao/deployMysoMainnetOracle.ts b/scripts/peer-to-peer/dao/deployMysoMainnetOracle.ts new file mode 100644 index 00000000..1dbbc1d5 --- /dev/null +++ b/scripts/peer-to-peer/dao/deployMysoMainnetOracle.ts @@ -0,0 +1,62 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + + const usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + const usdt = "0xdAC17F958D2ee523a2206206994597C13D831ec7" + const dai = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + const usdcEthOracle = "0x986b5E1e1755e3C2440e960477f25201B0a8bbD4" + const usdtEthOracle = "0xEe9F2375b4bdF6387aa8265dD4FB8F16512A1d46" + const daiEthOracle = "0x773616E4d11A78F511299002da57A0a94577F1f4" + const tokenAddrs = [usdc, usdt, dai] + const oracleAddrs = [usdcEthOracle, usdtEthOracle, daiEthOracle] + const owner = deployer.address + const mysoTokenManager = "0x795F84270B085F449A92E943De38062Aa5A2fBa4" + // for curve params, see: https://www.desmos.com/calculator/cpi1fcpcwz + // NOTE: price is token price, NOT FDV (ie to make consistent with desmos plot shift by 2 decimals) + const maxPrice = ethers.utils.parseUnits("0.55", 8) + // NOTE: k is in token price, NOT FDV (ie to make consistent with desmos plot shift by 2 decimals) + const k = ethers.utils.parseUnits("0.66", 18) + const a = ethers.utils.parseUnits("4.14", 3) + const b = ethers.utils.parseUnits("0.8", 3) + + console.log(tokenAddrs) + console.log(oracleAddrs) + console.log(owner) + console.log(mysoTokenManager) + console.log(maxPrice) + console.log(k) + console.log(a) + console.log(b) + + const MysoOracle = await ethers.getContractFactory('MysoOracle') + const mysoOracle = await MysoOracle.connect(deployer).deploy(tokenAddrs, oracleAddrs, owner, mysoTokenManager, maxPrice, k, a, b) + await mysoOracle.deployed() + logger.log('mysoOracle deployed at:', mysoOracle.address) + + // npx hardhat verify --constructor-args .\scripts\peer-to-peer\dao\mysoMainnetOracleArgs.js --network mainnet 0xd02cE60A586f74124BB94c9DBE27c151344e7cEa +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/dao/deployMysoMainnetTokenManager.ts b/scripts/peer-to-peer/dao/deployMysoMainnetTokenManager.ts new file mode 100644 index 00000000..6d57d268 --- /dev/null +++ b/scripts/peer-to-peer/dao/deployMysoMainnetTokenManager.ts @@ -0,0 +1,49 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer, earlyAccessSigner] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + + const mysoIOOVault = "0xB6307b0f7f85BB5c8DF1569ac1621A78A2Be11A7" + const mysoToken = "0x5fde99e121f3ac02e7d6acb081db1f89c1e93c17" + const stMysoToken = ethers.constants.AddressZero + const minMysoWeight = ethers.constants.MaxUint256 + const signer = earlyAccessSigner.address + const collatCaps : any = [] + + console.log(mysoIOOVault) + console.log(mysoToken) + console.log(stMysoToken) + console.log(minMysoWeight) + console.log(signer) + console.log(collatCaps) + + const MysoTokenManagerWithCaps = await ethers.getContractFactory('MysoTokenManagerWithCaps') + const mysoTokenManagerWithCaps = await MysoTokenManagerWithCaps.connect(deployer).deploy(mysoIOOVault, mysoToken, stMysoToken, minMysoWeight, signer, collatCaps) + await mysoTokenManagerWithCaps.deployed() + logger.log('mysoTokenManagerWithCaps deployed at:', mysoTokenManagerWithCaps.address) + + // npx hardhat verify 0x16B67F0dc94e0fAd995d4dFAB77170d30848c277 "0xcE3d0e78c15C30ecf631ef529581Af3de0478895" "0x0000000000000000000000000000000000000000" "5500000000000000" "66000000000000000000" "1350" "600" --network sepolia +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/dao/deployMysoMantleTokenManager.ts b/scripts/peer-to-peer/dao/deployMysoMantleTokenManager.ts new file mode 100644 index 00000000..ee6e7e86 --- /dev/null +++ b/scripts/peer-to-peer/dao/deployMysoMantleTokenManager.ts @@ -0,0 +1,49 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer, earlyAccessSigner] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + + const mysoIOOVault = "0xC9d073458C4C2737f9A81940FA744E17F9F11eB0" + const mysoToken = "0x25bA1ED5DEEA9d8e8add565dA069Ed1eDA397C12" + const stMysoToken = ethers.constants.AddressZero + const minMysoWeight = ethers.constants.MaxUint256 + const signer = earlyAccessSigner.address + const collatCaps : any = [] + + console.log(mysoIOOVault) + console.log(mysoToken) + console.log(stMysoToken) + console.log(minMysoWeight) + console.log(signer) + console.log(collatCaps) + + const MysoTokenManagerWithCaps = await ethers.getContractFactory('MysoTokenManagerWithCaps') + const mysoTokenManagerWithCaps = await MysoTokenManagerWithCaps.connect(deployer).deploy(mysoIOOVault, mysoToken, stMysoToken, minMysoWeight, signer, collatCaps) + await mysoTokenManagerWithCaps.deployed() + logger.log('mysoTokenManagerWithCaps deployed at:', mysoTokenManagerWithCaps.address) + + // npx hardhat verify 0x16B67F0dc94e0fAd995d4dFAB77170d30848c277 "0xcE3d0e78c15C30ecf631ef529581Af3de0478895" "0x0000000000000000000000000000000000000000" "5500000000000000" "66000000000000000000" "1350" "600" --network sepolia +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/dao/manageAddressRegistry.json b/scripts/peer-to-peer/dao/manageAddressRegistry.json index bf2badda..eabe5b52 100644 --- a/scripts/peer-to-peer/dao/manageAddressRegistry.json +++ b/scripts/peer-to-peer/dao/manageAddressRegistry.json @@ -20,12 +20,12 @@ ] }, "sepolia": { - "addressRegistry": "0x5EdDDFCAB93b4637b3365E4BB8c69aa1806AE46f", + "addressRegistry": "0x028A90c9834E5e52dB31886D9E05c592b6173F21", "actions": [ { "type": "setWhitelistState", - "addresses": ["0x45b5B91b2f507F881aD42cCb8446bE88BaAfcDAE"], - "state": 1 + "addresses": ["0xc2dDc2330C06E2A7BfE2084a9A4f1A38f83A6B6f"], + "state": 9 } ] }, @@ -33,8 +33,34 @@ "addressRegistry": "0x196A6649e2A7eb225Ee53BA507552cCc8BcdB07a", "actions": [ { - "type": "transferOwnership", - "newOwnerProposal": "0x8E2Dd4d57dF7E3480ABFe0eACa1c0Ccd41F1E18E" + "type": "setWhitelistState", + "addresses": ["0x5eeFfE74E10765e832df75760c7c7dA7531514F2"], + "state": 2 + } + ] + }, + "arbitrum": { + "addressRegistry": "0x196A6649e2A7eb225Ee53BA507552cCc8BcdB07a", + "actions": [ + { + "type": "setWhitelistState", + "addresses": ["0x5eeFfE74E10765e832df75760c7c7dA7531514F2"], + "state": 0 + }, + { + "type": "setWhitelistState", + "addresses": ["0x2da3354790f942E3A57c325220F10731a7887946"], + "state": 2 + } + ] + }, + "bnb": { + "addressRegistry": "0x5B1d65dCE6ade0c82678Ccf51ABE255266A9BD18", + "actions": [ + { + "type": "setWhitelistState", + "addresses": ["0xf5BfbdDaf0e0CD3551175b6296590BDc6362Fd69"], + "state": 2 } ] } diff --git a/scripts/peer-to-peer/dao/mysoArbitrumOracleArgs.js b/scripts/peer-to-peer/dao/mysoArbitrumOracleArgs.js new file mode 100644 index 00000000..193631e1 --- /dev/null +++ b/scripts/peer-to-peer/dao/mysoArbitrumOracleArgs.js @@ -0,0 +1,18 @@ +module.exports = [ + [ + '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1' + ], + [ + '0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3', + '0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7', + '0xc5C8E77B397E531B8EC06BFb0048328B30E9eCfB' + ], + "0x2b13a4AcFF9934f1c60582Dcd5a7db947E74AdEb", + "0x5241Aebb80db39b1A54c12b2A7eb96C431F8C9f1", + "55000000", + "660000000000000000", + "8000", + "800" +]; \ No newline at end of file diff --git a/scripts/peer-to-peer/dao/mysoArbitrumTokenManagerArgs.js b/scripts/peer-to-peer/dao/mysoArbitrumTokenManagerArgs.js new file mode 100644 index 00000000..4750a69d --- /dev/null +++ b/scripts/peer-to-peer/dao/mysoArbitrumTokenManagerArgs.js @@ -0,0 +1,8 @@ +module.exports = [ + "0xC9d073458C4C2737f9A81940FA744E17F9F11eB0", + "0x25bA1ED5DEEA9d8e8add565dA069Ed1eDA397C12", + "0x0000000000000000000000000000000000000000", + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "0x546881EbC10691072ECC0830Dba7E3E7281D9835", + [], + ]; \ No newline at end of file diff --git a/scripts/peer-to-peer/dao/mysoBnbTokenManagerArgs.js b/scripts/peer-to-peer/dao/mysoBnbTokenManagerArgs.js new file mode 100644 index 00000000..8c813c84 --- /dev/null +++ b/scripts/peer-to-peer/dao/mysoBnbTokenManagerArgs.js @@ -0,0 +1,8 @@ +module.exports = [ + "0x6b50BFf92863C1c70EfE0CA65602BAfE47a7577c", + "0x85be1fab94e9ab109ea339e164689281fad3f0df", + "0x0000000000000000000000000000000000000000", + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "0x546881EbC10691072ECC0830Dba7E3E7281D9835", + [], + ]; \ No newline at end of file diff --git a/scripts/peer-to-peer/dao/mysoBnbTokenOracleArgs.js b/scripts/peer-to-peer/dao/mysoBnbTokenOracleArgs.js new file mode 100644 index 00000000..811fdef7 --- /dev/null +++ b/scripts/peer-to-peer/dao/mysoBnbTokenOracleArgs.js @@ -0,0 +1,16 @@ +module.exports = [ + [ + '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + '0x8965349fb649A33a30cbFDa057D8eC2C48AbE2A2' + ], + [ + '0x51597f405303C4377E36123cBc172b13269EA163', + '0x51597f405303C4377E36123cBc172b13269EA163' + ], + "0x2b13a4AcFF9934f1c60582Dcd5a7db947E74AdEb", + "0x937EE6ddB61fE1d4576b34b7ab598004612A4cD8", + "55000000", + "660000000000000000", + "8000", + "800" + ]; \ No newline at end of file diff --git a/scripts/peer-to-peer/dao/mysoMainnetOracleArgs.js b/scripts/peer-to-peer/dao/mysoMainnetOracleArgs.js new file mode 100644 index 00000000..a72d4009 --- /dev/null +++ b/scripts/peer-to-peer/dao/mysoMainnetOracleArgs.js @@ -0,0 +1,18 @@ +module.exports = [ + [ + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + '0xdAC17F958D2ee523a2206206994597C13D831ec7', + '0x6B175474E89094C44Da98b954EedeAC495271d0F' + ], + [ + '0x986b5E1e1755e3C2440e960477f25201B0a8bbD4', + '0xEe9F2375b4bdF6387aa8265dD4FB8F16512A1d46', + '0x773616E4d11A78F511299002da57A0a94577F1f4' + ], + "0x2b13a4AcFF9934f1c60582Dcd5a7db947E74AdEb", + "0x795F84270B085F449A92E943De38062Aa5A2fBa4", + "55000000", + "660000000000000000", + "4140", + "800" +]; \ No newline at end of file diff --git a/scripts/peer-to-peer/dao/mysoMainnetTokenManagerArgs.js b/scripts/peer-to-peer/dao/mysoMainnetTokenManagerArgs.js new file mode 100644 index 00000000..bbdf7bcd --- /dev/null +++ b/scripts/peer-to-peer/dao/mysoMainnetTokenManagerArgs.js @@ -0,0 +1,8 @@ +module.exports = [ + "0xB6307b0f7f85BB5c8DF1569ac1621A78A2Be11A7", + "0x5fde99e121f3ac02e7d6acb081db1f89c1e93c17", + "0x0000000000000000000000000000000000000000", + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "0x546881EbC10691072ECC0830Dba7E3E7281D9835", + [], + ]; \ No newline at end of file diff --git a/scripts/peer-to-peer/utils/configTestMysoOracle.ts b/scripts/peer-to-peer/utils/configTestMysoOracle.ts new file mode 100644 index 00000000..3c2a1b29 --- /dev/null +++ b/scripts/peer-to-peer/utils/configTestMysoOracle.ts @@ -0,0 +1,42 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + const TestnetMysoOracle = await ethers.getContractFactory('TestnetMysoOracle') + const testnetMysoOracleAddr = "0x16B67F0dc94e0fAd995d4dFAB77170d30848c277" + const testnetMysoOracle = await TestnetMysoOracle.attach(testnetMysoOracleAddr) + const mysoTestnetTokenManager = "0xC71dBB6b1a9735F3259F0CF746C1c000BF02615c" + await testnetMysoOracle.connect(deployer).setMysoTokenManager(mysoTestnetTokenManager) + + /* + const maxPrice = "55000000" // 8 decimals + const k = "660000000000000000" // 18 decimals + const a = "1350" // scaled by 1000 + const b = "600" // scaled by 1000 + await testnetMysoOracle.connect(deployer).setMysoPriceParams(maxPrice, k, a, b) + logger.log('testnetMysoOracle deployed at:', testnetMysoOracle.address) + */ +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/utils/deployTestMysoOracle.ts b/scripts/peer-to-peer/utils/deployTestMysoOracle.ts new file mode 100644 index 00000000..e632188a --- /dev/null +++ b/scripts/peer-to-peer/utils/deployTestMysoOracle.ts @@ -0,0 +1,39 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + const TestnetMysoOracle = await ethers.getContractFactory('TestnetMysoOracle') + const owner = deployer.address + const mysoTokenManager = ethers.constants.AddressZero + const maxPrice = "55000000" // 8 decimals + const k = "66000000000000000000" // 18 decimals + const a = "1350" // scaled by 1000 + const b = "600" // scaled by 1000 + const testnetMysoOracle = await TestnetMysoOracle.connect(deployer).deploy(owner, mysoTokenManager, maxPrice, k, a, b) + await testnetMysoOracle.deployed() + logger.log('testnetMysoOracle deployed at:', testnetMysoOracle.address) + // npx hardhat verify 0x16B67F0dc94e0fAd995d4dFAB77170d30848c277 "0xcE3d0e78c15C30ecf631ef529581Af3de0478895" "0x0000000000000000000000000000000000000000" "5500000000000000" "66000000000000000000" "1350" "600" --network sepolia +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/utils/deployTestnetTokenManager.ts b/scripts/peer-to-peer/utils/deployTestnetTokenManager.ts new file mode 100644 index 00000000..ab869582 --- /dev/null +++ b/scripts/peer-to-peer/utils/deployTestnetTokenManager.ts @@ -0,0 +1,41 @@ +import { ethers } from 'hardhat' +import { Logger } from '../../helpers/misc' + +const hre = require('hardhat') +const path = require('path') +const scriptName = path.parse(__filename).name +const logger = new Logger(__dirname, scriptName) + +async function main() { + logger.log(`Starting ${scriptName}...`) + logger.log('Loading signer info (check hardhat.config.ts)...') + + const [deployer] = await ethers.getSigners() + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + + logger.log('Running script with the following deployer:', deployer.address) + logger.log('Deployer ETH balance:', ethers.utils.formatEther(deployerBal.toString())) + logger.log(`Deploying to network '${hardhatNetworkName}' (default provider network name '${network.name}')`) + logger.log(`Configured chain id '${hardhatChainId}' (default provider config chain id '${network.chainId}')`) + const MysoTokenManagerWithCaps = await ethers.getContractFactory('MysoTokenManagerWithCaps') + const mysoIOOVault = "0x060cceb8Cc54BaD7A01E02Ba11EeCE5304314726" + const mysoToken = "0x8fF1307ba7e5FDc3A411d259bAe641e2B1d897c4" + const stMysoToken = "0x0A6cBCB5Ac7Fc6B47f06c2cE3E828b6EEBf37B06" + const minMysoWeight = 0 + const signer = deployer.address + const collatCaps : any = [] + const mysoTokenManagerWithCaps = await MysoTokenManagerWithCaps.connect(deployer).deploy(mysoIOOVault, mysoToken, stMysoToken, minMysoWeight, signer, collatCaps) + await mysoTokenManagerWithCaps.deployed() + logger.log('testnetTokenManager deployed at:', mysoTokenManagerWithCaps.address) + logger.log(`'${mysoIOOVault}' '${mysoToken}' '${stMysoToken}' '${0}' '${signer}' '${collatCaps}'`) + + // npx hardhat verify 0xc2dDc2330C06E2A7BfE2084a9A4f1A38f83A6B6f --constructor-args .\scripts\peer-to-peer\utils\tokenManagerArgs.js --network sepolia +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-peer/utils/tokenManagerArgs.js b/scripts/peer-to-peer/utils/tokenManagerArgs.js new file mode 100644 index 00000000..24f5970b --- /dev/null +++ b/scripts/peer-to-peer/utils/tokenManagerArgs.js @@ -0,0 +1,8 @@ +module.exports = [ + "0x060cceb8Cc54BaD7A01E02Ba11EeCE5304314726", + "0x8fF1307ba7e5FDc3A411d259bAe641e2B1d897c4", + "0x0A6cBCB5Ac7Fc6B47f06c2cE3E828b6EEBf37B06", + 0, + "0xcE3d0e78c15C30ecf631ef529581Af3de0478895", + [] + ]; \ No newline at end of file diff --git a/scripts/peer-to-pool/createFundingPool.ts b/scripts/peer-to-pool/createFundingPool.ts new file mode 100644 index 00000000..6d73674e --- /dev/null +++ b/scripts/peer-to-pool/createFundingPool.ts @@ -0,0 +1,27 @@ +import { ethers } from 'hardhat' + +const hre = require('hardhat') + +async function main() { + const [deployer] = await ethers.getSigners() + + console.log(`Running deploy script for peer-to-peer system with account ${deployer.address}...`) + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + console.log(`deployerBal: ${deployerBal}`) + console.log(`network: ${network}`) + console.log(`hardhatNetworkName: ${hardhatNetworkName}`) + console.log(`hardhatChainId: ${hardhatChainId}`) + + const Factory = await ethers.getContractAt('Factory', '0xcbADd492221BB502e2811f74c1F6D3dE7BFe929e') + await Factory.createFundingPool("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8") +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/peer-to-pool/deploy.ts b/scripts/peer-to-pool/deploy.ts index 9cdfdec0..00e4b064 100644 --- a/scripts/peer-to-pool/deploy.ts +++ b/scripts/peer-to-pool/deploy.ts @@ -1,29 +1,42 @@ import { ethers } from 'hardhat' +const hre = require('hardhat') + async function main() { const [deployer] = await ethers.getSigners() console.log(`Running deploy script for peer-to-peer system with account ${deployer.address}...`) + const deployerBal = await ethers.provider.getBalance(deployer.address) + const network = await ethers.getDefaultProvider().getNetwork() + const hardhatNetworkName = hre.network.name + const hardhatChainId = hre.network.config.chainId + console.log(`deployerBal: ${deployerBal}`) + console.log(`network: ${network}`) + console.log(`hardhatNetworkName: ${hardhatNetworkName}`) + console.log(`hardhatChainId: ${hardhatChainId}`) + /* ************************************ */ /* DEPLOYMENT OF SYSTEM CONTRACTS START */ /* ************************************ */ - // deploy loan proposal implementation + const LoanProposalImpl = await ethers.getContractFactory('LoanProposalImpl') const loanProposalImpl = await LoanProposalImpl.connect(deployer).deploy() await loanProposalImpl.deployed() console.log(`LoanProposalImpl deployed to ${loanProposalImpl.address}`) - - // deploy loan proposal factory - const LoanProposalFactory = await ethers.getContractFactory('LoanProposalFactory') - const loanProposalFactory = await LoanProposalFactory.connect(deployer).deploy(loanProposalImpl.address) - await loanProposalFactory.deployed() - console.log(`LoanProposalFactory deployed to ${loanProposalFactory.address}`) + console.log(`npx hardhat verify ${loanProposalImpl.address} --network ${hardhatNetworkName}`) //example funding pool with usdc as deposit token - const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' - const FundingPool = await ethers.getContractFactory('FundingPool') - const fundingPool = await FundingPool.deploy(loanProposalFactory.address, USDC_ADDRESS) - await fundingPool.deployed() + const FundingPoolImpl = await ethers.getContractFactory('FundingPoolImpl') + const fundingPoolImpl = await FundingPoolImpl.deploy() + await fundingPoolImpl.deployed() + console.log(`npx hardhat verify ${fundingPoolImpl.address} --network ${hardhatNetworkName}`) + + // deploy loan proposal factory + const Factory = await ethers.getContractFactory('Factory') + const factory = await Factory.connect(deployer).deploy("0x1759eb38a2923b62562F8AD1c139509af1B1D52e", fundingPoolImpl.address) + await factory.deployed() + console.log(`Factory deployed to ${factory.address}`) + console.log(`npx hardhat verify ${factory.address} ${"0x1759eb38a2923b62562F8AD1c139509af1B1D52e"} ${fundingPoolImpl.address} --network ${hardhatNetworkName}`) } // We recommend this pattern to be able to use async/await everywhere diff --git a/test/peer-to-peer/arbitrum-myso-oracle-forked-tests.ts b/test/peer-to-peer/arbitrum-myso-oracle-forked-tests.ts new file mode 100644 index 00000000..4a970cdd --- /dev/null +++ b/test/peer-to-peer/arbitrum-myso-oracle-forked-tests.ts @@ -0,0 +1,488 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { HARDHAT_CHAIN_ID_AND_FORKING_CONFIG, getMysoOracleArbitrumForkingConfig } from '../../hardhat.config' + +// test config constants & vars +let snapshotId: String // use snapshot id to reset state before each test + +// constants +const hre = require('hardhat') +const BASE = ethers.BigNumber.from(10).pow(18) +const ONE_USDC = ethers.BigNumber.from(10).pow(6) +const ONE_WETH = ethers.BigNumber.from(10).pow(18) +const ONE_MYSO = ethers.BigNumber.from(10).pow(18) +const MAX_UINT128 = ethers.BigNumber.from(2).pow(128).sub(1) +const MAX_UINT256 = ethers.BigNumber.from(2).pow(256).sub(1) +const ONE_HOUR = ethers.BigNumber.from(60 * 60) +const ONE_DAY = ethers.BigNumber.from(60 * 60 * 24) +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' +const ZERO_BYTES32 = ethers.utils.formatBytes32String('') + +describe('Peer-to-Peer: Myso Recent Forked Arbitrum Oracle Tests', function () { + before(async () => { + console.log('Note: Running mainnet tests with the following forking config:') + console.log(HARDHAT_CHAIN_ID_AND_FORKING_CONFIG) + if (HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId !== 1) { + console.warn('Invalid hardhat forking config! Expected `HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId` to be 1!') + + console.warn('Assuming that current test run is using `npx hardhat coverage`!') + + console.warn('Re-importing mainnet forking config from `hardhat.config.ts`...') + const mainnetForkingConfig = getMysoOracleArbitrumForkingConfig() + + console.warn('Overwriting chainId to hardhat default `31337` to make off-chain signing consistent...') + HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId = 31337 + + console.log('block number: ', mainnetForkingConfig.blockNumber) + + console.warn('Trying to manually switch network to forked mainnet for this test file...') + await hre.network.provider.request({ + method: 'hardhat_reset', + params: [ + { + forking: { + jsonRpcUrl: mainnetForkingConfig.url, + blockNumber: mainnetForkingConfig.blockNumber + } + } + ] + }) + } + }) + + beforeEach(async () => { + snapshotId = await hre.network.provider.send('evm_snapshot') + }) + + afterEach(async () => { + await hre.network.provider.send('evm_revert', [snapshotId]) + }) + + async function setupTest() { + const [lender, signer, borrower, team, whitelistAuthority, someUser] = await ethers.getSigners() + /* ************************************ */ + /* DEPLOYMENT OF SYSTEM CONTRACTS START */ + /* ************************************ */ + // deploy address registry + const AddressRegistry = await ethers.getContractFactory('AddressRegistry') + const addressRegistry = await AddressRegistry.connect(team).deploy() + await addressRegistry.deployed() + + // deploy borrower gate way + const BorrowerGateway = await ethers.getContractFactory('BorrowerGateway') + const borrowerGateway = await BorrowerGateway.connect(team).deploy(addressRegistry.address) + await borrowerGateway.deployed() + + // deploy quote handler + const QuoteHandler = await ethers.getContractFactory('QuoteHandler') + const quoteHandler = await QuoteHandler.connect(team).deploy(addressRegistry.address) + await quoteHandler.deployed() + + // deploy lender vault implementation + const LenderVaultImplementation = await ethers.getContractFactory('LenderVaultImpl') + const lenderVaultImplementation = await LenderVaultImplementation.connect(team).deploy() + await lenderVaultImplementation.deployed() + + // deploy LenderVaultFactory + const LenderVaultFactory = await ethers.getContractFactory('LenderVaultFactory') + const lenderVaultFactory = await LenderVaultFactory.connect(team).deploy( + addressRegistry.address, + lenderVaultImplementation.address + ) + await lenderVaultFactory.deployed() + + // initialize address registry + await addressRegistry.connect(team).initialize(lenderVaultFactory.address, borrowerGateway.address, quoteHandler.address) + + /* ********************************** */ + /* DEPLOYMENT OF SYSTEM CONTRACTS END */ + /* ********************************** */ + + // create a vault + await lenderVaultFactory.connect(lender).createVault(ZERO_BYTES32) + const lenderVaultAddrs = await addressRegistry.registeredVaults() + const lenderVaultAddr = lenderVaultAddrs[0] + const lenderVault = await LenderVaultImplementation.attach(lenderVaultAddr) + + // add testnet token manager + const TestnetTokenManager = await ethers.getContractFactory('TestnetTokenManagerArbitrumOracle') + const testnetTokenManager = await TestnetTokenManager.connect(team).deploy() + await testnetTokenManager.connect(team).deployed() + await addressRegistry.connect(team).setWhitelistState([testnetTokenManager.address], 9) + + const DAI_ADDRESS = '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1' + const DAI_HOLDER = '0xd85e038593d7a098614721eae955ec2022b9b91b' + const dai = await ethers.getContractAt('IERC20', DAI_ADDRESS) + await ethers.provider.send('hardhat_setBalance', [DAI_HOLDER, '0x56BC75E2D63100000']) + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [DAI_HOLDER] + }) + + const daiHolder = await ethers.getSigner(DAI_HOLDER) + + await dai.connect(daiHolder).transfer(team.address, '15000000000000000000000000') + + const owner = await testnetTokenManager.owner() + + await testnetTokenManager.connect(team).setIOOVault(lenderVaultAddr) + + // prepare WETH balance + const WETH_ADDRESS = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' + const weth = await ethers.getContractAt('IWETH', WETH_ADDRESS) + await ethers.provider.send('hardhat_setBalance', [borrower.address, '0x204FCE5E3E25026110000000']) + await weth.connect(borrower).deposit({ value: ONE_WETH.mul(1000) }) + + return { + addressRegistry, + borrowerGateway, + quoteHandler, + lenderVaultImplementation, + lender, + signer, + borrower, + team, + whitelistAuthority, + weth, + dai, + lenderVault, + lenderVaultFactory, + lenderVaultAddr, + someUser, + testnetTokenManager + } + } + + describe('Myso Arbitrum Oracle Testing', function () { + it('Should set up myso arbitrum IOO price correctly', async function () { + const { + addressRegistry, + borrowerGateway, + quoteHandler, + lender, + borrower, + team, + weth, + dai, + lenderVault, + lenderVaultAddr, + testnetTokenManager + } = await setupTest() + + //const myso = '0x00000000000000000000000000000000DeaDBeef' + const myso = dai.address + const usdc = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' + const usdt = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9' + const usdcToUsdChainlinkAddr = '0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3' + const usdtToUsdChainlinkAddr = '0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7' + const wethToUsdChainlinkAddr = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' + const gdai = '0xd85E038593d7A098614721EaE955EC2022B9B91B' + const gusdc = '0xd3443ee1e91aF28e5FB858Fbd0D72A63bA8046E0' + const geth = '0x5977A9682D7AF81D347CFc338c61692163a2784C' + const glp = '0x4277f8F2c384827B5273592FF7CeBd9f2C1ac258' + + // deploy myso oracle + const MysoOracle = await ethers.getContractFactory('MysoArbitrumUsdOracle') + + const mysoOracle = await MysoOracle.connect(lender).deploy( + [usdc, usdt, weth.address], + [usdcToUsdChainlinkAddr, usdtToUsdChainlinkAddr, wethToUsdChainlinkAddr], + team.address, + testnetTokenManager.address, + 55000000, + ethers.BigNumber.from('660000000000000000'), + 1650, + 1000 + ) + await mysoOracle.deployed() + + const mysoPriceData = await mysoOracle.mysoPriceParams() + const mysoOracleOwner = await mysoOracle.owner() + + expect(mysoOracleOwner).to.equal(team.address) + + await expect(mysoOracle.getPrice(weth.address, usdc)).to.be.revertedWithCustomError(mysoOracle, 'NoMyso') + + const wethCollMysoLoanPrice = await mysoOracle.getPrice(weth.address, myso) + const usdcCollMysoLoanPrice = await mysoOracle.getPrice(usdc, myso) + const usdtCollMysoLoanPrice = await mysoOracle.getPrice(usdt, myso) + const glpCollMysoLoanPrice = await mysoOracle.getPrice(glp, myso) + const gdaiCollMysoLoanPrice = await mysoOracle.getPrice(gdai, myso) + const gusdcCollMysoLoanPrice = await mysoOracle.getPrice(gusdc, myso) + const gethCollMysoLoanPrice = await mysoOracle.getPrice(geth, myso) + + const mysoCollWethLoanPrice = await mysoOracle.getPrice(myso, weth.address) + const mysoCollUsdcLoanPrice = await mysoOracle.getPrice(myso, usdc) + const mysoCollUsdtLoanPrice = await mysoOracle.getPrice(myso, usdt) + const mysoCollGdaiLoanPrice = await mysoOracle.getPrice(myso, gdai) + const mysoCollGusdcLoanPrice = await mysoOracle.getPrice(myso, gusdc) + const mysoCollGlpLoanPrice = await mysoOracle.getPrice(myso, glp) + const mysoCollGethLoanPrice = await mysoOracle.getPrice(myso, geth) + + //toggle to show logs + const showLogs = true + if (showLogs) { + console.log( + 'wethCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(wethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'gdaiCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(gdaiCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'gethCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(gethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'usdcCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(usdcCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'gusdcCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(gusdcCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'glpCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(glpCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log(ethers.utils.formatUnits(usdcCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(usdtCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(mysoCollWethLoanPrice, 18)) + console.log(ethers.utils.formatUnits(mysoCollUsdcLoanPrice, 6)) + console.log(ethers.utils.formatUnits(mysoCollUsdtLoanPrice, 6)) + console.log('gdai coll myso loan', ethers.utils.formatUnits(gdaiCollMysoLoanPrice, 18)) + console.log('myso coll glp loan', ethers.utils.formatUnits(mysoCollGlpLoanPrice, 18)) + console.log('myso coll gUSDC loan', ethers.utils.formatUnits(mysoCollGusdcLoanPrice, 6)) + } + + await mysoOracle.connect(team).setMysoPriceParams(70000000, ethers.BigNumber.from('800000000000000000'), 1770, 1000) + const newMysoPriceData = await mysoOracle.mysoPriceParams() + expect(newMysoPriceData[0]).to.equal(70000000) + expect(newMysoPriceData[1]).to.equal(ethers.BigNumber.from('800000000000000000')) + expect(newMysoPriceData[2]).to.equal(1770) + expect(newMysoPriceData[3]).to.equal(1000) + + await mysoOracle.connect(team).setMysoPriceParams(55000000, ethers.BigNumber.from('660000000000000000'), 1650, 1000) + + const initialTotalMysoLoanAmount = await testnetTokenManager.totalMysoLoanAmount() + expect(initialTotalMysoLoanAmount).to.equal(0) + + await dai.connect(team).transfer(lenderVault.address, ONE_MYSO.mul(15000000)) + + await addressRegistry.connect(team).setWhitelistState([mysoOracle.address], 2) + + // lenderVault owner gives quote + const blocknum = await ethers.provider.getBlockNumber() + const timestamp = (await ethers.provider.getBlock(blocknum)).timestamp + let quoteTuples = [ + { + loanPerCollUnitOrLtv: BASE, + interestRatePctInBase: 0, + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(180) + } + ] + let onChainQuote = { + generalQuoteInfo: { + collToken: weth.address, + loanToken: myso, + oracleAddr: mysoOracle.address, + minLoan: ONE_MYSO.mul(1000), + maxLoan: MAX_UINT256, + validUntil: timestamp + 600, + earliestRepayTenor: 0, + borrowerCompartmentImplementation: ZERO_ADDR, + isSingleUse: false, + whitelistAddr: ZERO_ADDR, + isWhitelistAddrSingleBorrower: false + }, + quoteTuples: quoteTuples, + salt: ZERO_BYTES32 + } + await addressRegistry.connect(team).setWhitelistState([weth.address, myso], 1) + + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote)).to.emit( + quoteHandler, + 'OnChainQuoteAdded' + ) + + // borrower approves borrower gateway + await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) + + // borrow with on chain quote + const collSendAmount = ONE_WETH.mul(50) + const expectedProtocolAndVaultTransferFee = 0 + const expectedCompartmentTransferFee = 0 + const quoteTupleIdx = 0 + const callbackAddr = ZERO_ADDR + const callbackData = ZERO_BYTES32 + + const borrowInstructions = { + collSendAmount, + expectedProtocolAndVaultTransferFee, + expectedCompartmentTransferFee, + deadline: MAX_UINT256, + minLoanAmount: 0, + callbackAddr, + callbackData, + mysoTokenManagerData: ZERO_BYTES32 + } + + const borrowWithOnChainQuoteTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions, onChainQuote, quoteTupleIdx) + + const borrowWithOnChainQuoteReceipt = await borrowWithOnChainQuoteTransaction.wait() + + const borrowEvent = borrowWithOnChainQuoteReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountFirst = borrowEvent?.args?.loan?.['initRepayAmount'] + const initCollAmountFirst = borrowEvent?.args?.loan?.['initCollAmount'] + + const mysoLoanAmountFirstBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountFirst).to.equal(mysoLoanAmountFirstBorrow) + expect(initCollAmountFirst).to.equal(collSendAmount) + + console.log('mysoLoanAmountFirstBorrow', ethers.utils.formatUnits(mysoLoanAmountFirstBorrow, 18)) + + const wethCollMysoLoanPostFirstBorrowPrice = await mysoOracle.getPrice(weth.address, myso) + const mysoCollUsdcLoanPostFirstBorrowPrice = await mysoOracle.getPrice(myso, usdc) + const usdcCollMysoLoanPostFirstBorrowPrice = await mysoOracle.getPrice(usdc, myso) + expect(wethCollMysoLoanPostFirstBorrowPrice).to.be.lt(wethCollMysoLoanPrice) + expect(usdcCollMysoLoanPostFirstBorrowPrice).to.be.lt(usdcCollMysoLoanPrice) + if (showLogs) { + console.log( + 'usdcCollMysoLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(usdcCollMysoLoanPostFirstBorrowPrice, 18) + ) + console.log('usdcCollMysoLoanPrice', ethers.utils.formatUnits(usdcCollMysoLoanPrice, 18)) + console.log( + 'mysoCollUsdcLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostFirstBorrowPrice, 6) + ) + console.log('mysoCollUsdcLoanPrice', ethers.utils.formatUnits(mysoCollUsdcLoanPrice, 6)) + } + + const secondBorrowTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions, onChainQuote, quoteTupleIdx) + + const secondBorrowReceipt = await secondBorrowTransaction.wait() + + const secondBorrowEvent = secondBorrowReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountSecond = secondBorrowEvent?.args?.loan?.['initRepayAmount'] + + const mysoLoanAmountSecondBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountSecond).to.equal(mysoLoanAmountSecondBorrow.sub(mysoLoanAmountFirstBorrow)) + + console.log('mysoLoanAmountSecondBorrow', ethers.utils.formatUnits(mysoLoanAmountSecondBorrow, 18)) + + const mysoCollUsdcLoanPostSecondBorrowPrice = await mysoOracle.getPrice(myso, usdc) + const usdcCollMysoLoanPostSecondBorrowPrice = await mysoOracle.getPrice(usdc, myso) + if (showLogs) { + console.log( + 'usdcCollMysoLoanPostSecondBorrowPrice', + ethers.utils.formatUnits(usdcCollMysoLoanPostSecondBorrowPrice, 18) + ) + console.log( + 'usdcCollMysoLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(usdcCollMysoLoanPostFirstBorrowPrice, 18) + ) + console.log( + 'mysoCollUsdcLoanPostSecondBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostSecondBorrowPrice, 6) + ) + console.log( + 'mysoCollUsdcLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostFirstBorrowPrice, 6) + ) + } + + const largeBorrowInstructions = { + ...borrowInstructions, + collSendAmount: ONE_WETH.mul(250) + } + + const thirdBorrowTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, largeBorrowInstructions, onChainQuote, quoteTupleIdx) + + const thirdBorrowReceipt = await thirdBorrowTransaction.wait() + + const thirdBorrowEvent = thirdBorrowReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountThird = thirdBorrowEvent?.args?.loan?.['initRepayAmount'] + + const mysoLoanAmountThirdBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountThird).to.equal(mysoLoanAmountThirdBorrow.sub(mysoLoanAmountSecondBorrow)) + + console.log('mysoLoanAmountThirdBorrow', ethers.utils.formatUnits(mysoLoanAmountThirdBorrow, 18)) + + const mysoCollUsdcLoanPostThirdBorrowPrice = await mysoOracle.getPrice(myso, usdc) + + if (showLogs) { + console.log( + 'mysoCollUsdcLoanPostThirdBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostThirdBorrowPrice, 6) + ) + console.log( + 'mysoCollUsdcLoanPostSecondBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostSecondBorrowPrice, 6) + ) + } + + const fourthBorrowTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, largeBorrowInstructions, onChainQuote, quoteTupleIdx) + + const fourthBorrowReceipt = await fourthBorrowTransaction.wait() + + const fourthBorrowEvent = fourthBorrowReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountFourth = fourthBorrowEvent?.args?.loan?.['initRepayAmount'] + + const mysoLoanAmountFourthBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountFourth).to.equal(mysoLoanAmountFourthBorrow.sub(mysoLoanAmountThirdBorrow)) + + console.log('mysoLoanAmountFourthBorrow', ethers.utils.formatUnits(mysoLoanAmountFourthBorrow, 18)) + + const mysoCollUsdcLoanPostFourthBorrowPrice = await mysoOracle.getPrice(myso, usdc) + const wethCollMysoLoanPostFourthBorrowPrice = await mysoOracle.getPrice(weth.address, myso) + const glpCollMysoLoanPostFourthBorrowPrice = await mysoOracle.getPrice(glp, myso) + const gethCollMysoLoanPostFourthBorrowPrice = await mysoOracle.getPrice(geth, myso) + + if (showLogs) { + console.log( + 'mysoCollUsdcLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostFourthBorrowPrice, 6) + ) + console.log( + 'wethCollMysoLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(wethCollMysoLoanPostFourthBorrowPrice, 18) + ) + console.log( + 'glpCollMysoLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(glpCollMysoLoanPostFourthBorrowPrice, 18) + ) + console.log( + 'gethCollMysoLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(gethCollMysoLoanPostFourthBorrowPrice, 18) + ) + } + }) + }) +}) diff --git a/test/peer-to-peer/local-tests-tokenmanager.ts b/test/peer-to-peer/local-tests-tokenmanager.ts new file mode 100644 index 00000000..793132bf --- /dev/null +++ b/test/peer-to-peer/local-tests-tokenmanager.ts @@ -0,0 +1,481 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { StandardMerkleTree } from '@openzeppelin/merkle-tree' +import { LenderVaultImpl, MyERC20 } from '../../typechain-types' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { payloadScheme } from './helpers/abi' +import { setupBorrowerWhitelist, getSlot, findBalanceSlot, encodeGlobalPolicy, encodePairPolicy } from './helpers/misc' +import { HARDHAT_CHAIN_ID_AND_FORKING_CONFIG } from '../../hardhat.config' + +// test config vars +let snapshotId: String // use snapshot id to reset state before each test + +// constants +const hre = require('hardhat') +const BASE = ethers.BigNumber.from(10).pow(18) +const ONE_USDC = ethers.BigNumber.from(10).pow(6) +const ONE_WETH = ethers.BigNumber.from(10).pow(18) +const MAX_UINT256 = ethers.BigNumber.from(2).pow(256).sub(1) +const ONE_DAY = ethers.BigNumber.from(60 * 60 * 24) +const ZERO_BYTES32 = ethers.utils.formatBytes32String('') +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +async function deployCore(deployer: any) { + // deploy address registry + const AddressRegistry = await ethers.getContractFactory('AddressRegistry') + const addressRegistry = await AddressRegistry.connect(deployer).deploy() + await addressRegistry.deployed() + + // deploy borrower gateway + const BorrowerGateway = await ethers.getContractFactory('BorrowerGateway') + const borrowerGateway = await BorrowerGateway.connect(deployer).deploy(addressRegistry.address) + await borrowerGateway.deployed() + + // deploy quote handler + const QuoteHandler = await ethers.getContractFactory('QuoteHandler') + const quoteHandler = await QuoteHandler.connect(deployer).deploy(addressRegistry.address) + await quoteHandler.deployed() + + // deploy lender vault implementation + const LenderVaultImplementation = await ethers.getContractFactory('LenderVaultImpl') + const lenderVaultImplementation = await LenderVaultImplementation.connect(deployer).deploy() + await lenderVaultImplementation.deployed() + + // deploy LenderVaultFactory + const LenderVaultFactory = await ethers.getContractFactory('LenderVaultFactory') + const lenderVaultFactory = await LenderVaultFactory.connect(deployer).deploy( + addressRegistry.address, + lenderVaultImplementation.address + ) + await lenderVaultFactory.deployed() + + await addressRegistry.connect(deployer).initialize( + lenderVaultFactory.address, + borrowerGateway.address, + quoteHandler.address + ); + + return { + addressRegistry, + borrowerGateway, + quoteHandler, + lenderVaultImplementation, + lenderVaultFactory + }; +} + +async function deployTestTokens(deployer: any) { + const MyERC20 = await ethers.getContractFactory('MyERC20'); + + // deploy MYSO Token (MYT) + const myt = await MyERC20.deploy('MYSO Token', 'MYT', 18); + await myt.deployed(); + + // deploy USDC + const usdc = await MyERC20.connect(deployer).deploy('USDC', 'USDC', 6); + await usdc.deployed(); + + // deploy sMYSO Token (sMYT) + const name = "stMYSO Token"; + const symbol = "stMYT"; + const StMYT = await ethers.getContractFactory('MockStMysoToken'); + const stmyt = await StMYT.deploy(name, symbol, myt.address); + await stmyt.deployed(); + + return { + usdc, + myt, + stmyt + }; +} + +async function createAndFundIoo(deployer: any, myt: any, iooVault: any, usdc: any, quoteHandler: any) { + await myt.mint(deployer.address, ONE_WETH.mul("5000000")); + await myt.connect(deployer).transfer(iooVault.address, ONE_WETH.mul("5000000")); + + // create quote + const blocknum = await ethers.provider.getBlockNumber(); + const timestamp = (await ethers.provider.getBlock(blocknum)).timestamp; + const mytPerUsdc = "4545454545454545920"; + let quoteTuples = [ + { + loanPerCollUnitOrLtv: mytPerUsdc, + interestRatePctInBase: 0, + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(60) + } + ]; + const minSize = ONE_WETH.mul("2000"); + const maxSize = ONE_WETH.mul("50000"); + const iooDuration = 60 * 60 * 24 * 5; + let onChainQuote = { + generalQuoteInfo: { + collToken: usdc.address, + loanToken: myt.address, + oracleAddr: ZERO_ADDRESS, + minLoan: minSize, + maxLoan: maxSize, + validUntil: timestamp + iooDuration, + earliestRepayTenor: ONE_DAY, + borrowerCompartmentImplementation: ZERO_ADDRESS, + isSingleUse: false, + whitelistAddr: ZERO_ADDRESS, + isWhitelistAddrSingleBorrower: false + }, + quoteTuples: quoteTuples, + salt: ZERO_BYTES32 + }; + + // add on chain quote + await expect(quoteHandler.connect(deployer).addOnChainQuote(iooVault.address, onChainQuote)).to.emit( + quoteHandler, + 'OnChainQuoteAdded' + ); + return onChainQuote +} + +describe('Peer-to-Peer: Local Tests', function () { + before(async () => { + console.log('Note: Running local tests with the following hardhat chain id config:') + console.log(HARDHAT_CHAIN_ID_AND_FORKING_CONFIG) + if (HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId !== 31337) { + throw new Error( + `Invalid hardhat forking config! Expected 'HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId' to be 31337 but it is '${HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId}'!` + ) + } + }) + + beforeEach(async () => { + snapshotId = await hre.network.provider.send('evm_snapshot') + }) + + afterEach(async () => { + await hre.network.provider.send('evm_revert', [snapshotId]) + }) + + async function setupTest() { + const [deployer, borrower1, borrower2] = await ethers.getSigners() + + // deploy core + const {addressRegistry, borrowerGateway, quoteHandler, lenderVaultImplementation, lenderVaultFactory} = await deployCore(deployer); + + // deploy test tokens + const {usdc, myt, stmyt} = await deployTestTokens(deployer); + + // mint some test tokens + await usdc.mint(borrower1.address, ONE_USDC.mul("2000000")); + await usdc.mint(borrower2.address, ONE_USDC.mul("2000000")); + + // whitelist tokens + await addressRegistry.connect(deployer).setWhitelistState([myt.address, usdc.address], 1) + + // deploy ioo vault + await lenderVaultFactory.connect(deployer).createVault(ZERO_BYTES32) + const lenderVaultAddrs = await addressRegistry.registeredVaults() + const iooVaultAddr = lenderVaultAddrs[0] + const LenderVaultImplementation = await ethers.getContractFactory('LenderVaultImpl') + const iooVault = await LenderVaultImplementation.attach(iooVaultAddr) + + // create and fund IOO + const iooOnChainQuote = await createAndFundIoo(deployer, myt, iooVault, usdc, quoteHandler) + + // deploy token manager + const mysoIOOVault = iooVaultAddr + const mysoToken = myt.address + const stMysoToken = stmyt.address + const minMysoWeight = ONE_WETH.mul("100000") + const signer = deployer.address + const MysoTokenManager = await ethers.getContractFactory('MysoTokenManager') + const mysoTokenManager = await MysoTokenManager.connect(deployer).deploy(mysoIOOVault, mysoToken, stMysoToken, minMysoWeight, signer) + await mysoTokenManager.deployed() + + // whitelist myso token manager + await addressRegistry.connect(deployer).setWhitelistState([mysoTokenManager.address], 9) + + return { + deployer, + borrower1, + borrower2, + addressRegistry, + borrowerGateway, + quoteHandler, + iooVault, + mysoTokenManager, + usdc, + myt, + stmyt, + iooOnChainQuote + } + } + + describe('Token Manager', function () { + it('Should handle token manager access correctly', async function () { + const { + deployer, + borrower1, + mysoTokenManager, + usdc + } = await setupTest() + + await expect(mysoTokenManager.connect(borrower1).setIOOVault(ZERO_ADDRESS)).to.be.revertedWith("Ownable: caller is not the owner") + await mysoTokenManager.connect(deployer).setIOOVault(ZERO_ADDRESS) + expect(await mysoTokenManager.mysoIOOVault()).to.be.equal(ZERO_ADDRESS) + + await expect(mysoTokenManager.connect(borrower1).setMinMysoWeight(1)).to.be.revertedWith("Ownable: caller is not the owner") + await mysoTokenManager.connect(deployer).setMinMysoWeight(1) + expect(await mysoTokenManager.minMysoWeight()).to.be.equal(1) + + await expect(mysoTokenManager.connect(borrower1).setRewardInfo(usdc.address, "1", "2")).to.be.revertedWith("Ownable: caller is not the owner") + await mysoTokenManager.connect(deployer).setRewardInfo(usdc.address, "1", "2") + const rewardInfo = await mysoTokenManager.rewardInfos(usdc.address) + expect(rewardInfo.collThreshold).to.be.equal(1) + expect(rewardInfo.mysoTokenMultiplier).to.be.equal(2) + + await expect(mysoTokenManager.connect(borrower1).setSigner(borrower1.address)).to.be.revertedWith("Ownable: caller is not the owner") + await mysoTokenManager.connect(deployer).setSigner(borrower1.address) + expect(await mysoTokenManager.signingAuthority()).to.be.equal(borrower1.address) + + await expect(mysoTokenManager.connect(borrower1).setMysoToken(usdc.address)).to.be.revertedWith("Ownable: caller is not the owner") + await mysoTokenManager.connect(deployer).setMysoToken(usdc.address) + expect(await mysoTokenManager.mysoToken()).to.be.equal(usdc.address) + + await expect(mysoTokenManager.connect(borrower1).setStMysoToken(usdc.address)).to.be.revertedWith("Ownable: caller is not the owner") + await mysoTokenManager.connect(deployer).setStMysoToken(usdc.address) + expect(await mysoTokenManager.stMysoToken()).to.be.equal(usdc.address) + + await expect(mysoTokenManager.connect(borrower1).setTotalMysoLoanAmount(123)).to.be.revertedWith("Ownable: caller is not the owner") + await mysoTokenManager.connect(deployer).setTotalMysoLoanAmount(123) + expect(await mysoTokenManager.totalMysoLoanAmount()).to.be.equal(123) + + await expect(mysoTokenManager.connect(borrower1).withdraw(usdc.address, borrower1.address, 123)).to.be.revertedWith("Ownable: caller is not the owner") + }) + + it('Should process IOO quote correctly', async function () { + const { + deployer, + borrower1, + borrower2, + addressRegistry, + borrowerGateway, + quoteHandler, + iooVault, + mysoTokenManager, + usdc, + myt, + stmyt, + iooOnChainQuote + } = await setupTest() + + // create signature for borrower + const nonce = 0 + const payload = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256'], + [borrower1.address, nonce] + ) + const payloadHash = ethers.utils.keccak256(payload) + const signature = await deployer.signMessage(ethers.utils.arrayify(payloadHash)) + const sig = ethers.utils.splitSignature(signature) + const compactSig = sig.compact + let mysoTokenManagerData = ethers.utils.defaultAbiCoder.encode( + ['bytes', 'uint256'], + [compactSig, nonce] + ) + + // borrowers approves gateway + await usdc.connect(borrower1).approve(borrowerGateway.address, MAX_UINT256) + await usdc.connect(borrower2).approve(borrowerGateway.address, MAX_UINT256) + + // borrow initiation + const collSendAmount = ONE_USDC.mul(10000) + const expectedProtocolAndVaultTransferFee = 0 + const expectedCompartmentTransferFee = 0 + const quoteTupleIdx = 0 + const callbackAddr = ZERO_ADDRESS + const callbackData = ZERO_BYTES32 + const borrowInstructions = { + collSendAmount, + expectedProtocolAndVaultTransferFee, + expectedCompartmentTransferFee, + deadline: MAX_UINT256, + minLoanAmount: 0, + callbackAddr, + callbackData, + mysoTokenManagerData: ZERO_BYTES32 + } + + // check revert if no sig + await expect(borrowerGateway + .connect(borrower1) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx)).to.be.reverted + + // check pass if valid sig + borrowInstructions.mysoTokenManagerData = mysoTokenManagerData + await borrowerGateway + .connect(borrower1) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + let totalMysoLoanAmount1 = await mysoTokenManager.totalMysoLoanAmount() + let borrower1MytBal = await myt.balanceOf(borrower1.address) + expect(borrower1MytBal).to.be.equal(totalMysoLoanAmount1) + + // check revert that user can only borrow once with sig + await expect(borrowerGateway + .connect(borrower1) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx)).to.be.revertedWithCustomError(mysoTokenManager, "NotAllowed") + + // check revert if unauthorized borrower + await expect(borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx)).to.be.revertedWithCustomError(mysoTokenManager, "NotAllowed") + + // set signer to address(0) + mysoTokenManager.setSigner(ZERO_ADDRESS) + + // now anyone can borrow + borrowInstructions.mysoTokenManagerData = ZERO_BYTES32 + await borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + let borrower2MytBal = await myt.balanceOf(borrower2.address) + let totalMysoLoanAmount2 = await mysoTokenManager.totalMysoLoanAmount() + expect(borrower1MytBal.add(borrower2MytBal)).to.be.equal(totalMysoLoanAmount2) + + // check weight booster + const minMysoWeightForRewards = await mysoTokenManager.minMysoWeight() + await myt.mint(borrower1.address, minMysoWeightForRewards) + await myt.connect(borrower1).approve(stmyt.address, MAX_UINT256) + await stmyt.connect(borrower1).depositFor(borrower1.address, minMysoWeightForRewards) + const borrowerWeight = await stmyt.weight(borrower1.address) + expect(borrowerWeight).to.be.equal(minMysoWeightForRewards) + + // set rewards + const collToken = usdc.address + const collThreshold = ONE_USDC.mul(5000) + const mysoTokenMultiplier = ONE_WETH.mul(5).div(100) // in 1e18 units, i.e., 5% of pledge + await mysoTokenManager.setRewardInfo(collToken, collThreshold, mysoTokenMultiplier) + + // borrow with weight + borrowInstructions.collSendAmount = collThreshold + const preDeployerBal1 = await myt.balanceOf(deployer.address) + + // check that borrow passes also if token manager doesn't have enough reward tokens + await borrowerGateway + .connect(borrower1) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + const postDeployerBal1 = await myt.balanceOf(deployer.address) + expect(preDeployerBal1).to.be.equal(0) + expect(postDeployerBal1).to.be.equal(0) + + // fund token manager with rewards and check rewards + await myt.mint(mysoTokenManager.address, ONE_WETH.mul("1000000")) + await borrowerGateway + .connect(borrower1) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + const postDeployerBal2 = await myt.balanceOf(deployer.address) + const rewardAmount = borrowInstructions.collSendAmount.mul(mysoTokenMultiplier).div(ONE_USDC) + expect(rewardAmount).to.be.equal(postDeployerBal2) + + // unset MYT to discontinue volume tracking + let totalMysoLoanAmountPre = await mysoTokenManager.totalMysoLoanAmount() + mysoTokenManager.connect(deployer).setMysoToken(ZERO_ADDRESS) + borrowInstructions.mysoTokenManagerData = ZERO_BYTES32 + await borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + let totalMysoLoanAmountPost = await mysoTokenManager.totalMysoLoanAmount() + expect(totalMysoLoanAmountPre).to.be.equal(totalMysoLoanAmountPost) + + // withdraw remaining token rewards + const preBalTokenManager = await myt.balanceOf(mysoTokenManager.address) + const preBalDeployer = await myt.balanceOf(deployer.address) + await mysoTokenManager.connect(deployer).withdraw(myt.address, deployer.address, preBalTokenManager) + const postBalTokenManager = await myt.balanceOf(mysoTokenManager.address) + const postBalDeployer = await myt.balanceOf(deployer.address) + expect(preBalTokenManager.sub(postBalTokenManager)).to.be.equal(postBalDeployer.sub(preBalDeployer)) + expect(postBalTokenManager).to.be.equal(0) + }) + + it('Should process caps correctly', async function () { + const { + deployer, + borrower1, + borrower2, + addressRegistry, + borrowerGateway, + quoteHandler, + mysoTokenManager, + iooVault, + usdc, + myt, + stmyt, + iooOnChainQuote + } = await setupTest() + + // deploy token manager with pledge caps + const MysoTokenManagerWithCaps = await ethers.getContractFactory('MysoTokenManagerWithCaps') + const collatCaps = [{token: usdc.address, maxPledge: ONE_USDC.mul(10000)}] + const mysoTokenManagerWithCaps = await MysoTokenManagerWithCaps.connect(deployer).deploy(iooVault.address, myt.address, stmyt.address, 0, ethers.constants.AddressZero, collatCaps) + await mysoTokenManagerWithCaps.deployed() + + // whitelist new myso token manager + await addressRegistry.connect(deployer).setWhitelistState([mysoTokenManager.address], 0) + await addressRegistry.connect(deployer).setWhitelistState([mysoTokenManagerWithCaps.address], 9) + + // borrowers approves gateway + await usdc.connect(borrower1).approve(borrowerGateway.address, MAX_UINT256) + await usdc.connect(borrower2).approve(borrowerGateway.address, MAX_UINT256) + + // borrow initiation + const collSendAmount = ONE_USDC.mul(4000) + const expectedProtocolAndVaultTransferFee = 0 + const expectedCompartmentTransferFee = 0 + const quoteTupleIdx = 0 + const callbackAddr = ZERO_ADDRESS + const callbackData = ZERO_BYTES32 + const borrowInstructions = { + collSendAmount, + expectedProtocolAndVaultTransferFee, + expectedCompartmentTransferFee, + deadline: MAX_UINT256, + minLoanAmount: 0, + callbackAddr, + callbackData, + mysoTokenManagerData: ZERO_BYTES32 + } + + // check 2 borrows should work + borrowInstructions.mysoTokenManagerData = ZERO_BYTES32 + await borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + await borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + await expect(borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx)).to.be.revertedWithCustomError(mysoTokenManagerWithCaps, "NotAllowed") + + // set cap higher + const newCollatCaps = [{token: usdc.address, maxPledge: ONE_USDC.mul(12000)}] + await mysoTokenManagerWithCaps.setCollatCaps(newCollatCaps) + + // now borrow works again + await borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + + // next one fails again + await expect(borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx)).to.be.revertedWithCustomError(mysoTokenManagerWithCaps, "NotAllowed") + + // set cap to zero to disable + const newCollatCaps_ = [{token: usdc.address, maxPledge: 0}] + await mysoTokenManagerWithCaps.setCollatCaps(newCollatCaps_) + + // now borrow works again + await borrowerGateway + .connect(borrower2) + .borrowWithOnChainQuote(iooVault.address, borrowInstructions, iooOnChainQuote, quoteTupleIdx) + }) + }) +}) diff --git a/test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts b/test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts new file mode 100644 index 00000000..b413d500 --- /dev/null +++ b/test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts @@ -0,0 +1,573 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { HARDHAT_CHAIN_ID_AND_FORKING_CONFIG, getMysoOracleMainnetForkingConfig } from '../../hardhat.config' + +// test config constants & vars +let snapshotId: String // use snapshot id to reset state before each test + +// constants +const hre = require('hardhat') +const BASE = ethers.BigNumber.from(10).pow(18) +const ONE_USDC = ethers.BigNumber.from(10).pow(6) +const ONE_WETH = ethers.BigNumber.from(10).pow(18) +const ONE_MYSO = ethers.BigNumber.from(10).pow(18) +const ONE_WSTETH = ethers.BigNumber.from(10).pow(18) +const MAX_UINT128 = ethers.BigNumber.from(2).pow(128).sub(1) +const MAX_UINT256 = ethers.BigNumber.from(2).pow(256).sub(1) +const ONE_HOUR = ethers.BigNumber.from(60 * 60) +const ONE_DAY = ethers.BigNumber.from(60 * 60 * 24) +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' +const ZERO_BYTES32 = ethers.utils.formatBytes32String('') + +describe('Peer-to-Peer: Myso Recent Forked Mainnet Tests', function () { + before(async () => { + console.log('Note: Running mainnet tests with the following forking config:') + console.log(HARDHAT_CHAIN_ID_AND_FORKING_CONFIG) + if (HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId !== 1) { + console.warn('Invalid hardhat forking config! Expected `HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId` to be 1!') + + console.warn('Assuming that current test run is using `npx hardhat coverage`!') + + console.warn('Re-importing mainnet forking config from `hardhat.config.ts`...') + const mainnetForkingConfig = getMysoOracleMainnetForkingConfig() + + console.warn('Overwriting chainId to hardhat default `31337` to make off-chain signing consistent...') + HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId = 31337 + + console.log('block number: ', mainnetForkingConfig.url) + + console.warn('Trying to manually switch network to forked mainnet for this test file...') + await hre.network.provider.request({ + method: 'hardhat_reset', + params: [ + { + forking: { + jsonRpcUrl: mainnetForkingConfig.url, + blockNumber: mainnetForkingConfig.blockNumber + } + } + ] + }) + } + }) + + beforeEach(async () => { + snapshotId = await hre.network.provider.send('evm_snapshot') + }) + + afterEach(async () => { + await hre.network.provider.send('evm_revert', [snapshotId]) + }) + + async function setupTest() { + const [lender, signer, borrower, team, whitelistAuthority, someUser] = await ethers.getSigners() + /* ************************************ */ + /* DEPLOYMENT OF SYSTEM CONTRACTS START */ + /* ************************************ */ + // deploy address registry + const AddressRegistry = await ethers.getContractFactory('AddressRegistry') + const addressRegistry = await AddressRegistry.connect(team).deploy() + await addressRegistry.deployed() + + // deploy borrower gate way + const BorrowerGateway = await ethers.getContractFactory('BorrowerGateway') + const borrowerGateway = await BorrowerGateway.connect(team).deploy(addressRegistry.address) + await borrowerGateway.deployed() + + // deploy quote handler + const QuoteHandler = await ethers.getContractFactory('QuoteHandler') + const quoteHandler = await QuoteHandler.connect(team).deploy(addressRegistry.address) + await quoteHandler.deployed() + + // deploy lender vault implementation + const LenderVaultImplementation = await ethers.getContractFactory('LenderVaultImpl') + const lenderVaultImplementation = await LenderVaultImplementation.connect(team).deploy() + await lenderVaultImplementation.deployed() + + // deploy LenderVaultFactory + const LenderVaultFactory = await ethers.getContractFactory('LenderVaultFactory') + const lenderVaultFactory = await LenderVaultFactory.connect(team).deploy( + addressRegistry.address, + lenderVaultImplementation.address + ) + await lenderVaultFactory.deployed() + + // initialize address registry + await addressRegistry.connect(team).initialize(lenderVaultFactory.address, borrowerGateway.address, quoteHandler.address) + + /* ********************************** */ + /* DEPLOYMENT OF SYSTEM CONTRACTS END */ + /* ********************************** */ + + // create a vault + await lenderVaultFactory.connect(lender).createVault(ZERO_BYTES32) + const lenderVaultAddrs = await addressRegistry.registeredVaults() + const lenderVaultAddr = lenderVaultAddrs[0] + const lenderVault = await LenderVaultImplementation.attach(lenderVaultAddr) + + // add testnet token manager + const TestnetTokenManager = await ethers.getContractFactory('TestnetTokenManager') + const testnetTokenManager = await TestnetTokenManager.connect(team).deploy() + await testnetTokenManager.connect(team).deployed() + await addressRegistry.connect(team).setWhitelistState([testnetTokenManager.address], 9) + + const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + const DAI_HOLDER = '0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf' + const dai = await ethers.getContractAt('IERC20', DAI_ADDRESS) + await ethers.provider.send('hardhat_setBalance', [DAI_HOLDER, '0x56BC75E2D63100000']) + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [DAI_HOLDER] + }) + + const daiHolder = await ethers.getSigner(DAI_HOLDER) + + await dai.connect(daiHolder).transfer(team.address, '50000000000000000000000000') + + const owner = await testnetTokenManager.owner() + + await testnetTokenManager.connect(team).setIOOVault(lenderVaultAddr) + + // prepare WETH balance + const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' + const weth = await ethers.getContractAt('IWETH', WETH_ADDRESS) + await ethers.provider.send('hardhat_setBalance', [borrower.address, '0x204FCE5E3E25026110000000']) + await weth.connect(borrower).deposit({ value: ONE_WETH.mul(1000) }) + + //prepare wstEth balances + const WSTETH_ADDRESS = '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0' + const WSTETH_HOLDER = '0x5fEC2f34D80ED82370F733043B6A536d7e9D7f8d' + const wsteth = await ethers.getContractAt('IWETH', WSTETH_ADDRESS) + await ethers.provider.send('hardhat_setBalance', [WSTETH_HOLDER, '0x56BC75E2D63100000']) + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WSTETH_HOLDER] + }) + + const wstEthHolder = await ethers.getSigner(WSTETH_HOLDER) + + await wsteth.connect(wstEthHolder).transfer(team.address, '10000000000000000000') + + const reth = '0xae78736Cd615f374D3085123A210448E74Fc6393' + const cbeth = '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704' + const rethToEthChainlinkAddr = '0x536218f9E9Eb48863970252233c8F271f554C2d0' + const cbethToEthChainlinkAddr = '0xF017fcB346A1885194689bA23Eff2fE6fA5C483b' + + return { + addressRegistry, + borrowerGateway, + quoteHandler, + lenderVaultImplementation, + lender, + signer, + borrower, + team, + whitelistAuthority, + weth, + wsteth, + reth, + cbeth, + dai, + rethToEthChainlinkAddr, + cbethToEthChainlinkAddr, + lenderVault, + lenderVaultFactory, + lenderVaultAddr, + someUser, + testnetTokenManager + } + } + + describe('Myso Oracle Testing', function () { + it('Should set up myso IOO price correctly', async function () { + const { + addressRegistry, + borrowerGateway, + quoteHandler, + lender, + borrower, + team, + weth, + wsteth, + reth, + cbeth, + dai, + cbethToEthChainlinkAddr, + rethToEthChainlinkAddr, + lenderVault, + lenderVaultAddr, + testnetTokenManager + } = await setupTest() + + //const myso = '0x00000000000000000000000000000000DeaDBeef' + const myso = dai.address + const meth = '0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa' + const rpl = '0xD33526068D116cE69F19A9ee46F0bd304F21A51f' + const usdc = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' + const usdt = '0xdAC17F958D2ee523a2206206994597C13D831ec7' + const ankreth = '0xE95A203B1a91a908F9B9CE46459d101078c2c3cb' + const fxusd = '0x085780639CC2cACd35E474e71f4d000e2405d8f6' + const rsweth = '0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0' + const crvusd = '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E' + const usdcToEthChainlinkAddr = '0x986b5E1e1755e3C2440e960477f25201B0a8bbD4' + //const daiToEthChainlinkAddr = '0x773616E4d11A78F511299002da57A0a94577F1f4' + const usdtToEthChainlinkAddr = '0xEe9F2375b4bdF6387aa8265dD4FB8F16512A1d46' + + // deploy myso oracle + const MysoOracle = await ethers.getContractFactory('MysoOracle') + + const mysoOracle = await MysoOracle.connect(lender).deploy( + [reth, cbeth, usdc, usdt], + [rethToEthChainlinkAddr, cbethToEthChainlinkAddr, usdcToEthChainlinkAddr, usdtToEthChainlinkAddr], + team.address, + testnetTokenManager.address, + 55000000, + ethers.BigNumber.from('660000000000000000'), + 1650, + 1000 + ) + await mysoOracle.deployed() + + const mysoPriceData = await mysoOracle.mysoPriceParams() + const mysoOracleOwner = await mysoOracle.owner() + + expect(mysoOracleOwner).to.equal(team.address) + + await expect(mysoOracle.getPrice(weth.address, cbeth)).to.be.revertedWithCustomError(mysoOracle, 'NoMyso') + + const wethCollMysoLoanPrice = await mysoOracle.getPrice(weth.address, myso) + const wstEthCollMysoLoanPrice = await mysoOracle.getPrice(wsteth.address, myso) + const rethCollMysoLoanPrice = await mysoOracle.getPrice(reth, myso) + const cbethCollMysoLoanPrice = await mysoOracle.getPrice(cbeth, myso) + const usdcCollMysoLoanPrice = await mysoOracle.getPrice(usdc, myso) + const usdtCollMysoLoanPrice = await mysoOracle.getPrice(usdt, myso) + //const daiCollMysoLoanPrice = await mysoOracle.getPrice(dai, myso) + const rplCollMysoLoanPrice = await mysoOracle.getPrice(rpl, myso) + const methCollMysoLoanPrice = await mysoOracle.getPrice(meth, myso) + const ankrethCollMysoLoanPrice = await mysoOracle.getPrice(ankreth, myso) + const rswethCollMysoLoanPrice = await mysoOracle.getPrice(rsweth, myso) + + const mysoCollWethLoanPrice = await mysoOracle.getPrice(myso, weth.address) + const mysoCollWstEthLoanPrice = await mysoOracle.getPrice(myso, wsteth.address) + const mysoCollUsdcLoanPrice = await mysoOracle.getPrice(myso, usdc) + const mysoCollUsdtLoanPrice = await mysoOracle.getPrice(myso, usdt) + // const mysoCollFxUsdLoanPrice = await mysoOracle.getPrice(myso, fxusd) + const mysoCollRswethLoanPrice = await mysoOracle.getPrice(myso, rsweth) + //const mysoCollDaiLoanPrice = await mysoOracle.getPrice(myso, dai) + const mysoCollCrvusdLoanPrice = await mysoOracle.getPrice(myso, crvusd) + + //toggle to show logs + const showLogs = true + if (showLogs) { + console.log( + 'wethCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(wethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'wstEthCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(wstEthCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'rethCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(rethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'cbEthCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(cbethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'rplCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(rplCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'methCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(methCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'usdcCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(usdcCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'ankrethCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(ankrethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log(ethers.utils.formatUnits(wethCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(wstEthCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(rethCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(cbethCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(rplCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(methCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(usdcCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(usdtCollMysoLoanPrice, 18)) + console.log(ethers.utils.formatUnits(mysoCollWethLoanPrice, 18)) + console.log(ethers.utils.formatUnits(mysoCollWstEthLoanPrice, 18)) + console.log(ethers.utils.formatUnits(mysoCollUsdcLoanPrice, 6)) + console.log(ethers.utils.formatUnits(mysoCollUsdtLoanPrice, 6)) + // console.log('myso coll, fxUsd', ethers.utils.formatUnits(mysoCollFxUsdLoanPrice, 18)) + console.log('rsweth coll myso loan', ethers.utils.formatUnits(rswethCollMysoLoanPrice, 18)) + console.log('myso coll rswEth', ethers.utils.formatUnits(mysoCollRswethLoanPrice, 18)) + console.log('myso coll crvUsd', ethers.utils.formatUnits(mysoCollCrvusdLoanPrice, 18)) + } + + await mysoOracle.connect(team).setMysoPriceParams(70000000, ethers.BigNumber.from('800000000000000000'), 1770, 1000) + const newMysoPriceData = await mysoOracle.mysoPriceParams() + expect(newMysoPriceData[0]).to.equal(70000000) + expect(newMysoPriceData[1]).to.equal(ethers.BigNumber.from('800000000000000000')) + expect(newMysoPriceData[2]).to.equal(1770) + expect(newMysoPriceData[3]).to.equal(1000) + + await mysoOracle.connect(team).setMysoPriceParams(55000000, ethers.BigNumber.from('660000000000000000'), 1650, 1000) + + const initialTotalMysoLoanAmount = await testnetTokenManager.totalMysoLoanAmount() + expect(initialTotalMysoLoanAmount).to.equal(0) + + await dai.connect(team).transfer(lenderVault.address, ONE_MYSO.mul(20000000)) + + await addressRegistry.connect(team).setWhitelistState([mysoOracle.address], 2) + + // lenderVault owner gives quote + const blocknum = await ethers.provider.getBlockNumber() + const timestamp = (await ethers.provider.getBlock(blocknum)).timestamp + let quoteTuples = [ + { + loanPerCollUnitOrLtv: BASE, + interestRatePctInBase: 0, + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(180) + } + ] + let onChainQuote = { + generalQuoteInfo: { + collToken: weth.address, + loanToken: myso, + oracleAddr: mysoOracle.address, + minLoan: ONE_MYSO.mul(1000), + maxLoan: MAX_UINT256, + validUntil: timestamp + 600, + earliestRepayTenor: 0, + borrowerCompartmentImplementation: ZERO_ADDR, + isSingleUse: false, + whitelistAddr: ZERO_ADDR, + isWhitelistAddrSingleBorrower: false + }, + quoteTuples: quoteTuples, + salt: ZERO_BYTES32 + } + await addressRegistry.connect(team).setWhitelistState([weth.address, myso], 1) + + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote)).to.emit( + quoteHandler, + 'OnChainQuoteAdded' + ) + + // borrower approves borrower gateway + await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) + + // borrow with on chain quote + const collSendAmount = ONE_WETH.mul(50) + const expectedProtocolAndVaultTransferFee = 0 + const expectedCompartmentTransferFee = 0 + const quoteTupleIdx = 0 + const callbackAddr = ZERO_ADDR + const callbackData = ZERO_BYTES32 + + const borrowInstructions = { + collSendAmount, + expectedProtocolAndVaultTransferFee, + expectedCompartmentTransferFee, + deadline: MAX_UINT256, + minLoanAmount: 0, + callbackAddr, + callbackData, + mysoTokenManagerData: ZERO_BYTES32 + } + + const borrowWithOnChainQuoteTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions, onChainQuote, quoteTupleIdx) + + const borrowWithOnChainQuoteReceipt = await borrowWithOnChainQuoteTransaction.wait() + + const borrowEvent = borrowWithOnChainQuoteReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountFirst = borrowEvent?.args?.loan?.['initRepayAmount'] + const initCollAmountFirst = borrowEvent?.args?.loan?.['initCollAmount'] + + const mysoLoanAmountFirstBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountFirst).to.equal(mysoLoanAmountFirstBorrow) + expect(initCollAmountFirst).to.equal(collSendAmount) + + console.log('mysoLoanAmountFirstBorrow', ethers.utils.formatUnits(mysoLoanAmountFirstBorrow, 18)) + + const wethCollMysoLoanPostFirstBorrowPrice = await mysoOracle.getPrice(weth.address, myso) + const mysoCollUsdcLoanPostFirstBorrowPrice = await mysoOracle.getPrice(myso, usdc) + const usdcCollMysoLoanPostFirstBorrowPrice = await mysoOracle.getPrice(usdc, myso) + expect(wethCollMysoLoanPostFirstBorrowPrice).to.be.lt(wethCollMysoLoanPrice) + expect(usdcCollMysoLoanPostFirstBorrowPrice).to.be.lt(usdcCollMysoLoanPrice) + if (showLogs) { + console.log( + 'usdcCollMysoLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(usdcCollMysoLoanPostFirstBorrowPrice, 18) + ) + console.log('usdcCollMysoLoanPrice', ethers.utils.formatUnits(usdcCollMysoLoanPrice, 18)) + console.log( + 'mysoCollUsdcLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostFirstBorrowPrice, 6) + ) + console.log('mysoCollUsdcLoanPrice', ethers.utils.formatUnits(mysoCollUsdcLoanPrice, 6)) + } + + const secondBorrowTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions, onChainQuote, quoteTupleIdx) + + const secondBorrowReceipt = await secondBorrowTransaction.wait() + + const secondBorrowEvent = secondBorrowReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountSecond = secondBorrowEvent?.args?.loan?.['initRepayAmount'] + + const mysoLoanAmountSecondBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountSecond).to.equal(mysoLoanAmountSecondBorrow.sub(mysoLoanAmountFirstBorrow)) + + console.log('mysoLoanAmountSecondBorrow', ethers.utils.formatUnits(mysoLoanAmountSecondBorrow, 18)) + + const mysoCollUsdcLoanPostSecondBorrowPrice = await mysoOracle.getPrice(myso, usdc) + const usdcCollMysoLoanPostSecondBorrowPrice = await mysoOracle.getPrice(usdc, myso) + if (showLogs) { + console.log( + 'usdcCollMysoLoanPostSecondBorrowPrice', + ethers.utils.formatUnits(usdcCollMysoLoanPostSecondBorrowPrice, 18) + ) + console.log( + 'usdcCollMysoLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(usdcCollMysoLoanPostFirstBorrowPrice, 18) + ) + console.log( + 'mysoCollUsdcLoanPostSecondBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostSecondBorrowPrice, 6) + ) + console.log( + 'mysoCollUsdcLoanPostFirstBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostFirstBorrowPrice, 6) + ) + } + + const largeBorrowInstructions = { + ...borrowInstructions, + collSendAmount: ONE_WETH.mul(250) + } + + const thirdBorrowTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, largeBorrowInstructions, onChainQuote, quoteTupleIdx) + + const thirdBorrowReceipt = await thirdBorrowTransaction.wait() + + const thirdBorrowEvent = thirdBorrowReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountThird = thirdBorrowEvent?.args?.loan?.['initRepayAmount'] + + const mysoLoanAmountThirdBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountThird).to.equal(mysoLoanAmountThirdBorrow.sub(mysoLoanAmountSecondBorrow)) + + console.log('mysoLoanAmountThirdBorrow', ethers.utils.formatUnits(mysoLoanAmountThirdBorrow, 18)) + + const mysoCollUsdcLoanPostThirdBorrowPrice = await mysoOracle.getPrice(myso, usdc) + + if (showLogs) { + console.log( + 'mysoCollUsdcLoanPostThirdBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostThirdBorrowPrice, 6) + ) + console.log( + 'mysoCollUsdcLoanPostSecondBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostSecondBorrowPrice, 6) + ) + } + + const fourthBorrowTransaction = await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, largeBorrowInstructions, onChainQuote, quoteTupleIdx) + + const fourthBorrowReceipt = await fourthBorrowTransaction.wait() + + const fourthBorrowEvent = fourthBorrowReceipt.events?.find(x => { + return x.event === 'Borrowed' + }) + + const repayAmountFourth = fourthBorrowEvent?.args?.loan?.['initRepayAmount'] + + const mysoLoanAmountFourthBorrow = await testnetTokenManager.totalMysoLoanAmount() + + expect(repayAmountFourth).to.equal(mysoLoanAmountFourthBorrow.sub(mysoLoanAmountThirdBorrow)) + + console.log('mysoLoanAmountFourthBorrow', ethers.utils.formatUnits(mysoLoanAmountFourthBorrow, 18)) + + const mysoCollUsdcLoanPostFourthBorrowPrice = await mysoOracle.getPrice(myso, usdc) + const wethCollMysoLoanPostFourthBorrowPrice = await mysoOracle.getPrice(weth.address, myso) + const wstEthCollMysoLoanPostFourthBorrowPrice = await mysoOracle.getPrice(wsteth.address, myso) + const rplCollMysoLoanPostFourthBorrowPrice = await mysoOracle.getPrice(rpl, myso) + const methCollMysoLoanPostFourthBorrowPrice = await mysoOracle.getPrice(meth, myso) + + if (showLogs) { + console.log( + 'mysoCollUsdcLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(mysoCollUsdcLoanPostFourthBorrowPrice, 6) + ) + console.log( + 'wethCollMysoLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(wethCollMysoLoanPostFourthBorrowPrice, 18) + ) + console.log( + 'wstEthCollMysoLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(wstEthCollMysoLoanPostFourthBorrowPrice, 18) + ) + console.log( + 'rplCollMysoLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(rplCollMysoLoanPostFourthBorrowPrice, 18) + ) + console.log( + 'methCollMysoLoanPostFourthBorrowPrice', + ethers.utils.formatUnits(methCollMysoLoanPostFourthBorrowPrice, 18) + ) + } + }) + it('Should set up myso on mantle IOO price correctly', async function () { + const { lender, team, testnetTokenManager } = await setupTest() + + const myso = '0x25bA1ED5DEEA9d8e8add565dA069Ed1eDA397C12' + const usdc = '0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9' + + // deploy myso oracle + const MysoOracle = await ethers.getContractFactory('MysoMantleUsdOracle') + + const mysoOracle = await MysoOracle.connect(lender).deploy( + [], + [], + team.address, + testnetTokenManager.address, + 55000000, + ethers.BigNumber.from('660000000000000000'), + 1650, + 1000 + ) + await mysoOracle.deployed() + + const usdcCollMysoLoanPrice = await mysoOracle.getPrice(usdc, myso) + const mysoCollUsdcLoanPrice = await mysoOracle.getPrice(myso, usdc) + + console.log('usdcCollMysoLoanPrice', ethers.utils.formatUnits(usdcCollMysoLoanPrice, 18)) + console.log('mysoCollUsdcLoanPrice', ethers.utils.formatUnits(mysoCollUsdcLoanPrice, 6)) + }) + }) +})