Skip to content

Commit

Permalink
Merge pull request #3 from morpho-org/refactor/subscription-id
Browse files Browse the repository at this point in the history
Refactor subscription id
  • Loading branch information
peyha authored Sep 4, 2024
2 parents 54478a1 + 699341f commit 6977ea0
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 67 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ jobs:

- name: Run forge tests
run: forge test -vvv
env:
ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY }}
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ src = "src"
out = "out"
libs = ["lib"]

[profile.default.rpc_endpoints]
mainnet = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}"

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
87 changes: 40 additions & 47 deletions src/LiquidationProtection.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import {ERC20} from "../lib/solmate/src/tokens/ERC20.sol";
import {EventsLib} from "./libraries/EventsLib.sol";

struct SubscriptionParams {
Id marketId;
address borrower;
bool isValid;
uint256 slltv;
uint256 closeFactor;
uint256 liquidationIncentive;
Expand All @@ -38,26 +35,27 @@ contract LiquidationProtection {
IMorpho immutable MORPHO = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb);

/* STORAGE */
mapping(uint256 => SubscriptionParams) public subscriptions;
mapping(bytes32 => SubscriptionParams) public subscriptions;
uint256 public nbSubscription;

// TODO EIP-712 signature
// TODO authorize this contract on morpho
// TODO potential gas opti (keeping marketparams in SubscriptionParams instead of Id?)

function subscribe(SubscriptionParams calldata subscriptionParams) public returns (uint256) {
MarketParams memory marketParams = MORPHO.idToMarketParams(subscriptionParams.marketId);
function subscribe(Id marketId, SubscriptionParams calldata subscriptionParams) public returns (uint256) {
MarketParams memory marketParams = MORPHO.idToMarketParams(marketId);

require(msg.sender == subscriptionParams.borrower, "Unauthorized account");
require(subscriptionParams.slltv < marketParams.lltv, "Liquidation threshold higher than market LLTV");
// should close factor be lower than 100% ?
// should there be a max liquidation incentive ?

subscriptions[nbSubscription] = subscriptionParams;
bytes32 subscriptionId = computeSubscriptionId(msg.sender, marketId, nbSubscription);

subscriptions[subscriptionId] = subscriptionParams;

emit EventsLib.Subscribe(
subscriptionParams.marketId,
subscriptionParams.borrower,
msg.sender,
marketId,
nbSubscription,
subscriptionParams.slltv,
subscriptionParams.closeFactor,
Expand All @@ -69,25 +67,26 @@ contract LiquidationProtection {
return nbSubscription - 1;
}

function unsubscribe(uint256 subscriptionId) public {
require(msg.sender == subscriptions[subscriptionId].borrower, "Unauthorized account");
function unsubscribe(Id marketId, uint256 subscriptionNumber) public {
bytes32 subscriptionId = computeSubscriptionId(msg.sender, marketId, subscriptionNumber);

subscriptions[subscriptionId].isValid = false;
delete subscriptions[subscriptionId];

emit EventsLib.Unsubscribe(subscriptionId);
emit EventsLib.Unsubscribe(msg.sender, marketId, subscriptionNumber);
}

function liquidate(
uint256 subscriptionId,
uint256 subscriptionNumber,
MarketParams calldata marketParams,
address borrower,
uint256 seizedAssets,
uint256 repaidShares,
bytes calldata data
) public {
require(subscriptions[subscriptionId].isValid, "Non-valid subscription");
require(subscriptions[subscriptionId].borrower == borrower);
require(Id.unwrap(subscriptions[subscriptionId].marketId) == Id.unwrap(marketParams.id()));
bytes32 subscriptionId = computeSubscriptionId(borrower, marketParams.id(), subscriptionNumber);

require(subscriptions[subscriptionId].closeFactor != 0, "Non-valid subscription");

require(UtilsLib.exactlyOneZero(seizedAssets, repaidShares), "Inconsistent input");
uint256 collateralPrice = IOracle(marketParams.oracle).price();

Expand All @@ -97,47 +96,36 @@ contract LiquidationProtection {
"Position is healthy"
);

IMorpho morpho = IMorpho(MORPHO);
// Compute seizedAssets or repaidShares and repaidAssets
Market memory marketState = morpho.market(marketParams.id());
Market memory marketState = MORPHO.market(marketParams.id());

{
uint256 liquidationIncentive = subscriptions[subscriptionId].liquidationIncentive;
if (seizedAssets > 0) {
uint256 seizedAssetsQuoted = seizedAssets.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE);

repaidShares = seizedAssetsQuoted.wDivUp(liquidationIncentive).toSharesUp(
marketState.totalBorrowAssets,
marketState.totalBorrowShares
marketState.totalBorrowAssets, marketState.totalBorrowShares
);
} else {
seizedAssets = repaidShares
.toAssetsDown(marketState.totalBorrowAssets, marketState.totalBorrowShares)
.wMulDown(liquidationIncentive)
.mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
seizedAssets = repaidShares.toAssetsDown(marketState.totalBorrowAssets, marketState.totalBorrowShares)
.wMulDown(liquidationIncentive).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice);
}
}
uint256 repaidAssets = repaidShares.toAssetsUp(marketState.totalBorrowAssets, marketState.totalBorrowShares);

// Check if liquidation is ok with close factor
Position memory borrowerPosition = morpho.position(marketParams.id(), borrower);
Position memory borrowerPosition = MORPHO.position(marketParams.id(), borrower);
require(
borrowerPosition.collateral.wMulDown(subscriptions[subscriptionId].closeFactor) > seizedAssets,
"Cannot liquidate more than close factor"
);

bytes memory callbackData = abi.encode(marketParams, seizedAssets, repaidAssets, borrower, msg.sender, data);
morpho.repay(marketParams, 0, repaidShares, borrower, callbackData);
MORPHO.repay(marketParams, 0, repaidShares, borrower, callbackData);

emit EventsLib.Liquidate(
marketParams.id(),
msg.sender,
borrower,
repaidAssets,
repaidShares,
seizedAssets,
0,
0
marketParams.id(), msg.sender, borrower, repaidAssets, repaidShares, seizedAssets, 0, 0
);
}

Expand All @@ -160,22 +148,27 @@ contract LiquidationProtection {
ERC20(marketParams.loanToken).safeApprove(address(MORPHO), repaidAssets);
}

function _isHealthy(
Id id,
address borrower,
uint256 collateralPrice,
uint256 ltvThreshold
) internal view returns (bool) {
function computeSubscriptionId(address borrower, Id marketId, uint256 subscriptionNumber)
public
pure
returns (bytes32)
{
return keccak256(abi.encode(borrower, marketId, subscriptionNumber));
}

function _isHealthy(Id id, address borrower, uint256 collateralPrice, uint256 ltvThreshold)
internal
view
returns (bool)
{
Position memory borrowerPosition = MORPHO.position(id, borrower);
Market memory marketState = MORPHO.market(id);

uint256 borrowed = uint256(borrowerPosition.borrowShares).toAssetsUp(
marketState.totalBorrowAssets,
marketState.totalBorrowShares
marketState.totalBorrowAssets, marketState.totalBorrowShares
);
uint256 maxBorrow = uint256(borrowerPosition.collateral)
.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE)
.wMulDown(ltvThreshold);
uint256 maxBorrow =
uint256(borrowerPosition.collateral).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(ltvThreshold);

return maxBorrow >= borrowed;
}
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/EventsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ library EventsLib {
);

event Subscribe(
Id indexed id,
address indexed borrower,
uint256 indexed subscriptionId,
Id indexed marketId,
uint256 indexed subscriptionNumber,
uint256 slltv,
uint256 closeFactor,
uint256 liquidationIncentive
);

event Unsubscribe(uint256 indexed subscriptionId);
event Unsubscribe(address indexed borrower, Id indexed marketId, uint256 indexed subscriptionNumber);
}
34 changes: 17 additions & 17 deletions test/LiquidationProtectionTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ contract LiquidationProtectionTest is Test {
IERC20 collateralToken;

function setUp() public virtual {
vm.createSelectFork(vm.rpcUrl("mainnet"));

BORROWER = makeAddr("Borrower");
LIQUIDATOR = makeAddr("Liquidator");

Expand Down Expand Up @@ -57,52 +59,50 @@ contract LiquidationProtectionTest is Test {
vm.startPrank(BORROWER);

SubscriptionParams memory params;
params.borrower = BORROWER;
params.marketId = marketId;
params.slltv = 90 * 10 ** 16; // 90%
params.closeFactor = 10 ** 18; // 100%
params.liquidationIncentive = 10 ** 16; // 1%
params.slltv = 90 * 10 ** 16; // 90%

liquidationProtection.subscribe(params);
uint256 subscriptionNumber = liquidationProtection.subscribe(marketId, params);

assertEq(liquidationProtection.nbSubscription(), 1);
bytes32 subscriptionId = liquidationProtection.computeSubscriptionId(BORROWER, marketId, subscriptionNumber);
(uint256 slltv, uint256 closeFactor, uint256 liquidationIncentive) =
liquidationProtection.subscriptions(subscriptionId);
assertEq(params.slltv, slltv);
assertEq(params.closeFactor, closeFactor);
assertEq(params.liquidationIncentive, liquidationIncentive);
}

function testRemoveSubscription() public virtual {
vm.startPrank(BORROWER);

SubscriptionParams memory params;
params.borrower = BORROWER;
params.marketId = marketId;
params.slltv = 90 * 10 ** 16; // 90%
params.closeFactor = 10 ** 18; // 100%
params.liquidationIncentive = 10 ** 16; // 1%
params.slltv = 90 * 10 ** 16; // 90%

uint256 subscriptionId = liquidationProtection.subscribe(params);
uint256 subscriptionNumber = liquidationProtection.subscribe(marketId, params);

liquidationProtection.unsubscribe(subscriptionId);
liquidationProtection.unsubscribe(marketId, subscriptionNumber);

vm.startPrank(LIQUIDATOR);

vm.expectRevert(bytes("Non-valid subscription"));
liquidationProtection.liquidate(subscriptionId, market, BORROWER, 0, 0, hex"");
liquidationProtection.liquidate(subscriptionNumber, market, BORROWER, 0, 0, hex"");
}

function testSoftLiquidation() public virtual {
vm.startPrank(BORROWER);

SubscriptionParams memory params;
params.borrower = BORROWER;
params.marketId = marketId;
params.slltv = 10 * 10 ** 16; // 10%
params.closeFactor = 10 ** 18; // 100%
params.liquidationIncentive = 10 ** 16; // 1%
params.slltv = 10 * 10 ** 16; // 10%
params.isValid = true;

uint256 subscriptionId = liquidationProtection.subscribe(params);
uint256 subscriptionNumber = liquidationProtection.subscribe(marketId, params);

vm.startPrank(LIQUIDATOR);
Position memory position = morpho.position(marketId, BORROWER);
liquidationProtection.liquidate(subscriptionId, market, BORROWER, 0, position.borrowShares, hex"");
liquidationProtection.liquidate(subscriptionNumber, market, BORROWER, 0, position.borrowShares, hex"");
}
}

0 comments on commit 6977ea0

Please sign in to comment.