Skip to content

Commit

Permalink
Merge pull request #12 from clober-dex/feat/liq
Browse files Browse the repository at this point in the history
feat: implement more functions at Liquidator
  • Loading branch information
JhChoy authored Dec 1, 2023
2 parents 28f634e + bde2660 commit 967d8d1
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 40 deletions.
77 changes: 46 additions & 31 deletions contracts/CouponLiquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

import {LoanPosition} from "./libraries/LoanPosition.sol";
import {IWETH9} from "./external/weth/IWETH9.sol";
import {ISubstitute} from "./interfaces/ISubstitute.sol";
import {ILoanPositionManager} from "./interfaces/ILoanPositionManager.sol";
import {IPositionLocker} from "./interfaces/IPositionLocker.sol";
import {ICouponLiquidator} from "./interfaces/ICouponLiquidator.sol";
import {LoanPosition} from "./libraries/LoanPosition.sol";
import {SubstituteLibrary} from "./libraries/Substitute.sol";

contract CouponLiquidator is ICouponLiquidator, IPositionLocker {
using SafeERC20 for IERC20;
using SubstituteLibrary for ISubstitute;

ILoanPositionManager private immutable _loanPositionManager;
address private immutable _router;
Expand All @@ -27,7 +31,14 @@ contract CouponLiquidator is ICouponLiquidator, IPositionLocker {
}

function positionLockAcquired(bytes memory data) external returns (bytes memory) {
(uint256 positionId, uint256 swapAmount, bytes memory swapParams) = abi.decode(data, (uint256, uint256, bytes));
(
address payer,
uint256 positionId,
uint256 swapAmount,
bytes memory swapData,
uint256 allowedSupplementaryAmount,
address recipient
) = abi.decode(data, (address, uint256, uint256, bytes, uint256, address));

LoanPosition memory position = _loanPositionManager.getPosition(positionId);
address inToken = ISubstitute(position.collateralToken).underlyingToken();
Expand All @@ -37,45 +48,49 @@ contract CouponLiquidator is ICouponLiquidator, IPositionLocker {
if (inToken == address(_weth)) {
_weth.deposit{value: swapAmount}();
}
_swap(inToken, swapAmount, swapParams);

(uint256 liquidationAmount, uint256 repayAmount, uint256 protocolFeeAmount) =
_loanPositionManager.liquidate(positionId, IERC20(outToken).balanceOf(address(this)));
if (swapAmount > 0 && swapData.length > 0) {
_swap(inToken, swapAmount, swapData);
}

uint256 collateralAmount = liquidationAmount - protocolFeeAmount - swapAmount;
uint256 maxRepayAmount = IERC20(outToken).balanceOf(address(this))
+ Math.min(
allowedSupplementaryAmount,
Math.min(IERC20(outToken).balanceOf(payer), IERC20(outToken).allowance(payer, address(this)))
);

if (collateralAmount > 0) {
_loanPositionManager.withdrawToken(position.collateralToken, address(this), collateralAmount);
_burnAllSubstitute(position.collateralToken, address(this));
if (inToken == address(_weth)) {
_weth.deposit{value: collateralAmount}();
}
}
(uint256 liquidationAmount, uint256 repayAmount, uint256 protocolFeeAmount) =
_loanPositionManager.liquidate(positionId, maxRepayAmount);

IERC20(outToken).approve(position.debtToken, repayAmount);
ISubstitute(position.debtToken).mint(repayAmount, address(this));
ISubstitute(position.debtToken).ensureThisBalance(payer, repayAmount);
IERC20(position.debtToken).approve(address(_loanPositionManager), repayAmount);
_loanPositionManager.depositToken(position.debtToken, repayAmount);

return abi.encode(inToken, outToken);
}
uint256 debtAmount = IERC20(outToken).balanceOf(address(this));
if (debtAmount > 0) {
IERC20(outToken).safeTransfer(recipient, debtAmount);
}

uint256 collateralAmount = liquidationAmount - protocolFeeAmount - swapAmount;

function liquidate(uint256 positionId, uint256 swapAmount, bytes memory swapParams, address feeRecipient)
external
{
bytes memory lockData = abi.encode(positionId, swapAmount, swapParams);
(address collateralToken, address debtToken) =
abi.decode(_loanPositionManager.lock(lockData), (address, address));
_loanPositionManager.withdrawToken(position.collateralToken, address(this), collateralAmount);
_burnAllSubstitute(position.collateralToken, recipient);

uint256 collateralAmount = IERC20(collateralToken).balanceOf(address(this));
if (collateralAmount > 0) {
IERC20(collateralToken).safeTransfer(feeRecipient, collateralAmount);
}
return "";
}

uint256 debtAmount = IERC20(debtToken).balanceOf(address(this));
if (debtAmount > 0) {
IERC20(debtToken).safeTransfer(feeRecipient, debtAmount);
function liquidate(
uint256 positionId,
uint256 swapAmount,
bytes calldata swapData,
uint256 allowedSupplementaryAmount,
address recipient
) external payable {
if (msg.value > 0) {
_weth.deposit{value: msg.value}();
}
_loanPositionManager.lock(
abi.encode(msg.sender, positionId, swapAmount, swapData, allowedSupplementaryAmount, recipient)
);
}

function _swap(address inToken, uint256 inAmount, bytes memory swapParams) internal {
Expand Down
11 changes: 9 additions & 2 deletions contracts/interfaces/ICouponLiquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

pragma solidity ^0.8.0;

import {ERC20PermitParams} from "../libraries/PermitParams.sol";

interface ICouponLiquidator {
error CollateralSwapFailed(string reason);

function liquidate(uint256 positionId, uint256 swapAmount, bytes memory swapParams, address feeRecipient)
external;
function liquidate(
uint256 positionId,
uint256 swapAmount,
bytes calldata swapData,
uint256 allowedSupplementaryAmount,
address recipient
) external payable;
}
164 changes: 157 additions & 7 deletions test/foundry/integration/CouponLiquidator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {IAssetPool} from "../../../contracts/interfaces/IAssetPool.sol";
import {IAaveTokenSubstitute} from "../../../contracts/interfaces/IAaveTokenSubstitute.sol";
import {ICouponOracle} from "../../../contracts/interfaces/ICouponOracle.sol";
import {ICouponManager} from "../../../contracts/interfaces/ICouponManager.sol";
import {ICouponLiquidator} from "../../../contracts/interfaces/ICouponLiquidator.sol";
import {ILoanPositionManager, ILoanPositionManagerTypes} from "../../../contracts/interfaces/ILoanPositionManager.sol";
import {Coupon, CouponLibrary} from "../../../contracts/libraries/Coupon.sol";
import {CouponKey, CouponKeyLibrary} from "../../../contracts/libraries/CouponKey.sol";
Expand All @@ -42,6 +43,7 @@ contract CouponLiquidatorIntegrationTest is Test, CloberMarketSwapCallbackReceiv

address public constant MARKET_MAKER = address(999123);

ERC20PermitParams public emptyPermitParams;
IAssetPool public assetPool;
BorrowController public borrowController;
CouponLiquidator public couponLiquidator;
Expand Down Expand Up @@ -213,14 +215,14 @@ contract CouponLiquidatorIntegrationTest is Test, CloberMarketSwapCallbackReceiv
);
}

function testLiquidator() public {
function testLiquidatorOnlyWithRouter() public {
uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(700), 0.24 ether, 2);

LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId);

address feeRecipient = address(this);
uint256 beforeUSDCBalance = usdc.balanceOf(feeRecipient);
uint256 beforeWETHBalance = weth.balanceOf(feeRecipient);
address recipient = address(this);
uint256 beforeUSDCBalance = usdc.balanceOf(recipient);
uint256 beforeWETHBalance = weth.balanceOf(recipient);

address[] memory assets = new address[](3);
assets[0] = Constants.COUPON_USDC_SUBSTITUTE;
Expand All @@ -245,10 +247,158 @@ contract CouponLiquidatorIntegrationTest is Test, CloberMarketSwapCallbackReceiv
)
);

