From c3645a3a7c6fec4e9d5792771155759072dc1965 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 30 Jun 2025 15:37:25 +0200 Subject: [PATCH 01/10] new interface --- src/Terms.sol | 89 ++++++++++----------------------- src/libraries/SharesMathLib.sol | 40 --------------- test/LiquidationTest.sol | 14 +----- test/TermsTest.sol | 45 ++++++++++------- 4 files changed, 56 insertions(+), 132 deletions(-) delete mode 100644 src/libraries/SharesMathLib.sol diff --git a/src/Terms.sol b/src/Terms.sol index d1affdbd6..05512ccde 100644 --- a/src/Terms.sol +++ b/src/Terms.sol @@ -3,7 +3,6 @@ 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"; @@ -11,7 +10,6 @@ import "./interfaces/IMorphoLiquidationCallback.sol"; contract Terms is ITerms { using MathLib for uint256; - using SharesMathLib for uint256; /// CONSTANTS /// @@ -19,7 +17,6 @@ contract Terms is ITerms { 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 /// @@ -36,42 +33,29 @@ 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) public { - _checkOffers(buyOffer, buySig, sellOffer, sellSig); + _checkOffer(term, offer); + _checkSignature(offer, sig); - 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; - 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); + uint256 withdrawn = + UtilsLib.min(bondSharesOf[seller][id].mulDivDown(totalAssets[id] + 1, totalShares[id] + 1), amount); + uint256 withdrawnShares = withdrawn.mulDivDown(totalShares[id] + 1, totalAssets[id] + 1); + 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; @@ -83,11 +67,8 @@ 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. @@ -95,8 +76,8 @@ contract Terms is ITerms { 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; @@ -204,7 +185,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 /// @@ -213,39 +194,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"); - _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. - - 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 < term.collaterals.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"); + for (; j < offer.collaterals.length; j++) { + if (offer.collaterals[j].token == term.collaterals[i].token || j == offer.collaterals.length) break; + } + require(offer.collaterals[i].token == offer.collaterals[j].token, "Collaterals tokens do not match"); + require(offer.collaterals[i].lltv <= offer.collaterals[j].lltv, "LLTVs do not match"); + require(offer.collaterals[i].oracle == offer.collaterals[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 { diff --git a/src/libraries/SharesMathLib.sol b/src/libraries/SharesMathLib.sol deleted file mode 100644 index 7e252db2f..000000000 --- a/src/libraries/SharesMathLib.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.0; - -import {MathLib} from "./MathLib.sol"; - -/// @title SharesMathLib -/// @author Morpho Labs -/// @custom:contact security@morpho.org -/// @notice Shares management library. -/// @dev This implementation mitigates share price manipulations, using OpenZeppelin's method of virtual shares: -/// https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack. -library SharesMathLib { - using MathLib for uint256; - - /// @dev The number of virtual shares has been chosen low enough to prevent overflows, and high enough to ensure - /// high precision computations. - /// @dev Virtual shares can never be redeemed for the assets they are entitled to, but it is assumed the share price - /// stays low enough not to inflate these assets to a significant value. - /// @dev Warning: The assets to which virtual borrow shares are entitled behave like unrealizable bad debt. - uint256 internal constant VIRTUAL_SHARES = 1e6; - - /// @dev A number of virtual assets of 1 enforces a conversion rate between shares and assets when a market is - /// empty. - uint256 internal constant VIRTUAL_ASSETS = 1; - - /// @dev Calculates the value of `assets` quoted in shares, rounding down. - function toSharesDown(uint256 assets, uint256 totalAssets, uint256 totalShares) internal pure returns (uint256) { - return assets.mulDivDown(totalShares + VIRTUAL_SHARES, totalAssets + VIRTUAL_ASSETS); - } - - /// @dev Calculates the value of `shares` quoted in assets, rounding down. - function toAssetsDown(uint256 shares, uint256 totalAssets, uint256 totalShares) internal pure returns (uint256) { - return shares.mulDivDown(totalAssets + VIRTUAL_ASSETS, totalShares + VIRTUAL_SHARES); - } - - /// @dev Calculates the value of `assets` quoted in shares, rounding up. - function toSharesUp(uint256 assets, uint256 totalAssets, uint256 totalShares) internal pure returns (uint256) { - return assets.mulDivUp(totalShares + VIRTUAL_SHARES, totalAssets + VIRTUAL_ASSETS); - } -} diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index 136bc5b18..00af2ab66 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -57,16 +57,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, @@ -77,10 +68,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 { diff --git a/test/TermsTest.sol b/test/TermsTest.sol index d2f6d4c65..f57f37b69 100644 --- a/test/TermsTest.sol +++ b/test/TermsTest.sol @@ -26,6 +26,7 @@ contract TermsTest is BaseTest { (lender, lenderSK) = makeAddrAndKey("lender"); liquidator = makeAddr("liquidator"); + terms = new Terms(); loanToken = new ERC20("loan", "loan", 1 ether); loanToken.transfer(lender, 99); loanToken.transfer(borrower, 1); @@ -51,40 +52,48 @@ contract TermsTest is BaseTest { loanToken.approve(address(terms), type(uint256).max); } - function testMint() public { - Offer memory lendOffer = Offer({ - buy: true, - offering: lender, + function testLend() public { + Offer memory borrowOffer = Offer({ + buy: false, + offering: borrower, assets: 100, loanToken: address(loanToken), collaterals: collaterals, maturity: block.timestamp + 100, price: 99 }); - Offer memory borrowOffer = Offer({ - buy: false, - offering: borrower, + Signature memory borrowSig = _signOffer(borrowOffer, borrowerSK); + terms.take(term, 100, lender, borrowOffer, borrowSig); + + assertEq(terms.bondSharesOf(lender, id), 100, "lender bond shares"); + assertEq(terms.debtOf(borrower, id), 100, "borrower debt"); + + assertEq(loanToken.balanceOf(borrower), 100, "borrower balance"); + assertEq(loanToken.balanceOf(lender), 0, "lender balance"); + } + + function testBorrow() public { + Offer memory lendOffer = Offer({ + buy: true, + offering: lender, assets: 100, loanToken: address(loanToken), collaterals: collaterals, maturity: block.timestamp + 100, price: 99 }); - Signature memory lendSig = _signOffer(lendOffer, lenderSK); - Signature memory borrowSig = _signOffer(borrowOffer, borrowerSK); - - terms.MATCH(lendOffer, lendSig, borrowOffer, borrowSig); + terms.take(term, 100, borrower, lendOffer, lendSig); - assertEq(terms.bondOf(lender, id), 100); - assertEq(terms.debtOf(borrower, id), 100); + assertEq(terms.bondSharesOf(lender, id), 100, "bond shares"); + assertEq(terms.debtOf(borrower, id), 100, "lender debt"); - assertEq(loanToken.balanceOf(borrower), 100); - assertEq(loanToken.balanceOf(lender), 0); + assertEq(loanToken.balanceOf(borrower), 100, "borrower balance"); + assertEq(loanToken.balanceOf(lender), 0, "lender balance"); } function testRepay() public { - testMint(); + testLend(); vm.warp(block.timestamp + 99); @@ -104,7 +113,7 @@ contract TermsTest is BaseTest { vm.prank(lender); terms.withdrawBond(term, 100, 0, lender); - assertEq(terms.bondOf(lender, id), 0); + assertEq(terms.bondSharesOf(lender, id), 0); assertEq(terms.withdrawable(id), 0); assertEq(loanToken.balanceOf(address(terms)), 0); @@ -124,7 +133,7 @@ contract TermsTest is BaseTest { } function testBadDebt() public { - testMint(); + testLend(); loanToken.transfer(liquidator, 1000); Oracle(collaterals[0].oracle).setPrice(0.75e36); From c578b7430f3f1f93ccf3293cd81fe0f2938d61bb Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 30 Jun 2025 16:12:50 +0200 Subject: [PATCH 02/10] test: add match test --- test/LiquidationTest.sol | 10 +++++--- test/TermsTest.sol | 54 ++++++++++++++++++++++++++++++++-------- test/helpers/ERC20.sol | 4 +-- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index 00af2ab66..b8131f91e 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -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); @@ -79,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); diff --git a/test/TermsTest.sol b/test/TermsTest.sol index f57f37b69..22d49c2ea 100644 --- a/test/TermsTest.sol +++ b/test/TermsTest.sol @@ -13,7 +13,7 @@ contract TermsTest is BaseTest { address private borrower; uint256 private lenderSK; address private lender; - address private liquidator; + address private liquidator = makeAddr("liquidator"); Term private term; bytes32 private id; @@ -24,13 +24,14 @@ contract TermsTest is BaseTest { super.setUp(); (borrower, borrowerSK) = makeAddrAndKey("borrower"); (lender, lenderSK) = makeAddrAndKey("lender"); - liquidator = makeAddr("liquidator"); - terms = new Terms(); - loanToken = new ERC20("loan", "loan", 1 ether); - loanToken.transfer(lender, 99); - loanToken.transfer(borrower, 1); - collateralToken = new ERC20("collat", "collat", 1 ether); + loanToken = new ERC20("loan", "loan"); + collateralToken = new ERC20("collat", "collat"); + + deal(address(loanToken), address(this), 100); + deal(address(loanToken), address(lender), 99); + deal(address(loanToken), address(borrower), 1); + deal(address(collateralToken), address(this), 134); oracle = new Oracle(); collaterals = new Collateral[](1); @@ -46,10 +47,13 @@ contract TermsTest is BaseTest { loanToken.approve(address(terms), type(uint256).max); vm.prank(borrower); loanToken.approve(address(terms), type(uint256).max); - collateralToken.approve(address(terms), type(uint256).max); - terms.supplyCollateral(term, address(collateralToken), 134, borrower); vm.prank(liquidator); loanToken.approve(address(terms), type(uint256).max); + vm.prank(address(this)); + loanToken.approve(address(terms), type(uint256).max); + + collateralToken.approve(address(terms), type(uint256).max); + terms.supplyCollateral(term, address(collateralToken), 134, borrower); } function testLend() public { @@ -92,6 +96,36 @@ contract TermsTest is BaseTest { assertEq(loanToken.balanceOf(lender), 0, "lender balance"); } + function testMatch() public { + Offer memory lendOffer = Offer({ + buy: true, + offering: lender, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory lendSig = _signOffer(lendOffer, lenderSK); + Offer memory borrowOffer = Offer({ + buy: false, + offering: borrower, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory borrowSig = _signOffer(borrowOffer, borrowerSK); + + terms.take(term, 100, address(this), borrowOffer, borrowSig); + terms.take(term, 100, address(this), lendOffer, lendSig); + + assertEq(terms.bondSharesOf(address(this), id), 0, "bond shares"); + assertEq(terms.debtOf(address(this), id), 0, "debt"); + assertEq(loanToken.balanceOf(address(this)), 100, "balance"); + } + function testRepay() public { testLend(); @@ -135,7 +169,7 @@ contract TermsTest is BaseTest { function testBadDebt() public { testLend(); - loanToken.transfer(liquidator, 1000); + deal(address(loanToken), address(liquidator), 1000); Oracle(collaterals[0].oracle).setPrice(0.75e36); vm.prank(liquidator); diff --git a/test/helpers/ERC20.sol b/test/helpers/ERC20.sol index 72825edd2..19d3651b9 100644 --- a/test/helpers/ERC20.sol +++ b/test/helpers/ERC20.sol @@ -8,11 +8,9 @@ contract ERC20 { mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; - constructor(string memory _name, string memory _symbol, uint256 _totalSupply) { + constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; - totalSupply = _totalSupply; - balanceOf[msg.sender] = _totalSupply; } function transfer(address recipient, uint256 amount) public returns (bool) { From 8197d75ea15203f2e7677659f829a2ebdf4c6c36 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 30 Jun 2025 16:13:18 +0200 Subject: [PATCH 03/10] fmt --- test/TermsTest.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TermsTest.sol b/test/TermsTest.sol index 22d49c2ea..7228c9501 100644 --- a/test/TermsTest.sol +++ b/test/TermsTest.sol @@ -27,7 +27,7 @@ contract TermsTest is BaseTest { loanToken = new ERC20("loan", "loan"); collateralToken = new ERC20("collat", "collat"); - + deal(address(loanToken), address(this), 100); deal(address(loanToken), address(lender), 99); deal(address(loanToken), address(borrower), 1); From e87e09cb0fba9fa0dd52a3993a6f756ace1c3d9d Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 30 Jun 2025 17:04:41 +0200 Subject: [PATCH 04/10] verif: update --- certora/Terms.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/Terms.spec b/certora/Terms.spec index 446a6ee20..4650e1c8a 100644 --- a/certora/Terms.spec +++ b/certora/Terms.spec @@ -43,7 +43,7 @@ invariant sanitySumDebt(bytes32 id) sumDebtOf[id] >= 0; rule satisfyMatch(env e, calldataarg args) { - MATCH(e, args); + take(e, args); satisfy true; } From 2684223aa454c513bdcb0feb07a765a5c65e24ad Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:47:48 +0200 Subject: [PATCH 05/10] fix rounding Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/Terms.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terms.sol b/src/Terms.sol index 05512ccde..1da595be3 100644 --- a/src/Terms.sol +++ b/src/Terms.sol @@ -52,7 +52,7 @@ contract Terms is ITerms { uint256 boughtShares = bought.mulDivDown(totalShares[id] + 1, totalAssets[id] + 1); uint256 withdrawn = UtilsLib.min(bondSharesOf[seller][id].mulDivDown(totalAssets[id] + 1, totalShares[id] + 1), amount); - uint256 withdrawnShares = withdrawn.mulDivDown(totalShares[id] + 1, totalAssets[id] + 1); + uint256 withdrawnShares = withdrawn.mulDivUp(totalShares[id] + 1, totalAssets[id] + 1); debtOf[buyer][id] -= repaid; bondSharesOf[buyer][id] += boughtShares; From 5a05d2b7818978e98461f926d97fab981e4db8b3 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 1 Jul 2025 19:36:04 +0200 Subject: [PATCH 06/10] fix: consumed --- src/Terms.sol | 1 + test/TermsTest.sol | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Terms.sol b/src/Terms.sol index 1da595be3..e906e08b3 100644 --- a/src/Terms.sol +++ b/src/Terms.sol @@ -44,6 +44,7 @@ contract Terms is ITerms { (address buyer, address seller) = offer.buy ? (offer.offering, onBehalf) : (onBehalf, offer.offering); consumed[abi.encode(offer)] += amount; + require(consumed[abi.encode(offer)] <= offer.assets, "consumed"); bytes32 id = _id(term); diff --git a/test/TermsTest.sol b/test/TermsTest.sol index 7228c9501..00cccd346 100644 --- a/test/TermsTest.sol +++ b/test/TermsTest.sol @@ -180,4 +180,22 @@ contract TermsTest is BaseTest { assertEq(terms.bondOf(lender, id), 87); assertEq(terms.totalAssets(id), 87); } + + function testConsumed() public { + Offer memory lendOffer = Offer({ + buy: true, + offering: lender, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory lendSig = _signOffer(lendOffer, lenderSK); + + terms.take(term, 100, borrower, lendOffer, lendSig); + + vm.expectRevert("consumed"); + terms.take(term, 100, borrower, lendOffer, lendSig); + } } From f37de56c937bc1bd6a40c4a42cc35144bee5a5d6 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 1 Jul 2025 19:50:30 +0200 Subject: [PATCH 07/10] fix: no take after maturity --- src/Terms.sol | 3 ++- test/TermsTest.sol | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Terms.sol b/src/Terms.sol index e906e08b3..697195117 100644 --- a/src/Terms.sol +++ b/src/Terms.sol @@ -38,8 +38,9 @@ contract Terms is ITerms { function take(Term memory term, uint256 amount, address onBehalf, Offer memory offer, Signature memory sig) public { - _checkOffer(term, offer); + require(term.maturity >= block.timestamp, "maturity"); _checkSignature(offer, sig); + _checkOffer(term, offer); (address buyer, address seller) = offer.buy ? (offer.offering, onBehalf) : (onBehalf, offer.offering); diff --git a/test/TermsTest.sol b/test/TermsTest.sol index 00cccd346..e55f617cd 100644 --- a/test/TermsTest.sol +++ b/test/TermsTest.sol @@ -56,6 +56,15 @@ contract TermsTest is BaseTest { terms.supplyCollateral(term, address(collateralToken), 134, borrower); } + function testTakePostMaturity(uint256 maturity) public { + maturity = bound(maturity, 0, block.timestamp - 1); + Term memory _term = Term(address(loanToken), collaterals, maturity); + Offer memory offer; + Signature memory sig; + vm.expectRevert("maturity"); + terms.take(_term, 100, lender, offer, sig); + } + function testLend() public { Offer memory borrowOffer = Offer({ buy: false, From 2ef5378433a9e25a64c2f938e9c4739118a55839 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 2 Jul 2025 00:04:17 +0200 Subject: [PATCH 08/10] fix matching --- src/Terms.sol | 16 +++---- test/BaseTest.sol | 2 +- test/TermsTest.sol | 108 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/src/Terms.sol b/src/Terms.sol index 697195117..def1bf483 100644 --- a/src/Terms.sol +++ b/src/Terms.sol @@ -200,17 +200,17 @@ contract Terms is ITerms { require(offer.loanToken == term.loanToken, "Loan tokens do not match"); require(offer.maturity == term.maturity, "Maturities do not match"); + Collateral[] memory subset = offer.buy ? term.collaterals : offer.collaterals; + Collateral[] memory superset = offer.buy ? offer.collaterals : term.collaterals; + uint256 j = 0; - for (uint256 i = 0; i < term.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. - for (; j < offer.collaterals.length; j++) { - if (offer.collaterals[j].token == term.collaterals[i].token || j == offer.collaterals.length) break; - } - require(offer.collaterals[i].token == offer.collaterals[j].token, "Collaterals tokens do not match"); - require(offer.collaterals[i].lltv <= offer.collaterals[j].lltv, "LLTVs do not match"); - require(offer.collaterals[i].oracle == offer.collaterals[j].oracle, "Oracles do not match"); + // If they are not, the matching could fail. + for (; 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++; } } diff --git a/test/BaseTest.sol b/test/BaseTest.sol index b9b886933..f3d728815 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -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"; diff --git a/test/TermsTest.sol b/test/TermsTest.sol index e55f617cd..d28a7f78f 100644 --- a/test/TermsTest.sol +++ b/test/TermsTest.sol @@ -207,4 +207,112 @@ contract TermsTest is BaseTest { vm.expectRevert("consumed"); terms.take(term, 100, borrower, lendOffer, lendSig); } + + function testTakeLendOfferCollateralMissing() public { + collaterals[0].token = address(0); + + Offer memory lendOffer = Offer({ + buy: true, + offering: lender, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory lendSig = _signOffer(lendOffer, lenderSK); + + vm.expectRevert(stdError.indexOOBError); + terms.take(term, 100, borrower, lendOffer, lendSig); + } + + function testTakeLendOfferLLTVMismatch() public { + collaterals[0].lltv = 0.5e18; + + Offer memory lendOffer = Offer({ + buy: true, + offering: lender, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory lendSig = _signOffer(lendOffer, lenderSK); + + vm.expectRevert("LLTVs do not match"); + terms.take(term, 100, borrower, lendOffer, lendSig); + } + + function testTakeLendOfferOraclesMismatch() public { + collaterals[0].oracle = address(0); + + Offer memory lendOffer = Offer({ + buy: true, + offering: lender, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory lendSig = _signOffer(lendOffer, lenderSK); + + vm.expectRevert("Oracles do not match"); + terms.take(term, 100, borrower, lendOffer, lendSig); + } + + function testTakeBorrowOfferTooMuchCollaterals() public { + collaterals[0].token = address(0); + + Offer memory borrowOffer = Offer({ + buy: false, + offering: borrower, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory borrowSig = _signOffer(borrowOffer, borrowerSK); + + vm.expectRevert(stdError.indexOOBError); + terms.take(term, 100, lender, borrowOffer, borrowSig); + } + + function testTakeBorrowOfferLLTVMismatch() public { + collaterals[0].lltv = 0.99e18; + + Offer memory borrowOffer = Offer({ + buy: false, + offering: borrower, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory borrowSig = _signOffer(borrowOffer, borrowerSK); + + vm.expectRevert("LLTVs do not match"); + terms.take(term, 100, lender, borrowOffer, borrowSig); + } + + function testTakeBorrowOfferOraclesMismatch() public { + collaterals[0].oracle = address(0); + + Offer memory borrowOffer = Offer({ + buy: false, + offering: borrower, + assets: 100, + loanToken: address(loanToken), + collaterals: collaterals, + maturity: block.timestamp + 100, + price: 99 + }); + Signature memory borrowSig = _signOffer(borrowOffer, borrowerSK); + + vm.expectRevert("Oracles do not match"); + terms.take(term, 100, lender, borrowOffer, borrowSig); + } } From be7fd7afbdeb894b29bcd6aaa2314999fad02b2d Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:58:34 +0200 Subject: [PATCH 09/10] Update src/Terms.sol Co-authored-by: Quentin Garchery Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/Terms.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terms.sol b/src/Terms.sol index def1bf483..24c5c8d74 100644 --- a/src/Terms.sol +++ b/src/Terms.sol @@ -208,7 +208,7 @@ contract Terms is ITerms { // Relies on the fact that the collaterals are sorted. // Note that we actually never check that. // If they are not, the matching could fail. - for (; superset[j].token != subset[i].token; j++) {} + 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++; From c15d6f7073bf460f0df428b54b6be3aeebc5b2b1 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 2 Jul 2025 10:06:54 +0200 Subject: [PATCH 10/10] verif: renaming --- certora/Terms.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/Terms.spec b/certora/Terms.spec index 4650e1c8a..e91c6e3de 100644 --- a/certora/Terms.spec +++ b/certora/Terms.spec @@ -42,7 +42,7 @@ invariant sanitySumBondShares(bytes32 id) invariant sanitySumDebt(bytes32 id) sumDebtOf[id] >= 0; -rule satisfyMatch(env e, calldataarg args) { +rule satisfyTake(env e, calldataarg args) { take(e, args); satisfy true; }