diff --git a/script/deploy_marketFactory.sol b/script/deploy_marketFactory.sol new file mode 100644 index 00000000..3ac493a5 --- /dev/null +++ b/script/deploy_marketFactory.sol @@ -0,0 +1,58 @@ +pragma solidity 0.8.34; + +import "forge-std/Script.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { MarketFactory } from "../src/moolah/MarketFactory.sol"; + +contract MarketFactoryDeploy is Script { + address moolah = 0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C; + address liquidator = 0x6a87C15598929B2db22cF68a9a0dDE5Bf297a59a; + address publicLiquidator = 0x882475d622c687b079f149B69a15683FCbeCC6D9; + address listaRevenueDistributor = 0x34B504A5CF0fF41F8A480580533b6Dda687fa3Da; + address buyback = 0x3b99A4177E3f430590A8473f353dD87a5a2e1BfC; + address autoBuyback = 0xFfd3a57E8DB4f51FA01c72F06Ff30BDFDa9908e6; + address WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c; + address slisBNB = 0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B; + address BNBProvider = 0x367384C54756a25340c63057D87eA22d47Fd5701; + address slisBNBProvider = 0x33f7A980a246f9B8FEA2254E3065576E127D4D5f; + address rateCalculator = 0xF81A3067ACF683B7f2f40a22bCF17c8310be2330; + address brokerLiquidator = 0x3AA647a1e902833b61E503DbBFbc58992daa4868; + + address operator = 0x8d388136d578dCD791D081c6042284CED6d9B0c6; + address pauser = 0xEEfebb1546d88EA0909435DF6f615084DD3c5Bd8; + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + console.log("Deployer: ", deployer); + vm.startBroadcast(deployerPrivateKey); + + // Deploy implementation + MarketFactory impl = new MarketFactory( + moolah, + liquidator, + publicLiquidator, + listaRevenueDistributor, + buyback, + autoBuyback, + WBNB, + slisBNB, + BNBProvider, + slisBNBProvider, + rateCalculator, + brokerLiquidator + ); + console.log("Implementation: ", address(impl)); + + // Deploy proxy + ERC1967Proxy proxy = new ERC1967Proxy( + address(impl), + abi.encodeWithSelector(impl.initialize.selector, deployer, operator, pauser) + ); + console.log("Loop WBNB Vault BNBProvider proxy: ", address(proxy)); + + vm.stopBroadcast(); + } +} diff --git a/script/deploy_marketFactoryTransferRole.sol b/script/deploy_marketFactoryTransferRole.sol new file mode 100644 index 00000000..2ccf4810 --- /dev/null +++ b/script/deploy_marketFactoryTransferRole.sol @@ -0,0 +1,27 @@ +pragma solidity 0.8.34; + +import "forge-std/Script.sol"; + +import { MarketFactory } from "../src/moolah/MarketFactory.sol"; + +contract MarketFactoryTransferRoleDeploy is Script { + MarketFactory marketFactory = MarketFactory(0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C); + address admin = 0x07D274a68393E8b8a2CCf19A2ce4Ba3518735253; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + console.log("Deployer: ", deployer); + vm.startBroadcast(deployerPrivateKey); + + // setup roles + marketFactory.grantRole(DEFAULT_ADMIN_ROLE, admin); + marketFactory.revokeRole(DEFAULT_ADMIN_ROLE, deployer); + + vm.stopBroadcast(); + + console.log("setup role done!"); + } +} diff --git a/src/broker/interfaces/IBroker.sol b/src/broker/interfaces/IBroker.sol index b4ea54e2..7e789b05 100644 --- a/src/broker/interfaces/IBroker.sol +++ b/src/broker/interfaces/IBroker.sol @@ -167,4 +167,13 @@ interface IBroker is IBrokerBase { /// @dev get the total debt of a user including principal and interest /// @param user The address of the user function getUserTotalDebt(address user) external view returns (uint256 totalDebt); + + /// @dev set the market id for the broker, callable by owner only + /// @param marketId The market id to set + function setMarketId(Id marketId) external; + + /// @dev toggle the liquidation whitelist status of an account + /// @param account The address of the account + /// @param isAddition Whether to add or remove the account from the whitelist + function toggleLiquidationWhitelist(address account, bool isAddition) external; } diff --git a/src/broker/interfaces/IRateCalculator.sol b/src/broker/interfaces/IRateCalculator.sol index 2343ae26..adfb4723 100644 --- a/src/broker/interfaces/IRateCalculator.sol +++ b/src/broker/interfaces/IRateCalculator.sol @@ -25,6 +25,8 @@ interface IRateCalculator { */ function getRate(address broker) external view returns (uint256); + function registerBroker(address _broker, uint256 _ratePerSecond, uint256 _maxRatePerSecond) external; + /// ------------------------------ /// Events /// ------------------------------ diff --git a/src/liquidator/IBrokerLiquidator.sol b/src/liquidator/IBrokerLiquidator.sol index 81f7e877..12d5f4b2 100644 --- a/src/liquidator/IBrokerLiquidator.sol +++ b/src/liquidator/IBrokerLiquidator.sol @@ -49,4 +49,6 @@ interface IBrokerLiquidator { function marketIdToBroker(bytes32 id) external view returns (address); function brokerToMarketId(address broker) external view returns (bytes32); + + function tokenWhitelist(address token) external view returns (bool); } diff --git a/src/liquidator/ILiquidator.sol b/src/liquidator/ILiquidator.sol index e04291ec..1e356888 100644 --- a/src/liquidator/ILiquidator.sol +++ b/src/liquidator/ILiquidator.sol @@ -57,4 +57,10 @@ interface ILiquidator { function setPairWhitelist(address pair, bool status) external; function marketWhitelist(bytes32 id) external view returns (bool); + + function tokenWhitelist(address token) external view returns (bool); + + function batchSetSmartProviders(address[] calldata smartProviders, bool status) external; + + function smartProviders(address provider) external view returns (bool); } diff --git a/src/liquidator/IPublicLiquidator.sol b/src/liquidator/IPublicLiquidator.sol index b9aaeb29..27caf5e2 100644 --- a/src/liquidator/IPublicLiquidator.sol +++ b/src/liquidator/IPublicLiquidator.sol @@ -36,4 +36,8 @@ interface IPublicLiquidator { function setMarketUserWhitelist(bytes32 id, address user, bool status) external; function setPairWhitelist(address pair, bool status) external; + + function batchSetSmartProviders(address[] calldata providers, bool status) external; + + function smartProviders(address provider) external view returns (bool); } diff --git a/src/moolah/MarketFactory.sol b/src/moolah/MarketFactory.sol new file mode 100644 index 00000000..8682c2da --- /dev/null +++ b/src/moolah/MarketFactory.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; +import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { MarketParams, Id, IMoolah } from "moolah/interfaces/IMoolah.sol"; +import { MarketParamsLib } from "moolah/libraries/MarketParamsLib.sol"; +import { ILiquidator } from "liquidator/ILiquidator.sol"; +import { IListaRevenueDistributor } from "moolah/interfaces/IListaRevenueDistributor.sol"; +import { IBuyBack } from "moolah/interfaces/IBuyBack.sol"; +import { IListaAutoBuyBack } from "moolah/interfaces/IListaAutoBuyBack.sol"; +import { IPublicLiquidator } from "liquidator/IPublicLiquidator.sol"; +import { ISmartProvider } from "../provider/interfaces/IProvider.sol"; +import { IBroker } from "../broker/interfaces/IBroker.sol"; +import { IBrokerLiquidator } from "../liquidator/IBrokerLiquidator.sol"; +import { IRateCalculator } from "../broker/interfaces/IRateCalculator.sol"; + +contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, PausableUpgradeable { + using MarketParamsLib for MarketParams; + + struct FixedTermMarketParams { + address broker; + address loanToken; + address collateralToken; + address irm; + uint256 lltv; + uint256 ratePerSecond; + uint256 maxRatePerSecond; + } + + IMoolah public immutable moolah; + ILiquidator public immutable liquidator; + IListaRevenueDistributor public immutable revenueDistributor; + IBuyBack public immutable buyBack; + IListaAutoBuyBack public immutable autoBuyBack; + IPublicLiquidator public immutable publicLiquidator; + address public immutable WBNB; + address public immutable sliBNB; + address public immutable BNBProvider; + address public immutable slisBNBProvider; + IRateCalculator public immutable rateCalculator; + IBrokerLiquidator public immutable brokerLiquidator; + + bytes32 public constant OPERATOR = keccak256("OPERATOR"); + bytes32 public constant PAUSER = keccak256("PAUSER"); + + event BrokerMarketDeployed(FixedTermMarketParams fixedTermMarketParams, Id marketId, address broker); + event CommonMarketDeployed(MarketParams marketParams, Id marketId); + /** + * @dev constructor to set immutable variables + * @param _moolah The address of the Moolah contract + * @param _liquidator The address of the Liquidator contract + * @param _publicLiquidator The address of the PublicLiquidator contract + * @param _revenueDistributor The address of the RevenueDistributor contract + * @param _buyBack The address of the BuyBack contract + * @param _autoBuyBack The address of the AutoBuyBack contract + * @param _WBNB The address of the WBNB token + * @param _sliBNB The address of the sliBNB token + * @param _BNBProvider The address of the BNB provider + * @param _slisBNBProvider The address of the slisBNB provider + * @param _rateCalculator The address of the rate calculator contract + * @param _brokerLiquidator The address of the broker liquidator contract + */ + constructor( + address _moolah, + address _liquidator, + address _publicLiquidator, + address _revenueDistributor, + address _buyBack, + address _autoBuyBack, + address _WBNB, + address _sliBNB, + address _BNBProvider, + address _slisBNBProvider, + address _rateCalculator, + address _brokerLiquidator + ) { + // sanity check for constructor arguments + require(_moolah != address(0), "ZeroAddress"); + require(_liquidator != address(0), "ZeroAddress"); + require(_publicLiquidator != address(0), "ZeroAddress"); + require(_revenueDistributor != address(0), "ZeroAddress"); + require(_buyBack != address(0), "ZeroAddress"); + require(_autoBuyBack != address(0), "ZeroAddress"); + require(_WBNB != address(0), "ZeroAddress"); + require(_sliBNB != address(0), "ZeroAddress"); + require(_BNBProvider != address(0), "ZeroAddress"); + require(_slisBNBProvider != address(0), "ZeroAddress"); + require(_rateCalculator != address(0), "ZeroAddress"); + require(_brokerLiquidator != address(0), "ZeroAddress"); + // set immutable variables + moolah = IMoolah(_moolah); + liquidator = ILiquidator(_liquidator); + publicLiquidator = IPublicLiquidator(_publicLiquidator); + revenueDistributor = IListaRevenueDistributor(_revenueDistributor); + buyBack = IBuyBack(_buyBack); + autoBuyBack = IListaAutoBuyBack(_autoBuyBack); + WBNB = _WBNB; + sliBNB = _sliBNB; + BNBProvider = _BNBProvider; + slisBNBProvider = _slisBNBProvider; + rateCalculator = IRateCalculator(_rateCalculator); + brokerLiquidator = IBrokerLiquidator(_brokerLiquidator); + + _disableInitializers(); + } + + /** + * @dev Initializes the contract with the given addresses + * @param admin The address of the admin role + * @param operator The address of the operator role + */ + function initialize(address admin, address operator, address pauser) public initializer { + require(admin != address(0), "ZeroAddress"); + require(operator != address(0), "ZeroAddress"); + require(pauser != address(0), "ZeroAddress"); + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(OPERATOR, operator); + _grantRole(PAUSER, pauser); + } + + /** + * @dev Creates new markets with the given parameters and configures the related contracts + * @param params An array of MarketParams for the markets to be created + * @param liquidatorWhitelist An array of address arrays for the liquidation whitelist of each market + * @param supplyWhitelist An array of address arrays for the supply whitelist of each market + * @param liquidatorMarketWhitelist An array of booleans indicating whether to whitelist the market in the liquidator for each market + * @param liquidatorSmartProviders An array of booleans indicating whether the market is a smart collateral market that requires special provider configuration for each market + */ + function batchCreateMarkets( + MarketParams[] calldata params, + address[][] calldata liquidatorWhitelist, + address[][] calldata supplyWhitelist, + bool[] calldata liquidatorMarketWhitelist, + bool[] calldata liquidatorSmartProviders + ) external onlyRole(OPERATOR) { + require(params.length > 0, "empty market params"); + require( + params.length == liquidatorWhitelist.length && + params.length == supplyWhitelist.length && + params.length == liquidatorMarketWhitelist.length && + params.length == liquidatorSmartProviders.length, + "array length mismatch" + ); + + for (uint256 i = 0; i < params.length; i++) { + _createMarket( + params[i], + liquidatorWhitelist[i], + supplyWhitelist[i], + liquidatorMarketWhitelist[i], + liquidatorSmartProviders[i] + ); + } + } + + /** + * @dev Creates a new market with the given parameters and configures the related contracts + * @param param The MarketParams for the market to be created + * @param liquidatorWhitelist An array of addresses for the liquidation whitelist of the market + * @param supplyWhitelist An array of addresses for the supply whitelist of the market + * @param liquidatorMarketWhitelist A boolean indicating whether to whitelist the market in the liquidator + * @param liquidatorSmartProvider A boolean indicating whether the market is a smart collateral market that requires special provider configuration + */ + function createMarket( + MarketParams calldata param, + address[] calldata liquidatorWhitelist, + address[] calldata supplyWhitelist, + bool liquidatorMarketWhitelist, + bool liquidatorSmartProvider + ) external onlyRole(OPERATOR) { + _createMarket(param, liquidatorWhitelist, supplyWhitelist, liquidatorMarketWhitelist, liquidatorSmartProvider); + } + + /** + * @dev Creates new fixed term markets with the given parameters and configures the related contracts + * @param params An array of FixedTermMarketParams for the markets to be created + */ + function batchCreateFixedTermMarkets( + FixedTermMarketParams[] calldata params + ) external onlyRole(OPERATOR) returns (Id[] memory) { + require(params.length > 0, "empty market params"); + + Id[] memory ids = new Id[](params.length); + for (uint256 i = 0; i < params.length; i++) { + ids[i] = _createFixedTermMarket(params[i]); + } + return ids; + } + + /** + * @dev Creates a new fixed term market with the given parameters and configures the related contracts + * @param param The FixedTermMarketParams for the market to be created + */ + function createFixedTermMarket(FixedTermMarketParams calldata param) external onlyRole(OPERATOR) returns (Id) { + return _createFixedTermMarket(param); + } + + function _createMarket( + MarketParams memory param, + address[] memory liquidatorWhitelist, + address[] memory supplyWhitelist, + bool liquidatorMarketWhitelist, + bool liquidatorSmartProvider + ) private whenNotPaused { + Id id = param.id(); + // moolah create market + moolah.createMarket(param); + // moolah set liquidation whitelist + if (liquidatorWhitelist.length > 0) { + Id[] memory ids = new Id[](1); + ids[0] = id; + address[][] memory whitelist = new address[][](1); + whitelist[0] = liquidatorWhitelist; + moolah.batchToggleLiquidationWhitelist(ids, whitelist, true); + } + // liquidator set market whitelist + if (liquidatorMarketWhitelist) { + liquidator.setMarketWhitelist(Id.unwrap(id), true); + } + // liquidator set token whitelist + if (!liquidator.tokenWhitelist(param.loanToken)) { + liquidator.setTokenWhitelist(param.loanToken, true); + } + if (!liquidator.tokenWhitelist(param.collateralToken)) { + liquidator.setTokenWhitelist(param.collateralToken, true); + } + // revenue distributor set token whitelist + if (!revenueDistributor.tokenWhitelist(param.loanToken)) { + address[] memory tokens = new address[](1); + tokens[0] = param.loanToken; + revenueDistributor.addTokensToWhitelist(tokens); + } + // buyback set token whitelist + if (!buyBack.tokenInWhitelist(param.loanToken)) { + buyBack.addTokenInWhitelist(param.loanToken); + } + // auto buyback set token whitelist + if (!autoBuyBack.tokenWhitelist(param.loanToken)) { + autoBuyBack.setTokenWhitelist(param.loanToken, true); + } + // set BNBProvider for BNB markets + if (param.loanToken == WBNB || param.collateralToken == WBNB) { + moolah.setProvider(id, BNBProvider, true); + } + // set slisBNBProvider for sliBNB markets + if (param.collateralToken == sliBNB) { + moolah.setProvider(id, slisBNBProvider, true); + } + // set supply whitelist + if (supplyWhitelist.length > 0) { + for (uint256 i = 0; i < supplyWhitelist.length; i++) { + moolah.setWhiteList(id, supplyWhitelist[i], true); + } + } + + // if market is smart collateral + if (liquidatorSmartProvider) { + _configSmartProvider(id, param.oracle, param.collateralToken); + } + + emit CommonMarketDeployed(param, id); + } + + function _createFixedTermMarket(FixedTermMarketParams memory param) private whenNotPaused returns (Id) { + IBroker broker = IBroker(param.broker); + require(param.broker != address(0), "Zero broker address"); + + // moolah create market + MarketParams memory marketParam = MarketParams({ + loanToken: param.loanToken, + collateralToken: param.collateralToken, + oracle: param.broker, + irm: param.irm, + lltv: param.lltv + }); + moolah.createMarket(marketParam); + Id id = marketParam.id(); + + // moolah set liquidation whitelist + Id[] memory ids = new Id[](1); + ids[0] = id; + address[][] memory whitelist = new address[][](1); + whitelist[0] = new address[](1); + whitelist[0][0] = param.broker; + moolah.batchToggleLiquidationWhitelist(ids, whitelist, true); + + // broker set market id + broker.setMarketId(id); + + // broker set liquidator whitelist + broker.toggleLiquidationWhitelist(address(brokerLiquidator), true); + + // set slisBNBProvider for sliBNB markets + if (param.collateralToken == sliBNB) { + moolah.setProvider(id, slisBNBProvider, true); + } + + // moolah set broker + moolah.setMarketBroker(id, param.broker, true); + + // rate calculator register broker + rateCalculator.registerBroker(param.broker, param.ratePerSecond, param.maxRatePerSecond); + + // broker liquidator set token whitelist + if (!brokerLiquidator.tokenWhitelist(param.loanToken)) { + brokerLiquidator.setTokenWhitelist(param.loanToken, true); + } + if (!brokerLiquidator.tokenWhitelist(param.collateralToken)) { + brokerLiquidator.setTokenWhitelist(param.collateralToken, true); + } + + // broker liquidator set market whitelist + brokerLiquidator.setMarketToBroker(Id.unwrap(id), param.broker, true); + + emit BrokerMarketDeployed(param, id, param.broker); + return id; + } + + function _configSmartProvider(Id id, address provider, address collateral) private { + // moolah set provider + moolah.setProvider(id, provider, true); + // moolah set flashloan blacklist + if (!moolah.flashLoanTokenBlacklist(collateral)) { + moolah.setFlashLoanTokenBlacklist(collateral, true); + } + // liquidator and public liquidator set smart provider whitelist + address[] memory smartProviders = new address[](1); + smartProviders[0] = provider; + if (!liquidator.smartProviders(provider)) { + liquidator.batchSetSmartProviders(smartProviders, true); + } + if (!publicLiquidator.smartProviders(provider)) { + publicLiquidator.batchSetSmartProviders(smartProviders, true); + } + // set token whitelist for liquidator if not set + address token0 = ISmartProvider(provider).token(0); + address token1 = ISmartProvider(provider).token(1); + if (!liquidator.tokenWhitelist(token0)) { + liquidator.setTokenWhitelist(token0, true); + } + if (!liquidator.tokenWhitelist(token1)) { + liquidator.setTokenWhitelist(token1, true); + } + } + + /** + * @dev pause contract + */ + function pause() external onlyRole(PAUSER) { + _pause(); + } + + /** + * @dev unpause contract + */ + function unpause() external onlyRole(OPERATOR) { + _unpause(); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/src/moolah/interfaces/IBuyBack.sol b/src/moolah/interfaces/IBuyBack.sol new file mode 100644 index 00000000..cf2ca2b0 --- /dev/null +++ b/src/moolah/interfaces/IBuyBack.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface IBuyBack { + function addTokenInWhitelist(address token) external; + function tokenInWhitelist(address token) external view returns (bool); +} diff --git a/src/moolah/interfaces/IListaAutoBuyBack.sol b/src/moolah/interfaces/IListaAutoBuyBack.sol new file mode 100644 index 00000000..2b5f9275 --- /dev/null +++ b/src/moolah/interfaces/IListaAutoBuyBack.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface IListaAutoBuyBack { + function setTokenWhitelist(address token, bool status) external; + function tokenWhitelist(address token) external view returns (bool); +} diff --git a/src/moolah/interfaces/IListaRevenueDistributor.sol b/src/moolah/interfaces/IListaRevenueDistributor.sol new file mode 100644 index 00000000..4b2b4101 --- /dev/null +++ b/src/moolah/interfaces/IListaRevenueDistributor.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface IListaRevenueDistributor { + function addTokensToWhitelist(address[] memory tokens) external; + function tokenWhitelist(address token) external view returns (bool); +} diff --git a/test/moolah/MarketFactoryTest.sol b/test/moolah/MarketFactoryTest.sol new file mode 100644 index 00000000..b42bb59b --- /dev/null +++ b/test/moolah/MarketFactoryTest.sol @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import { Test } from "forge-std/Test.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { InterestRateModel } from "interest-rate-model/InterestRateModel.sol"; +import { MarketFactory } from "moolah/MarketFactory.sol"; +import { MarketParams, Id } from "moolah/interfaces/IMoolah.sol"; +import { MarketParamsLib } from "moolah/libraries/MarketParamsLib.sol"; +import { MockBuyBack } from "./mocks/MockBuyBack.sol"; +import { MockLiquidator } from "./mocks/MockLiquidator.sol"; +import { MockListaAutoBuyBack } from "./mocks/MockListaAutoBuyBack.sol"; +import { MockListaRevenueDistributor } from "./mocks/MockListaRevenueDistributor.sol"; +import { Moolah } from "moolah/Moolah.sol"; +import { ERC20Mock } from "moolah/mocks/ERC20Mock.sol"; +import { MockProvider } from "./mocks/MockProvider.sol"; +import { OracleMock } from "moolah/mocks/OracleMock.sol"; +import { MockSmartProvider } from "./mocks/MockSmartProvider.sol"; +import { RateCalculator, RateConfig } from "../../src/broker/RateCalculator.sol"; +import { BrokerLiquidator } from "../../src/liquidator/BrokerLiquidator.sol"; +import { LendingBroker } from "../../src/broker/LendingBroker.sol"; + +contract MarketFactoryTest is Test { + using MarketParamsLib for MarketParams; + + Moolah moolah; + InterestRateModel irm; + MarketFactory marketFactory; + MockLiquidator liquidator; + MockLiquidator publicLiquidator; + MockListaRevenueDistributor listaRevenueDistributor; + MockBuyBack buyBack; + MockListaAutoBuyBack listaAutoBuyBack; + MockProvider bnbProvider; + MockProvider slisBNBProvider; + ERC20Mock WBNB; + ERC20Mock slisBNB; + OracleMock oracle; + RateCalculator rateCalculator; + BrokerLiquidator brokerLiquidator; + + address admin; + address manager; + address pauser; + address operator; + address bot; + uint256 minLoanValue; + + uint256 lltv80 = 0.8 ether; + + function setUp() public virtual { + admin = makeAddr("admin"); + manager = makeAddr("manager"); + pauser = makeAddr("pauser"); + operator = makeAddr("operator"); + bot = makeAddr("bot"); + minLoanValue = 0; + + Moolah moolahImpl = new Moolah(); + ERC1967Proxy moolahProxy = new ERC1967Proxy( + address(moolahImpl), + abi.encodeWithSelector(moolahImpl.initialize.selector, admin, manager, pauser, minLoanValue) + ); + moolah = Moolah(address(moolahProxy)); + + InterestRateModel irmImpl = new InterestRateModel(address(moolah)); + ERC1967Proxy irmProxy = new ERC1967Proxy( + address(irmImpl), + abi.encodeWithSelector(irmImpl.initialize.selector, admin) + ); + irm = InterestRateModel(address(irmProxy)); + + RateCalculator rateCalculatorImpl = new RateCalculator(); + ERC1967Proxy rateCalculatorProxy = new ERC1967Proxy( + address(rateCalculatorImpl), + abi.encodeWithSelector(rateCalculatorImpl.initialize.selector, admin, manager, bot) + ); + rateCalculator = RateCalculator(address(rateCalculatorProxy)); + + BrokerLiquidator brokerLiquidatorImpl = new BrokerLiquidator(address(moolah)); + ERC1967Proxy brokerLiquidatorProxy = new ERC1967Proxy( + address(brokerLiquidatorImpl), + abi.encodeWithSelector(brokerLiquidatorImpl.initialize.selector, admin, manager, bot) + ); + brokerLiquidator = BrokerLiquidator(address(brokerLiquidatorProxy)); + + liquidator = new MockLiquidator(); + publicLiquidator = new MockLiquidator(); + listaRevenueDistributor = new MockListaRevenueDistributor(); + buyBack = new MockBuyBack(); + listaAutoBuyBack = new MockListaAutoBuyBack(); + + WBNB = new ERC20Mock(); + slisBNB = new ERC20Mock(); + + bnbProvider = new MockProvider(address(WBNB)); + slisBNBProvider = new MockProvider(address(slisBNB)); + oracle = new OracleMock(); + + MarketFactory marketFactoryImpl = new MarketFactory( + address(moolah), + address(liquidator), + address(publicLiquidator), + address(listaRevenueDistributor), + address(buyBack), + address(listaAutoBuyBack), + address(WBNB), + address(slisBNB), + address(bnbProvider), + address(slisBNBProvider), + address(rateCalculator), + address(brokerLiquidator) + ); + ERC1967Proxy marketFactoryProxy = new ERC1967Proxy( + address(marketFactoryImpl), + abi.encodeWithSelector(marketFactoryImpl.initialize.selector, admin, operator, pauser) + ); + marketFactory = MarketFactory(address(marketFactoryProxy)); + + vm.startPrank(manager); + moolah.enableIrm(address(irm)); + moolah.enableLltv(lltv80); + vm.stopPrank(); + + vm.startPrank(admin); + moolah.grantRole(moolah.OPERATOR(), address(marketFactory)); + moolah.grantRole(moolah.MANAGER(), address(marketFactory)); + rateCalculator.grantRole(rateCalculator.MANAGER(), address(marketFactory)); + brokerLiquidator.grantRole(brokerLiquidator.MANAGER(), address(marketFactory)); + vm.stopPrank(); + } + + function testCreateCommonMarket() public { + ERC20Mock loanToken1 = new ERC20Mock(); + ERC20Mock collateralToken1 = new ERC20Mock(); + ERC20Mock loanToken2 = new ERC20Mock(); + ERC20Mock collateralToken2 = new ERC20Mock(); + + oracle.setPrice(address(loanToken1), 1e8); + oracle.setPrice(address(collateralToken1), 1e8); + oracle.setPrice(address(loanToken2), 1e8); + oracle.setPrice(address(collateralToken2), 1e8); + + MarketParams memory params1 = MarketParams({ + loanToken: address(loanToken1), + collateralToken: address(collateralToken1), + lltv: lltv80, + irm: address(irm), + oracle: address(oracle) + }); + + MarketParams memory params2 = MarketParams({ + loanToken: address(loanToken2), + collateralToken: address(collateralToken2), + lltv: lltv80, + irm: address(irm), + oracle: address(oracle) + }); + + MarketParams[] memory markets = new MarketParams[](2); + markets[0] = params1; + markets[1] = params2; + + address[] memory liquidators = new address[](3); + liquidators[0] = address(liquidator); + liquidators[1] = address(publicLiquidator); + liquidators[2] = bot; + + address[] memory suppliers1 = new address[](2); + suppliers1[0] = makeAddr("1"); + suppliers1[1] = makeAddr("2"); + + address[] memory suppliers2 = new address[](1); + suppliers2[0] = makeAddr("3"); + + address[][] memory liquidatorWhitelist = new address[][](2); + liquidatorWhitelist[0] = liquidators; + liquidatorWhitelist[1] = liquidators; + + address[][] memory supplyWhitelist = new address[][](2); + supplyWhitelist[0] = suppliers1; + supplyWhitelist[1] = suppliers2; + + bool[] memory liquidatorMarketWhitelist = new bool[](2); + liquidatorMarketWhitelist[0] = true; + liquidatorMarketWhitelist[1] = true; + + bool[] memory liquidatorSmartProviders = new bool[](2); + liquidatorSmartProviders[0] = false; + liquidatorSmartProviders[1] = false; + + vm.startPrank(operator); + marketFactory.batchCreateMarkets( + markets, + liquidatorWhitelist, + supplyWhitelist, + liquidatorMarketWhitelist, + liquidatorSmartProviders + ); + vm.stopPrank(); + + assertEq(moolah.getLiquidationWhitelist(params1.id()), liquidators, "Liquidation whitelist mismatch for market 1"); + assertEq(moolah.getLiquidationWhitelist(params2.id()), liquidators, "Liquidation whitelist mismatch for market 2"); + assertTrue( + liquidator.marketWhitelist(Id.unwrap(params1.id())), + "Market whitelist not set for liquidator for market 1" + ); + assertTrue( + liquidator.marketWhitelist(Id.unwrap(params2.id())), + "Market whitelist not set for liquidator for market 2" + ); + assertTrue( + liquidator.tokenWhitelist(address(params1.loanToken)), + "Loan token whitelist not set for liquidator for market 1" + ); + assertTrue( + liquidator.tokenWhitelist(address(params1.collateralToken)), + "Collateral token whitelist not set for liquidator for market 1" + ); + assertTrue( + liquidator.tokenWhitelist(address(params2.loanToken)), + "Loan token whitelist not set for liquidator for market 2" + ); + assertTrue( + liquidator.tokenWhitelist(address(params2.collateralToken)), + "Collateral token whitelist not set for liquidator for market 2" + ); + assertTrue( + listaRevenueDistributor.tokenWhitelist(address(params1.loanToken)), + "Loan token whitelist not set for revenue distributor for market 1" + ); + assertTrue( + listaRevenueDistributor.tokenWhitelist(address(params2.loanToken)), + "Loan token whitelist not set for revenue distributor for market 2" + ); + assertTrue( + buyBack.tokenInWhitelist(address(params1.loanToken)), + "Loan token whitelist not set for buyback for market 1" + ); + assertTrue( + buyBack.tokenInWhitelist(address(params2.loanToken)), + "Loan token whitelist not set for buyback for market 2" + ); + assertTrue( + listaAutoBuyBack.tokenWhitelist(address(params1.loanToken)), + "Loan token whitelist not set for auto buyback for market 1" + ); + assertTrue( + listaAutoBuyBack.tokenWhitelist(address(params2.loanToken)), + "Loan token whitelist not set for auto buyback for market 2" + ); + assertEq(moolah.getWhiteList(params1.id()), suppliers1, "Supply whitelist mismatch for market 1"); + assertEq(moolah.getWhiteList(params2.id()), suppliers2, "Supply whitelist mismatch for market 2"); + } + + function testCreateSmartProviderMarket() public { + ERC20Mock loanToken1 = new ERC20Mock(); + ERC20Mock collateralToken1 = new ERC20Mock(); + ERC20Mock loanToken2 = new ERC20Mock(); + ERC20Mock collateralToken2 = new ERC20Mock(); + + ERC20Mock token0 = new ERC20Mock(); + ERC20Mock token1 = new ERC20Mock(); + + MockSmartProvider smartProvider1 = new MockSmartProvider(address(collateralToken1)); + MockSmartProvider smartProvider2 = new MockSmartProvider(address(collateralToken2)); + + smartProvider1.setPrice(address(loanToken1), 1e8); + smartProvider1.setPrice(address(collateralToken1), 1e8); + smartProvider2.setPrice(address(loanToken2), 1e8); + smartProvider2.setPrice(address(collateralToken2), 1e8); + smartProvider1.addToken(address(token0)); + smartProvider1.addToken(address(token1)); + smartProvider2.addToken(address(token0)); + smartProvider2.addToken(address(token1)); + + MarketParams memory params1 = MarketParams({ + loanToken: address(loanToken1), + collateralToken: address(collateralToken1), + lltv: lltv80, + irm: address(irm), + oracle: address(smartProvider1) + }); + MarketParams memory params2 = MarketParams({ + loanToken: address(loanToken2), + collateralToken: address(collateralToken2), + lltv: lltv80, + irm: address(irm), + oracle: address(smartProvider2) + }); + + MarketParams[] memory markets = new MarketParams[](2); + markets[0] = params1; + markets[1] = params2; + + address[][] memory liquidatorWhitelist = new address[][](2); + address[][] memory supplyWhitelist = new address[][](2); + + bool[] memory liquidatorMarketWhitelist = new bool[](2); + + bool[] memory liquidatorSmartProviders = new bool[](2); + liquidatorSmartProviders[0] = true; + liquidatorSmartProviders[1] = true; + + vm.startPrank(operator); + marketFactory.batchCreateMarkets( + markets, + liquidatorWhitelist, + supplyWhitelist, + liquidatorMarketWhitelist, + liquidatorSmartProviders + ); + vm.stopPrank(); + + assertEq( + moolah.providers(params1.id(), params1.collateralToken), + address(smartProvider1), + "Provider mismatch for market 1" + ); + assertEq( + moolah.providers(params2.id(), params2.collateralToken), + address(smartProvider2), + "Provider mismatch for market 2" + ); + assertTrue( + moolah.flashLoanTokenBlacklist(address(params1.collateralToken)), + "Collateral token should be blacklisted for flash loan for market 1" + ); + assertTrue( + moolah.flashLoanTokenBlacklist(address(params2.collateralToken)), + "Collateral token should be blacklisted for flash loan for market 2" + ); + assertTrue( + liquidator.smartProviders(address(smartProvider1)), + "Smart provider whitelist not set for liquidator for market 1" + ); + assertTrue( + liquidator.smartProviders(address(smartProvider2)), + "Smart provider whitelist not set for liquidator for market 2" + ); + assertTrue( + publicLiquidator.smartProviders(address(smartProvider1)), + "Smart provider whitelist not set for public liquidator for market 1" + ); + assertTrue( + publicLiquidator.smartProviders(address(smartProvider2)), + "Smart provider whitelist not set for public liquidator for market 2" + ); + assertTrue(liquidator.tokenWhitelist(address(token0)), "Liquidator token whitelist not set for token0"); + assertTrue(liquidator.tokenWhitelist(address(token1)), "Liquidator token whitelist not set for token1"); + } + + function testCreateBNBMarket() public { + ERC20Mock collateralToken = new ERC20Mock(); + MarketParams memory params = MarketParams({ + loanToken: address(WBNB), + collateralToken: address(collateralToken), + lltv: lltv80, + irm: address(irm), + oracle: address(oracle) + }); + + oracle.setPrice(address(collateralToken), 1e8); + oracle.setPrice(address(WBNB), 1e8); + + MarketParams[] memory markets = new MarketParams[](1); + markets[0] = params; + + address[] memory liquidators = new address[](3); + liquidators[0] = address(liquidator); + liquidators[1] = address(publicLiquidator); + liquidators[2] = bot; + + address[] memory suppliers = new address[](0); + + address[][] memory liquidatorWhitelist = new address[][](1); + liquidatorWhitelist[0] = liquidators; + + address[][] memory supplyWhitelist = new address[][](1); + supplyWhitelist[0] = suppliers; + + bool[] memory liquidatorMarketWhitelist = new bool[](1); + liquidatorMarketWhitelist[0] = true; + + bool[] memory liquidatorSmartProviders = new bool[](1); + liquidatorSmartProviders[0] = false; + + vm.startPrank(operator); + marketFactory.batchCreateMarkets( + markets, + liquidatorWhitelist, + supplyWhitelist, + liquidatorMarketWhitelist, + liquidatorSmartProviders + ); + vm.stopPrank(); + + assertEq(moolah.providers(params.id(), address(WBNB)), address(bnbProvider), "Provider mismatch for BNB market"); + } + + function testCreateSlisBNBMarket() public { + ERC20Mock loanToken = new ERC20Mock(); + MarketParams memory params = MarketParams({ + loanToken: address(loanToken), + collateralToken: address(slisBNB), + lltv: lltv80, + irm: address(irm), + oracle: address(oracle) + }); + + oracle.setPrice(address(loanToken), 1e8); + oracle.setPrice(address(slisBNB), 1e8); + MarketParams[] memory markets = new MarketParams[](1); + markets[0] = params; + + address[] memory liquidators = new address[](3); + liquidators[0] = address(liquidator); + liquidators[1] = address(publicLiquidator); + liquidators[2] = bot; + + address[] memory suppliers = new address[](0); + + address[][] memory liquidatorWhitelist = new address[][](1); + liquidatorWhitelist[0] = liquidators; + + address[][] memory supplyWhitelist = new address[][](1); + supplyWhitelist[0] = suppliers; + + bool[] memory liquidatorMarketWhitelist = new bool[](1); + liquidatorMarketWhitelist[0] = true; + + bool[] memory liquidatorSmartProviders = new bool[](1); + liquidatorSmartProviders[0] = false; + + vm.startPrank(operator); + marketFactory.batchCreateMarkets( + markets, + liquidatorWhitelist, + supplyWhitelist, + liquidatorMarketWhitelist, + liquidatorSmartProviders + ); + vm.stopPrank(); + + assertEq( + moolah.providers(params.id(), address(slisBNB)), + address(slisBNBProvider), + "Provider mismatch for BNB market" + ); + } + + function testCreateFixedTermMarket() public { + address relayer = makeAddr("relayer"); + uint256 ratePerSecond = 1000000000195993755570992534; + uint256 maxRatePerSecond = 1000000008319516284844716199; + ERC20Mock loanToken = new ERC20Mock(); + ERC20Mock collateralToken = new ERC20Mock(); + LendingBroker broker = newLendingBroker(relayer); + + MarketFactory.FixedTermMarketParams memory params = MarketFactory.FixedTermMarketParams({ + broker: address(broker), + loanToken: address(loanToken), + collateralToken: address(collateralToken), + irm: address(irm), + lltv: lltv80, + ratePerSecond: ratePerSecond, + maxRatePerSecond: maxRatePerSecond + }); + oracle.setPrice(address(loanToken), 1e8); + oracle.setPrice(address(collateralToken), 1e8); + + vm.startPrank(admin); + broker.grantRole(broker.MANAGER(), address(marketFactory)); + vm.stopPrank(); + + vm.startPrank(operator); + Id id = marketFactory.createFixedTermMarket(params); + vm.stopPrank(); + + assertEq(Id.unwrap(id), Id.unwrap(broker.MARKET_ID()), "Market ID mismatch between broker and market factory"); + assertEq(moolah.brokers(id), address(broker), "Broker not set for market"); + assertTrue(moolah.isLiquidationWhitelist(id, address(broker)), "Broker should be in liquidation whitelist"); + assertEq( + broker.getLiquidationWhitelist()[0], + address(brokerLiquidator), + "Liquidation whitelist mismatch for broker" + ); + assertEq( + brokerLiquidator.brokerToMarketId(address(broker)), + Id.unwrap(id), + "Market ID mismatch in broker liquidator" + ); + } + + function newLendingBroker(address replayer) private returns (LendingBroker) { + LendingBroker lendingBrokerImpl = new LendingBroker(address(moolah), replayer, address(oracle)); + ERC1967Proxy lendingBrokerProxy = new ERC1967Proxy( + address(lendingBrokerImpl), + abi.encodeWithSelector( + lendingBrokerImpl.initialize.selector, + admin, + manager, + bot, + pauser, + address(rateCalculator), + 100 + ) + ); + + return LendingBroker(address(lendingBrokerProxy)); + } +} diff --git a/test/moolah/mocks/MockBuyBack.sol b/test/moolah/mocks/MockBuyBack.sol new file mode 100644 index 00000000..511efec9 --- /dev/null +++ b/test/moolah/mocks/MockBuyBack.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import { IBuyBack } from "moolah/interfaces/IBuyBack.sol"; + +contract MockBuyBack is IBuyBack { + mapping(address => bool) public tokenInWhitelist; + function addTokenInWhitelist(address token) external { + tokenInWhitelist[token] = true; + } +} diff --git a/test/moolah/mocks/MockLiquidator.sol b/test/moolah/mocks/MockLiquidator.sol new file mode 100644 index 00000000..05aa8d21 --- /dev/null +++ b/test/moolah/mocks/MockLiquidator.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +contract MockLiquidator { + mapping(address => bool) public tokenWhitelist; + mapping(bytes32 => bool) public marketWhitelist; + mapping(address => bool) public smartProviders; + + function setTokenWhitelist(address token, bool status) external { + tokenWhitelist[token] = status; + } + + function setMarketWhitelist(bytes32 id, bool status) external { + marketWhitelist[id] = status; + } + + function batchSetSmartProviders(address[] calldata providers, bool status) external { + for (uint256 i = 0; i < providers.length; i++) { + smartProviders[providers[i]] = status; + } + } +} diff --git a/test/moolah/mocks/MockListaAutoBuyBack.sol b/test/moolah/mocks/MockListaAutoBuyBack.sol new file mode 100644 index 00000000..b1fdaffe --- /dev/null +++ b/test/moolah/mocks/MockListaAutoBuyBack.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import { IListaAutoBuyBack } from "moolah/interfaces/IListaAutoBuyBack.sol"; + +contract MockListaAutoBuyBack is IListaAutoBuyBack { + mapping(address => bool) public tokenWhitelist; + + function setTokenWhitelist(address token, bool status) external { + tokenWhitelist[token] = status; + } +} diff --git a/test/moolah/mocks/MockListaRevenueDistributor.sol b/test/moolah/mocks/MockListaRevenueDistributor.sol new file mode 100644 index 00000000..4155541e --- /dev/null +++ b/test/moolah/mocks/MockListaRevenueDistributor.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import { IListaRevenueDistributor } from "moolah/interfaces/IListaRevenueDistributor.sol"; + +contract MockListaRevenueDistributor is IListaRevenueDistributor { + mapping(address => bool) public tokenWhitelist; + + function addTokensToWhitelist(address[] memory tokens) external { + for (uint256 i = 0; i < tokens.length; i++) { + tokenWhitelist[tokens[i]] = true; + } + } +} diff --git a/test/moolah/mocks/MockProvider.sol b/test/moolah/mocks/MockProvider.sol new file mode 100644 index 00000000..177dcc55 --- /dev/null +++ b/test/moolah/mocks/MockProvider.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +contract MockProvider { + address public TOKEN; + + constructor(address _token) { + TOKEN = _token; + } +} diff --git a/test/moolah/mocks/MockSmartProvider.sol b/test/moolah/mocks/MockSmartProvider.sol new file mode 100644 index 00000000..c7634f5a --- /dev/null +++ b/test/moolah/mocks/MockSmartProvider.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +contract MockSmartProvider { + address public TOKEN; + address[] public tokens; + + mapping(address => uint256) public price; + + constructor(address _token) { + TOKEN = _token; + } + + function peek(address asset) external view returns (uint256) { + return price[asset]; + } + + function setPrice(address asset, uint256 newPrice) external { + price[asset] = newPrice; + } + + function token(uint256 i) external view returns (address) { + return tokens[i]; + } + + function addToken(address token) external { + tokens.push(token); + } +}