couponLiquidator.liquidate(positionId, usdc.amount(500), data, feeRecipient);
couponLiquidator.liquidate(positionId, usdc.amount(500), data, 0, recipient);

assertEq(usdc.balanceOf(feeRecipient) - beforeUSDCBalance, 3344321, "USDC_BALANCE");
assertEq(weth.balanceOf(feeRecipient) - beforeWETHBalance, 3348150879705280, "WETH_BALANCE");
assertEq(usdc.balanceOf(recipient) - beforeUSDCBalance, 3344321, "USDC_BALANCE");
assertEq(weth.balanceOf(recipient) - beforeWETHBalance, 3348150879705280, "WETH_BALANCE");
}

function testLiquidatorWithRouterAndOwnLiquidity() public {
uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(700), 0.25 ether, 2);

LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId);

address recipient = address(this);
uint256 beforeUSDCBalance = usdc.balanceOf(recipient);
uint256 beforeWETHBalance = weth.balanceOf(recipient);

address[] memory assets = new address[](3);
assets[0] = Constants.COUPON_USDC_SUBSTITUTE;
assets[1] = Constants.COUPON_WETH_SUBSTITUTE;
assets[2] = address(0);

uint256[] memory prices = new uint256[](3);
prices[0] = 99997900;
prices[1] = 205485580000;
prices[2] = 205485580000;

