diff --git a/contracts/AdminContract.sol b/contracts/AdminContract.sol index e80d7ca..a7ce2be 100644 --- a/contracts/AdminContract.sol +++ b/contracts/AdminContract.sol @@ -40,6 +40,8 @@ contract AdminContract is IAdminContract, UUPSUpgradeable, OwnableUpgradeable, A mapping(address => mapping(address => bool)) internal collateralWhitelistedAddresses; + mapping(address => bool) internal whitelistedLiquidators; + // list of all collateral types in collateralParams (active and deprecated) // Addresses for easy access address[] public validCollateral; // index maps to token address. @@ -257,6 +259,11 @@ contract AdminContract is IAdminContract, UUPSUpgradeable, OwnableUpgradeable, A emit AddressCollateralWhitelisted(_collateral, _address, _whitelisted); } + function setLiquidatorWhitelisted(address _liquidator, bool _whitelisted) external onlyTimelock { + whitelistedLiquidators[_liquidator] = _whitelisted; + emit LiquidatorWhitelisted(_liquidator, _whitelisted); + } + function setRedemptionBaseFeeEnabled(address _collateral, bool _enabled) external onlyTimelock { collateralParams[_collateral].redemptionBaseFeeEnabled = _enabled; emit BaseFeeEnabledChanged(_collateral, _enabled); @@ -333,6 +340,10 @@ contract AdminContract is IAdminContract, UUPSUpgradeable, OwnableUpgradeable, A return collateralWhitelistedAddresses[_collateral][_address]; } + function getIsLiquidatorWhitelisted(address _liquidator) external view returns (bool) { + return whitelistedLiquidators[_liquidator]; + } + function getRedemptionBaseFeeEnabled(address _collateral) external view override returns (bool) { return collateralParams[_collateral].redemptionBaseFeeEnabled; } diff --git a/contracts/Interfaces/IAdminContract.sol b/contracts/Interfaces/IAdminContract.sol index fa49211..b4f320e 100644 --- a/contracts/Interfaces/IAdminContract.sol +++ b/contracts/Interfaces/IAdminContract.sol @@ -45,6 +45,7 @@ interface IAdminContract { event RedemptionBlockTimestampChanged(address _collateral, uint256 _blockTimestamp); event AddressCollateralWhitelisted(address _collateral, address _address, bool _whitelisted); event BaseFeeEnabledChanged(address _collateral, bool _enabled); + event LiquidatorWhitelisted(address _liquidator, bool _whitelisted); // Functions -------------------------------------------------------------------------------------------------------- @@ -83,6 +84,8 @@ interface IAdminContract { function setAddressCollateralWhitelisted(address _collateral, address _address, bool _whitelisted) external; + function setLiquidatorWhitelisted(address _liquidator, bool _whitelisted) external; + function setRedemptionBaseFeeEnabled(address _collateral, bool _enabled) external; function getIndex(address _collateral) external view returns (uint256); @@ -111,5 +114,7 @@ interface IAdminContract { function getIsAddressCollateralWhitelisted(address _collateral, address _address) external view returns (bool); + function getIsLiquidatorWhitelisted(address _liquidator) external view returns (bool); + function getRedemptionBaseFeeEnabled(address _collateral) external view returns (bool); } diff --git a/contracts/Interfaces/IVesselManagerOperations.sol b/contracts/Interfaces/IVesselManagerOperations.sol index 8d324a3..5473559 100644 --- a/contracts/Interfaces/IVesselManagerOperations.sol +++ b/contracts/Interfaces/IVesselManagerOperations.sol @@ -49,6 +49,7 @@ interface IVesselManagerOperations is ITrinityBase { error VesselManagerOperations__InvalidParam(); error VesselManagerOperations__NotTimelock(); error VesselManagerOperations__AddressNotCollateralWhitelisted(); + error VesselManagerOperations__LiquidatorNotWhitelisted(); // Structs ---------------------------------------------------------------------------------------------------------- diff --git a/contracts/VesselManagerOperations.sol b/contracts/VesselManagerOperations.sol index 0e54e6f..96cab5b 100644 --- a/contracts/VesselManagerOperations.sol +++ b/contracts/VesselManagerOperations.sol @@ -59,6 +59,11 @@ contract VesselManagerOperations is IVesselManagerOperations, UUPSUpgradeable, R * starting from the one with the lowest collateral ratio in the system, and moving upwards. */ function liquidateVessels(address _asset, uint256 _n) external override nonReentrant { + address liquidator = msg.sender; + if (!IAdminContract(adminContract).getIsLiquidatorWhitelisted(liquidator)) { + revert VesselManagerOperations__LiquidatorNotWhitelisted(); + } + LocalVariables_OuterLiquidationFunction memory vars; LiquidationTotals memory totals; vars.price = IPriceFeed(priceFeed).fetchPrice(_asset); @@ -102,6 +107,10 @@ contract VesselManagerOperations is IVesselManagerOperations, UUPSUpgradeable, R * Attempt to liquidate a custom list of vessels provided by the caller. */ function batchLiquidateVessels(address _asset, address[] memory _vesselArray) public override nonReentrant { + address liquidator = msg.sender; + if (!IAdminContract(adminContract).getIsLiquidatorWhitelisted(liquidator)) { + revert VesselManagerOperations__LiquidatorNotWhitelisted(); + } if (_vesselArray.length == 0 || _vesselArray.length > BATCH_SIZE_LIMIT) { revert VesselManagerOperations__InvalidArraySize(); } diff --git a/test/trinity/AdminContractTest.js b/test/trinity/AdminContractTest.js index 14201a1..84af679 100644 --- a/test/trinity/AdminContractTest.js +++ b/test/trinity/AdminContractTest.js @@ -320,6 +320,18 @@ contract("AdminContract", async accounts => { await assertRevert(adminContract.setAddressCollateralWhitelisted(erc20.address, ZERO_ADDRESS, false, {from: user})) }) + it('setLiquidatorWhitelisted: Owner change parameter - Valid Owner', async () => { + await adminContract.setLiquidatorWhitelisted(ZERO_ADDRESS, true) + assert.isTrue(await adminContract.getIsLiquidatorWhitelisted(ZERO_ADDRESS)) + await adminContract.setLiquidatorWhitelisted(ZERO_ADDRESS, false) + assert.isFalse(await adminContract.getIsLiquidatorWhitelisted(ZERO_ADDRESS)) + }) + + it('setLiquidatorWhitelisted: Owner change parameter - Invalid Owner', async () => { + await assertRevert(adminContract.setLiquidatorWhitelisted(ZERO_ADDRESS, true, {from: user})) + await assertRevert(adminContract.setLiquidatorWhitelisted(ZERO_ADDRESS, false, {from: user})) + }) + it('setRedemptionBaseFeeEnabled: Owner change parameter - Valid Owner', async () => { await adminContract.setRedemptionBaseFeeEnabled(ZERO_ADDRESS, true) assert.isTrue(await adminContract.getRedemptionBaseFeeEnabled(ZERO_ADDRESS)) diff --git a/test/trinity/BorrowerOperationsTest.js b/test/trinity/BorrowerOperationsTest.js index 656e0d6..4fdc386 100644 --- a/test/trinity/BorrowerOperationsTest.js +++ b/test/trinity/BorrowerOperationsTest.js @@ -40,6 +40,10 @@ const deploy = async (treasury, mintingAccounts) => { vesselManagerOperations = contracts.core.vesselManagerOperations shortTimelock = contracts.core.shortTimelock longTimelock = contracts.core.longTimelock + + for(const account of mintingAccounts) { + await adminContract.setLiquidatorWhitelisted(account, true) + } } contract("BorrowerOperations", async accounts => { @@ -59,7 +63,7 @@ contract("BorrowerOperations", async accounts => { describe("BorrowerOperations Mechanisms", async () => { before(async () => { - await deploy(treasury, []) + await deploy(treasury, accounts.slice(0, 20)) MIN_NET_DEBT_ERC20 = await adminContract.getMinNetDebt(erc20.address) BORROWING_FEE_ERC20 = await adminContract.getBorrowingFee(erc20.address) diff --git a/test/trinity/CollSurplusPoolTest.js b/test/trinity/CollSurplusPoolTest.js index de99249..dc07cbe 100644 --- a/test/trinity/CollSurplusPoolTest.js +++ b/test/trinity/CollSurplusPoolTest.js @@ -31,6 +31,10 @@ const deploy = async (treasury, mintingAccounts) => { vesselManagerOperations = contracts.core.vesselManagerOperations shortTimelock = contracts.core.shortTimelock longTimelock = contracts.core.longTimelock + + for(const account of mintingAccounts) { + await adminContract.setLiquidatorWhitelisted(account, true) + } } contract("CollSurplusPool", async accounts => { diff --git a/test/trinity/GasCompensationTest.js b/test/trinity/GasCompensationTest.js index bc62ea4..1fa3bed 100644 --- a/test/trinity/GasCompensationTest.js +++ b/test/trinity/GasCompensationTest.js @@ -32,6 +32,10 @@ const deploy = async (treasury, mintingAccounts) => { longTimelock = contracts.core.longTimelock validCollateral = await adminContract.getValidCollateral() + + for(const account of mintingAccounts) { + await adminContract.setLiquidatorWhitelisted(account, true) + } } contract("Gas compensation tests", async accounts => { diff --git a/test/trinity/StabilityPoolTest.js b/test/trinity/StabilityPoolTest.js index 5faa1b3..0a63fab 100644 --- a/test/trinity/StabilityPoolTest.js +++ b/test/trinity/StabilityPoolTest.js @@ -36,6 +36,10 @@ const deploy = async (treasury, mintingAccounts) => { // getDepositorGains() expects a sorted collateral array validCollateral = validCollateral.slice(0).sort((a, b) => toBN(a.toLowerCase()).sub(toBN(b.toLowerCase()))) + + for(const account of mintingAccounts) { + await adminContract.setLiquidatorWhitelisted(account, true) + } } contract("StabilityPool", async accounts => { diff --git a/test/trinity/VesselManagerTest.js b/test/trinity/VesselManagerTest.js index b16d653..af4c12e 100644 --- a/test/trinity/VesselManagerTest.js +++ b/test/trinity/VesselManagerTest.js @@ -48,6 +48,7 @@ const deploy = async (treasury, mintingAccounts) => { for(const account of mintingAccounts) { await adminContract.setAddressCollateralWhitelisted(erc20.address, account, true) + await adminContract.setLiquidatorWhitelisted(account, true) } } @@ -130,6 +131,24 @@ contract("VesselManager", async accounts => { }) describe("Liquidations", async () => { + it("liquidate(): reverts when not whitelisted", async () => { + await openVessel({ + asset: erc20.address, + ICR: toBN(dec(4, 18)), + extraParams: { from: alice }, + }) + + await adminContract.setLiquidatorWhitelisted(alice, false) + + try { + const tx = await vesselManagerOperations.liquidate(erc20.address, alice, {from: alice}) + assert.isFalse(tx.receipt.status) + } catch (err) { + assert.include(err.message, "revert") + assert.include(err.message, "VesselManagerOperations__LiquidatorNotWhitelisted()") + } + }) + it("liquidate(): closes a Vessel that has ICR < MCR", async () => { await openVessel({ asset: erc20.address, @@ -1398,6 +1417,17 @@ contract("VesselManager", async accounts => { }) // --- liquidateVessels() --- + it("liquidateVessels(): reverts when not whitelisted", async () => { + await adminContract.setLiquidatorWhitelisted(alice, false) + + try { + const tx = await vesselManagerOperations.liquidateVessels(erc20.address, 2, {from: alice}) + assert.isFalse(tx.receipt.status) + } catch (err) { + assert.include(err.message, "revert") + assert.include(err.message, "VesselManagerOperations__LiquidatorNotWhitelisted()") + } + }) it("liquidateVessels(): liquidates a Vessel that a) was skipped in a previous liquidation and b) has pending rewards", async () => { // A, B, C, D, E open vessels @@ -2429,6 +2459,18 @@ contract("VesselManager", async accounts => { // --- batchLiquidateVessels() --- describe("Batch Liquidations", async () => { + it("batchLiquidateVessels(): reverts when not whitelisted", async () => { + await adminContract.setLiquidatorWhitelisted(alice, false) + + try { + const tx = await vesselManagerOperations.batchLiquidateVessels(erc20.address, [], {from: alice}) + assert.isFalse(tx.receipt.status) + } catch (err) { + assert.include(err.message, "revert") + assert.include(err.message, "VesselManagerOperations__LiquidatorNotWhitelisted()") + } + }) + it("batchLiquidateVessels(): liquidates a Vessel that a) was skipped in a previous liquidation and b) has pending rewards", async () => { // A, B, C, D, E open vessels diff --git a/test/trinity/VesselManager_LiquidationRewardsTest.js b/test/trinity/VesselManager_LiquidationRewardsTest.js index a6004b7..3fe3247 100644 --- a/test/trinity/VesselManager_LiquidationRewardsTest.js +++ b/test/trinity/VesselManager_LiquidationRewardsTest.js @@ -28,6 +28,10 @@ const deploy = async (treasury, mintingAccounts) => { vesselManagerOperations = contracts.core.vesselManagerOperations shortTimelock = contracts.core.shortTimelock longTimelock = contracts.core.longTimelock + + for(const account of mintingAccounts) { + await adminContract.setLiquidatorWhitelisted(account, true) + } } contract("VesselManager - Redistribution reward calculations", async accounts => { diff --git a/test/trinity/VesselManager_RecoveryModeTest.js b/test/trinity/VesselManager_RecoveryModeTest.js index 7f38204..52534e0 100644 --- a/test/trinity/VesselManager_RecoveryModeTest.js +++ b/test/trinity/VesselManager_RecoveryModeTest.js @@ -45,6 +45,7 @@ const deploy = async (treasury, mintingAccounts) => { for(const account of mintingAccounts) { await adminContract.setAddressCollateralWhitelisted(erc20.address, account, true) + await adminContract.setLiquidatorWhitelisted(account, true) } } diff --git a/test/trinity/VesselManager_RecoveryMode_BatchLiquidationTest.js b/test/trinity/VesselManager_RecoveryMode_BatchLiquidationTest.js index c185123..902faca 100644 --- a/test/trinity/VesselManager_RecoveryMode_BatchLiquidationTest.js +++ b/test/trinity/VesselManager_RecoveryMode_BatchLiquidationTest.js @@ -39,6 +39,10 @@ const deploy = async (treasury, mintingAccounts) => { // getDepositorGains() expects a sorted collateral array validCollateral = validCollateral.slice(0).sort((a, b) => toBN(a.toLowerCase()).sub(toBN(b.toLowerCase()))) + + for(const account of mintingAccounts) { + await adminContract.setLiquidatorWhitelisted(account, true) + } } contract("VesselManager - in Recovery Mode - back to normal mode in 1 tx", async accounts => {