From f143dfc5fe98f5e2f0b02ff946a2272b60b095f4 Mon Sep 17 00:00:00 2001 From: Sean Casey Date: Thu, 7 Mar 2024 08:02:22 -0400 Subject: [PATCH] fix: handle surplus collateral in Liquity positions * fix: handle surplus collateral in liquity positions * test: add temp fork test case --- .../ILiquityBorrowerOperations.sol | 2 + .../ILiquityColSurplusPool.sol | 18 +++ .../liquity-debt/ILiquityDebtPosition.sol | 3 +- .../liquity-debt/LiquityDebtPositionLib.sol | 28 ++++- .../LiquityDebtPositionParser.sol | 3 + .../liquity/LiquityDebtPosition.t.sol | 108 ++++++++++++++++++ tests/utils/Constants.sol | 1 + 7 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 contracts/external-interfaces/ILiquityColSurplusPool.sol create mode 100644 tests/tests/protocols/liquity/LiquityDebtPosition.t.sol diff --git a/contracts/external-interfaces/ILiquityBorrowerOperations.sol b/contracts/external-interfaces/ILiquityBorrowerOperations.sol index 92f6fa8f1..285cc047f 100644 --- a/contracts/external-interfaces/ILiquityBorrowerOperations.sol +++ b/contracts/external-interfaces/ILiquityBorrowerOperations.sol @@ -17,6 +17,8 @@ pragma solidity >=0.6.0 <0.9.0; interface ILiquityBorrowerOperations { function addColl(address, address) external payable; + function claimCollateral() external; + function closeTrove() external; function openTrove(uint256, uint256, address, address) external payable; diff --git a/contracts/external-interfaces/ILiquityColSurplusPool.sol b/contracts/external-interfaces/ILiquityColSurplusPool.sol new file mode 100644 index 000000000..451d2bf00 --- /dev/null +++ b/contracts/external-interfaces/ILiquityColSurplusPool.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0 + +/* + This file is part of the Enzyme Protocol. + + (c) Enzyme Council + + For the full license information, please view the LICENSE + file that was distributed with this source code. +*/ + +pragma solidity >=0.6.0 <0.9.0; + +/// @title ILiquityColSurplusPool Interface +/// @author Enzyme Council +interface ILiquityColSurplusPool { + function getCollateral(address _account) external view returns (uint256 collateral_); +} diff --git a/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/ILiquityDebtPosition.sol b/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/ILiquityDebtPosition.sol index 054e70c2d..0fdfeb2c8 100644 --- a/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/ILiquityDebtPosition.sol +++ b/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/ILiquityDebtPosition.sol @@ -20,6 +20,7 @@ interface ILiquityDebtPosition is IExternalPosition { RemoveCollateral, Borrow, RepayBorrow, - CloseTrove + CloseTrove, + ClaimCollateral } } diff --git a/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionLib.sol b/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionLib.sol index 800d9a4a9..4fae6aa6d 100644 --- a/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionLib.sol +++ b/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionLib.sol @@ -11,6 +11,7 @@ pragma solidity 0.6.12; import {IERC20} from "../../../../../external-interfaces/IERC20.sol"; import {ILiquityBorrowerOperations} from "../../../../../external-interfaces/ILiquityBorrowerOperations.sol"; +import {ILiquityColSurplusPool} from "../../../../../external-interfaces/ILiquityColSurplusPool.sol"; import {ILiquityTroveManager} from "../../../../../external-interfaces/ILiquityTroveManager.sol"; import {IWETH} from "../../../../../external-interfaces/IWETH.sol"; import {WrappedSafeERC20 as SafeERC20} from "../../../../../utils/0.6.12/open-zeppelin/WrappedSafeERC20.sol"; @@ -24,15 +25,21 @@ contract LiquityDebtPositionLib is ILiquityDebtPosition, LiquityDebtPositionData using SafeERC20 for IERC20; address private immutable LIQUITY_BORROWER_OPERATIONS; + address private immutable LIQUITY_COL_SURPLUS_POOL; address private immutable LIQUITY_TROVE_MANAGER; address private immutable LUSD_TOKEN; address private immutable WETH_TOKEN; - constructor(address _liquityBorrowerOperations, address _liquityTroveManager, address _lusd, address _weth) - public - { + constructor( + address _liquityBorrowerOperations, + address _liquityColSurplusPool, + address _liquityTroveManager, + address _lusd, + address _weth + ) public { LIQUITY_BORROWER_OPERATIONS = _liquityBorrowerOperations; + LIQUITY_COL_SURPLUS_POOL = _liquityColSurplusPool; LIQUITY_TROVE_MANAGER = _liquityTroveManager; LUSD_TOKEN = _lusd; WETH_TOKEN = _weth; @@ -73,6 +80,8 @@ contract LiquityDebtPositionLib is ILiquityDebtPosition, LiquityDebtPositionData __repayBorrow(lusdAmount, upperHint, lowerHint); } else if (actionId == uint256(Actions.CloseTrove)) { __closeTrove(); + } else if (actionId == uint256(Actions.ClaimCollateral)) { + __claimCollateral(); } else { revert("receiveCallFromVault: Invalid actionId"); } @@ -94,6 +103,16 @@ contract LiquityDebtPositionLib is ILiquityDebtPosition, LiquityDebtPositionData IERC20(LUSD_TOKEN).safeTransfer(msg.sender, _amount); } + /// @dev Claims collateral from the collateral surplus pool + function __claimCollateral() private { + ILiquityBorrowerOperations(LIQUITY_BORROWER_OPERATIONS).claimCollateral(); + + uint256 ethBalance = address(this).balance; + + IWETH(WETH_TOKEN).deposit{value: ethBalance}(); + IERC20(WETH_TOKEN).safeTransfer(msg.sender, ethBalance); + } + /// @dev Closes a trove /// It doesn't require to approve LUSD since the balance is directly managed by the borrower operations contract. function __closeTrove() private { @@ -169,7 +188,8 @@ contract LiquityDebtPositionLib is ILiquityDebtPosition, LiquityDebtPositionData /// @return amounts_ Managed asset amounts function getManagedAssets() external override returns (address[] memory assets_, uint256[] memory amounts_) { amounts_ = new uint256[](1); - amounts_[0] = ILiquityTroveManager(LIQUITY_TROVE_MANAGER).getTroveColl(address(this)); + amounts_[0] = ILiquityTroveManager(LIQUITY_TROVE_MANAGER).getTroveColl(address(this)) + + ILiquityColSurplusPool(LIQUITY_COL_SURPLUS_POOL).getCollateral(address(this)); // If there's no collateral balance, return empty arrays if (amounts_[0] == 0) { diff --git a/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionParser.sol b/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionParser.sol index a21fb430f..a3530423c 100644 --- a/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionParser.sol +++ b/contracts/release/extensions/external-position-manager/external-positions/liquity-debt/LiquityDebtPositionParser.sol @@ -86,6 +86,9 @@ contract LiquityDebtPositionParser is IExternalPositionParser, LiquityDebtPositi assetsToTransfer_[0] = LUSD_TOKEN; amountsToTransfer_[0] = lusdAmount; assetsToReceive_[0] = WETH_TOKEN; + } else if (_actionId == uint256(ILiquityDebtPosition.Actions.ClaimCollateral)) { + assetsToReceive_ = new address[](1); + assetsToReceive_[0] = WETH_TOKEN; } return (assetsToTransfer_, amountsToTransfer_, assetsToReceive_); diff --git a/tests/tests/protocols/liquity/LiquityDebtPosition.t.sol b/tests/tests/protocols/liquity/LiquityDebtPosition.t.sol new file mode 100644 index 000000000..32aa0ed83 --- /dev/null +++ b/tests/tests/protocols/liquity/LiquityDebtPosition.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.19; + +import {ILiquityDebtPosition as ILiquityDebtPositionProd} from + "contracts/release/extensions/external-position-manager/external-positions/liquity-debt/ILiquityDebtPosition.sol"; + +import {IntegrationTest} from "tests/bases/IntegrationTest.sol"; + +import {ILiquityDebtPositionLib} from "tests/interfaces/internal/ILiquityDebtPositionLib.sol"; + +address constant LIQUITY_BORROWER_OPERATIONS = 0x24179CD81c9e782A4096035f7eC97fB8B783e007; +address constant LIQUITY_COL_SURPLUS_POOL = 0x3D32e8b97Ed5881324241Cf03b2DA5E2EBcE5521; +address constant LIQUITY_TROVE_MANAGER = 0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2; + +contract TestBase is IntegrationTest { + ILiquityDebtPositionLib internal liquityPosition; + address internal comptrollerProxyAddress; + address internal vaultProxyAddress; + address internal fundOwner; + + EnzymeVersion internal version; + + function setUp() public virtual override { + // TODO: remove fork block number later; needed now for bugfix test + setUpMainnetEnvironment(19378000); + + // TODO: update these later to locally-deployed stuff; needed now for bugfix test + liquityPosition = ILiquityDebtPositionLib(0xB5829dfc366EEcDdfec5600a751E1d0906DfBd19); + comptrollerProxyAddress = 0x1A6E4f75EeD0e610C3C0c2F5AF7dA6eE2a3593c6; + vaultProxyAddress = 0x86758FdE8e8924BE2b9Fa440fF9D8C33a4E064A5; + fundOwner = 0x6C48814701c98F0D24b1B891fAC254A817Aadfdf; + + // TODO: only testing against v4 for now + version = EnzymeVersion.V4; + } + + // DEPLOYMENT HELPERS + + function __deployLib() internal returns (address libAddress_) { + bytes memory args = abi.encode( + LIQUITY_BORROWER_OPERATIONS, LIQUITY_COL_SURPLUS_POOL, LIQUITY_TROVE_MANAGER, ETHEREUM_LUSD, wethToken + ); + + return deployCode("LiquityDebtPositionLib.sol", args); + } + + function __deployParser() internal returns (address parserAddress_) { + bytes memory args = abi.encode(LIQUITY_TROVE_MANAGER, ETHEREUM_LUSD, wethToken); + + return deployCode("LiquityDebtPositionParser.sol", args); + } + + // ACTION HELPERS + + function __claimCollateral() internal { + vm.prank(fundOwner); + callOnExternalPositionForVersion({ + _version: version, + _comptrollerProxyAddress: comptrollerProxyAddress, + _externalPositionAddress: address(liquityPosition), + _actionId: uint256(ILiquityDebtPositionProd.Actions.ClaimCollateral), + _actionArgs: "" + }); + } + + function test_temp_surplus_collateral_fix() public { + uint256 surplusCollateralAmount = 247791387261780588060; + uint256 preClaimVaultWethBalance = wethToken.balanceOf(vaultProxyAddress); + + // Position should have no value to start + { + (address[] memory assets, uint256[] memory amounts) = liquityPosition.getManagedAssets(); + assertEq(assets, new address[](0)); + assertEq(amounts, new uint256[](0)); + } + + address newLib = __deployLib(); + address newParser = __deployParser(); + + // Update the EP contracts + vm.prank(v4ReleaseContracts.externalPositionManager.getOwner()); + v4ReleaseContracts.externalPositionManager.updateExternalPositionTypesInfo({ + _typeIds: toArray(5), + _libs: toArray(newLib), + _parsers: toArray(newParser) + }); + + // Position should now have value + { + (address[] memory assets, uint256[] memory amounts) = liquityPosition.getManagedAssets(); + assertEq(assets, toArray(address(wethToken))); + assertEq(amounts, toArray(surplusCollateralAmount)); + } + + // Claim the surplus collateral + __claimCollateral(); + + // Vault should have received the weth + assertEq(wethToken.balanceOf(vaultProxyAddress), preClaimVaultWethBalance + surplusCollateralAmount); + + // Position should now have no value + { + (address[] memory assets, uint256[] memory amounts) = liquityPosition.getManagedAssets(); + assertEq(assets, new address[](0)); + assertEq(amounts, new uint256[](0)); + } + } +} diff --git a/tests/utils/Constants.sol b/tests/utils/Constants.sol index ec612a925..7b3ccb437 100644 --- a/tests/utils/Constants.sol +++ b/tests/utils/Constants.sol @@ -54,6 +54,7 @@ abstract contract Constants { address internal constant ETHEREUM_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address internal constant ETHEREUM_LDO = 0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32; address internal constant ETHEREUM_LINK = 0x514910771AF9Ca656af840dff83E8264EcF986CA; + address internal constant ETHEREUM_LUSD = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; address internal constant ETHEREUM_MLN = 0xec67005c4E498Ec7f55E092bd1d35cbC47C91892; address internal constant ETHEREUM_STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; address internal constant ETHEREUM_STKAAVE = 0x4da27a545c0c5B758a6BA100e3a049001de870f5;