Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions certora/Terms.spec
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ invariant sanitySumBondShares(bytes32 id)
invariant sanitySumDebt(bytes32 id)
sumDebtOf[id] >= 0;

rule satisfyMatch(env e, calldataarg args) {
MATCH(e, args);
rule satisfyTake(env e, calldataarg args) {
take(e, args);
satisfy true;
}

Expand Down
91 changes: 29 additions & 62 deletions src/Terms.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@ pragma solidity 0.8.28;

import "./libraries/UtilsLib.sol";
import {MathLib, WAD} from "./libraries/MathLib.sol";
import {SharesMathLib} from "./libraries/SharesMathLib.sol";
import "./interfaces/IERC20.sol";
import "./interfaces/IOracle.sol";
import "./interfaces/ITerms.sol";
import "./interfaces/IMorphoLiquidationCallback.sol";

contract Terms is ITerms {
using MathLib for uint256;
using SharesMathLib for uint256;

/// CONSTANTS ///

bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
bytes32 public constant OFFER_TYPEHASH = keccak256(
"Offer(bool lend,address offering,uint256 assets,address loanToken,Collateral[] collaterals,uint256 maturity,uint256 price)"
);

uint256 public constant ORACLE_PRICE_SCALE = 1e36;

/// STORAGE ///
Expand All @@ -36,42 +33,31 @@ contract Terms is ITerms {

/// ENTRY-POINTS ///

/// @dev This function is used for both primary and secondary markets.
function MATCH(Offer memory buyOffer, Signature memory buySig, Offer memory sellOffer, Signature memory sellSig)
/// @dev Same function used to buy and sell.
/// @dev If one wants to make to offers without taking a position, they can batch take them and not have a position at the end.
function take(Term memory term, uint256 amount, address onBehalf, Offer memory offer, Signature memory sig)
Comment thread
MathisGD marked this conversation as resolved.
public
{
_checkOffers(buyOffer, buySig, sellOffer, sellSig);
require(term.maturity >= block.timestamp, "maturity");
_checkSignature(offer, sig);
_checkOffer(term, offer);

uint256 amount = UtilsLib.min(
buyOffer.assets - consumed[abi.encode(buyOffer)], sellOffer.assets - consumed[abi.encode(sellOffer)]
);
require(amount > 0, "No assets to match");
address buyer = buyOffer.offering;
address seller = sellOffer.offering;
(address buyer, address seller) = offer.buy ? (offer.offering, onBehalf) : (onBehalf, offer.offering);

consumed[abi.encode(buyOffer)] += amount;
consumed[abi.encode(sellOffer)] += amount;
consumed[abi.encode(offer)] += amount;
Comment thread
MathisGD marked this conversation as resolved.
require(consumed[abi.encode(offer)] <= offer.assets, "consumed");

Term memory term = Term(sellOffer.loanToken, sellOffer.collaterals, sellOffer.maturity);
bytes32 id = _id(term);

uint256 repaid = UtilsLib.min(debtOf[buyer][id], amount);
uint256 bought = amount - repaid;
uint256 boughtShares = bought.toSharesDown(totalAssets[id], totalShares[id]);
uint256 boughtShares = bought.mulDivDown(totalShares[id] + 1, totalAssets[id] + 1);
Comment thread
MathisGD marked this conversation as resolved.
uint256 withdrawn =
UtilsLib.min(bondSharesOf[seller][id].mulDivDown(totalAssets[id] + 1, totalShares[id] + 1), amount);
uint256 withdrawnShares = withdrawn.mulDivUp(totalShares[id] + 1, totalAssets[id] + 1);
Comment thread
MathisGD marked this conversation as resolved.

debtOf[buyer][id] -= repaid;
bondSharesOf[buyer][id] += boughtShares;

uint256 withdrawn;
uint256 withdrawnShares;

if (bondSharesOf[seller][id].toAssetsDown(totalAssets[id], totalShares[id]) < amount) {
withdrawn = bondSharesOf[seller][id].toAssetsDown(totalAssets[id], totalShares[id]);
withdrawnShares = bondSharesOf[seller][id];
} else {
withdrawn = amount;
withdrawnShares = amount.toSharesUp(totalAssets[id], totalShares[id]);
}

bondSharesOf[seller][id] -= withdrawnShares;
debtOf[seller][id] += amount - withdrawn;

Expand All @@ -83,20 +69,17 @@ contract Terms is ITerms {
require(debtOf[buyer][id] == 0 || _isHealthy(term, buyer), "Buyer is unhealthy");
require(debtOf[seller][id] == 0 || _isHealthy(term, seller), "Seller is unhealthy");

uint256 sellerScaledPrice = sellOffer.price * amount / sellOffer.assets;
uint256 buyerScaledPrice = buyOffer.price * amount / buyOffer.assets;

IERC20(buyOffer.loanToken).transferFrom(buyer, seller, sellerScaledPrice);
IERC20(buyOffer.loanToken).transferFrom(buyer, msg.sender, buyerScaledPrice - sellerScaledPrice);
uint256 scaledPrice = offer.price * amount / offer.assets;
IERC20(offer.loanToken).transferFrom(buyer, seller, scaledPrice);
}

/// @dev Will revert if there is no withdrawable funds.
function withdrawBond(Term memory term, uint256 amount, uint256 shares, address onBehalf) external {
require(UtilsLib.exactlyOneZero(amount, shares), "INCONSISTENT_INPUT");
bytes32 id = _id(term);

if (amount > 0) shares = amount.toSharesUp(totalAssets[id], totalShares[id]);
else amount = shares.toAssetsDown(totalAssets[id], totalShares[id]);
if (amount > 0) shares = amount.mulDivUp(totalShares[id] + 1, totalAssets[id] + 1);
else amount = shares.mulDivDown(totalAssets[id] + 1, totalShares[id] + 1);

bondSharesOf[onBehalf][id] -= shares;
withdrawable[id] -= amount;
Expand Down Expand Up @@ -204,7 +187,7 @@ contract Terms is ITerms {
}

function bondOf(address owner, bytes32 id) public view returns (uint256) {
return bondSharesOf[owner][id].toAssetsDown(totalAssets[id], totalShares[id]);
return bondSharesOf[owner][id].mulDivDown(totalAssets[id] + 1, totalShares[id] + 1);
}

/// INTERNAL ///
Expand All @@ -213,39 +196,23 @@ contract Terms is ITerms {
return keccak256(abi.encode(term));
}

function _checkOffers(
Offer memory buyOffer,
Signature memory buySig,
Offer memory sellOffer,
Signature memory sellSig
) internal view {
// Check consistency.

require(buyOffer.buy && !sellOffer.buy, "Inconsistent lend flags");
require(buyOffer.maturity > block.timestamp, "Buy offer has expired");
Comment thread
MathisGD marked this conversation as resolved.
_checkSignature(buyOffer, buySig);
_checkSignature(sellOffer, sellSig);
function _checkOffer(Term memory term, Offer memory offer) internal pure {
require(offer.loanToken == term.loanToken, "Loan tokens do not match");
require(offer.maturity == term.maturity, "Maturities do not match");

// Check compatibility.
Collateral[] memory subset = offer.buy ? term.collaterals : offer.collaterals;
Collateral[] memory superset = offer.buy ? offer.collaterals : term.collaterals;

require(buyOffer.offering != sellOffer.offering, "Same offering");
require(buyOffer.loanToken == sellOffer.loanToken, "Loan tokens do not match");
uint256 j = 0;
for (uint256 i = 0; i < sellOffer.collaterals.length; i++) {
for (uint256 i = 0; i < subset.length; i++) {
// Relies on the fact that the collaterals are sorted.
// Note that we actually never check that.
// If they are not, the match could fail.
while (
bytes20(sellOffer.collaterals[i].token) < bytes20(buyOffer.collaterals[j].token)
&& j++ < buyOffer.collaterals.length
) {}
require(sellOffer.collaterals[i].token == buyOffer.collaterals[j].token, "Collaterals tokens do not match");
require(sellOffer.collaterals[i].lltv <= buyOffer.collaterals[j].lltv, "LLTVs do not match");
require(sellOffer.collaterals[i].oracle == buyOffer.collaterals[j].oracle, "Oracles do not match");
// If they are not, the matching could fail.
while (superset[j].token != subset[i].token) j++;
require(superset[j].lltv >= subset[i].lltv, "LLTVs do not match");
require(subset[i].oracle == superset[j].oracle, "Oracles do not match");
j++;
}
require(buyOffer.maturity == sellOffer.maturity, "Maturities do not match");
require(buyOffer.price >= sellOffer.price, "Buy offer price is less than sell offer price");
}

function _checkSignature(Offer memory offer, Signature memory signature) internal view {
Expand Down
40 changes: 0 additions & 40 deletions src/libraries/SharesMathLib.sol

This file was deleted.

2 changes: 1 addition & 1 deletion test/BaseTest.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {Test} from "../lib/forge-std/src/Test.sol";
import "../lib/forge-std/src/Test.sol";
import {ERC20} from "./helpers/ERC20.sol";
import "../src/Terms.sol";

Expand Down
24 changes: 8 additions & 16 deletions test/LiquidationTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ contract LiquidationTest is BaseTest {
ERC20[] memory tokens = new ERC20[](n);

for (uint256 i = 0; i < n; i++) {
tokens[i] = new ERC20("collat", "c", 1 ether);
tokens[i] = new ERC20("collat", "c");
deal(address(tokens[i]), address(this), 1 ether);
}

tokens = sortTokens(tokens);
Expand Down Expand Up @@ -57,16 +58,7 @@ contract LiquidationTest is BaseTest {
}

function mintBond(Collateral[] memory cs) internal {
Offer memory lendOffer = Offer({
buy: true,
offering: lender,
assets: 1000,
loanToken: address(loanToken),
collaterals: cs,
maturity: block.timestamp + 100,
price: 990
});

Term memory term = Term(address(loanToken), cs, block.timestamp + 100);
Offer memory borrowOffer = Offer({
buy: false,
offering: borrower,
Expand All @@ -77,10 +69,9 @@ contract LiquidationTest is BaseTest {
price: 990
});

Signature memory lendSig = _signOffer(lendOffer, lenderSK);
Signature memory borrowSig = _signOffer(borrowOffer, borrowerSK);

terms.MATCH(lendOffer, lendSig, borrowOffer, borrowSig);
terms.take(term, 1000, lender, borrowOffer, borrowSig);
}

function setUp() public override {
Expand All @@ -89,9 +80,10 @@ contract LiquidationTest is BaseTest {
(lender, lenderSK) = makeAddrAndKey("lender");
liquidator = makeAddr("liquidator");

loanToken = new ERC20("loan", "loan", 1 ether);
loanToken.transfer(lender, 99);
loanToken.transfer(borrower, 1);
loanToken = new ERC20("loan", "loan");
deal(address(loanToken), address(this), type(uint256).max);
deal(address(loanToken), address(lender), 99);
deal(address(loanToken), address(borrower), 1);

vm.prank(lender);
loanToken.approve(address(terms), type(uint256).max);
Expand Down
Loading
Loading