vm.warp(loanPosition.expiredWith.endTime() + 1);

vm.mockCall(address(oracle), abi.encodeWithSignature("getAssetsPrices(address[])", assets), abi.encode(prices));
assertEq(oracle.getAssetsPrices(assets)[1], 205485580000, "MANIPULATE_ORACLE");

bytes memory data = fromHex(
string.concat(
"83bd37f9000a000b041dcd65000803608bda99eed8c0028f5c00017F137D1D8d20BA54004Ba358E9C229DA26FA3Fa900000001",
this.remove0x(Strings.toHexString(address(couponLiquidator))),
"000000010501020601a0a52cd80b010001020000270100030200020b0001040500ff000000fae2ae0a9f87fd35b5b0e24b47bac796a7eefea1af88d065e77c8cc2239327c5edb3a432268e5831d87899d10eaa10f3ade05038a38251f758e5c0ebc6f780497a95e246eb9449f5e4770916dcd6396a912ce59144191c1204e64559fe8253a0e49e654800000000000000000000000000000000000000000000000000000000"
)
);

weth.approve(address(couponLiquidator), type(uint256).max);
couponLiquidator.liquidate(positionId, usdc.amount(500), data, type(uint256).max, recipient);

assertEq(usdc.balanceOf(recipient) - beforeUSDCBalance, 24317002, "USDC_BALANCE");
assertEq(weth.balanceOf(recipient), beforeWETHBalance - 6651849120294720, "WETH_BALANCE");
}

function testLiquidateOnlyWithOwnLiquidity() public {
uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(700), 0.24 ether, 2);

LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId);

address recipient = address(this);
uint256 beforeUSDCBalance = usdc.balanceOf(recipient);
uint256 beforeWETHBalance = weth.balanceOf(recipient);
uint256 beforeETHBalance = recipient.balance;

address[] memory assets = new address[](3);
assets[0] = Constants.COUPON_USDC_SUBSTITUTE;
assets[1] = Constants.COUPON_WETH_SUBSTITUTE;
assets[2] = address(0);

uint256[] memory prices = new uint256[](3);
prices[0] = 99997900;
prices[1] = 205485580000;
prices[2] = 205485580000;

vm.warp(loanPosition.expiredWith.endTime() + 1);

vm.mockCall(address(oracle), abi.encodeWithSignature("getAssetsPrices(address[])", assets), abi.encode(prices));
assertEq(oracle.getAssetsPrices(assets)[1], 205485580000, "MANIPULATE_ORACLE");

weth.approve(address(couponLiquidator), type(uint256).max);
(uint256 liquidationAmount, uint256 repayAmount, uint256 protocolFee) =
loanPositionManager.getLiquidationStatus(positionId, type(uint256).max);

couponLiquidator.liquidate(positionId, 0, new bytes(0), type(uint256).max, recipient);

