From 946d61493b87c86c190aad6f98776af434ac4976 Mon Sep 17 00:00:00 2001 From: PowVT Date: Fri, 28 Jun 2024 16:48:12 -0400 Subject: [PATCH] add vaulted currency to interest rate swap sigs --- ...IOriginationControllerInterestRateSwap.sol | 11 +++ contracts/libraries/OriginationLibrary.sol | 70 ++++++++++------- .../origination/OriginationControllerBase.sol | 31 +++++++- .../OriginationControllerInterestRateSwap.sol | 38 ++++++++- .../OriginationControllerMigrate.sol | 2 +- contracts/rollover/CrossCurrencyRollover.sol | 2 +- test/OriginationControllerInterestRateSwap.ts | 77 +++++++++++++++++-- test/utils/eip712.ts | 74 +++++++++++++++++- test/utils/types.ts | 15 ++++ 9 files changed, 278 insertions(+), 42 deletions(-) diff --git a/contracts/interfaces/IOriginationControllerInterestRateSwap.sol b/contracts/interfaces/IOriginationControllerInterestRateSwap.sol index 3953e71c..e5d3a92b 100644 --- a/contracts/interfaces/IOriginationControllerInterestRateSwap.sol +++ b/contracts/interfaces/IOriginationControllerInterestRateSwap.sol @@ -25,4 +25,15 @@ interface IOriginationControllerInterestRateSwap is IOriginationControllerBase { Signature calldata sig, SigProperties calldata sigProperties ) external returns (uint256 loanId, uint256 bundleId); + + // ============= Signature Verification ============= + + function recoverInterestRateSwapSignature( + LoanLibrary.LoanTerms calldata loanTerms, + Signature calldata sig, + SigProperties calldata sigProperties, + address vaultedCurrency, + Side side, + address signingCounterparty + ) external view returns (bytes32 sighash, address signer); } diff --git a/contracts/libraries/OriginationLibrary.sol b/contracts/libraries/OriginationLibrary.sol index 443793d2..0097269c 100644 --- a/contracts/libraries/OriginationLibrary.sol +++ b/contracts/libraries/OriginationLibrary.sol @@ -2,9 +2,6 @@ pragma solidity 0.8.18; -import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; -import "@openzeppelin/contracts/interfaces/IERC1271.sol"; - import "../libraries/LoanLibrary.sol"; import "../interfaces/IOriginationController.sol"; @@ -13,7 +10,9 @@ import "../interfaces/IOriginationController.sol"; * @title OriginationLibrary * @author Non-Fungible Technologies, Inc. * - * Library for loan origination functions. + * This library is a collection of shared logic used across various origination controller contracts. + * It includes constants for EIP712 type hashes, the functions for encoding these type hashes, and + * various data structures shared by the origination controller contracts. */ library OriginationLibrary { // ======================================= STRUCTS ================================================ @@ -42,21 +41,26 @@ library OriginationLibrary { } // ======================================= CONSTANTS ============================================== + // solhint-disable max-line-length /// @notice EIP712 type hash for bundle-based signatures. bytes32 public constant _TOKEN_ID_TYPEHASH = keccak256( - // solhint-disable-next-line max-line-length "LoanTerms(uint32 interestRate,uint64 durationSecs,address collateralAddress,uint96 deadline,address payableCurrency,uint256 principal,uint256 collateralId,bytes32 affiliateCode,SigProperties sigProperties,uint8 side,address signingCounterparty)SigProperties(uint160 nonce,uint96 maxUses)" ); /// @notice EIP712 type hash for item-based signatures. bytes32 public constant _ITEMS_TYPEHASH = keccak256( - // solhint-disable max-line-length "LoanTermsWithItems(uint32 interestRate,uint64 durationSecs,address collateralAddress,uint96 deadline,address payableCurrency,uint256 principal,bytes32 affiliateCode,Predicate[] items,SigProperties sigProperties,uint8 side,address signingCounterparty)Predicate(bytes data,address verifier)SigProperties(uint160 nonce,uint96 maxUses)" ); + /// @notice EIP712 type hash for interest rate swap signatures. + bytes32 public constant _INTEREST_RATE_SWAP_TYPEHASH = + keccak256( + "LoanTermsWithCurrencyPair(uint32 interestRate,uint64 durationSecs,address vaultedCurrency,address collateralAddress,uint96 deadline,address payableCurrency,uint256 principal,uint256 collateralId,bytes32 affiliateCode,SigProperties sigProperties,uint8 side,address signingCounterparty)SigProperties(uint160 nonce,uint96 maxUses)" + ); + /// @notice EIP712 type hash for Predicate. bytes32 public constant _PREDICATE_TYPEHASH = keccak256( @@ -192,33 +196,41 @@ library OriginationLibrary { ); } - // ==================================== PERMISSION MANAGEMENT ===================================== /** - * @notice Reports whether the signer matches the target or is approved by the target. + * @notice Hashes a loan with interest rate swap for inclusion in the EIP712 signature. * - * @param target The grantor of permission - should be a smart contract. - * @param sig A struct containing the signature data (for checking EIP-1271). - * @param sighash The hash of the signature payload (used for EIP-1271 check). + * @param terms The loan terms. + * @param sigProperties The signature properties. + * @param vaultedCurrency The currency to be vaulted. + * @param side The side of the signature. + * @param signingCounterparty The address of the signing counterparty. * - * @return bool Whether the signer is either the grantor themselves, or approved. + * @return loanHash The hash of the loan. */ - function isApprovedForContract( - address target, - IOriginationController.Signature memory sig, - bytes32 sighash - ) public view returns (bool) { - bytes memory signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // Append extra data if it exists - if (sig.extraData.length > 0) { - signature = bytes.concat(signature, sig.extraData); - } - - // Convert sig struct to bytes - (bool success, bytes memory result) = target.staticcall( - abi.encodeWithSelector(IERC1271.isValidSignature.selector, sighash, signature) + function encodeLoanWithInterestRateSwap( + LoanLibrary.LoanTerms calldata terms, + IOriginationController.SigProperties calldata sigProperties, + address vaultedCurrency, + uint8 side, + address signingCounterparty + ) public pure returns (bytes32 loanHash) { + loanHash = keccak256( + abi.encode( + _INTEREST_RATE_SWAP_TYPEHASH, + terms.interestRate, + terms.durationSecs, + vaultedCurrency, + terms.collateralAddress, + terms.deadline, + terms.payableCurrency, + terms.principal, + terms.collateralId, + terms.affiliateCode, + encodeSigProperties(sigProperties), + uint8(side), + signingCounterparty + ) ); - return (success && result.length == 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); } -} \ No newline at end of file +} diff --git a/contracts/origination/OriginationControllerBase.sol b/contracts/origination/OriginationControllerBase.sol index 94ef54bf..46fd5091 100644 --- a/contracts/origination/OriginationControllerBase.sol +++ b/contracts/origination/OriginationControllerBase.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.18; import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/interfaces/IERC1271.sol"; import "./OriginationCalculator.sol"; @@ -102,6 +103,34 @@ abstract contract OriginationControllerBase is IOriginationControllerBase, EIP71 return target == signer || isApproved(target, signer); } + /** + * @notice Reports whether the signer matches the target or is approved by the target. + * + * @param target The grantor of permission - should be a smart contract. + * @param sig A struct containing the signature data (for checking EIP-1271). + * @param sighash The hash of the signature payload (used for EIP-1271 check). + * + * @return bool Whether the signer is either the grantor themselves, or approved. + */ + function isApprovedForContract( + address target, + IOriginationController.Signature memory sig, + bytes32 sighash + ) public view returns (bool) { + bytes memory signature = abi.encodePacked(sig.r, sig.s, sig.v); + + // Append extra data if it exists + if (sig.extraData.length > 0) { + signature = bytes.concat(signature, sig.extraData); + } + + // Convert sig struct to bytes + (bool success, bytes memory result) = target.staticcall( + abi.encodeWithSelector(IERC1271.isValidSignature.selector, sighash, signature) + ); + return (success && result.length == 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); + } + // ==================================== SIGNATURE VERIFICATION ====================================== /** @@ -246,7 +275,7 @@ abstract contract OriginationControllerBase is IOriginationControllerBase, EIP71 } // Check signature validity - if (!isSelfOrApproved(signingCounterparty, signer) && !OriginationLibrary.isApprovedForContract(signingCounterparty, sig, sighash)) { + if (!isSelfOrApproved(signingCounterparty, signer) && !isApprovedForContract(signingCounterparty, sig, sighash)) { revert OC_InvalidSignature(signingCounterparty, signer); } diff --git a/contracts/origination/OriginationControllerInterestRateSwap.sol b/contracts/origination/OriginationControllerInterestRateSwap.sol index 170d0222..c5662946 100644 --- a/contracts/origination/OriginationControllerInterestRateSwap.sol +++ b/contracts/origination/OriginationControllerInterestRateSwap.sol @@ -97,7 +97,7 @@ contract OriginationControllerInterestRateSwap is address lender, Signature calldata sig, SigProperties calldata sigProperties - ) external returns (uint256 loanId, uint256 bundleId) { + ) public override returns (uint256 loanId, uint256 bundleId) { // input validation originationHelpers.validateLoanTerms(loanTerms); @@ -109,10 +109,11 @@ contract OriginationControllerInterestRateSwap is address signingCounterparty = neededSide == Side.LEND ? lender : borrower; address callingCounterparty = neededSide == Side.LEND ? borrower : lender; - (bytes32 sighash, address externalSigner) = recoverTokenSignature( + (bytes32 sighash, address externalSigner) = recoverInterestRateSwapSignature( loanTerms, sig, sigProperties, + swapData.vaultedCurrency, neededSide, signingCounterparty ); @@ -149,6 +150,39 @@ contract OriginationControllerInterestRateSwap is if(!currencyPairs[key]) revert OCIRS_InvalidPair(loanTerms.payableCurrency, swapData.vaultedCurrency); } + /** + * @notice Determine the external signer for a signature. + * + * @param loanTerms The terms of the loan. + * @param sig The signature, with v, r, s fields. + * @param sigProperties Signature nonce and max uses for this nonce. + * @param vaultedCurrency The currency to be vaulted. + * @param side The side of the loan being signed. + * @param signingCounterparty The address of the counterparty who signed the terms. + * + * @return sighash The hash that was signed. + * @return signer The address of the recovered signer. + */ + function recoverInterestRateSwapSignature( + LoanLibrary.LoanTerms calldata loanTerms, + Signature calldata sig, + SigProperties calldata sigProperties, + address vaultedCurrency, + Side side, + address signingCounterparty + ) public view override returns (bytes32 sighash, address signer) { + bytes32 loanHash = OriginationLibrary.encodeLoanWithInterestRateSwap( + loanTerms, + sigProperties, + vaultedCurrency, + uint8(side), + signingCounterparty + ); + + sighash = _hashTypedDataV4(loanHash); + signer = ECDSA.recover(sighash, sig.v, sig.r, sig.s); + } + /** * @notice Mint a new asset vault, then deposit the vaulted currency amounts from both parties * into the asset vault. Update the loan terms to reflect the new bundle ID. Then, diff --git a/contracts/origination/OriginationControllerMigrate.sol b/contracts/origination/OriginationControllerMigrate.sol index 1251014e..2d527db1 100644 --- a/contracts/origination/OriginationControllerMigrate.sol +++ b/contracts/origination/OriginationControllerMigrate.sol @@ -101,7 +101,7 @@ contract OriginationControllerMigrate is IMigrationBase, OriginationController, (bytes32 sighash, address externalSigner) = _recoverSignature(newTerms, sig, sigProperties, Side.LEND, lender, itemPredicates); // counterparty validation - if (!isSelfOrApproved(lender, externalSigner) && !OriginationLibrary.isApprovedForContract(lender, sig, sighash)) { + if (!isSelfOrApproved(lender, externalSigner) && !isApprovedForContract(lender, sig, sighash)) { revert OCM_SideMismatch(externalSigner); } diff --git a/contracts/rollover/CrossCurrencyRollover.sol b/contracts/rollover/CrossCurrencyRollover.sol index de449af8..1b3d18af 100644 --- a/contracts/rollover/CrossCurrencyRollover.sol +++ b/contracts/rollover/CrossCurrencyRollover.sol @@ -114,7 +114,7 @@ contract CrossCurrencyRollover is (bytes32 sighash, address externalSigner) = _recoverSignature(newTerms, sig, sigProperties, Side.LEND, lender, itemPredicates); // counterparty validation - if (!isSelfOrApproved(lender, externalSigner) && !OriginationLibrary.isApprovedForContract(lender, sig, sighash)) { + if (!isSelfOrApproved(lender, externalSigner) && !isApprovedForContract(lender, sig, sighash)) { revert CCR_SideMismatch(externalSigner); } diff --git a/test/OriginationControllerInterestRateSwap.ts b/test/OriginationControllerInterestRateSwap.ts index 8ab4f3bd..61418219 100644 --- a/test/OriginationControllerInterestRateSwap.ts +++ b/test/OriginationControllerInterestRateSwap.ts @@ -15,7 +15,6 @@ import { AssetVault, PromissoryNote, LoanCore, - ArcadeItemsVerifier, FeeController, BaseURIDescriptor, OriginationHelpers, @@ -25,7 +24,7 @@ import { } from "../typechain"; import { mint, ZERO_ADDRESS } from "./utils/erc20"; import { LoanTerms, SignatureProperties, SwapData } from "./utils/types"; -import { createLoanTermsSignature } from "./utils/eip712"; +import { createInterestRateSwapSignature } from "./utils/eip712"; import { ORIGINATOR_ROLE, @@ -208,10 +207,11 @@ describe("OriginationControllerInterestRateSwap", () => { durationSecs: BigNumber.from(60 * 60 * 24 * 365), // 1 year }, ); - const sig = await createLoanTermsSignature( + const sig = await createInterestRateSwapSignature( originationControllerIRS.address, "OriginationController", loanTerms, + sUSDe.address, lender, EIP712_VERSION, defaultSigProperties, @@ -273,10 +273,11 @@ describe("OriginationControllerInterestRateSwap", () => { durationSecs: BigNumber.from(60 * 60 * 24 * 365), // 1 year }, ); - const sig = await createLoanTermsSignature( + const sig = await createInterestRateSwapSignature( originationControllerIRS.address, "OriginationController", loanTerms, + sUSDe.address, lender, EIP712_VERSION, defaultSigProperties, @@ -368,10 +369,11 @@ describe("OriginationControllerInterestRateSwap", () => { durationSecs: BigNumber.from(60 * 60 * 24 * 365), // 1 year }, ); - const sig = await createLoanTermsSignature( + const sig = await createInterestRateSwapSignature( originationControllerIRS.address, "OriginationController", loanTerms, + sUSDe.address, lender, EIP712_VERSION, defaultSigProperties, @@ -442,10 +444,11 @@ describe("OriginationControllerInterestRateSwap", () => { durationSecs: BigNumber.from(60 * 60 * 24 * 365), // 1 year }, ); - const sig = await createLoanTermsSignature( + const sig = await createInterestRateSwapSignature( originationControllerIRS.address, "OriginationController", loanTerms, + sUSDe.address, borrower, EIP712_VERSION, defaultSigProperties, @@ -540,10 +543,11 @@ describe("OriginationControllerInterestRateSwap", () => { durationSecs: BigNumber.from(60 * 60 * 24 * 365), // 1 year }, ); - const sig = await createLoanTermsSignature( + const sig = await createInterestRateSwapSignature( originationControllerIRS.address, "OriginationController", loanTerms, + sUSDe.address, lender, EIP712_VERSION, defaultSigProperties, @@ -582,5 +586,64 @@ describe("OriginationControllerInterestRateSwap", () => { ) .to.be.revertedWith("OCIRS_InvalidPair"); }) + + it("Reverts on invalid signature", async () => { + const { vaultFactory, originationControllerIRS, USDC, sUSDe, user: lender, other: borrower, signers } = ctx; + + // Lender has 1,000,000 sUSDe they want to lock in a fixed rate of 15% APR on + const susdeLenderSwapAmount = ethers.utils.parseEther("1000000"); + await mint(sUSDe, lender, susdeLenderSwapAmount); + + // Signature and loan terms + const loanTerms = createLoanTerms( + USDC.address, vaultFactory.address, { + collateralId: BigNumber.from(0), // completed by the origination controller + principal: BigNumber.from(1000000000000), // 1,000,000 USDC + interestRate: BigNumber.from(1500), // 15% interest amount makes the repayment amount 1,150,000 USDC after 1 year + durationSecs: BigNumber.from(60 * 60 * 24 * 365), // 1 year + }, + ); + const sig = await createInterestRateSwapSignature( + originationControllerIRS.address, + "OriginationController", + loanTerms, + sUSDe.address, + signers[3], // 3rd party signer is not approved to sign + EIP712_VERSION, + defaultSigProperties, + "l", + ); + + // lender approves sUSDe to swap + await sUSDe.connect(lender).approve(originationControllerIRS.address, susdeLenderSwapAmount); + + // borrower approves sUSDe to swap + const susdeBorrowerSwapAmount = ethers.utils.parseEther("150000"); + await mint(sUSDe, borrower, susdeBorrowerSwapAmount); + await sUSDe.connect(borrower).approve(originationControllerIRS.address, susdeBorrowerSwapAmount); + + // check sUSDe balance of borrower and lender + expect(await sUSDe.balanceOf(borrower.address)).to.equal(susdeBorrowerSwapAmount); + expect(await sUSDe.balanceOf(lender.address)).to.equal(susdeLenderSwapAmount); + + // Borrower initiates interest rate swap + const swapData: SwapData = { + vaultedCurrency: sUSDe.address, + payableToVaultedCurrencyRatio: ethers.utils.parseEther("1").div(BigNumber.from(1000000)), + } + + // initialize swap + await expect( originationControllerIRS + .connect(borrower) + .initializeSwap( + loanTerms, + swapData, + borrower.address, + lender.address, + sig, + defaultSigProperties, + ) + ).to.be.revertedWith("OC_InvalidSignature"); + }); }); }); diff --git a/test/utils/eip712.ts b/test/utils/eip712.ts index 7c84dd78..98c301e9 100644 --- a/test/utils/eip712.ts +++ b/test/utils/eip712.ts @@ -1,7 +1,7 @@ import hre from "hardhat"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { BigNumberish } from "ethers"; -import { LoanTerms, ItemsPredicate, InitializeLoanSignature, SignatureProperties, LoanTermsWithItems, LoanWithItems, Loan } from "./types"; +import { LoanTerms, ItemsPredicate, InitializeLoanSignature, SignatureProperties, LoanWithItems, Loan, LoanInterestRateSwap } from "./types"; import { fromRpcSig, ECDSASignature } from "ethereumjs-util"; import { EIP712_VERSION } from "./constants"; @@ -83,6 +83,30 @@ const typedLoanItemsData: TypeData = { primaryType: "LoanWithItems" as const, }; +const typedLoanInterestRateSwapData: TypeData = { + types: { + LoanTermsWithCurrencyPair: [ + { name: "interestRate", type: "uint32" }, + { name: "durationSecs", type: "uint64" }, + { name: "vaultedCurrency", type: "address" }, + { name: "collateralAddress", type: "address" }, + { name: "deadline", type: "uint96" }, + { name: "payableCurrency", type: "address" }, + { name: "principal", type: "uint256" }, + { name: "collateralId", type: "uint256" }, + { name: "affiliateCode", type: "bytes32" }, + { name: "sigProperties", type: "SigProperties" }, + { name: "side", type: "uint8" }, + { name: "signingCounterparty", type: "address"}, + ], + SigProperties: [ + { name: "nonce", type: "uint160" }, + { name: "maxUses", type: "uint96" }, + ], + }, + primaryType: "LoanTermsWithCurrencyPair" as const, +}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const buildData = (verifyingContract: string, name: string, version: string, message: any, typeData: TypeData) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -190,6 +214,54 @@ export async function createLoanItemsSignature( return { v: sig.v, r: sig.r, s: sig.s, extraData }; } +/** + * Create an EIP712 signature for loan terms + * @param verifyingContract The address of the contract that will be verifying this signature + * @param name The name of the contract that will be verifying this signature + * @param terms the LoanTerms object to sign + * @param vaultedCurrency The vaulted currency + * @param signer The EOA to create the signature + * @param version The EIP712 version of the contract to use + * @param sigProperties The signature nonce and max uses for that nonce + * @param side The side of the loan + * @param extraData Any data to append to the signature + */ +export async function createInterestRateSwapSignature( + verifyingContract: string, + name: string, + terms: LoanTerms, + vaultedCurrency: string, + signer: SignerWithAddress, + version = EIP712_VERSION, + sigProperties: SignatureProperties, + _side: "b" | "l", + extraData = "0x", + _signingCounterparty?: string, +): Promise { + const side = _side === "b" ? 0 : 1; + const signingCounterparty = _signingCounterparty ?? signer.address; + const message: LoanInterestRateSwap = { + interestRate: terms.interestRate, + durationSecs: terms.durationSecs, + vaultedCurrency: vaultedCurrency, + collateralAddress: terms.collateralAddress, + deadline: terms.deadline, + payableCurrency: terms.payableCurrency, + principal: terms.principal, + collateralId: terms.collateralId, + affiliateCode: terms.affiliateCode, + sigProperties, + side, + signingCounterparty, + } + const data = buildData(verifyingContract, name, version, message, typedLoanInterestRateSwapData); + const signature = await signer._signTypedData(data.domain, data.types, data.message); + + const sig: ECDSASignature = fromRpcSig(signature); + + return { v: sig.v, r: sig.r, s: sig.s, extraData }; +} + /** * Create an EIP712 signature for ERC721 permit * @param verifyingContract The address of the contract that will be verifying this signature diff --git a/test/utils/types.ts b/test/utils/types.ts index 23b72707..1c0ef15d 100644 --- a/test/utils/types.ts +++ b/test/utils/types.ts @@ -73,6 +73,21 @@ export interface LoanWithItems { signingCounterparty: string; } +export interface LoanInterestRateSwap { + interestRate: BigNumberish; + durationSecs: BigNumberish; + vaultedCurrency: string; + collateralAddress: string; + deadline: BigNumberish; + payableCurrency: string; + principal: BigNumber; + collateralId: BigNumberish; + affiliateCode: BytesLike; + sigProperties: SignatureProperties; + side: number; + signingCounterparty: string; +} + export interface LoanTermsWithItems { interestRate: BigNumberish; durationSecs: BigNumberish;