From 63e8a75472695dd73fac41ec6a60a75b2dfd6cdc Mon Sep 17 00:00:00 2001 From: nezouse Date: Thu, 25 Apr 2024 09:11:18 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9D=20Add=20liquidators=20whitelist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/AdminContract.sol | 11 +++++ contracts/Interfaces/IAdminContract.sol | 5 +++ .../Interfaces/IVesselManagerOperations.sol | 1 + contracts/VesselManagerOperations.sol | 13 +++++- test/trinity/AdminContractTest.js | 12 ++++++ test/trinity/BorrowerOperationsTest.js | 6 ++- test/trinity/CollSurplusPoolTest.js | 4 ++ test/trinity/GasCompensationTest.js | 4 ++ test/trinity/StabilityPoolTest.js | 4 ++ test/trinity/VesselManagerTest.js | 42 +++++++++++++++++++ .../VesselManager_LiquidationRewardsTest.js | 4 ++ .../trinity/VesselManager_RecoveryModeTest.js | 1 + ...nager_RecoveryMode_BatchLiquidationTest.js | 4 ++ 13 files changed, 108 insertions(+), 3 deletions(-) diff --git a/contracts/AdminContract.sol b/contracts/AdminContract.sol index 555eefe..442003d 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); @@ -337,6 +344,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 09591e6..6da65bc 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); @@ -113,5 +116,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 a0e6387..ec4d31b 100644 --- a/contracts/Interfaces/IVesselManagerOperations.sol +++ b/contracts/Interfaces/IVesselManagerOperations.sol @@ -51,6 +51,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 4a12b7b..b395aca 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); @@ -100,7 +105,7 @@ contract VesselManagerOperations is IVesselManagerOperations, UUPSUpgradeable, R ); IVesselManager(vesselManager).sendGasCompensation( _asset, - msg.sender, + liquidator, totals.totalDebtTokenGasCompensation, totals.totalCollGasCompensation ); @@ -110,6 +115,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(); } @@ -157,7 +166,7 @@ contract VesselManagerOperations is IVesselManagerOperations, UUPSUpgradeable, R ); IVesselManager(vesselManager).sendGasCompensation( _asset, - msg.sender, + liquidator, totals.totalDebtTokenGasCompensation, totals.totalCollGasCompensation ); diff --git a/test/trinity/AdminContractTest.js b/test/trinity/AdminContractTest.js index abef539..88f0626 100644 --- a/test/trinity/AdminContractTest.js +++ b/test/trinity/AdminContractTest.js @@ -322,6 +322,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 f10b181..14432ef 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 => { @@ -60,7 +64,7 @@ contract("BorrowerOperations", async accounts => { describe("BorrowerOperations Mechanisms", async () => { before(async () => { - await deploy(treasury, []) + await deploy(treasury, accounts.slice(0, 20)) TRI_GAS_COMPENSATION_ERC20 = await adminContract.getDebtTokenGasCompensation(erc20.address) MIN_NET_DEBT_ERC20 = await adminContract.getMinNetDebt(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 cbab829..3c719c9 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 a31f044..bb19662 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 b0f825c..43dc66c 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, @@ -1415,6 +1434,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 @@ -2446,6 +2476,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 9c5dd31..fe9a41c 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 b92acbd..8ac7496 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 f1eab34..cc651d9 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 => {