assertEq(usdc.balanceOf(recipient), beforeUSDCBalance + liquidationAmount - protocolFee, "USDC_BALANCE");
assertEq(weth.balanceOf(recipient) + repayAmount, beforeWETHBalance, "WETH_BALANCE");
assertEq(recipient.balance, beforeETHBalance, "ETH_BALANCE");
}

function testLiquidatorOnlyWithOwnLiquidityWithNative() public {
uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(700), 0.24 ether, 2);

LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId);

address recipient = address(this);
uint256 beforeUSDCBalance = usdc.balanceOf(recipient);
uint256 beforeWETHBalance = weth.balanceOf(recipient);
uint256 beforeETHBalance = recipient.balance;

address[] memory assets = new address[](3);
assets[0] = Constants.COUPON_USDC_SUBSTITUTE;
assets[1] = Constants.COUPON_WETH_SUBSTITUTE;
assets[2] = address(0);

uint256[] memory prices = new uint256[](3);
prices[0] = 99997900;
prices[1] = 205485580000;
prices[2] = 205485580000;

vm.warp(loanPosition.expiredWith.endTime() + 1);

vm.mockCall(address(oracle), abi.encodeWithSignature("getAssetsPrices(address[])", assets), abi.encode(prices));
assertEq(oracle.getAssetsPrices(assets)[1], 205485580000, "MANIPULATE_ORACLE");

weth.approve(address(couponLiquidator), type(uint256).max);
(uint256 liquidationAmount, uint256 repayAmount, uint256 protocolFee) =
loanPositionManager.getLiquidationStatus(positionId, type(uint256).max);

couponLiquidator.liquidate{value: 0.1 ether}(positionId, 0, new bytes(0), type(uint256).max, recipient);

assertEq(usdc.balanceOf(recipient), beforeUSDCBalance + liquidationAmount - protocolFee, "USDC_BALANCE");
assertEq(weth.balanceOf(recipient) + repayAmount - 0.1 ether, beforeWETHBalance, "WETH_BALANCE");
assertEq(recipient.balance + 0.1 ether, beforeETHBalance, "ETH_BALANCE");
}

function testLiquidateOnlyWithOwnLiquidityPartially() public {
uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(700), 0.24 ether, 2);

LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId);

address recipient = address(this);
uint256 beforeUSDCBalance = usdc.balanceOf(recipient);
uint256 beforeWETHBalance = weth.balanceOf(recipient);
uint256 beforeETHBalance = recipient.balance;

address[] memory assets = new address[](3);
assets[0] = Constants.COUPON_USDC_SUBSTITUTE;
assets[1] = Constants.COUPON_WETH_SUBSTITUTE;
assets[2] = address(0);

uint256[] memory prices = new uint256[](3);
prices[0] = 99997900;
prices[1] = 205485580000;
prices[2] = 205485580000;

vm.warp(loanPosition.expiredWith.endTime() + 1);

vm.mockCall(address(oracle), abi.encodeWithSignature("getAssetsPrices(address[])", assets), abi.encode(prices));
assertEq(oracle.getAssetsPrices(assets)[1], 205485580000, "MANIPULATE_ORACLE");

weth.approve(address(couponLiquidator), type(uint256).max);
(uint256 liquidationAmount, uint256 repayAmount, uint256 protocolFee) =
loanPositionManager.getLiquidationStatus(positionId, 0.2 ether);

couponLiquidator.liquidate{value: 0.05 ether}(positionId, 0, new bytes(0), 0.15 ether, recipient);

assertEq(repayAmount, 0.2 ether, "REPAY_AMOUNT");
assertEq(usdc.balanceOf(recipient), beforeUSDCBalance + liquidationAmount - protocolFee, "USDC_BALANCE");
assertEq(weth.balanceOf(recipient) + 0.15 ether, beforeWETHBalance, "WETH_BALANCE");
assertEq(recipient.balance + 0.05 ether, beforeETHBalance, "ETH_BALANCE");
}

// Convert an hexadecimal character to their value
Expand Down

0 comments on commit 967d8d1

Please sign in to comment.