From 107487ec8c65c9b2c0ae0874564d8cb1c12d2a5a Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Thu, 20 Jul 2023 22:21:30 -0400 Subject: [PATCH 01/51] changed update and delete on chain quote to use hash and store in array hash and valid until timestamp --- .../peer-to-peer/DataTypesPeerToPeer.sol | 7 + contracts/peer-to-peer/QuoteHandler.sol | 34 ++++- .../peer-to-peer/interfaces/IQuoteHandler.sol | 20 ++- test/peer-to-peer/local-tests.ts | 11 +- test/peer-to-peer/mainnet-forked-tests.ts | 130 +++++++----------- 5 files changed, 108 insertions(+), 94 deletions(-) diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index 8a8dbd21..6d69addf 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -153,6 +153,13 @@ library DataTypesPeerToPeer { uint256 tokenAmount; } + struct QuoteHashAndValidDeadline { + // hash of on chain quote + bytes32 quoteHash; + // valid until timestamp + uint256 validUntil; + } + enum WhitelistState { // not whitelisted NOT_WHITELISTED, diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 9f7a6ef0..92dc9b33 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -19,6 +19,8 @@ contract QuoteHandler is IQuoteHandler { mapping(address => mapping(bytes32 => bool)) public offChainQuoteIsInvalidated; mapping(address => mapping(bytes32 => bool)) public isOnChainQuote; + mapping(address => DataTypesPeerToPeer.QuoteHashAndValidDeadline[]) + internal quoteHashAndValidDeadlinePerVault; constructor(address _addressRegistry) { if (_addressRegistry == address(0)) { @@ -41,13 +43,21 @@ contract QuoteHandler is IQuoteHandler { if (isOnChainQuoteFromVault[onChainQuoteHash]) { revert Errors.OnChainQuoteAlreadyAdded(); } + // note: in case of a vault re-adding a prior invalidated quote, this does create duplicate entry in array + // but that should be very rare and not worth tracking index with extra storage variable + quoteHashAndValidDeadlinePerVault[lenderVault].push( + DataTypesPeerToPeer.QuoteHashAndValidDeadline({ + quoteHash: onChainQuoteHash, + validUntil: onChainQuote.generalQuoteInfo.validUntil + }) + ); isOnChainQuoteFromVault[onChainQuoteHash] = true; emit OnChainQuoteAdded(lenderVault, onChainQuote, onChainQuoteHash); } function updateOnChainQuote( address lenderVault, - DataTypesPeerToPeer.OnChainQuote calldata oldOnChainQuote, + bytes32 oldOnChainQuoteHash, DataTypesPeerToPeer.OnChainQuote calldata newOnChainQuote ) external { _checkIsRegisteredVaultAndSenderIsOwner(lenderVault); @@ -56,7 +66,6 @@ contract QuoteHandler is IQuoteHandler { } mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; - bytes32 oldOnChainQuoteHash = _hashOnChainQuote(oldOnChainQuote); bytes32 newOnChainQuoteHash = _hashOnChainQuote(newOnChainQuote); // this check will catch the case where the old quote is the same as the new quote if (isOnChainQuoteFromVault[newOnChainQuoteHash]) { @@ -65,6 +74,14 @@ contract QuoteHandler is IQuoteHandler { if (!isOnChainQuoteFromVault[oldOnChainQuoteHash]) { revert Errors.UnknownOnChainQuote(); } + // note: in case of a vault re-adding a prior invalidated quote, this does create duplicate entry in array + // but that should be very rare and not worth tracking index with extra storage variable + quoteHashAndValidDeadlinePerVault[lenderVault].push( + DataTypesPeerToPeer.QuoteHashAndValidDeadline({ + quoteHash: newOnChainQuoteHash, + validUntil: newOnChainQuote.generalQuoteInfo.validUntil + }) + ); isOnChainQuoteFromVault[oldOnChainQuoteHash] = false; emit OnChainQuoteDeleted(lenderVault, oldOnChainQuoteHash); @@ -78,12 +95,11 @@ contract QuoteHandler is IQuoteHandler { function deleteOnChainQuote( address lenderVault, - DataTypesPeerToPeer.OnChainQuote calldata onChainQuote + bytes32 onChainQuoteHash ) external { _checkIsRegisteredVaultAndSenderIsOwner(lenderVault); mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; - bytes32 onChainQuoteHash = _hashOnChainQuote(onChainQuote); if (!isOnChainQuoteFromVault[onChainQuoteHash]) { revert Errors.UnknownOnChainQuote(); } @@ -205,6 +221,16 @@ contract QuoteHandler is IQuoteHandler { ); } + function getQuoteHashAndValidDeadlinePerVault( + address lenderVault + ) + external + view + returns (DataTypesPeerToPeer.QuoteHashAndValidDeadline[] memory) + { + return quoteHashAndValidDeadlinePerVault[lenderVault]; + } + /** * @dev The passed signatures must be sorted such that recovered addresses are increasing. */ diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index d73a7856..ff302888 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -56,12 +56,12 @@ interface IQuoteHandler { * @notice function updates on chain quote * @dev function can only be called by vault owner * @param lenderVault address of the vault updating quote - * @param oldOnChainQuote data for the old onChain quote (See notes in DataTypesPeerToPeer.sol) + * @param oldOnChainQuoteHash quote hash for the old onChain quote marked for deletion * @param newOnChainQuote data for the new onChain quote (See notes in DataTypesPeerToPeer.sol) */ function updateOnChainQuote( address lenderVault, - DataTypesPeerToPeer.OnChainQuote calldata oldOnChainQuote, + bytes32 oldOnChainQuoteHash, DataTypesPeerToPeer.OnChainQuote calldata newOnChainQuote ) external; @@ -69,11 +69,11 @@ interface IQuoteHandler { * @notice function deletes on chain quote * @dev function can only be called by vault owner * @param lenderVault address of the vault deleting - * @param onChainQuote data for the onChain quote marked for deletion (See notes in DataTypesPeerToPeer.sol) + * @param onChainQuoteHash quote hash for the onChain quote marked for deletion */ function deleteOnChainQuote( address lenderVault, - DataTypesPeerToPeer.OnChainQuote calldata onChainQuote + bytes32 onChainQuoteHash ) external; /** @@ -163,4 +163,16 @@ interface IQuoteHandler { address lenderVault, bytes32 hashToCheck ) external view returns (bool); + + /** + * @notice function returns array of structs containing the on chain quote hash and validUntil timestamp + * @param lenderVault address of vault + * @return array of quote hash and validUntil data for every on chain quote in a vault + */ + function getQuoteHashAndValidDeadlinePerVault( + address lenderVault + ) + external + view + returns (DataTypesPeerToPeer.QuoteHashAndValidDeadline[] memory); } diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 4f40b0d2..dcd0cabb 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -3354,10 +3354,13 @@ describe('Peer-to-Peer: Local Tests', function () { 'OnChainQuoteAdded' ) - await expect(quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, onChainQuote)).to.emit( - quoteHandler, - 'OnChainQuoteDeleted' - ) + const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address) + + expect(quoteHashAndValidUntilArr.length).to.equal(2) + + await expect( + quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash) + ).to.emit(quoteHandler, 'OnChainQuoteDeleted') }) }) diff --git a/test/peer-to-peer/mainnet-forked-tests.ts b/test/peer-to-peer/mainnet-forked-tests.ts index b13b787b..87cc2e7a 100644 --- a/test/peer-to-peer/mainnet-forked-tests.ts +++ b/test/peer-to-peer/mainnet-forked-tests.ts @@ -879,6 +879,10 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { 'OnChainQuoteAdded' ) + const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address) + + expect(quoteHashAndValidUntilArr.length).to.equal(2) + let newOnChainQuote = { ...onChainQuote, generalQuoteInfo: { @@ -892,25 +896,35 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { await addressRegistry.connect(team).setWhitelistState([usdc.address], 0) await expect( - quoteHandler.connect(lender).updateOnChainQuote(borrower.address, onChainQuote, newOnChainQuote) + quoteHandler + .connect(lender) + .updateOnChainQuote(borrower.address, quoteHashAndValidUntilArr[0].quoteHash, newOnChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') await expect( - quoteHandler.connect(borrower).updateOnChainQuote(lenderVault.address, onChainQuote, newOnChainQuote) + quoteHandler + .connect(borrower) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash, newOnChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') await expect( - quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, onChainQuote, newOnChainQuote) + quoteHandler + .connect(lender) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash, newOnChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') newOnChainQuote.generalQuoteInfo.loanToken = usdc.address await expect( - quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, onChainQuote, newOnChainQuote) + quoteHandler + .connect(lender) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash, newOnChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'NonWhitelistedToken') await addressRegistry.connect(team).setWhitelistState([compAddress], 1) await expect( - quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, onChainQuote, newOnChainQuote) + quoteHandler + .connect(lender) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash, newOnChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'NonWhitelistedToken') await addressRegistry.connect(team).setWhitelistState([usdc.address], 1) @@ -918,23 +932,27 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { onChainQuote.generalQuoteInfo.loanToken = compAddress await expect( - quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, onChainQuote, newOnChainQuote) + quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, ZERO_BYTES32, newOnChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'UnknownOnChainQuote') onChainQuote.generalQuoteInfo.loanToken = usdc.address // should revert if you add new quote same as old quote await expect( - quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, onChainQuote, onChainQuote) + quoteHandler + .connect(lender) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash, onChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'OnChainQuoteAlreadyAdded') // should revert if you add new quote which is already added await expect( - quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, onChainQuote, otherOnChainQuote) + quoteHandler + .connect(lender) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash, otherOnChainQuote) ).to.be.revertedWithCustomError(quoteHandler, 'OnChainQuoteAlreadyAdded') const updateOnChainQuoteTransaction = await quoteHandler .connect(lender) - .updateOnChainQuote(lenderVault.address, onChainQuote, newOnChainQuote) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash, newOnChainQuote) const updateOnChainQuoteReceipt = await updateOnChainQuoteTransaction.wait() @@ -950,7 +968,17 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { expect(borrowQuoteAddedEvent).to.be.not.undefined - await quoteHandler.connect(lender).updateOnChainQuote(lenderVault.address, newOnChainQuote, onChainQuote) + const quoteHashAndValidUntilArrAfterUpdate = await quoteHandler.getQuoteHashAndValidDeadlinePerVault( + lenderVault.address + ) + + expect(quoteHashAndValidUntilArrAfterUpdate.length).to.equal(3) + + await quoteHandler + .connect(lender) + .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArrAfterUpdate[2].quoteHash, onChainQuote) + + expect(await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address)).to.have.lengthOf(4) // borrower approves borrower gateway await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) @@ -1870,85 +1898,23 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { 'OnChainQuoteAdded' ) - await expect( - quoteHandler.connect(lender).deleteOnChainQuote(borrower.address, onChainQuote) - ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') - await expect( - quoteHandler.connect(borrower).deleteOnChainQuote(lenderVault.address, onChainQuote) - ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') - onChainQuote.generalQuoteInfo.loanToken = weth.address - await expect(quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, onChainQuote)).to.reverted + const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address) - onChainQuote.generalQuoteInfo.loanToken = usdc.address - - await expect(quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, onChainQuote)).to.emit( - quoteHandler, - 'OnChainQuoteDeleted' - ) - }) - - it('Should validate correctly the wrong deleteOnChainQuote', async function () { - const { addressRegistry, quoteHandler, lender, borrower, team, usdc, weth, lenderVault } = await setupTest() - - // lenderVault owner deposits usdc - await usdc.connect(lender).transfer(lenderVault.address, ONE_USDC.mul(100000)) - - // lenderVault owner gives quote - const blocknum = await ethers.provider.getBlockNumber() - const timestamp = (await ethers.provider.getBlock(blocknum)).timestamp - let quoteTuples = [ - { - loanPerCollUnitOrLtv: ONE_USDC.mul(1000), - interestRatePctInBase: BASE.mul(10).div(100), - upfrontFeePctInBase: BASE.mul(1).div(100), - tenor: ONE_DAY.mul(365) - }, - { - loanPerCollUnitOrLtv: ONE_USDC.mul(1000), - interestRatePctInBase: BASE.mul(20).div(100), - upfrontFeePctInBase: 0, - tenor: ONE_DAY.mul(180) - } - ] - let onChainQuote = { - generalQuoteInfo: { - collToken: weth.address, - loanToken: usdc.address, - oracleAddr: ZERO_ADDR, - minLoan: ONE_USDC.mul(1000), - maxLoan: MAX_UINT256, - validUntil: timestamp + 60, - earliestRepayTenor: 0, - borrowerCompartmentImplementation: ZERO_ADDR, - isSingleUse: false, - whitelistAddr: ZERO_ADDR, - isWhitelistAddrSingleBorrower: false - }, - quoteTuples: quoteTuples, - salt: ZERO_BYTES32 - } - await addressRegistry.connect(team).setWhitelistState([weth.address, usdc.address], 1) - - await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote)).to.emit( - quoteHandler, - 'OnChainQuoteAdded' - ) + expect(quoteHashAndValidUntilArr.length).to.equal(1) await expect( - quoteHandler.connect(lender).deleteOnChainQuote(borrower.address, onChainQuote) + quoteHandler.connect(lender).deleteOnChainQuote(borrower.address, quoteHashAndValidUntilArr[0].quoteHash) ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') await expect( - quoteHandler.connect(borrower).deleteOnChainQuote(lenderVault.address, onChainQuote) + quoteHandler.connect(borrower).deleteOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') - onChainQuote.generalQuoteInfo.loanToken = weth.address - await expect(quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, onChainQuote)).to.reverted - - onChainQuote.generalQuoteInfo.loanToken = usdc.address + await expect( + quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, ZERO_BYTES32) + ).to.revertedWithCustomError(quoteHandler, 'UnknownOnChainQuote') - await expect(quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, onChainQuote)).to.emit( - quoteHandler, - 'OnChainQuoteDeleted' - ) + await expect( + quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash) + ).to.emit(quoteHandler, 'OnChainQuoteDeleted') }) }) From 823acd3f402303ad3463e8e13a968f5384c5ee45 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Thu, 20 Jul 2023 22:34:40 -0400 Subject: [PATCH 02/51] updated function name --- contracts/peer-to-peer/QuoteHandler.sol | 10 +++++----- contracts/peer-to-peer/interfaces/IQuoteHandler.sol | 2 +- test/peer-to-peer/local-tests.ts | 2 +- test/peer-to-peer/mainnet-forked-tests.ts | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 92dc9b33..988c665f 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -20,7 +20,7 @@ contract QuoteHandler is IQuoteHandler { public offChainQuoteIsInvalidated; mapping(address => mapping(bytes32 => bool)) public isOnChainQuote; mapping(address => DataTypesPeerToPeer.QuoteHashAndValidDeadline[]) - internal quoteHashAndValidDeadlinePerVault; + internal quoteHashesAndValidUntilTimestampsPerVault; constructor(address _addressRegistry) { if (_addressRegistry == address(0)) { @@ -45,7 +45,7 @@ contract QuoteHandler is IQuoteHandler { } // note: in case of a vault re-adding a prior invalidated quote, this does create duplicate entry in array // but that should be very rare and not worth tracking index with extra storage variable - quoteHashAndValidDeadlinePerVault[lenderVault].push( + quoteHashesAndValidUntilTimestampsPerVault[lenderVault].push( DataTypesPeerToPeer.QuoteHashAndValidDeadline({ quoteHash: onChainQuoteHash, validUntil: onChainQuote.generalQuoteInfo.validUntil @@ -76,7 +76,7 @@ contract QuoteHandler is IQuoteHandler { } // note: in case of a vault re-adding a prior invalidated quote, this does create duplicate entry in array // but that should be very rare and not worth tracking index with extra storage variable - quoteHashAndValidDeadlinePerVault[lenderVault].push( + quoteHashesAndValidUntilTimestampsPerVault[lenderVault].push( DataTypesPeerToPeer.QuoteHashAndValidDeadline({ quoteHash: newOnChainQuoteHash, validUntil: newOnChainQuote.generalQuoteInfo.validUntil @@ -221,14 +221,14 @@ contract QuoteHandler is IQuoteHandler { ); } - function getQuoteHashAndValidDeadlinePerVault( + function getQuoteHashesAndValidUntilTimestampsPerVault( address lenderVault ) external view returns (DataTypesPeerToPeer.QuoteHashAndValidDeadline[] memory) { - return quoteHashAndValidDeadlinePerVault[lenderVault]; + return quoteHashesAndValidUntilTimestampsPerVault[lenderVault]; } /** diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index ff302888..b5075de5 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -169,7 +169,7 @@ interface IQuoteHandler { * @param lenderVault address of vault * @return array of quote hash and validUntil data for every on chain quote in a vault */ - function getQuoteHashAndValidDeadlinePerVault( + function getQuoteHashesAndValidUntilTimestampsPerVault( address lenderVault ) external diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index dcd0cabb..4dbdc93d 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -3354,7 +3354,7 @@ describe('Peer-to-Peer: Local Tests', function () { 'OnChainQuoteAdded' ) - const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address) + const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address) expect(quoteHashAndValidUntilArr.length).to.equal(2) diff --git a/test/peer-to-peer/mainnet-forked-tests.ts b/test/peer-to-peer/mainnet-forked-tests.ts index 87cc2e7a..c7c7a3ef 100644 --- a/test/peer-to-peer/mainnet-forked-tests.ts +++ b/test/peer-to-peer/mainnet-forked-tests.ts @@ -879,7 +879,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { 'OnChainQuoteAdded' ) - const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address) + const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address) expect(quoteHashAndValidUntilArr.length).to.equal(2) @@ -968,7 +968,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { expect(borrowQuoteAddedEvent).to.be.not.undefined - const quoteHashAndValidUntilArrAfterUpdate = await quoteHandler.getQuoteHashAndValidDeadlinePerVault( + const quoteHashAndValidUntilArrAfterUpdate = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault( lenderVault.address ) @@ -978,7 +978,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { .connect(lender) .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArrAfterUpdate[2].quoteHash, onChainQuote) - expect(await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address)).to.have.lengthOf(4) + expect(await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address)).to.have.lengthOf(4) // borrower approves borrower gateway await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) @@ -1898,7 +1898,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { 'OnChainQuoteAdded' ) - const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashAndValidDeadlinePerVault(lenderVault.address) + const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address) expect(quoteHashAndValidUntilArr.length).to.equal(1) From d73cfba197d95a4dda52bd2f8bf5a79607564c76 Mon Sep 17 00:00:00 2001 From: asardon Date: Fri, 21 Jul 2023 12:42:19 +0200 Subject: [PATCH 03/51] added fixes for myso-26 --- README.md | 6 +-- .../peer-to-peer/DataTypesPeerToPeer.sol | 2 +- contracts/peer-to-peer/LenderVaultImpl.sol | 4 ++ contracts/peer-to-peer/QuoteHandler.sol | 41 +++++++++++-------- .../interfaces/ILenderVaultImpl.sol | 6 +++ .../peer-to-peer/interfaces/IQuoteHandler.sol | 30 ++++++++++---- test/peer-to-peer/local-tests.ts | 9 +++- test/peer-to-peer/mainnet-forked-tests.ts | 17 ++++---- 8 files changed, 79 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 73fbf8f5..07ac52d4 100644 --- a/README.md +++ b/README.md @@ -164,13 +164,13 @@ File | % Stmts | % Branch | Helpers.sol | 100 | 50 | 100 | 100 | | contracts\interfaces\ | 100 | 100 | 100 | 100 | | IMysoTokenManager.sol | 100 | 100 | 100 | 100 | | - contracts\peer-to-peer\ | 99.72 | 94.74 | 98.72 | 98.75 | | + contracts\peer-to-peer\ | 99.73 | 94.74 | 98.78 | 98.76 | | AddressRegistry.sol | 100 | 96.74 | 100 | 99.17 | 116 | BorrowerGateway.sol | 98.57 | 90.91 | 90.91 | 96.97 | 241,317,358 | DataTypesPeerToPeer.sol | 100 | 100 | 100 | 100 | | LenderVaultFactory.sol | 100 | 87.5 | 100 | 100 | | LenderVaultImpl.sol | 100 | 92.86 | 100 | 98.88 | 63,206 | - QuoteHandler.sol | 100 | 98.04 | 100 | 99.34 | 365 | + QuoteHandler.sol | 100 | 98.04 | 100 | 99.35 | 412 | contracts\peer-to-peer\callbacks\ | 100 | 75 | 88.89 | 96.88 | | BalancerV2Looping.sol | 100 | 100 | 100 | 100 | | UniV3Looping.sol | 100 | 100 | 100 | 100 | | @@ -245,6 +245,6 @@ File | % Stmts | % Branch | IFundingPoolImpl.sol | 100 | 100 | 100 | 100 | | ILoanProposalImpl.sol | 100 | 100 | 100 | 100 | | ---------------------------------------------------------|----------|----------|----------|----------|----------------| -All files | 98.99 | 88.83 | 98.63 | 96.8 | | +All files | 98.99 | 88.83 | 98.65 | 96.81 | | ---------------------------------------------------------|----------|----------|----------|----------|----------------| ``` diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index 6d69addf..942ae33a 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -153,7 +153,7 @@ library DataTypesPeerToPeer { uint256 tokenAmount; } - struct QuoteHashAndValidDeadline { + struct OnChainQuoteInfo { // hash of on chain quote bytes32 quoteHash; // valid until timestamp diff --git a/contracts/peer-to-peer/LenderVaultImpl.sol b/contracts/peer-to-peer/LenderVaultImpl.sol index a4af3893..3308f834 100644 --- a/contracts/peer-to-peer/LenderVaultImpl.sol +++ b/contracts/peer-to-peer/LenderVaultImpl.sol @@ -394,6 +394,10 @@ contract LenderVaultImpl is return _loans.length; } + function totalNumSigners() external view returns (uint256) { + return signers.length; + } + function getTokenBalancesAndLockedAmounts( address[] calldata tokens ) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 988c665f..a92e1ef8 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -19,8 +19,8 @@ contract QuoteHandler is IQuoteHandler { mapping(address => mapping(bytes32 => bool)) public offChainQuoteIsInvalidated; mapping(address => mapping(bytes32 => bool)) public isOnChainQuote; - mapping(address => DataTypesPeerToPeer.QuoteHashAndValidDeadline[]) - internal quoteHashesAndValidUntilTimestampsPerVault; + mapping(address => DataTypesPeerToPeer.OnChainQuoteInfo[]) + internal onChainQuoteHistory; constructor(address _addressRegistry) { if (_addressRegistry == address(0)) { @@ -43,10 +43,9 @@ contract QuoteHandler is IQuoteHandler { if (isOnChainQuoteFromVault[onChainQuoteHash]) { revert Errors.OnChainQuoteAlreadyAdded(); } - // note: in case of a vault re-adding a prior invalidated quote, this does create duplicate entry in array - // but that should be very rare and not worth tracking index with extra storage variable - quoteHashesAndValidUntilTimestampsPerVault[lenderVault].push( - DataTypesPeerToPeer.QuoteHashAndValidDeadline({ + // @dev: on-chain quote history is append only + onChainQuoteHistory[lenderVault].push( + DataTypesPeerToPeer.OnChainQuoteInfo({ quoteHash: onChainQuoteHash, validUntil: onChainQuote.generalQuoteInfo.validUntil }) @@ -74,10 +73,9 @@ contract QuoteHandler is IQuoteHandler { if (!isOnChainQuoteFromVault[oldOnChainQuoteHash]) { revert Errors.UnknownOnChainQuote(); } - // note: in case of a vault re-adding a prior invalidated quote, this does create duplicate entry in array - // but that should be very rare and not worth tracking index with extra storage variable - quoteHashesAndValidUntilTimestampsPerVault[lenderVault].push( - DataTypesPeerToPeer.QuoteHashAndValidDeadline({ + // @dev: on-chain quote history is append only + onChainQuoteHistory[lenderVault].push( + DataTypesPeerToPeer.OnChainQuoteInfo({ quoteHash: newOnChainQuoteHash, validUntil: newOnChainQuote.generalQuoteInfo.validUntil }) @@ -221,14 +219,23 @@ contract QuoteHandler is IQuoteHandler { ); } - function getQuoteHashesAndValidUntilTimestampsPerVault( + function getOnChainQuoteHistory( + address lenderVault, + uint256 idx + ) external view returns (DataTypesPeerToPeer.OnChainQuoteInfo memory) { + return onChainQuoteHistory[lenderVault][idx]; + } + + function getFullOnChainQuoteHistory( + address lenderVault + ) external view returns (DataTypesPeerToPeer.OnChainQuoteInfo[] memory) { + return onChainQuoteHistory[lenderVault]; + } + + function getOnChainQuoteHistoryLength( address lenderVault - ) - external - view - returns (DataTypesPeerToPeer.QuoteHashAndValidDeadline[] memory) - { - return quoteHashesAndValidUntilTimestampsPerVault[lenderVault]; + ) external view returns (uint256) { + return onChainQuoteHistory[lenderVault].length; } /** diff --git a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol index 3533c0fc..2e22c02a 100644 --- a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol +++ b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol @@ -297,4 +297,10 @@ interface ILenderVaultImpl { * @return total number of loans */ function totalNumLoans() external view returns (uint256); + + /** + * @notice function returns total number of signers + * @return total number of signers + */ + function totalNumSigners() external view returns (uint256); } diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index b5075de5..eaa1d5d5 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -165,14 +165,30 @@ interface IQuoteHandler { ) external view returns (bool); /** - * @notice function returns array of structs containing the on chain quote hash and validUntil timestamp + * @notice function returns element of on-chain history * @param lenderVault address of vault - * @return array of quote hash and validUntil data for every on chain quote in a vault + * @return element of on-chain quote history */ - function getQuoteHashesAndValidUntilTimestampsPerVault( + function getOnChainQuoteHistory( + address lenderVault, + uint256 idx + ) external view returns (DataTypesPeerToPeer.OnChainQuoteInfo memory); + + /** + * @notice function returns array of structs containing the on-chain quote hash and validUntil timestamp + * @param lenderVault address of vault + * @return array of quote hash and validUntil data for on-chain quote history of a vault + */ + function getFullOnChainQuoteHistory( + address lenderVault + ) external view returns (DataTypesPeerToPeer.OnChainQuoteInfo[] memory); + + /** + * @notice function returns the number of on-chain quotes that were added or updated + * @param lenderVault address of vault + * @return number of on-chain quotes that were added or updated + */ + function getOnChainQuoteHistoryLength( address lenderVault - ) - external - view - returns (DataTypesPeerToPeer.QuoteHashAndValidDeadline[] memory); + ) external view returns (uint256); } diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 4dbdc93d..37a71b61 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -142,7 +142,10 @@ async function generateOffChainQuote({ expect(recoveredAddr).to.equal(signer.address) // add signer + const preNumSigners = await lenderVault.totalNumSigners() await lenderVault.connect(lender).addSigners([signer.address]) + const postNumSigners = await lenderVault.totalNumSigners() + expect(postNumSigners.sub(preNumSigners)).to.be.equal(1) // lender add sig to quote and pass to borrower offChainQuote.compactSigs = customSignatures.length != 0 ? customSignatures : [compactSig] @@ -3343,6 +3346,8 @@ describe('Peer-to-Peer: Local Tests', function () { salt: ZERO_BYTES32 } + expect(await quoteHandler.getOnChainQuoteHistoryLength(lenderVault.address)).to.equal(0) + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote)).to.emit( quoteHandler, 'OnChainQuoteAdded' @@ -3354,9 +3359,11 @@ describe('Peer-to-Peer: Local Tests', function () { 'OnChainQuoteAdded' ) - const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address) + const quoteHashAndValidUntilArr = await quoteHandler.getFullOnChainQuoteHistory(lenderVault.address) + const historyLen = await quoteHandler.getOnChainQuoteHistoryLength(lenderVault.address) expect(quoteHashAndValidUntilArr.length).to.equal(2) + expect(quoteHashAndValidUntilArr.length).to.equal(historyLen) await expect( quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash) diff --git a/test/peer-to-peer/mainnet-forked-tests.ts b/test/peer-to-peer/mainnet-forked-tests.ts index c7c7a3ef..48d4a670 100644 --- a/test/peer-to-peer/mainnet-forked-tests.ts +++ b/test/peer-to-peer/mainnet-forked-tests.ts @@ -879,7 +879,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { 'OnChainQuoteAdded' ) - const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address) + const quoteHashAndValidUntilArr = await quoteHandler.getFullOnChainQuoteHistory(lenderVault.address) expect(quoteHashAndValidUntilArr.length).to.equal(2) @@ -968,9 +968,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { expect(borrowQuoteAddedEvent).to.be.not.undefined - const quoteHashAndValidUntilArrAfterUpdate = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault( - lenderVault.address - ) + const quoteHashAndValidUntilArrAfterUpdate = await quoteHandler.getFullOnChainQuoteHistory(lenderVault.address) expect(quoteHashAndValidUntilArrAfterUpdate.length).to.equal(3) @@ -978,7 +976,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { .connect(lender) .updateOnChainQuote(lenderVault.address, quoteHashAndValidUntilArrAfterUpdate[2].quoteHash, onChainQuote) - expect(await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address)).to.have.lengthOf(4) + expect(await quoteHandler.getFullOnChainQuoteHistory(lenderVault.address)).to.have.lengthOf(4) // borrower approves borrower gateway await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) @@ -1898,9 +1896,11 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { 'OnChainQuoteAdded' ) - const quoteHashAndValidUntilArr = await quoteHandler.getQuoteHashesAndValidUntilTimestampsPerVault(lenderVault.address) - + const quoteHashAndValidUntilArr = await quoteHandler.getFullOnChainQuoteHistory(lenderVault.address) + const onChainQuoteHistoryElem = await quoteHandler.getOnChainQuoteHistory(lenderVault.address, 0) expect(quoteHashAndValidUntilArr.length).to.equal(1) + expect(quoteHashAndValidUntilArr[0].quoteHash).to.be.equal(onChainQuoteHistoryElem.quoteHash) + expect(quoteHashAndValidUntilArr[0].validUntil).to.be.equal(onChainQuoteHistoryElem.validUntil) await expect( quoteHandler.connect(lender).deleteOnChainQuote(borrower.address, quoteHashAndValidUntilArr[0].quoteHash) @@ -5175,7 +5175,10 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { // lenderVault owner deposits usdc await usdc.connect(lender).transfer(lenderVault.address, ONE_USDC.mul(100000)) + const preTotalNumSigners = await lenderVault.numSigners() await lenderVault.connect(lender).addSigners([team.address]) + const postTotalNumSigners = await lenderVault.numSigners() + expect(postTotalNumSigners.sub(preTotalNumSigners)).to.be.equal(1) // deploy chainlinkOracleContract const usdcEthChainlinkAddr = '0x986b5e1e1755e3c2440e960477f25201b0a8bbd4' From 102b2ef4d5c3bdfde4a76986d73a9d6bab7b92e9 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Fri, 21 Jul 2023 08:58:12 -0400 Subject: [PATCH 04/51] added bounds check in getter --- contracts/peer-to-peer/QuoteHandler.sol | 6 +++++- test/peer-to-peer/mainnet-forked-tests.ts | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index a92e1ef8..236b64f9 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -223,7 +223,11 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, uint256 idx ) external view returns (DataTypesPeerToPeer.OnChainQuoteInfo memory) { - return onChainQuoteHistory[lenderVault][idx]; + if (idx < onChainQuoteHistory[lenderVault].length) { + return onChainQuoteHistory[lenderVault][idx]; + } else { + revert Errors.InvalidArrayIndex(); + } } function getFullOnChainQuoteHistory( diff --git a/test/peer-to-peer/mainnet-forked-tests.ts b/test/peer-to-peer/mainnet-forked-tests.ts index 48d4a670..b6eadf4d 100644 --- a/test/peer-to-peer/mainnet-forked-tests.ts +++ b/test/peer-to-peer/mainnet-forked-tests.ts @@ -1897,6 +1897,11 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { ) const quoteHashAndValidUntilArr = await quoteHandler.getFullOnChainQuoteHistory(lenderVault.address) + // revert if index out of bounds + await expect(quoteHandler.getOnChainQuoteHistory(lenderVault.address, 5)).to.be.revertedWithCustomError( + quoteHandler, + 'InvalidArrayIndex' + ) const onChainQuoteHistoryElem = await quoteHandler.getOnChainQuoteHistory(lenderVault.address, 0) expect(quoteHashAndValidUntilArr.length).to.equal(1) expect(quoteHashAndValidUntilArr[0].quoteHash).to.be.equal(onChainQuoteHistoryElem.quoteHash) From 984b157256939866a0b3c1471f57e801e918d6df Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Tue, 25 Jul 2023 22:38:11 -0400 Subject: [PATCH 05/51] added in quote policy manager changes --- contracts/Errors.sol | 1 + contracts/peer-to-peer/AddressRegistry.sol | 37 ++++++++---- .../peer-to-peer/DataTypesPeerToPeer.sol | 4 +- contracts/peer-to-peer/LenderVaultImpl.sol | 11 ++++ contracts/peer-to-peer/QuoteHandler.sol | 60 ++++++++++++++++--- .../interfaces/IAddressRegistry.sol | 6 ++ .../interfaces/ILenderVaultImpl.sol | 20 +++++++ .../peer-to-peer/interfaces/IQuoteHandler.sol | 22 +++++++ .../interfaces/IQuotePolicyManager.sol | 19 ++++++ 9 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol diff --git a/contracts/Errors.sol b/contracts/Errors.sol index a25d835a..a5a319d4 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -123,4 +123,5 @@ library Errors { error TokenNotOwnedByWrapper(); error TokenDoesNotBelongInWrapper(address tokenAddr, uint256 tokenId); error InvalidMintAmount(); + error QuoteViolatesPolicy(); } diff --git a/contracts/peer-to-peer/AddressRegistry.sol b/contracts/peer-to-peer/AddressRegistry.sol index 8f282c2c..ab39ffdb 100644 --- a/contracts/peer-to-peer/AddressRegistry.sol +++ b/contracts/peer-to-peer/AddressRegistry.sol @@ -27,6 +27,7 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { address public borrowerGateway; address public quoteHandler; address public mysoTokenManager; + address public quotePolicyManager; address public erc721Wrapper; address public erc20Wrapper; mapping(address => bool) public isRegisteredVault; @@ -83,14 +84,16 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { ( address _erc721Wrapper, address _erc20Wrapper, - address _mysoTokenManager - ) = (erc721Wrapper, erc20Wrapper, mysoTokenManager); - // note (1/2): ERC721WRAPPER, ERC20WRAPPER and MYSO_TOKEN_MANAGER state can only be "occupied" by + address _mysoTokenManager, + address _quotePolicyManager + ) = (erc721Wrapper, erc20Wrapper, mysoTokenManager, quotePolicyManager); + // note (1/2): ERC721WRAPPER, ERC20WRAPPER, MYSO_TOKEN_MANAGER, and QUOTE_POLICY_MANAGER state can only be "occupied" by // one addresses ("singleton state") if ( state == DataTypesPeerToPeer.WhitelistState.ERC721WRAPPER || state == DataTypesPeerToPeer.WhitelistState.ERC20WRAPPER || - state == DataTypesPeerToPeer.WhitelistState.MYSO_TOKEN_MANAGER + state == DataTypesPeerToPeer.WhitelistState.MYSO_TOKEN_MANAGER || + state == DataTypesPeerToPeer.WhitelistState.QUOTE_POLICY_MANAGER ) { if (addrsLen != 1) { revert Errors.InvalidArrayLength(); @@ -103,7 +106,8 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { state, _erc721Wrapper, _erc20Wrapper, - _mysoTokenManager + _mysoTokenManager, + _quotePolicyManager ); whitelistState[addrs[0]] = state; } else { @@ -120,7 +124,8 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { addrs[i], _erc721Wrapper, _erc20Wrapper, - _mysoTokenManager + _mysoTokenManager, + _quotePolicyManager ); whitelistState[addrs[i]] = state; unchecked { @@ -406,7 +411,8 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { DataTypesPeerToPeer.WhitelistState state, address _erc721Wrapper, address _erc20Wrapper, - address _mysoTokenManager + address _mysoTokenManager, + address _quotePolicyManager ) internal { // check if address already has given state set or // other singleton addresses occupy target state @@ -414,7 +420,8 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { whitelistState[newAddr] == state || whitelistState[_erc721Wrapper] == state || whitelistState[_erc20Wrapper] == state || - whitelistState[_mysoTokenManager] == state + whitelistState[_mysoTokenManager] == state || + whitelistState[_quotePolicyManager] == state ) { revert Errors.StateAlreadySet(); } @@ -423,14 +430,19 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { newAddr, _erc721Wrapper, _erc20Wrapper, - _mysoTokenManager + _mysoTokenManager, + _quotePolicyManager ); if (state == DataTypesPeerToPeer.WhitelistState.ERC721WRAPPER) { erc721Wrapper = newAddr; } else if (state == DataTypesPeerToPeer.WhitelistState.ERC20WRAPPER) { erc20Wrapper = newAddr; - } else { + } else if ( + state == DataTypesPeerToPeer.WhitelistState.MYSO_TOKEN_MANAGER + ) { mysoTokenManager = newAddr; + } else { + quotePolicyManager = newAddr; } } @@ -438,7 +450,8 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { address addr, address _erc721Wrapper, address _erc20Wrapper, - address _mysoTokenManager + address _mysoTokenManager, + address _quotePolicyManager ) internal { if (addr == _erc721Wrapper) { delete erc721Wrapper; @@ -446,6 +459,8 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { delete erc20Wrapper; } else if (addr == _mysoTokenManager) { delete mysoTokenManager; + } else if (addr == _quotePolicyManager) { + delete quotePolicyManager; } } diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index 942ae33a..32ce596d 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -182,6 +182,8 @@ library DataTypesPeerToPeer { // can be used as ERC20 wrapper contract ERC20WRAPPER, // can be used as MYSO token manager contract - MYSO_TOKEN_MANAGER + MYSO_TOKEN_MANAGER, + // can be used as quote policy manager contract + QUOTE_POLICY_MANAGER } } diff --git a/contracts/peer-to-peer/LenderVaultImpl.sol b/contracts/peer-to-peer/LenderVaultImpl.sol index 3308f834..9d5ce211 100644 --- a/contracts/peer-to-peer/LenderVaultImpl.sol +++ b/contracts/peer-to-peer/LenderVaultImpl.sol @@ -42,6 +42,7 @@ contract LenderVaultImpl is address[] public signers; address public circuitBreaker; address public reverseCircuitBreaker; + address public approvedQuoteHandler; uint256 public minNumOfSigners; mapping(address => bool) public isSigner; bool public withdrawEntered; @@ -366,6 +367,16 @@ contract LenderVaultImpl is ); } + function setApprovedQuoteHandler(address newQuoteHandler) external { + _checkOwner(); + address oldQuoteHandler = approvedQuoteHandler; + if (newQuoteHandler == oldQuoteHandler || newQuoteHandler == owner()) { + revert Errors.InvalidAddress(); + } + approvedQuoteHandler = newQuoteHandler; + emit ApprovedQuoteHandlerUpdated(newQuoteHandler, oldQuoteHandler); + } + function pauseQuotes() external { if (msg.sender != circuitBreaker && msg.sender != owner()) { revert Errors.InvalidSender(); diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 236b64f9..50d5a820 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -10,6 +10,7 @@ import {Helpers} from "../Helpers.sol"; import {IAddressRegistry} from "./interfaces/IAddressRegistry.sol"; import {ILenderVaultImpl} from "./interfaces/ILenderVaultImpl.sol"; import {IQuoteHandler} from "./interfaces/IQuoteHandler.sol"; +import {IQuotePolicyManager} from "./interfaces/IQuotePolicyManager.sol"; contract QuoteHandler is IQuoteHandler { using ECDSA for bytes32; @@ -19,6 +20,7 @@ contract QuoteHandler is IQuoteHandler { mapping(address => mapping(bytes32 => bool)) public offChainQuoteIsInvalidated; mapping(address => mapping(bytes32 => bool)) public isOnChainQuote; + mapping(address => address) public quotePolicyManagerForVault; mapping(address => DataTypesPeerToPeer.OnChainQuoteInfo[]) internal onChainQuoteHistory; @@ -33,8 +35,8 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external { - _checkIsRegisteredVaultAndSenderIsOwner(lenderVault); - if (!_isValidOnChainQuote(onChainQuote)) { + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + if (!_isValidOnChainQuote(lenderVault, onChainQuote)) { revert Errors.InvalidQuote(); } mapping(bytes32 => bool) @@ -59,8 +61,8 @@ contract QuoteHandler is IQuoteHandler { bytes32 oldOnChainQuoteHash, DataTypesPeerToPeer.OnChainQuote calldata newOnChainQuote ) external { - _checkIsRegisteredVaultAndSenderIsOwner(lenderVault); - if (!_isValidOnChainQuote(newOnChainQuote)) { + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + if (!_isValidOnChainQuote(lenderVault, newOnChainQuote)) { revert Errors.InvalidQuote(); } mapping(bytes32 => bool) @@ -95,7 +97,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 onChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsOwner(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; if (!isOnChainQuoteFromVault[onChainQuoteHash]) { @@ -106,7 +108,7 @@ contract QuoteHandler is IQuoteHandler { } function incrementOffChainQuoteNonce(address lenderVault) external { - _checkIsRegisteredVaultAndSenderIsOwner(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); uint256 newNonce = offChainQuoteNonce[lenderVault] + 1; offChainQuoteNonce[lenderVault] = newNonce; emit OffChainQuoteNonceIncremented(lenderVault, newNonce); @@ -116,7 +118,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 offChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsOwner(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); offChainQuoteIsInvalidated[lenderVault][offChainQuoteHash] = true; emit OffChainQuoteInvalidated(lenderVault, offChainQuoteHash); } @@ -132,6 +134,7 @@ contract QuoteHandler is IQuoteHandler { } _checkSenderAndQuoteInfo( borrower, + lenderVault, onChainQuote.generalQuoteInfo, onChainQuote.quoteTuples[quoteTupleIdx] ); @@ -163,6 +166,7 @@ contract QuoteHandler is IQuoteHandler { ) external { _checkSenderAndQuoteInfo( borrower, + lenderVault, offChainQuote.generalQuoteInfo, quoteTuple ); @@ -219,6 +223,20 @@ contract QuoteHandler is IQuoteHandler { ); } + function updateQuotePolicyManagerForVault( + address lenderVault, + bool isRevoke + ) external { + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + if (isRevoke) { + delete quotePolicyManagerForVault[lenderVault]; + } else { + quotePolicyManagerForVault[lenderVault] = IAddressRegistry( + addressRegistry + ).quotePolicyManager(); + } + } + function getOnChainQuoteHistory( address lenderVault, uint256 idx @@ -294,12 +312,25 @@ contract QuoteHandler is IQuoteHandler { function _checkSenderAndQuoteInfo( address borrower, + address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple ) internal view { if (msg.sender != IAddressRegistry(addressRegistry).borrowerGateway()) { revert Errors.InvalidSender(); } + if ( + quotePolicyManagerForVault[lenderVault] != address(0) && + !IQuotePolicyManager(quotePolicyManagerForVault[lenderVault]) + .checkPendingBorrowQuoteInfoAndTuple( + borrower, + lenderVault, + generalQuoteInfo, + quoteTuple + ) + ) { + revert Errors.QuoteViolatesPolicy(); + } _checkWhitelist( generalQuoteInfo.collToken, generalQuoteInfo.loanToken, @@ -333,8 +364,16 @@ contract QuoteHandler is IQuoteHandler { } function _isValidOnChainQuote( + address lenderVault, DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) internal view returns (bool) { + if ( + quotePolicyManagerForVault[lenderVault] != address(0) && + !IQuotePolicyManager(quotePolicyManagerForVault[lenderVault]) + .checkNewOnChainQuote(lenderVault, onChainQuote) + ) { + return false; + } if ( onChainQuote.generalQuoteInfo.collToken == onChainQuote.generalQuoteInfo.loanToken @@ -434,13 +473,16 @@ contract QuoteHandler is IQuoteHandler { } } - function _checkIsRegisteredVaultAndSenderIsOwner( + function _checkIsRegisteredVaultAndSenderIsApproved( address lenderVault ) internal view { if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { revert Errors.UnregisteredVault(); } - if (ILenderVaultImpl(lenderVault).owner() != msg.sender) { + if ( + ILenderVaultImpl(lenderVault).owner() != msg.sender && + ILenderVaultImpl(lenderVault).approvedQuoteHandler() != msg.sender + ) { revert Errors.InvalidSender(); } } diff --git a/contracts/peer-to-peer/interfaces/IAddressRegistry.sol b/contracts/peer-to-peer/interfaces/IAddressRegistry.sol index c330154a..c4e3adc3 100644 --- a/contracts/peer-to-peer/interfaces/IAddressRegistry.sol +++ b/contracts/peer-to-peer/interfaces/IAddressRegistry.sol @@ -186,6 +186,12 @@ interface IAddressRegistry { */ function mysoTokenManager() external view returns (address); + /** + * @notice Returns the address of the quote policy manager + * @return Address of the quote policy manager contract + */ + function quotePolicyManager() external view returns (address); + /** * @notice Returns boolean flag indicating whether given address is a registered vault * @param addr Address to check if it is a registered vault diff --git a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol index 2e22c02a..15e6db8b 100644 --- a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol +++ b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol @@ -33,11 +33,17 @@ interface ILenderVaultImpl { address indexed newCircuitBreaker, address indexed oldCircuitBreaker ); + event ReverseCircuitBreakerUpdated( address indexed newReverseCircuitBreaker, address indexed oldReverseCircuitBreaker ); + event ApprovedQuoteHandlerUpdated( + address indexed newQuoteHandler, + address indexed oldQuoteHandler + ); + /** * @notice function to initialize lender vault * @dev factory creates clone and then initializes the vault @@ -182,6 +188,14 @@ interface ILenderVaultImpl { */ function setReverseCircuitBreaker(address reverseCircuitBreaker) external; + /** + * @notice function to set an quote handler + * @dev the quote handler (and vault owner) can add, delete and update on chain quotes + * as well as invalidate off chain quotes or increment the off chain quote nonce + * @param quoteHandler address of the quote handler + */ + function setApprovedQuoteHandler(address quoteHandler) external; + /** * @notice function to pause all quotes from lendervault * @dev only vault owner and circuit breaker can pause quotes @@ -260,6 +274,12 @@ interface ILenderVaultImpl { */ function reverseCircuitBreaker() external view returns (address); + /** + * @notice function to return address of the approved quote handler + * @return approved quote handler address + */ + function approvedQuoteHandler() external view returns (address); + /** * @notice function returns signer at given index * @param index of the signers array diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index eaa1d5d5..ec577821 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -129,6 +129,17 @@ interface IQuoteHandler { bytes32[] memory proof ) external; + /** + * @notice function to update the quote policy manager for a vault + * @param lenderVault address for which quote policy manager is being updated + * @param isRevoke true if policy manager is being revoked, else set to current whitelisted policy manager in registry + * @dev function can only be called by vault owner or approved quote handler + */ + function updateQuotePolicyManagerForVault( + address lenderVault, + bool isRevoke + ) external; + /** * @notice function to return address of registry * @return registry address @@ -164,6 +175,17 @@ interface IQuoteHandler { bytes32 hashToCheck ) external view returns (bool); + /** + * @notice function returns the address of the policy manager for a vault + * @param lenderVault address of vault + * @return address of quote policy manager for vault + * @dev if policy manager address changes in registry, this function will still return the old address + * unless and until the vault owner calls updateQuotePolicyManagerForVault + */ + function quotePolicyManagerForVault( + address lenderVault + ) external view returns (address); + /** * @notice function returns element of on-chain history * @param lenderVault address of vault diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol new file mode 100644 index 00000000..68b144c7 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; + +interface IQuotePolicyManager { + function checkPendingBorrowQuoteInfoAndTuple( + address borrower, + address lenderVault, + DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple + ) external view returns (bool isValid); + + function checkNewOnChainQuote( + address lenderVault, + DataTypesPeerToPeer.OnChainQuote calldata onChainQuote + ) external view returns (bool isValid); +} From 07cecf26143fca83187e2b2dae2d427ce904f239 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 07:30:07 -0400 Subject: [PATCH 06/51] add if pending borrow is on chain or off chain --- contracts/peer-to-peer/QuoteHandler.sol | 12 ++++++++---- .../peer-to-peer/interfaces/IQuotePolicyManager.sol | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 50d5a820..d57158f8 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -136,7 +136,8 @@ contract QuoteHandler is IQuoteHandler { borrower, lenderVault, onChainQuote.generalQuoteInfo, - onChainQuote.quoteTuples[quoteTupleIdx] + onChainQuote.quoteTuples[quoteTupleIdx], + true ); mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; @@ -168,7 +169,8 @@ contract QuoteHandler is IQuoteHandler { borrower, lenderVault, offChainQuote.generalQuoteInfo, - quoteTuple + quoteTuple, + false ); if (offChainQuote.nonce < offChainQuoteNonce[lenderVault]) { revert Errors.InvalidQuote(); @@ -314,7 +316,8 @@ contract QuoteHandler is IQuoteHandler { address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, - DataTypesPeerToPeer.QuoteTuple calldata quoteTuple + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, + bool onChainQuote ) internal view { if (msg.sender != IAddressRegistry(addressRegistry).borrowerGateway()) { revert Errors.InvalidSender(); @@ -326,7 +329,8 @@ contract QuoteHandler is IQuoteHandler { borrower, lenderVault, generalQuoteInfo, - quoteTuple + quoteTuple, + onChainQuote ) ) { revert Errors.QuoteViolatesPolicy(); diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index 68b144c7..b6db5cbb 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -9,7 +9,8 @@ interface IQuotePolicyManager { address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, - DataTypesPeerToPeer.QuoteTuple calldata quoteTuple + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, + bool onChainQuote ) external view returns (bool isValid); function checkNewOnChainQuote( From c784d794536db6fef9fcaa2620cfe792c5f6e56a Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 11:46:33 -0400 Subject: [PATCH 07/51] remove quote policy as singleton --- contracts/peer-to-peer/AddressRegistry.sol | 36 +++++++--------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/contracts/peer-to-peer/AddressRegistry.sol b/contracts/peer-to-peer/AddressRegistry.sol index ab39ffdb..a4417039 100644 --- a/contracts/peer-to-peer/AddressRegistry.sol +++ b/contracts/peer-to-peer/AddressRegistry.sol @@ -84,16 +84,14 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { ( address _erc721Wrapper, address _erc20Wrapper, - address _mysoTokenManager, - address _quotePolicyManager - ) = (erc721Wrapper, erc20Wrapper, mysoTokenManager, quotePolicyManager); - // note (1/2): ERC721WRAPPER, ERC20WRAPPER, MYSO_TOKEN_MANAGER, and QUOTE_POLICY_MANAGER state can only be "occupied" by + address _mysoTokenManager + ) = (erc721Wrapper, erc20Wrapper, mysoTokenManager); + // note (1/2): ERC721WRAPPER, ERC20WRAPPER, and MYSO_TOKEN_MANAGER state can only be "occupied" by // one addresses ("singleton state") if ( state == DataTypesPeerToPeer.WhitelistState.ERC721WRAPPER || state == DataTypesPeerToPeer.WhitelistState.ERC20WRAPPER || - state == DataTypesPeerToPeer.WhitelistState.MYSO_TOKEN_MANAGER || - state == DataTypesPeerToPeer.WhitelistState.QUOTE_POLICY_MANAGER + state == DataTypesPeerToPeer.WhitelistState.MYSO_TOKEN_MANAGER ) { if (addrsLen != 1) { revert Errors.InvalidArrayLength(); @@ -106,8 +104,7 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { state, _erc721Wrapper, _erc20Wrapper, - _mysoTokenManager, - _quotePolicyManager + _mysoTokenManager ); whitelistState[addrs[0]] = state; } else { @@ -124,8 +121,7 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { addrs[i], _erc721Wrapper, _erc20Wrapper, - _mysoTokenManager, - _quotePolicyManager + _mysoTokenManager ); whitelistState[addrs[i]] = state; unchecked { @@ -411,8 +407,7 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { DataTypesPeerToPeer.WhitelistState state, address _erc721Wrapper, address _erc20Wrapper, - address _mysoTokenManager, - address _quotePolicyManager + address _mysoTokenManager ) internal { // check if address already has given state set or // other singleton addresses occupy target state @@ -420,8 +415,7 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { whitelistState[newAddr] == state || whitelistState[_erc721Wrapper] == state || whitelistState[_erc20Wrapper] == state || - whitelistState[_mysoTokenManager] == state || - whitelistState[_quotePolicyManager] == state + whitelistState[_mysoTokenManager] == state ) { revert Errors.StateAlreadySet(); } @@ -430,19 +424,14 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { newAddr, _erc721Wrapper, _erc20Wrapper, - _mysoTokenManager, - _quotePolicyManager + _mysoTokenManager ); if (state == DataTypesPeerToPeer.WhitelistState.ERC721WRAPPER) { erc721Wrapper = newAddr; } else if (state == DataTypesPeerToPeer.WhitelistState.ERC20WRAPPER) { erc20Wrapper = newAddr; - } else if ( - state == DataTypesPeerToPeer.WhitelistState.MYSO_TOKEN_MANAGER - ) { - mysoTokenManager = newAddr; } else { - quotePolicyManager = newAddr; + mysoTokenManager = newAddr; } } @@ -450,8 +439,7 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { address addr, address _erc721Wrapper, address _erc20Wrapper, - address _mysoTokenManager, - address _quotePolicyManager + address _mysoTokenManager ) internal { if (addr == _erc721Wrapper) { delete erc721Wrapper; @@ -459,8 +447,6 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { delete erc20Wrapper; } else if (addr == _mysoTokenManager) { delete mysoTokenManager; - } else if (addr == _quotePolicyManager) { - delete quotePolicyManager; } } From 1c616169be185f738ea7bef8385a4e06da4667ec Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 12:22:24 -0400 Subject: [PATCH 08/51] updated to have quote policy manager not be a multi-sig --- contracts/peer-to-peer/QuoteHandler.sol | 31 ++++++++++++------- .../peer-to-peer/interfaces/IQuoteHandler.sol | 4 ++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index d57158f8..f0711df9 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -35,7 +35,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); if (!_isValidOnChainQuote(lenderVault, onChainQuote)) { revert Errors.InvalidQuote(); } @@ -61,7 +61,7 @@ contract QuoteHandler is IQuoteHandler { bytes32 oldOnChainQuoteHash, DataTypesPeerToPeer.OnChainQuote calldata newOnChainQuote ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); if (!_isValidOnChainQuote(lenderVault, newOnChainQuote)) { revert Errors.InvalidQuote(); } @@ -97,7 +97,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 onChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; if (!isOnChainQuoteFromVault[onChainQuoteHash]) { @@ -108,7 +108,7 @@ contract QuoteHandler is IQuoteHandler { } function incrementOffChainQuoteNonce(address lenderVault) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); uint256 newNonce = offChainQuoteNonce[lenderVault] + 1; offChainQuoteNonce[lenderVault] = newNonce; emit OffChainQuoteNonceIncremented(lenderVault, newNonce); @@ -118,7 +118,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 offChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); offChainQuoteIsInvalidated[lenderVault][offChainQuoteHash] = true; emit OffChainQuoteInvalidated(lenderVault, offChainQuoteHash); } @@ -227,15 +227,21 @@ contract QuoteHandler is IQuoteHandler { function updateQuotePolicyManagerForVault( address lenderVault, + address newPolicyManagerAddress, bool isRevoke ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); if (isRevoke) { delete quotePolicyManagerForVault[lenderVault]; } else { - quotePolicyManagerForVault[lenderVault] = IAddressRegistry( - addressRegistry - ).quotePolicyManager(); + if ( + IAddressRegistry(addressRegistry).whitelistState( + newPolicyManagerAddress + ) != DataTypesPeerToPeer.WhitelistState.QUOTE_POLICY_MANAGER + ) { + revert Errors.InvalidAddress(); + } + quotePolicyManagerForVault[lenderVault] = newPolicyManagerAddress; } } @@ -478,14 +484,17 @@ contract QuoteHandler is IQuoteHandler { } function _checkIsRegisteredVaultAndSenderIsApproved( - address lenderVault + address lenderVault, + bool onlyOwner ) internal view { if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { revert Errors.UnregisteredVault(); } if ( ILenderVaultImpl(lenderVault).owner() != msg.sender && - ILenderVaultImpl(lenderVault).approvedQuoteHandler() != msg.sender + (onlyOwner || + ILenderVaultImpl(lenderVault).approvedQuoteHandler() != + msg.sender) ) { revert Errors.InvalidSender(); } diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index ec577821..1652c9aa 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -132,11 +132,13 @@ interface IQuoteHandler { /** * @notice function to update the quote policy manager for a vault * @param lenderVault address for which quote policy manager is being updated + * @param newPolicyManagerAddress address of new quote policy manager * @param isRevoke true if policy manager is being revoked, else set to current whitelisted policy manager in registry - * @dev function can only be called by vault owner or approved quote handler + * @dev function can only be called by vault owner, not even quoteHandler can update policy manager */ function updateQuotePolicyManagerForVault( address lenderVault, + address newPolicyManagerAddress, bool isRevoke ) external; From 38be2644d407f2f25cdbc1f580b2b050210291a9 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 12:49:53 -0400 Subject: [PATCH 09/51] added tests for vault impl --- test/peer-to-peer/local-tests.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 37a71b61..4bdd8622 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -172,7 +172,7 @@ describe('Peer-to-Peer: Local Tests', function () { }) async function setupTest() { - const [lender, signer, borrower, team, circuitBreaker, whitelistAuthority, addr1, addr2, addr3] = + const [lender, signer, borrower, team, circuitBreaker, approvedQuoteHandler, whitelistAuthority, addr1, addr2, addr3] = await ethers.getSigners() /* ************************************ */ /* DEPLOYMENT OF SYSTEM CONTRACTS START */ @@ -543,6 +543,7 @@ describe('Peer-to-Peer: Local Tests', function () { signer2: sortedAddrs[1], signer3: sortedAddrs[2], circuitBreaker, + approvedQuoteHandler, usdc, weth, lenderVault, @@ -1121,6 +1122,7 @@ describe('Peer-to-Peer: Local Tests', function () { borrower, whitelistAuthority, circuitBreaker, + approvedQuoteHandler, usdc, weth, lenderVault, @@ -1285,6 +1287,25 @@ describe('Peer-to-Peer: Local Tests', function () { 'InvalidAddress' ) + // only owner can set approved quote handler + await expect(lenderVault.connect(borrower).setApprovedQuoteHandler(lender.address)).to.be.revertedWith( + 'Ownable: caller is not the owner' + ) + + // should revert when trying to set invalid quote handler address (owner address) + await expect(lenderVault.connect(lender).setApprovedQuoteHandler(lender.address)).to.be.revertedWithCustomError( + lenderVault, + 'InvalidAddress' + ) + + // set valid approved quote handler + await lenderVault.connect(lender).setApprovedQuoteHandler(approvedQuoteHandler.address) + + // should revert if new handler same as old one + await expect( + lenderVault.connect(lender).setApprovedQuoteHandler(approvedQuoteHandler.address) + ).to.be.revertedWithCustomError(lenderVault, 'InvalidAddress') + // set valid circuit breaker await lenderVault.connect(lender).setCircuitBreaker(circuitBreaker.address) From 6b05bbc86cafa11adaedc121349d91767f438d98 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 14:28:32 -0400 Subject: [PATCH 10/51] added test quote policy contract and event --- contracts/peer-to-peer/QuoteHandler.sol | 4 ++ .../peer-to-peer/interfaces/IQuoteHandler.sol | 4 ++ contracts/test/TestQuotePolicyManager.sol | 34 +++++++++++ test/peer-to-peer/local-tests.ts | 59 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 contracts/test/TestQuotePolicyManager.sol diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index f0711df9..0b745249 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -243,6 +243,10 @@ contract QuoteHandler is IQuoteHandler { } quotePolicyManagerForVault[lenderVault] = newPolicyManagerAddress; } + emit QuotePolicyManagerUpdated( + lenderVault, + isRevoke ? address(0) : newPolicyManagerAddress + ); } function getOnChainQuoteHistory( diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index 1652c9aa..377d71f2 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -40,6 +40,10 @@ interface IQuoteHandler { uint256 indexed toBeRegisteredLoanId, DataTypesPeerToPeer.QuoteTuple quoteTuple ); + event QuotePolicyManagerUpdated( + address indexed lenderVault, + address indexed newPolicyManagerAddress + ); /** * @notice function adds on chain quote diff --git a/contracts/test/TestQuotePolicyManager.sol b/contracts/test/TestQuotePolicyManager.sol new file mode 100644 index 00000000..42247cf6 --- /dev/null +++ b/contracts/test/TestQuotePolicyManager.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.19; + +import {DataTypesPeerToPeer} from "../peer-to-peer/DataTypesPeerToPeer.sol"; +import {IQuotePolicyManager} from "../peer-to-peer/interfaces/IQuotePolicyManager.sol"; + +contract TestQuotePolicyManager is IQuotePolicyManager { + mapping(address => bool) public allow; + + // solhint-disable-next-line no-empty-blocks + constructor() {} + + function updatePolicy(address lenderVault, bool _allow) external { + allow[lenderVault] = _allow; + } + + function checkPendingBorrowQuoteInfoAndTuple( + address, + address lenderVault, + DataTypesPeerToPeer.GeneralQuoteInfo calldata, + DataTypesPeerToPeer.QuoteTuple calldata, + bool + ) external view returns (bool isValid) { + isValid = allow[lenderVault]; + } + + function checkNewOnChainQuote( + address lenderVault, + DataTypesPeerToPeer.OnChainQuote calldata + ) external view returns (bool isValid) { + isValid = allow[lenderVault]; + } +} diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 4bdd8622..c0a5515a 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -5775,4 +5775,63 @@ describe('Peer-to-Peer: Local Tests', function () { expect(vaultLockedWeth).to.be.lte(vaultBalWeth) }) }) + + describe('Quote Policy Manager', function () { + it('Should handle checks on policy updates correctly', async function () { + const { + quoteHandler, + addressRegistry, + borrowerGateway, + lender, + approvedQuoteHandler, + borrower, + team, + usdc, + weth, + lenderVault + } = await setupTest() + + const TestQuotePolicyManager = await ethers.getContractFactory('TestQuotePolicyManager') + const testQuotePolicyManager = await TestQuotePolicyManager.connect(team).deploy() + await testQuotePolicyManager.deployed() + + await addressRegistry.connect(team).setWhitelistState([testQuotePolicyManager.address], 10) + + // should revert if unregistered vault + await expect( + quoteHandler.connect(team).updateQuotePolicyManagerForVault(borrower.address, testQuotePolicyManager.address, false) + ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') + + // should revert if not vault owner + await expect( + quoteHandler + .connect(team) + .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') + + await expect(lenderVault.connect(lender).setApprovedQuoteHandler(approvedQuoteHandler.address)) + .to.emit(lenderVault, 'ApprovedQuoteHandlerUpdated') + .withArgs(approvedQuoteHandler.address, ZERO_ADDRESS) + + // should revert even if called by approved quote handler + await expect( + quoteHandler + .connect(approvedQuoteHandler) + .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') + + // should revert if not whitelisted quote policy manager + await expect( + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, quoteHandler.address, false) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidAddress') + + await expect( + quoteHandler + .connect(lender) + .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) + ) + .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') + .withArgs(lenderVault.address, testQuotePolicyManager.address) + }) + }) }) From 42b981deab79056d3b897106b84d252a213e7493 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 14:31:56 -0400 Subject: [PATCH 11/51] add note in function --- contracts/peer-to-peer/QuoteHandler.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 0b745249..543b82a6 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -241,6 +241,7 @@ contract QuoteHandler is IQuoteHandler { ) { revert Errors.InvalidAddress(); } + // note: this will overwrite any existing policy manager to a new valid quote policy manager quotePolicyManagerForVault[lenderVault] = newPolicyManagerAddress; } emit QuotePolicyManagerUpdated( From 641a1371a416c7135c12a726718cdf9e9e249ff6 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 14:57:05 -0400 Subject: [PATCH 12/51] added to tests on quote policy --- test/peer-to-peer/local-tests.ts | 78 ++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index c0a5515a..544e5da7 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -5825,6 +5825,38 @@ describe('Peer-to-Peer: Local Tests', function () { quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, quoteHandler.address, false) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidAddress') + // lenderVault owner deposits usdc + await usdc.connect(lender).transfer(lenderVault.address, ONE_USDC.mul(100000)) + + // lender owner gives regular quote + const loanPerCollUnit = ONE_USDC.mul(1000) + let quoteTuples1 = [ + { + loanPerCollUnitOrLtv: loanPerCollUnit, + interestRatePctInBase: 0, + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(30) + } + ] + let onChainQuote1 = { + generalQuoteInfo: { + collToken: weth.address, + loanToken: usdc.address, + oracleAddr: ZERO_ADDRESS, + minLoan: 1, + maxLoan: MAX_UINT256, + validUntil: MAX_UINT256, + earliestRepayTenor: 0, + borrowerCompartmentImplementation: ZERO_ADDRESS, + isSingleUse: false, + whitelistAddr: ZERO_ADDRESS, + isWhitelistAddrSingleBorrower: false + }, + quoteTuples: quoteTuples1, + salt: ZERO_BYTES32 + } + + // set policy manager address await expect( quoteHandler .connect(lender) @@ -5832,6 +5864,52 @@ describe('Peer-to-Peer: Local Tests', function () { ) .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') .withArgs(lenderVault.address, testQuotePolicyManager.address) + + // should revert since policy is set to not allow + await expect( + quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') + + // set policy to allow + await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, true) + + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1)).to.emit( + quoteHandler, + 'OnChainQuoteAdded' + ) + + // set borrow policy to not allowed + await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, false) + + // borrower approves gateway and executes quote + await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) + + const collSendAmount1 = ONE_WETH + const quoteTupleIdx1 = 0 + const borrowInstructions1 = { + collSendAmount: collSendAmount1, + expectedProtocolAndVaultTransferFee: 0, + expectedCompartmentTransferFee: 0, + deadline: MAX_UINT256, + minLoanAmount: 0, + callbackAddr: ZERO_ADDRESS, + callbackData: ZERO_BYTES32, + mysoTokenManagerData: ZERO_BYTES32 + } + + // should revert since quote violates policy + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // set policy to allow + await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, true) + + await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) }) }) }) From 884ab2314f278ab8bdc89cf06d844239e8544da2 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 17:36:45 -0400 Subject: [PATCH 13/51] removed check of policy on quote --- contracts/peer-to-peer/QuoteHandler.sol | 12 ++---------- .../peer-to-peer/interfaces/IQuotePolicyManager.sol | 5 ----- contracts/test/TestQuotePolicyManager.sol | 7 ------- test/peer-to-peer/local-tests.ts | 12 ------------ 4 files changed, 2 insertions(+), 34 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 543b82a6..8817762f 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -36,7 +36,7 @@ contract QuoteHandler is IQuoteHandler { DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); - if (!_isValidOnChainQuote(lenderVault, onChainQuote)) { + if (!_isValidOnChainQuote(onChainQuote)) { revert Errors.InvalidQuote(); } mapping(bytes32 => bool) @@ -62,7 +62,7 @@ contract QuoteHandler is IQuoteHandler { DataTypesPeerToPeer.OnChainQuote calldata newOnChainQuote ) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); - if (!_isValidOnChainQuote(lenderVault, newOnChainQuote)) { + if (!_isValidOnChainQuote(newOnChainQuote)) { revert Errors.InvalidQuote(); } mapping(bytes32 => bool) @@ -379,16 +379,8 @@ contract QuoteHandler is IQuoteHandler { } function _isValidOnChainQuote( - address lenderVault, DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) internal view returns (bool) { - if ( - quotePolicyManagerForVault[lenderVault] != address(0) && - !IQuotePolicyManager(quotePolicyManagerForVault[lenderVault]) - .checkNewOnChainQuote(lenderVault, onChainQuote) - ) { - return false; - } if ( onChainQuote.generalQuoteInfo.collToken == onChainQuote.generalQuoteInfo.loanToken diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index b6db5cbb..e8088e68 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -12,9 +12,4 @@ interface IQuotePolicyManager { DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bool onChainQuote ) external view returns (bool isValid); - - function checkNewOnChainQuote( - address lenderVault, - DataTypesPeerToPeer.OnChainQuote calldata onChainQuote - ) external view returns (bool isValid); } diff --git a/contracts/test/TestQuotePolicyManager.sol b/contracts/test/TestQuotePolicyManager.sol index 42247cf6..1dc30cf9 100644 --- a/contracts/test/TestQuotePolicyManager.sol +++ b/contracts/test/TestQuotePolicyManager.sol @@ -24,11 +24,4 @@ contract TestQuotePolicyManager is IQuotePolicyManager { ) external view returns (bool isValid) { isValid = allow[lenderVault]; } - - function checkNewOnChainQuote( - address lenderVault, - DataTypesPeerToPeer.OnChainQuote calldata - ) external view returns (bool isValid) { - isValid = allow[lenderVault]; - } } diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 544e5da7..15b7ac6a 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -5865,22 +5865,11 @@ describe('Peer-to-Peer: Local Tests', function () { .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') .withArgs(lenderVault.address, testQuotePolicyManager.address) - // should revert since policy is set to not allow - await expect( - quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1) - ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') - - // set policy to allow - await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, true) - await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1)).to.emit( quoteHandler, 'OnChainQuoteAdded' ) - // set borrow policy to not allowed - await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, false) - // borrower approves gateway and executes quote await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) @@ -5897,7 +5886,6 @@ describe('Peer-to-Peer: Local Tests', function () { mysoTokenManagerData: ZERO_BYTES32 } - // should revert since quote violates policy await expect( borrowerGateway .connect(borrower) From ed17ebd551f17f25068c1182a6b9ba1b2996e9b9 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 17:40:06 -0400 Subject: [PATCH 14/51] edit note --- contracts/peer-to-peer/AddressRegistry.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/peer-to-peer/AddressRegistry.sol b/contracts/peer-to-peer/AddressRegistry.sol index a4417039..6dd2400b 100644 --- a/contracts/peer-to-peer/AddressRegistry.sol +++ b/contracts/peer-to-peer/AddressRegistry.sol @@ -86,7 +86,7 @@ contract AddressRegistry is Initializable, Ownable2Step, IAddressRegistry { address _erc20Wrapper, address _mysoTokenManager ) = (erc721Wrapper, erc20Wrapper, mysoTokenManager); - // note (1/2): ERC721WRAPPER, ERC20WRAPPER, and MYSO_TOKEN_MANAGER state can only be "occupied" by + // note (1/2): ERC721WRAPPER, ERC20WRAPPER and MYSO_TOKEN_MANAGER state can only be "occupied" by // one addresses ("singleton state") if ( state == DataTypesPeerToPeer.WhitelistState.ERC721WRAPPER || From d510d3757aa174c3eed43b492436cd784156df44 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 26 Jul 2023 22:10:20 -0400 Subject: [PATCH 15/51] updated names, comments and removed ability to edit off chain functions --- contracts/peer-to-peer/LenderVaultImpl.sol | 20 +++++++++++----- contracts/peer-to-peer/QuoteHandler.sol | 6 ++--- .../interfaces/ILenderVaultImpl.sol | 19 +++++++-------- .../peer-to-peer/interfaces/IQuoteHandler.sol | 6 ++--- test/peer-to-peer/local-tests.ts | 24 +++++++++---------- 5 files changed, 41 insertions(+), 34 deletions(-) diff --git a/contracts/peer-to-peer/LenderVaultImpl.sol b/contracts/peer-to-peer/LenderVaultImpl.sol index 9d5ce211..d648bccf 100644 --- a/contracts/peer-to-peer/LenderVaultImpl.sol +++ b/contracts/peer-to-peer/LenderVaultImpl.sol @@ -42,7 +42,7 @@ contract LenderVaultImpl is address[] public signers; address public circuitBreaker; address public reverseCircuitBreaker; - address public approvedQuoteHandler; + address public delegateOnChainQuoting; uint256 public minNumOfSigners; mapping(address => bool) public isSigner; bool public withdrawEntered; @@ -367,14 +367,22 @@ contract LenderVaultImpl is ); } - function setApprovedQuoteHandler(address newQuoteHandler) external { + function setDelegateOnChainQuoting( + address newDelegateOnChainQuoting + ) external { _checkOwner(); - address oldQuoteHandler = approvedQuoteHandler; - if (newQuoteHandler == oldQuoteHandler || newQuoteHandler == owner()) { + address oldDelegateOnChainQuoting = delegateOnChainQuoting; + if ( + newDelegateOnChainQuoting == oldDelegateOnChainQuoting || + newDelegateOnChainQuoting == owner() + ) { revert Errors.InvalidAddress(); } - approvedQuoteHandler = newQuoteHandler; - emit ApprovedQuoteHandlerUpdated(newQuoteHandler, oldQuoteHandler); + delegateOnChainQuoting = newDelegateOnChainQuoting; + emit DelegateOnChainQuotingUpdated( + newDelegateOnChainQuoting, + oldDelegateOnChainQuoting + ); } function pauseQuotes() external { diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 8817762f..812d68cb 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -108,7 +108,7 @@ contract QuoteHandler is IQuoteHandler { } function incrementOffChainQuoteNonce(address lenderVault) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); uint256 newNonce = offChainQuoteNonce[lenderVault] + 1; offChainQuoteNonce[lenderVault] = newNonce; emit OffChainQuoteNonceIncremented(lenderVault, newNonce); @@ -118,7 +118,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 offChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); offChainQuoteIsInvalidated[lenderVault][offChainQuoteHash] = true; emit OffChainQuoteInvalidated(lenderVault, offChainQuoteHash); } @@ -490,7 +490,7 @@ contract QuoteHandler is IQuoteHandler { if ( ILenderVaultImpl(lenderVault).owner() != msg.sender && (onlyOwner || - ILenderVaultImpl(lenderVault).approvedQuoteHandler() != + ILenderVaultImpl(lenderVault).delegateOnChainQuoting() != msg.sender) ) { revert Errors.InvalidSender(); diff --git a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol index 15e6db8b..b3a43614 100644 --- a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol +++ b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol @@ -39,9 +39,9 @@ interface ILenderVaultImpl { address indexed oldReverseCircuitBreaker ); - event ApprovedQuoteHandlerUpdated( - address indexed newQuoteHandler, - address indexed oldQuoteHandler + event DelegateOnChainQuotingUpdated( + address indexed newDelegateOnChainQuoting, + address indexed oldDelegateOnChainQuoting ); /** @@ -189,12 +189,11 @@ interface ILenderVaultImpl { function setReverseCircuitBreaker(address reverseCircuitBreaker) external; /** - * @notice function to set an quote handler + * @notice function to set a delegate for on chain quoting * @dev the quote handler (and vault owner) can add, delete and update on chain quotes - * as well as invalidate off chain quotes or increment the off chain quote nonce - * @param quoteHandler address of the quote handler + * @param delegateOnChainQuoting address of the delegate */ - function setApprovedQuoteHandler(address quoteHandler) external; + function setDelegateOnChainQuoting(address delegateOnChainQuoting) external; /** * @notice function to pause all quotes from lendervault @@ -275,10 +274,10 @@ interface ILenderVaultImpl { function reverseCircuitBreaker() external view returns (address); /** - * @notice function to return address of the approved quote handler - * @return approved quote handler address + * @notice function to return address of the delegate for on chain quoting + * @return approved delegate address */ - function approvedQuoteHandler() external view returns (address); + function delegateOnChainQuoting() external view returns (address); /** * @notice function returns signer at given index diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index 377d71f2..bf6ebee8 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -47,7 +47,7 @@ interface IQuoteHandler { /** * @notice function adds on chain quote - * @dev function can only be called by vault owner + * @dev function can only be called by vault owner or delegated on chain quoter * @param lenderVault address of the vault adding quote * @param onChainQuote data for the onChain quote (See notes in DataTypesPeerToPeer.sol) */ @@ -58,7 +58,7 @@ interface IQuoteHandler { /** * @notice function updates on chain quote - * @dev function can only be called by vault owner + * @dev function can only be called by vault owner or delegated on chain quoter * @param lenderVault address of the vault updating quote * @param oldOnChainQuoteHash quote hash for the old onChain quote marked for deletion * @param newOnChainQuote data for the new onChain quote (See notes in DataTypesPeerToPeer.sol) @@ -71,7 +71,7 @@ interface IQuoteHandler { /** * @notice function deletes on chain quote - * @dev function can only be called by vault owner + * @dev function can only be called by vault owner or delegated on chain quoter * @param lenderVault address of the vault deleting * @param onChainQuoteHash quote hash for the onChain quote marked for deletion */ diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 15b7ac6a..986b4abf 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -172,7 +172,7 @@ describe('Peer-to-Peer: Local Tests', function () { }) async function setupTest() { - const [lender, signer, borrower, team, circuitBreaker, approvedQuoteHandler, whitelistAuthority, addr1, addr2, addr3] = + const [lender, signer, borrower, team, circuitBreaker, delegateOnChainQuoting, whitelistAuthority, addr1, addr2, addr3] = await ethers.getSigners() /* ************************************ */ /* DEPLOYMENT OF SYSTEM CONTRACTS START */ @@ -543,7 +543,7 @@ describe('Peer-to-Peer: Local Tests', function () { signer2: sortedAddrs[1], signer3: sortedAddrs[2], circuitBreaker, - approvedQuoteHandler, + delegateOnChainQuoting, usdc, weth, lenderVault, @@ -1122,7 +1122,7 @@ describe('Peer-to-Peer: Local Tests', function () { borrower, whitelistAuthority, circuitBreaker, - approvedQuoteHandler, + delegateOnChainQuoting, usdc, weth, lenderVault, @@ -1288,22 +1288,22 @@ describe('Peer-to-Peer: Local Tests', function () { ) // only owner can set approved quote handler - await expect(lenderVault.connect(borrower).setApprovedQuoteHandler(lender.address)).to.be.revertedWith( + await expect(lenderVault.connect(borrower).setDelegateOnChainQuoting(lender.address)).to.be.revertedWith( 'Ownable: caller is not the owner' ) // should revert when trying to set invalid quote handler address (owner address) - await expect(lenderVault.connect(lender).setApprovedQuoteHandler(lender.address)).to.be.revertedWithCustomError( + await expect(lenderVault.connect(lender).setDelegateOnChainQuoting(lender.address)).to.be.revertedWithCustomError( lenderVault, 'InvalidAddress' ) // set valid approved quote handler - await lenderVault.connect(lender).setApprovedQuoteHandler(approvedQuoteHandler.address) + await lenderVault.connect(lender).setDelegateOnChainQuoting(delegateOnChainQuoting.address) // should revert if new handler same as old one await expect( - lenderVault.connect(lender).setApprovedQuoteHandler(approvedQuoteHandler.address) + lenderVault.connect(lender).setDelegateOnChainQuoting(delegateOnChainQuoting.address) ).to.be.revertedWithCustomError(lenderVault, 'InvalidAddress') // set valid circuit breaker @@ -5783,7 +5783,7 @@ describe('Peer-to-Peer: Local Tests', function () { addressRegistry, borrowerGateway, lender, - approvedQuoteHandler, + delegateOnChainQuoting, borrower, team, usdc, @@ -5809,14 +5809,14 @@ describe('Peer-to-Peer: Local Tests', function () { .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') - await expect(lenderVault.connect(lender).setApprovedQuoteHandler(approvedQuoteHandler.address)) - .to.emit(lenderVault, 'ApprovedQuoteHandlerUpdated') - .withArgs(approvedQuoteHandler.address, ZERO_ADDRESS) + await expect(lenderVault.connect(lender).setDelegateOnChainQuoting(delegateOnChainQuoting.address)) + .to.emit(lenderVault, 'DelegateOnChainQuotingUpdated') + .withArgs(delegateOnChainQuoting.address, ZERO_ADDRESS) // should revert even if called by approved quote handler await expect( quoteHandler - .connect(approvedQuoteHandler) + .connect(delegateOnChainQuoting) .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') From c7c8380f473b36494d854bd2683b4eaefa5c1129 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Mon, 31 Jul 2023 22:13:17 -0400 Subject: [PATCH 16/51] added suggested edits --- contracts/peer-to-peer/LenderVaultImpl.sol | 21 ++++++----- contracts/peer-to-peer/QuoteHandler.sol | 37 +++++++++---------- .../interfaces/ILenderVaultImpl.sol | 12 +++--- .../peer-to-peer/interfaces/IQuoteHandler.sol | 12 +++--- .../interfaces/IQuotePolicyManager.sol | 6 +-- contracts/test/TestQuotePolicyManager.sol | 6 +-- test/peer-to-peer/local-tests.ts | 33 +++++++++-------- 7 files changed, 63 insertions(+), 64 deletions(-) diff --git a/contracts/peer-to-peer/LenderVaultImpl.sol b/contracts/peer-to-peer/LenderVaultImpl.sol index d648bccf..d5683b71 100644 --- a/contracts/peer-to-peer/LenderVaultImpl.sol +++ b/contracts/peer-to-peer/LenderVaultImpl.sol @@ -42,7 +42,7 @@ contract LenderVaultImpl is address[] public signers; address public circuitBreaker; address public reverseCircuitBreaker; - address public delegateOnChainQuoting; + address public onChainQuotingDelegate; uint256 public minNumOfSigners; mapping(address => bool) public isSigner; bool public withdrawEntered; @@ -367,21 +367,22 @@ contract LenderVaultImpl is ); } - function setDelegateOnChainQuoting( - address newDelegateOnChainQuoting + function setOnChainQuotingDelegate( + address newOnChainQuotingDelegate ) external { _checkOwner(); - address oldDelegateOnChainQuoting = delegateOnChainQuoting; + address oldOnChainQuotingDelegate = onChainQuotingDelegate; + // delegate is allowed to be a signer, unlike owner, circuit breaker or reverse circuit breaker if ( - newDelegateOnChainQuoting == oldDelegateOnChainQuoting || - newDelegateOnChainQuoting == owner() + newOnChainQuotingDelegate == oldOnChainQuotingDelegate || + newOnChainQuotingDelegate == owner() ) { revert Errors.InvalidAddress(); } - delegateOnChainQuoting = newDelegateOnChainQuoting; - emit DelegateOnChainQuotingUpdated( - newDelegateOnChainQuoting, - oldDelegateOnChainQuoting + onChainQuotingDelegate = newOnChainQuotingDelegate; + emit OnChainQuotingDelegateUpdated( + newOnChainQuotingDelegate, + oldOnChainQuotingDelegate ); } diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 812d68cb..3a6455cb 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -227,27 +227,26 @@ contract QuoteHandler is IQuoteHandler { function updateQuotePolicyManagerForVault( address lenderVault, - address newPolicyManagerAddress, - bool isRevoke + address newPolicyManagerAddress ) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); - if (isRevoke) { + if (newPolicyManagerAddress == address(0)) { delete quotePolicyManagerForVault[lenderVault]; } else { if ( IAddressRegistry(addressRegistry).whitelistState( newPolicyManagerAddress - ) != DataTypesPeerToPeer.WhitelistState.QUOTE_POLICY_MANAGER + ) != + DataTypesPeerToPeer.WhitelistState.QUOTE_POLICY_MANAGER || + newPolicyManagerAddress == + quotePolicyManagerForVault[lenderVault] ) { revert Errors.InvalidAddress(); } // note: this will overwrite any existing policy manager to a new valid quote policy manager quotePolicyManagerForVault[lenderVault] = newPolicyManagerAddress; } - emit QuotePolicyManagerUpdated( - lenderVault, - isRevoke ? address(0) : newPolicyManagerAddress - ); + emit QuotePolicyManagerUpdated(lenderVault, newPolicyManagerAddress); } function getOnChainQuoteHistory( @@ -328,21 +327,21 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, - bool onChainQuote + bool _isOnChainQuote ) internal view { if (msg.sender != IAddressRegistry(addressRegistry).borrowerGateway()) { revert Errors.InvalidSender(); } + address quotePolicyManager = quotePolicyManagerForVault[lenderVault]; if ( - quotePolicyManagerForVault[lenderVault] != address(0) && - !IQuotePolicyManager(quotePolicyManagerForVault[lenderVault]) - .checkPendingBorrowQuoteInfoAndTuple( - borrower, - lenderVault, - generalQuoteInfo, - quoteTuple, - onChainQuote - ) + quotePolicyManager != address(0) && + IQuotePolicyManager(quotePolicyManager).borrowViolatesPolicy( + borrower, + lenderVault, + generalQuoteInfo, + quoteTuple, + _isOnChainQuote + ) ) { revert Errors.QuoteViolatesPolicy(); } @@ -490,7 +489,7 @@ contract QuoteHandler is IQuoteHandler { if ( ILenderVaultImpl(lenderVault).owner() != msg.sender && (onlyOwner || - ILenderVaultImpl(lenderVault).delegateOnChainQuoting() != + ILenderVaultImpl(lenderVault).onChainQuotingDelegate() != msg.sender) ) { revert Errors.InvalidSender(); diff --git a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol index b3a43614..5ea60307 100644 --- a/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol +++ b/contracts/peer-to-peer/interfaces/ILenderVaultImpl.sol @@ -39,9 +39,9 @@ interface ILenderVaultImpl { address indexed oldReverseCircuitBreaker ); - event DelegateOnChainQuotingUpdated( - address indexed newDelegateOnChainQuoting, - address indexed oldDelegateOnChainQuoting + event OnChainQuotingDelegateUpdated( + address indexed newOnChainQuotingDelegate, + address indexed oldOnChainQuotingDelegate ); /** @@ -191,9 +191,9 @@ interface ILenderVaultImpl { /** * @notice function to set a delegate for on chain quoting * @dev the quote handler (and vault owner) can add, delete and update on chain quotes - * @param delegateOnChainQuoting address of the delegate + * @param onChainQuotingDelegate address of the delegate */ - function setDelegateOnChainQuoting(address delegateOnChainQuoting) external; + function setOnChainQuotingDelegate(address onChainQuotingDelegate) external; /** * @notice function to pause all quotes from lendervault @@ -277,7 +277,7 @@ interface ILenderVaultImpl { * @notice function to return address of the delegate for on chain quoting * @return approved delegate address */ - function delegateOnChainQuoting() external view returns (address); + function onChainQuotingDelegate() external view returns (address); /** * @notice function returns signer at given index diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index bf6ebee8..57c7b876 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -47,7 +47,7 @@ interface IQuoteHandler { /** * @notice function adds on chain quote - * @dev function can only be called by vault owner or delegated on chain quoter + * @dev function can only be called by vault owner or on chain quote delegate * @param lenderVault address of the vault adding quote * @param onChainQuote data for the onChain quote (See notes in DataTypesPeerToPeer.sol) */ @@ -58,7 +58,7 @@ interface IQuoteHandler { /** * @notice function updates on chain quote - * @dev function can only be called by vault owner or delegated on chain quoter + * @dev function can only be called by vault owner or on chain quote delegate * @param lenderVault address of the vault updating quote * @param oldOnChainQuoteHash quote hash for the old onChain quote marked for deletion * @param newOnChainQuote data for the new onChain quote (See notes in DataTypesPeerToPeer.sol) @@ -71,7 +71,7 @@ interface IQuoteHandler { /** * @notice function deletes on chain quote - * @dev function can only be called by vault owner or delegated on chain quoter + * @dev function can only be called by vault owner or on chain quote delegate * @param lenderVault address of the vault deleting * @param onChainQuoteHash quote hash for the onChain quote marked for deletion */ @@ -137,13 +137,11 @@ interface IQuoteHandler { * @notice function to update the quote policy manager for a vault * @param lenderVault address for which quote policy manager is being updated * @param newPolicyManagerAddress address of new quote policy manager - * @param isRevoke true if policy manager is being revoked, else set to current whitelisted policy manager in registry - * @dev function can only be called by vault owner, not even quoteHandler can update policy manager + * @dev function can only be called by vault owner */ function updateQuotePolicyManagerForVault( address lenderVault, - address newPolicyManagerAddress, - bool isRevoke + address newPolicyManagerAddress ) external; /** diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index e8088e68..03553f4b 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -5,11 +5,11 @@ pragma solidity 0.8.19; import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; interface IQuotePolicyManager { - function checkPendingBorrowQuoteInfoAndTuple( + function borrowViolatesPolicy( address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, - bool onChainQuote - ) external view returns (bool isValid); + bool _isOnChainQuote + ) external view returns (bool _borrowViolatesPolicy); } diff --git a/contracts/test/TestQuotePolicyManager.sol b/contracts/test/TestQuotePolicyManager.sol index 1dc30cf9..9ace6288 100644 --- a/contracts/test/TestQuotePolicyManager.sol +++ b/contracts/test/TestQuotePolicyManager.sol @@ -15,13 +15,13 @@ contract TestQuotePolicyManager is IQuotePolicyManager { allow[lenderVault] = _allow; } - function checkPendingBorrowQuoteInfoAndTuple( + function borrowViolatesPolicy( address, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata, DataTypesPeerToPeer.QuoteTuple calldata, bool - ) external view returns (bool isValid) { - isValid = allow[lenderVault]; + ) external view returns (bool _borrowViolatesPolicy) { + _borrowViolatesPolicy = !allow[lenderVault]; } } diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 986b4abf..7faab74c 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -1288,22 +1288,22 @@ describe('Peer-to-Peer: Local Tests', function () { ) // only owner can set approved quote handler - await expect(lenderVault.connect(borrower).setDelegateOnChainQuoting(lender.address)).to.be.revertedWith( + await expect(lenderVault.connect(borrower).setOnChainQuotingDelegate(lender.address)).to.be.revertedWith( 'Ownable: caller is not the owner' ) // should revert when trying to set invalid quote handler address (owner address) - await expect(lenderVault.connect(lender).setDelegateOnChainQuoting(lender.address)).to.be.revertedWithCustomError( + await expect(lenderVault.connect(lender).setOnChainQuotingDelegate(lender.address)).to.be.revertedWithCustomError( lenderVault, 'InvalidAddress' ) // set valid approved quote handler - await lenderVault.connect(lender).setDelegateOnChainQuoting(delegateOnChainQuoting.address) + await lenderVault.connect(lender).setOnChainQuotingDelegate(delegateOnChainQuoting.address) // should revert if new handler same as old one await expect( - lenderVault.connect(lender).setDelegateOnChainQuoting(delegateOnChainQuoting.address) + lenderVault.connect(lender).setOnChainQuotingDelegate(delegateOnChainQuoting.address) ).to.be.revertedWithCustomError(lenderVault, 'InvalidAddress') // set valid circuit breaker @@ -5799,30 +5799,28 @@ describe('Peer-to-Peer: Local Tests', function () { // should revert if unregistered vault await expect( - quoteHandler.connect(team).updateQuotePolicyManagerForVault(borrower.address, testQuotePolicyManager.address, false) + quoteHandler.connect(team).updateQuotePolicyManagerForVault(borrower.address, testQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') // should revert if not vault owner await expect( - quoteHandler - .connect(team) - .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) + quoteHandler.connect(team).updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') - await expect(lenderVault.connect(lender).setDelegateOnChainQuoting(delegateOnChainQuoting.address)) - .to.emit(lenderVault, 'DelegateOnChainQuotingUpdated') + await expect(lenderVault.connect(lender).setOnChainQuotingDelegate(delegateOnChainQuoting.address)) + .to.emit(lenderVault, 'OnChainQuotingDelegateUpdated') .withArgs(delegateOnChainQuoting.address, ZERO_ADDRESS) - // should revert even if called by approved quote handler + // should revert even if called by approved on chain delegate await expect( quoteHandler .connect(delegateOnChainQuoting) - .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) + .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') // should revert if not whitelisted quote policy manager await expect( - quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, quoteHandler.address, false) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, quoteHandler.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidAddress') // lenderVault owner deposits usdc @@ -5858,13 +5856,16 @@ describe('Peer-to-Peer: Local Tests', function () { // set policy manager address await expect( - quoteHandler - .connect(lender) - .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address, false) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) ) .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') .withArgs(lenderVault.address, testQuotePolicyManager.address) + // should revert if new address matches current policy manager + await expect( + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidAddress') + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1)).to.emit( quoteHandler, 'OnChainQuoteAdded' From 2cb1d01dbbf1c7d60b12137f8c0e2f2a3a154f62 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Tue, 1 Aug 2023 14:18:34 -0400 Subject: [PATCH 17/51] added proposal and approved quote functions --- contracts/Errors.sol | 2 + contracts/peer-to-peer/QuoteHandler.sol | 54 +++++++++++++++++++ .../peer-to-peer/interfaces/IQuoteHandler.sol | 43 +++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/contracts/Errors.sol b/contracts/Errors.sol index a5a319d4..4d730165 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -124,4 +124,6 @@ library Errors { error TokenDoesNotBelongInWrapper(address tokenAddr, uint256 tokenId); error InvalidMintAmount(); error QuoteViolatesPolicy(); + error RedundantOnChainQuoteProposed(); + error InvalidProposedQuoteApproval(); } diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 3a6455cb..1007b508 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -20,6 +20,7 @@ contract QuoteHandler is IQuoteHandler { mapping(address => mapping(bytes32 => bool)) public offChainQuoteIsInvalidated; mapping(address => mapping(bytes32 => bool)) public isOnChainQuote; + mapping(address => mapping(bytes32 => bool)) public isProposedOnChainQuote; mapping(address => address) public quotePolicyManagerForVault; mapping(address => DataTypesPeerToPeer.OnChainQuoteInfo[]) internal onChainQuoteHistory; @@ -107,6 +108,59 @@ contract QuoteHandler is IQuoteHandler { emit OnChainQuoteDeleted(lenderVault, onChainQuoteHash); } + function approveProposedOnChainQuote( + address lenderVault, + bytes32 onChainQuoteHash + ) external { + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); + mapping(bytes32 => bool) + storage isOnChainProposedForVault = isProposedOnChainQuote[ + lenderVault + ]; + mapping(bytes32 => bool) + storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; + if ( + !isOnChainProposedForVault[onChainQuoteHash] || + isOnChainQuoteFromVault[onChainQuoteHash] + ) { + revert Errors.InvalidProposedQuoteApproval(); + } + delete isOnChainProposedForVault[onChainQuoteHash]; + isOnChainQuoteFromVault[onChainQuoteHash] = true; + emit ProposedOnChainQuoteApproved(lenderVault, onChainQuoteHash); + } + + function proposeOnChainQuoteForVault( + address lenderVault, + DataTypesPeerToPeer.OnChainQuote calldata onChainQuote + ) external { + // caller can be any address, so only check vault is registered + if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { + revert Errors.UnregisteredVault(); + } + if (!_isValidOnChainQuote(onChainQuote)) { + revert Errors.InvalidQuote(); + } + mapping(bytes32 => bool) + storage isOnChainProposedForVault = isProposedOnChainQuote[ + lenderVault + ]; + bytes32 onChainQuoteHash = _hashOnChainQuote(onChainQuote); + if ( + isOnChainQuote[lenderVault][onChainQuoteHash] || + isOnChainProposedForVault[onChainQuoteHash] + ) { + revert Errors.RedundantOnChainQuoteProposed(); + } + isOnChainProposedForVault[onChainQuoteHash] = true; + emit OnChainQuoteProposed( + lenderVault, + onChainQuote, + onChainQuoteHash, + msg.sender + ); + } + function incrementOffChainQuoteNonce(address lenderVault) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); uint256 newNonce = offChainQuoteNonce[lenderVault] + 1; diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index 57c7b876..a81cc6be 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -44,6 +44,16 @@ interface IQuoteHandler { address indexed lenderVault, address indexed newPolicyManagerAddress ); + event OnChainQuoteProposed( + address indexed lenderVault, + DataTypesPeerToPeer.OnChainQuote onChainQuote, + bytes32 indexed onChainQuoteHash, + address indexed proposer + ); + event ProposedOnChainQuoteApproved( + address indexed lenderVault, + bytes32 indexed onChainQuoteHash + ); /** * @notice function adds on chain quote @@ -80,6 +90,28 @@ interface IQuoteHandler { bytes32 onChainQuoteHash ) external; + /** + * @notice function approves proposed on chain quote + * @dev function can only be called by vault owner or on chain quote delegate + * @param lenderVault address of the vault approving + * @param onChainQuoteHash quote hash for the onChain quote marked for approval + */ + function approveProposedOnChainQuote( + address lenderVault, + bytes32 onChainQuoteHash + ) external; + + /** + * @notice function proposes on chain quote for vault + * @dev function can be called by anyone + * @param lenderVault address of the vault adding quote + * @param onChainQuote data for the onChain quote (See notes in DataTypesPeerToPeer.sol) + */ + function proposeOnChainQuoteForVault( + address lenderVault, + DataTypesPeerToPeer.OnChainQuote calldata onChainQuote + ) external; + /** * @notice function increments the nonce for a vault * @dev function can only be called by vault owner @@ -179,6 +211,17 @@ interface IQuoteHandler { bytes32 hashToCheck ) external view returns (bool); + /** + * @notice function returns if hash is for an on chain quote that has been proposed + * @param lenderVault address of vault + * @param hashToCheck hash of the on chain quote + * @return true if hash belongs to a valid on-chain quote, else false + */ + function isProposedOnChainQuote( + address lenderVault, + bytes32 hashToCheck + ) external view returns (bool); + /** * @notice function returns the address of the policy manager for a vault * @param lenderVault address of vault From 14d08e47411db04b912aac772402d6b53fea6fc8 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 2 Aug 2023 12:48:16 -0400 Subject: [PATCH 18/51] added simple policy manager contract --- .../peer-to-peer/DataTypesPeerToPeer.sol | 20 ++ contracts/peer-to-peer/QuoteHandler.sol | 41 ++-- .../interfaces/IQuotePolicyManager.sol | 33 ++- .../policies/SimplePolicyManager.sol | 213 ++++++++++++++++++ contracts/test/TestQuotePolicyManager.sol | 21 +- 5 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 contracts/peer-to-peer/policies/SimplePolicyManager.sol diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index 32ce596d..bbd5888d 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -186,4 +186,24 @@ library DataTypesPeerToPeer { // can be used as quote policy manager contract QUOTE_POLICY_MANAGER } + + enum DefaultPolicyState { + // if no explicit policy set, then default to ALLOW + ALLOW_ALL, + // allow only on chain quotes when no policy + ALLOW_ONLY_ON_CHAIN_QUOTES, + // allow only off chain quotes when no policy + ALLOW_ONLY_OFF_CHAIN_QUOTES, + // only allow quotes when explicit policy is set + DISALLOW_ALL + } + + enum PolicyType { + // apply policy to both on chain and off chain quotes by default + ALL_QUOTES, + // apply policy only to on chain quotes + ONLY_ON_CHAIN_QUOTES, + // apply policy only to off chain quotes + ONLY_OFF_CHAIN_QUOTES + } } diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 1007b508..b7c1a778 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -219,7 +219,8 @@ contract QuoteHandler is IQuoteHandler { DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bytes32[] calldata proof ) external { - _checkSenderAndQuoteInfo( + // this returns 0 if no policy manager or policy is set + uint256 policyMinNumSigners = _checkSenderAndQuoteInfo( borrower, lenderVault, offChainQuote.generalQuoteInfo, @@ -244,6 +245,7 @@ contract QuoteHandler is IQuoteHandler { !_areValidSignatures( lenderVault, offChainQuoteHash, + policyMinNumSigners, offChainQuote.compactSigs ) ) { @@ -332,12 +334,16 @@ contract QuoteHandler is IQuoteHandler { function _areValidSignatures( address lenderVault, bytes32 offChainQuoteHash, + uint256 policyMinNumSigners, bytes[] calldata compactSigs ) internal view returns (bool) { uint256 compactSigsLength = compactSigs.length; - if ( - compactSigsLength < ILenderVaultImpl(lenderVault).minNumOfSigners() - ) { + // note: if policy min num signers returns 0, use the vault's min num signers + // this protects against case where for policy manager is set, but no policy is set for this pair + uint256 minNumSigners = policyMinNumSigners == 0 + ? ILenderVaultImpl(lenderVault).minNumOfSigners() + : policyMinNumSigners; + if (compactSigsLength < minNumSigners) { return false; } bytes32 messageHash = ECDSA.toEthSignedMessageHash(offChainQuoteHash); @@ -382,22 +388,25 @@ contract QuoteHandler is IQuoteHandler { DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bool _isOnChainQuote - ) internal view { + ) internal view returns (uint256 policyMinNumSigners) { if (msg.sender != IAddressRegistry(addressRegistry).borrowerGateway()) { revert Errors.InvalidSender(); } address quotePolicyManager = quotePolicyManagerForVault[lenderVault]; - if ( - quotePolicyManager != address(0) && - IQuotePolicyManager(quotePolicyManager).borrowViolatesPolicy( - borrower, - lenderVault, - generalQuoteInfo, - quoteTuple, - _isOnChainQuote - ) - ) { - revert Errors.QuoteViolatesPolicy(); + if (quotePolicyManager != address(0)) { + bool _violatesPolicy; + (_violatesPolicy, policyMinNumSigners) = IQuotePolicyManager( + quotePolicyManager + ).borrowViolatesPolicy( + borrower, + lenderVault, + generalQuoteInfo, + quoteTuple, + _isOnChainQuote + ); + if (_violatesPolicy) { + revert Errors.QuoteViolatesPolicy(); + } } _checkWhitelist( generalQuoteInfo.collToken, diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index 03553f4b..53c6c5f7 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -5,11 +5,42 @@ pragma solidity 0.8.19; import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; interface IQuotePolicyManager { + /** + * @notice Checks if a borrow violates the policy set by the lender + * this function should always return 0 for _minSignersForThisPolicy if the policy is not set + * @param borrower Address of the borrower + * @param lenderVault Address of the lender vault + * @param generalQuoteInfo General quote info (see DataTypesPeerToPeer.sol) + * @param quoteTuple Quote tuple (see DataTypesPeerToPeer.sol) + * @param _isOnChainQuote Flag to indicate if the quote is on-chain or off-chain + * @return _borrowViolatesPolicy Flag to indicate if the borrow violates the policy + * @return _minSignersForThisPolicy Minimum number of signers required for this policy (if off-chain quote) + */ function borrowViolatesPolicy( address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bool _isOnChainQuote - ) external view returns (bool _borrowViolatesPolicy); + ) + external + view + returns (bool _borrowViolatesPolicy, uint256 _minSignersForThisPolicy); + + /** + * @notice Gets the default policy state when no policy is set for a vault + * @param lenderVault Address of the lender vault + */ + function defaultRulesWhenNoPolicySet( + address lenderVault + ) + external + view + returns (DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState); + + /** + * @notice Gets the address registry + * @return Address of the address registry + */ + function addressRegistry() external view returns (address); } diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol new file mode 100644 index 00000000..243dcf9a --- /dev/null +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; +import {Errors} from "../../Errors.sol"; +import {IAddressRegistry} from "../interfaces/IAddressRegistry.sol"; +import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; +import {IQuotePolicyManager} from "../interfaces/IQuotePolicyManager.sol"; + +contract SimplePolicyManager is IQuotePolicyManager { + struct Policy { + // check if policy is set + bool isSet; + // requires oracle + bool requiresOracle; + // is policy for all quotes, on chain quotes only, or off chain quotes only + DataTypesPeerToPeer.PolicyType policyType; + // min signers for this policy if off chain quote + uint8 minNumSigners; + // min allowable tenor + uint40 minTenor; + // max allowable tenor + uint40 maxTenor; + // global min fee + uint64 minFee; + // global min apr + uint80 minAPR; + // min allowable LTV or loan per collateral amount + uint128 minAllowableLTVorLoanPerColl; + // max allowbale LTV or loan per collateral amount + uint128 maxAllowableLTVorLoanPerColl; + } + + mapping(address => DataTypesPeerToPeer.DefaultPolicyState) + public defaultRulesWhenNoPolicySet; + mapping(address => mapping(address => mapping(address => Policy))) + internal policies; + address public immutable addressRegistry; + + event PolicySet( + address indexed lenderVault, + address indexed collToken, + address indexed loanToken, + Policy policy + ); + + error PolicyNotSet(); + error InvalidTenors(); + error MinLTVGreaterThanMaxLTV(); + + constructor(address _addressRegistry) { + addressRegistry = _addressRegistry; + } + + function setPolicyForPair( + address lenderVault, + address collToken, + address loanToken, + Policy calldata policy + ) external { + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + _isValidPolicy(policy); + policies[lenderVault][collToken][loanToken] = policy; + emit PolicySet(lenderVault, collToken, loanToken, policy); + } + + function deletePolicyForPair( + address lenderVault, + address collToken, + address loanToken + ) external { + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + delete policies[lenderVault][collToken][loanToken]; + } + + function borrowViolatesPolicy( + address, + address lenderVault, + DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, + bool isOnChainQuote + ) + external + view + returns (bool _borrowViolatesPolicy, uint256 _minSignersForThisPolicy) + { + Policy memory policy = policies[lenderVault][ + generalQuoteInfo.collToken + ][generalQuoteInfo.loanToken]; + // this will only affect off chain quotes once returned to the quote handler + // @dev: quote handler will handle a return value of 0, so no need for special handling here + _minSignersForThisPolicy = policy.isSet && !isOnChainQuote + ? policy.minNumSigners + : 0; + bool doesPolicyApplyToThisQuote = policy.isSet + ? _doesPolicyApplyToThisQuote(policy.policyType, isOnChainQuote) + : false; + if (!doesPolicyApplyToThisQuote) { + _borrowViolatesPolicy = _checkDefaultPolicy(isOnChainQuote); + } else { + _borrowViolatesPolicy = _checkPolicy( + policy, + generalQuoteInfo, + quoteTuple + ); + } + } + + function _checkDefaultPolicy( + bool isOnChainQuote + ) internal view returns (bool _borrowViolatesPolicy) { + DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState = defaultRulesWhenNoPolicySet[ + msg.sender + ]; + if ( + defaultPolicyState == + DataTypesPeerToPeer.DefaultPolicyState.ALLOW_ALL + ) { + _borrowViolatesPolicy = false; + } else if ( + defaultPolicyState == + DataTypesPeerToPeer.DefaultPolicyState.DISALLOW_ALL + ) { + _borrowViolatesPolicy = true; + } else if ( + defaultPolicyState == + DataTypesPeerToPeer.DefaultPolicyState.ALLOW_ONLY_ON_CHAIN_QUOTES && + !isOnChainQuote + ) { + _borrowViolatesPolicy = true; + } else if ( + defaultPolicyState == + DataTypesPeerToPeer + .DefaultPolicyState + .ALLOW_ONLY_OFF_CHAIN_QUOTES && + isOnChainQuote + ) { + _borrowViolatesPolicy = true; + } + } + + function _checkIsRegisteredVaultAndSenderIsApproved( + address lenderVault + ) internal view { + if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { + revert Errors.UnregisteredVault(); + } + if (ILenderVaultImpl(lenderVault).owner() != msg.sender) { + revert Errors.InvalidSender(); + } + } + + function _checkPolicy( + Policy memory policy, + DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple + ) internal pure returns (bool _borrowViolatesPolicy) { + if ( + policy.requiresOracle && generalQuoteInfo.oracleAddr == address(0) + ) { + _borrowViolatesPolicy = true; + } else if ( + quoteTuple.tenor < policy.minTenor || + quoteTuple.tenor > policy.maxTenor + ) { + _borrowViolatesPolicy = true; + } else if ( + quoteTuple.loanPerCollUnitOrLtv < + policy.minAllowableLTVorLoanPerColl || + quoteTuple.loanPerCollUnitOrLtv > + policy.maxAllowableLTVorLoanPerColl + ) { + _borrowViolatesPolicy = true; + } else if ( + SafeCast.toUint256(quoteTuple.interestRatePctInBase) < policy.minAPR + ) { + _borrowViolatesPolicy = true; + } else if (quoteTuple.upfrontFeePctInBase < policy.minFee) { + _borrowViolatesPolicy = true; + } + } + + function _doesPolicyApplyToThisQuote( + DataTypesPeerToPeer.PolicyType policyType, + bool isOnChainQuote + ) internal pure returns (bool) { + return + policyType == DataTypesPeerToPeer.PolicyType.ALL_QUOTES || + (policyType == + DataTypesPeerToPeer.PolicyType.ONLY_ON_CHAIN_QUOTES && + isOnChainQuote) || + (policyType == + DataTypesPeerToPeer.PolicyType.ONLY_OFF_CHAIN_QUOTES && + !isOnChainQuote); + } + + function _isValidPolicy(Policy calldata policy) internal pure { + if (!policy.isSet) { + revert PolicyNotSet(); + } + if (policy.minTenor > policy.maxTenor) { + revert InvalidTenors(); + } + if ( + policy.minAllowableLTVorLoanPerColl > + policy.maxAllowableLTVorLoanPerColl + ) { + revert MinLTVGreaterThanMaxLTV(); + } + } +} diff --git a/contracts/test/TestQuotePolicyManager.sol b/contracts/test/TestQuotePolicyManager.sol index 9ace6288..611f2c5d 100644 --- a/contracts/test/TestQuotePolicyManager.sol +++ b/contracts/test/TestQuotePolicyManager.sol @@ -21,7 +21,26 @@ contract TestQuotePolicyManager is IQuotePolicyManager { DataTypesPeerToPeer.GeneralQuoteInfo calldata, DataTypesPeerToPeer.QuoteTuple calldata, bool - ) external view returns (bool _borrowViolatesPolicy) { + ) + external + view + returns (bool _borrowViolatesPolicy, uint256 _minSignersForThisPolicy) + { _borrowViolatesPolicy = !allow[lenderVault]; + _minSignersForThisPolicy = 0; + } + + function defaultRulesWhenNoPolicySet( + address + ) + external + pure + returns (DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState) + { + defaultPolicyState = DataTypesPeerToPeer.DefaultPolicyState.ALLOW_ALL; + } + + function addressRegistry() external pure returns (address) { + return address(0); } } From fb6a283ffa9ba4570b4373f6ef7688d251a09474 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 2 Aug 2023 16:03:17 -0400 Subject: [PATCH 19/51] added note --- contracts/peer-to-peer/policies/SimplePolicyManager.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index 243dcf9a..98424f9a 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -152,6 +152,8 @@ contract SimplePolicyManager is IQuotePolicyManager { } } + // note: off chain quotes are allowed to leave _minNumSignersForThisPolicy as 0 + // since the quote handler will just always use the vault min num signers in that case function _checkPolicy( Policy memory policy, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, From 2135bf06049c858f96b1343ec1f51ca3d6bf6fba Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 2 Aug 2023 16:07:49 -0400 Subject: [PATCH 20/51] added another note --- contracts/peer-to-peer/policies/SimplePolicyManager.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index 98424f9a..58e0b878 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -18,6 +18,10 @@ contract SimplePolicyManager is IQuotePolicyManager { // is policy for all quotes, on chain quotes only, or off chain quotes only DataTypesPeerToPeer.PolicyType policyType; // min signers for this policy if off chain quote + // if 0, then quote handler will use vault min num signers + // if > 0, then quote handler will use this value + // this is convenient for automated quotes or RFQs. e.g., lender only wants 1 key + // for quotes covered by policy, but vault requires more signers for pairs without policies uint8 minNumSigners; // min allowable tenor uint40 minTenor; From 5543f608bdccb4b85caba8e43261c5cb5f4c77d7 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 2 Aug 2023 16:20:33 -0400 Subject: [PATCH 21/51] add setter for default policy --- contracts/peer-to-peer/DataTypesPeerToPeer.sol | 2 +- contracts/peer-to-peer/QuoteHandler.sol | 1 + contracts/peer-to-peer/policies/SimplePolicyManager.sol | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index bbd5888d..afdd1334 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -188,7 +188,7 @@ library DataTypesPeerToPeer { } enum DefaultPolicyState { - // if no explicit policy set, then default to ALLOW + // if no explicit policy set, then default to allow all quotes ALLOW_ALL, // allow only on chain quotes when no policy ALLOW_ONLY_ON_CHAIN_QUOTES, diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index b7c1a778..c4f1a33d 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -186,6 +186,7 @@ contract QuoteHandler is IQuoteHandler { if (quoteTupleIdx >= onChainQuote.quoteTuples.length) { revert Errors.InvalidArrayIndex(); } + // note: return value for minNumSigners not consumed for on-chain quotes _checkSenderAndQuoteInfo( borrower, lenderVault, diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index 58e0b878..57a3a9b1 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -79,6 +79,14 @@ contract SimplePolicyManager is IQuotePolicyManager { delete policies[lenderVault][collToken][loanToken]; } + function setDefaultPolicy( + address lenderVault, + DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState + ) external { + _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); + defaultRulesWhenNoPolicySet[lenderVault] = defaultPolicyState; + } + function borrowViolatesPolicy( address, address lenderVault, From b4ad70e97985b5a345f216950060c5a7f13324b7 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 2 Aug 2023 16:24:55 -0400 Subject: [PATCH 22/51] edited check when no policy applies --- contracts/peer-to-peer/policies/SimplePolicyManager.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index 57a3a9b1..27d38a47 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -110,7 +110,10 @@ contract SimplePolicyManager is IQuotePolicyManager { ? _doesPolicyApplyToThisQuote(policy.policyType, isOnChainQuote) : false; if (!doesPolicyApplyToThisQuote) { - _borrowViolatesPolicy = _checkDefaultPolicy(isOnChainQuote); + _borrowViolatesPolicy = _checkDefaultPolicy( + lenderVault, + isOnChainQuote + ); } else { _borrowViolatesPolicy = _checkPolicy( policy, @@ -121,10 +124,11 @@ contract SimplePolicyManager is IQuotePolicyManager { } function _checkDefaultPolicy( + address lenderVault, bool isOnChainQuote ) internal view returns (bool _borrowViolatesPolicy) { DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState = defaultRulesWhenNoPolicySet[ - msg.sender + lenderVault ]; if ( defaultPolicyState == From 06b7a6e3b5381e188ecd4d776647bec3e470f240 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 2 Aug 2023 16:33:32 -0400 Subject: [PATCH 23/51] renamed policy members to be more in line with other names --- .../policies/SimplePolicyManager.sol | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index 27d38a47..c3efc86f 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -31,10 +31,10 @@ contract SimplePolicyManager is IQuotePolicyManager { uint64 minFee; // global min apr uint80 minAPR; - // min allowable LTV or loan per collateral amount - uint128 minAllowableLTVorLoanPerColl; - // max allowbale LTV or loan per collateral amount - uint128 maxAllowableLTVorLoanPerColl; + // min allowable loan per collateral amount or LTV + uint128 minAllowableLoanPerCollUnitOrLtv; + // max allowbale loan per collateral amount or LTV + uint128 maxAllowableLoanPerCollUnitOrLtv; } mapping(address => DataTypesPeerToPeer.DefaultPolicyState) @@ -52,7 +52,7 @@ contract SimplePolicyManager is IQuotePolicyManager { error PolicyNotSet(); error InvalidTenors(); - error MinLTVGreaterThanMaxLTV(); + error InvalidLoanPerCollOrLTV(); constructor(address _addressRegistry) { addressRegistry = _addressRegistry; @@ -130,12 +130,8 @@ contract SimplePolicyManager is IQuotePolicyManager { DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState = defaultRulesWhenNoPolicySet[ lenderVault ]; + // check three cases where violations could occur, else by default no violation if ( - defaultPolicyState == - DataTypesPeerToPeer.DefaultPolicyState.ALLOW_ALL - ) { - _borrowViolatesPolicy = false; - } else if ( defaultPolicyState == DataTypesPeerToPeer.DefaultPolicyState.DISALLOW_ALL ) { @@ -186,9 +182,9 @@ contract SimplePolicyManager is IQuotePolicyManager { _borrowViolatesPolicy = true; } else if ( quoteTuple.loanPerCollUnitOrLtv < - policy.minAllowableLTVorLoanPerColl || + policy.minAllowableLoanPerCollUnitOrLtv || quoteTuple.loanPerCollUnitOrLtv > - policy.maxAllowableLTVorLoanPerColl + policy.maxAllowableLoanPerCollUnitOrLtv ) { _borrowViolatesPolicy = true; } else if ( @@ -222,10 +218,10 @@ contract SimplePolicyManager is IQuotePolicyManager { revert InvalidTenors(); } if ( - policy.minAllowableLTVorLoanPerColl > - policy.maxAllowableLTVorLoanPerColl + policy.minAllowableLoanPerCollUnitOrLtv > + policy.minAllowableLoanPerCollUnitOrLtv ) { - revert MinLTVGreaterThanMaxLTV(); + revert InvalidLoanPerCollOrLTV(); } } } From 16564bf380dd0ffc96c4fea338d9ae8461b60120 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Wed, 2 Aug 2023 18:10:31 -0400 Subject: [PATCH 24/51] added test for proposed and approved on chain quotes --- .../policies/SimplePolicyManager.sol | 18 +++- test/peer-to-peer/local-tests.ts | 102 ++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index c3efc86f..22f63c69 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -20,8 +20,9 @@ contract SimplePolicyManager is IQuotePolicyManager { // min signers for this policy if off chain quote // if 0, then quote handler will use vault min num signers // if > 0, then quote handler will use this value - // this is convenient for automated quotes or RFQs. e.g., lender only wants 1 key - // for quotes covered by policy, but vault requires more signers for pairs without policies + // this is convenient for automated quotes or RFQs or easy third party handling. + // e.g., lender only wants 1 key for quotes covered by policy, + // but vault requires more signers for pairs without policies. uint8 minNumSigners; // min allowable tenor uint40 minTenor; @@ -50,6 +51,17 @@ contract SimplePolicyManager is IQuotePolicyManager { Policy policy ); + event PolicyDeleted( + address indexed lenderVault, + address indexed collToken, + address indexed loanToken + ); + + event DefaultPolicySet( + address indexed lenderVault, + DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState + ); + error PolicyNotSet(); error InvalidTenors(); error InvalidLoanPerCollOrLTV(); @@ -77,6 +89,7 @@ contract SimplePolicyManager is IQuotePolicyManager { ) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); delete policies[lenderVault][collToken][loanToken]; + emit PolicyDeleted(lenderVault, collToken, loanToken); } function setDefaultPolicy( @@ -85,6 +98,7 @@ contract SimplePolicyManager is IQuotePolicyManager { ) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); defaultRulesWhenNoPolicySet[lenderVault] = defaultPolicyState; + emit DefaultPolicySet(lenderVault, defaultPolicyState); } function borrowViolatesPolicy( diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 7faab74c..b79764b6 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -3390,6 +3390,108 @@ describe('Peer-to-Peer: Local Tests', function () { quoteHandler.connect(lender).deleteOnChainQuote(lenderVault.address, quoteHashAndValidUntilArr[0].quoteHash) ).to.emit(quoteHandler, 'OnChainQuoteDeleted') }) + + it('Should process proposed on-chain quote correctly', async function () { + const { + addressRegistry, + borrowerGateway, + quoteHandler, + lender, + borrower, + whitelistAuthority, + usdc, + weth, + lenderVault, + testnetTokenManager + } = await setupTest() + + // lenderVault owner deposits usdc + await usdc.connect(lender).transfer(lenderVault.address, ONE_USDC.mul(100000)) + + // lenderVault owner gives quote + const blocknum = await ethers.provider.getBlockNumber() + const timestamp = (await ethers.provider.getBlock(blocknum)).timestamp + let quoteTuples = [ + { + loanPerCollUnitOrLtv: ONE_USDC.mul(1000), + interestRatePctInBase: BASE.mul(10).div(100), + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(90) + }, + { + loanPerCollUnitOrLtv: ONE_USDC.mul(1000), + interestRatePctInBase: BASE.mul(20).div(100), + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(180) + } + ] + let onChainQuote = { + generalQuoteInfo: { + collToken: weth.address, + loanToken: usdc.address, + oracleAddr: ZERO_ADDRESS, + minLoan: ONE_USDC.mul(1000), + maxLoan: MAX_UINT256, + validUntil: timestamp + 60, + earliestRepayTenor: ONE_DAY, + borrowerCompartmentImplementation: ZERO_ADDRESS, + isSingleUse: false, + whitelistAddr: whitelistAuthority.address, + isWhitelistAddrSingleBorrower: false + }, + quoteTuples: quoteTuples, + salt: ZERO_BYTES32 + } + + await expect( + quoteHandler.connect(lender).proposeOnChainQuoteForVault(borrower.address, onChainQuote) + ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') + + await expect( + quoteHandler + .connect(lender) + .proposeOnChainQuoteForVault(lenderVault.address, { + ...onChainQuote, + generalQuoteInfo: { ...onChainQuote.generalQuoteInfo, maxLoan: ethers.BigNumber.from(0) } + }) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') + + const proposedQuoteTransaction = await quoteHandler + .connect(borrower) + .proposeOnChainQuoteForVault(lenderVault.address, onChainQuote) + + const proposedQuoteReceipt = await proposedQuoteTransaction.wait() + + const proposeQuoteEvent = proposedQuoteReceipt.events?.find(x => { + return x.event === 'OnChainQuoteProposed' + }) + + const proposedOnChainQuoteHash = proposeQuoteEvent?.args?.onChainQuoteHash || ZERO_BYTES32 + + expect(await quoteHandler.isProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash)).to.be.true + + await expect( + quoteHandler.connect(borrower).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') + + await expect( + quoteHandler.connect(lender).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) + ).to.emit(quoteHandler, 'ProposedOnChainQuoteApproved') + + expect(await quoteHandler.isProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash)).to.be.false + + await expect( + quoteHandler.connect(lender).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidProposedQuoteApproval') + + await expect( + quoteHandler.connect(borrower).proposeOnChainQuoteForVault(lenderVault.address, onChainQuote) + ).to.be.revertedWithCustomError(quoteHandler, 'RedundantOnChainQuoteProposed') + + await expect( + quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote) + ).to.be.revertedWithCustomError(quoteHandler, 'OnChainQuoteAlreadyAdded') + }) }) describe('ERC721 Wrapper Testing', function () { From ea06363be24e0e2404b3381cbbbe72a6b88f4016 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Thu, 3 Aug 2023 12:44:52 -0400 Subject: [PATCH 25/51] added tests and edited internal function name --- contracts/peer-to-peer/QuoteHandler.sol | 16 +-- test/peer-to-peer/local-tests.ts | 128 ++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index c4f1a33d..43dd21b9 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -36,7 +36,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); + _checkIsVaultAndSenderIsApproved(lenderVault, false); if (!_isValidOnChainQuote(onChainQuote)) { revert Errors.InvalidQuote(); } @@ -62,7 +62,7 @@ contract QuoteHandler is IQuoteHandler { bytes32 oldOnChainQuoteHash, DataTypesPeerToPeer.OnChainQuote calldata newOnChainQuote ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); + _checkIsVaultAndSenderIsApproved(lenderVault, false); if (!_isValidOnChainQuote(newOnChainQuote)) { revert Errors.InvalidQuote(); } @@ -98,7 +98,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 onChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); + _checkIsVaultAndSenderIsApproved(lenderVault, false); mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; if (!isOnChainQuoteFromVault[onChainQuoteHash]) { @@ -112,7 +112,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 onChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, false); + _checkIsVaultAndSenderIsApproved(lenderVault, false); mapping(bytes32 => bool) storage isOnChainProposedForVault = isProposedOnChainQuote[ lenderVault @@ -162,7 +162,7 @@ contract QuoteHandler is IQuoteHandler { } function incrementOffChainQuoteNonce(address lenderVault) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); + _checkIsVaultAndSenderIsApproved(lenderVault, true); uint256 newNonce = offChainQuoteNonce[lenderVault] + 1; offChainQuoteNonce[lenderVault] = newNonce; emit OffChainQuoteNonceIncremented(lenderVault, newNonce); @@ -172,7 +172,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, bytes32 offChainQuoteHash ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); + _checkIsVaultAndSenderIsApproved(lenderVault, true); offChainQuoteIsInvalidated[lenderVault][offChainQuoteHash] = true; emit OffChainQuoteInvalidated(lenderVault, offChainQuoteHash); } @@ -286,7 +286,7 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, address newPolicyManagerAddress ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault, true); + _checkIsVaultAndSenderIsApproved(lenderVault, true); if (newPolicyManagerAddress == address(0)) { delete quotePolicyManagerForVault[lenderVault]; } else { @@ -543,7 +543,7 @@ contract QuoteHandler is IQuoteHandler { } } - function _checkIsRegisteredVaultAndSenderIsApproved( + function _checkIsVaultAndSenderIsApproved( address lenderVault, bool onlyOwner ) internal view { diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index b79764b6..6b5aa160 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -3448,12 +3448,10 @@ describe('Peer-to-Peer: Local Tests', function () { ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') await expect( - quoteHandler - .connect(lender) - .proposeOnChainQuoteForVault(lenderVault.address, { - ...onChainQuote, - generalQuoteInfo: { ...onChainQuote.generalQuoteInfo, maxLoan: ethers.BigNumber.from(0) } - }) + quoteHandler.connect(lender).proposeOnChainQuoteForVault(lenderVault.address, { + ...onChainQuote, + generalQuoteInfo: { ...onChainQuote.generalQuoteInfo, maxLoan: ethers.BigNumber.from(0) } + }) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') const proposedQuoteTransaction = await quoteHandler @@ -6002,5 +6000,123 @@ describe('Peer-to-Peer: Local Tests', function () { .connect(borrower) .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) }) + + it('Should handle simple policy implementation', async function () { + const { + quoteHandler, + addressRegistry, + borrowerGateway, + lender, + delegateOnChainQuoting, + borrower, + team, + usdc, + weth, + lenderVault + } = await setupTest() + + const SimplePolicyManager = await ethers.getContractFactory('SimplePolicyManager') + const simplePolicyManager = await SimplePolicyManager.connect(team).deploy(addressRegistry.address) + await simplePolicyManager.deployed() + + await addressRegistry.connect(team).setWhitelistState([simplePolicyManager.address], 10) + + // set policy manager address + await expect( + quoteHandler.connect(team).updateQuotePolicyManagerForVault(lenderVault.address, simplePolicyManager.address) + ) + .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') + .withArgs(lenderVault.address, simplePolicyManager.address) + + // lenderVault owner deposits usdc + await usdc.connect(lender).transfer(lenderVault.address, ONE_USDC.mul(100000)) + + // lender owner gives regular quote + const loanPerCollUnit = ONE_USDC.mul(1000) + let quoteTuples1 = [ + { + loanPerCollUnitOrLtv: loanPerCollUnit, + interestRatePctInBase: 0, + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(30) + } + ] + let onChainQuote1 = { + generalQuoteInfo: { + collToken: weth.address, + loanToken: usdc.address, + oracleAddr: ZERO_ADDRESS, + minLoan: 1, + maxLoan: MAX_UINT256, + validUntil: MAX_UINT256, + earliestRepayTenor: 0, + borrowerCompartmentImplementation: ZERO_ADDRESS, + isSingleUse: false, + whitelistAddr: ZERO_ADDRESS, + isWhitelistAddrSingleBorrower: false + }, + quoteTuples: quoteTuples1, + salt: ZERO_BYTES32 + } + + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1)).to.emit( + quoteHandler, + 'OnChainQuoteAdded' + ) + + const policy = { + isSet: true, + requiresOracle: false, + policyType: 0, + minNumSigners: 0, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(30), + minFee: 0, + minAPR: 0, + minAllowableLoanPerCollUnitOrLtv: BASE.div(10), + maxAllowableLoanPerCollUnitOrLtv: BASE.div(2) + } + + // should revert if unregistered vault + await expect( + simplePolicyManager.connect(team).setPolicyForPair(borrower.address, weth.address, usdc.address, policy) + ).to.be.revertedWithCustomError(simplePolicyManager, 'UnregisteredVault') + + // should revert if sender is not vault owner + await expect( + simplePolicyManager.connect(borrower).setPolicyForPair(lenderVault.address, weth.address, usdc.address, policy) + ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidSender') + + // should revert if sender is not whitelisted + + // borrower approves gateway and executes quote + await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) + + const collSendAmount1 = ONE_WETH + const quoteTupleIdx1 = 0 + const borrowInstructions1 = { + collSendAmount: collSendAmount1, + expectedProtocolAndVaultTransferFee: 0, + expectedCompartmentTransferFee: 0, + deadline: MAX_UINT256, + minLoanAmount: 0, + callbackAddr: ZERO_ADDRESS, + callbackData: ZERO_BYTES32, + mysoTokenManagerData: ZERO_BYTES32 + } + + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // set policy to allow + await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, true) + + await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + }) }) }) From 1572ba95f2ac12e6378a58a639a6ce51a39bc626 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Thu, 3 Aug 2023 13:10:58 -0400 Subject: [PATCH 26/51] edited simple policy test --- .../policies/SimplePolicyManager.sol | 2 +- test/peer-to-peer/local-tests.ts | 80 +++++++++++++++++-- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index 22f63c69..8c24979a 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -233,7 +233,7 @@ contract SimplePolicyManager is IQuotePolicyManager { } if ( policy.minAllowableLoanPerCollUnitOrLtv > - policy.minAllowableLoanPerCollUnitOrLtv + policy.maxAllowableLoanPerCollUnitOrLtv ) { revert InvalidLoanPerCollOrLTV(); } diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 6b5aa160..9b493dcd 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -6021,9 +6021,13 @@ describe('Peer-to-Peer: Local Tests', function () { await addressRegistry.connect(team).setWhitelistState([simplePolicyManager.address], 10) + const reth = '0xae78736Cd615f374D3085123A210448E74Fc6393' + + await addressRegistry.connect(team).setWhitelistState([reth], 1) + // set policy manager address await expect( - quoteHandler.connect(team).updateQuotePolicyManagerForVault(lenderVault.address, simplePolicyManager.address) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, simplePolicyManager.address) ) .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') .withArgs(lenderVault.address, simplePolicyManager.address) @@ -6070,7 +6074,7 @@ describe('Peer-to-Peer: Local Tests', function () { policyType: 0, minNumSigners: 0, minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(30), + maxTenor: ONE_DAY.mul(25), minFee: 0, minAPR: 0, minAllowableLoanPerCollUnitOrLtv: BASE.div(10), @@ -6087,7 +6091,16 @@ describe('Peer-to-Peer: Local Tests', function () { simplePolicyManager.connect(borrower).setPolicyForPair(lenderVault.address, weth.address, usdc.address, policy) ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidSender') - // should revert if sender is not whitelisted + // should revert if unregistered vault + await expect(simplePolicyManager.connect(team).setDefaultPolicy(borrower.address, 1)).to.be.revertedWithCustomError( + simplePolicyManager, + 'UnregisteredVault' + ) + + // should revert if sender is not vault owner + await expect( + simplePolicyManager.connect(borrower).setDefaultPolicy(lenderVault.address, 1) + ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidSender') // borrower approves gateway and executes quote await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) @@ -6105,18 +6118,75 @@ describe('Peer-to-Peer: Local Tests', function () { mysoTokenManagerData: ZERO_BYTES32 } + /*** Test default policy ***/ + + // before default policy or policy is set this should go through as default policy is by default, allow all + await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + + // set default policy to only allow off-chain quotes + await simplePolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 2) + await expect( borrowerGateway .connect(borrower) .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - // set policy to allow - await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, true) + // set policy to allow no quotes without a policy + await simplePolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 3) + + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // set policy to allow on chain quotes without a policy and borrow should go through again + await simplePolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 1) await borrowerGateway .connect(borrower) .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + + /*** Test specific policy ***/ + const policy2 = { + isSet: true, + requiresOracle: false, + policyType: 0, + minNumSigners: 0, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(30), + minFee: 0, + minAPR: 0, + minAllowableLoanPerCollUnitOrLtv: BASE.div(10), + maxAllowableLoanPerCollUnitOrLtv: BASE.div(2) + } + + // should default if not set + await expect( + simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, isSet: false }) + ).to.be.revertedWithCustomError(simplePolicyManager, 'PolicyNotSet') + + // should default if min tenor < max tenor + await expect( + simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, maxTenor: ONE_DAY.div(2) }) + ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidTenors') + + // should default if min ltv > max ltv + await expect( + simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) + }) + ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidLoanPerCollOrLTV') }) }) }) From f19260f29acb8d50e6446c23342a659e2e2a42c1 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Thu, 3 Aug 2023 14:14:39 -0400 Subject: [PATCH 27/51] added more tests for policy --- .../policies/SimplePolicyManager.sol | 2 +- test/peer-to-peer/local-tests.ts | 154 +++++++++++++++++- 2 files changed, 146 insertions(+), 10 deletions(-) diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimplePolicyManager.sol index 8c24979a..7e3576fa 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimplePolicyManager.sol @@ -41,7 +41,7 @@ contract SimplePolicyManager is IQuotePolicyManager { mapping(address => DataTypesPeerToPeer.DefaultPolicyState) public defaultRulesWhenNoPolicySet; mapping(address => mapping(address => mapping(address => Policy))) - internal policies; + public policies; address public immutable addressRegistry; event PolicySet( diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 9b493dcd..9bc60d48 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -6040,8 +6040,8 @@ describe('Peer-to-Peer: Local Tests', function () { let quoteTuples1 = [ { loanPerCollUnitOrLtv: loanPerCollUnit, - interestRatePctInBase: 0, - upfrontFeePctInBase: 0, + interestRatePctInBase: BASE.div(10), + upfrontFeePctInBase: BASE.div(10), tenor: ONE_DAY.mul(30) } ] @@ -6156,8 +6156,8 @@ describe('Peer-to-Peer: Local Tests', function () { requiresOracle: false, policyType: 0, minNumSigners: 0, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(30), + minTenor: ONE_DAY.mul(35), + maxTenor: ONE_DAY.mul(35), minFee: 0, minAPR: 0, minAllowableLoanPerCollUnitOrLtv: BASE.div(10), @@ -6179,14 +6179,150 @@ describe('Peer-to-Peer: Local Tests', function () { ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidTenors') // should default if min ltv > max ltv + await expect( + simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) + }) + ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidLoanPerCollOrLTV') + + // policy set correctly await expect( simplePolicyManager .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) - }) - ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidLoanPerCollOrLTV') + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: true }) + ).to.emit(simplePolicyManager, 'PolicySet') + + // borrow should fail without oracle address + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: false }) + + // borrow fail if tenor less than min tenor + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(25) + }) + + // borrow fail if tenor greater than max tenor + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minFee: BASE.div(5) + }) + + // borrow should fail if upfront fee is below min fee + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAPR: BASE.div(5) + }) + + // borrow should fail if apr is below min apr + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAllowableLoanPerCollUnitOrLtv: BASE.mul(4).div(5), + maxAllowableLoanPerCollUnitOrLtv: BASE.mul(9).div(10) + }) + + // borrow should fail if loan per coll unit is below min allowable loan per coll unit + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAllowableLoanPerCollUnitOrLtv: ONE_USDC, + maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(800) + }) + + // borrow should fail if loan per coll unit is above max allowable loan per coll unit + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await simplePolicyManager + .connect(lender) + .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAllowableLoanPerCollUnitOrLtv: ONE_USDC, + maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(1000) + }) + + // borrow should go through as all conditions are met + await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + + const currPolicyPreDelete = await simplePolicyManager.policies(lenderVault.address, weth.address, usdc.address) + expect(currPolicyPreDelete.isSet).to.be.true + + await expect( + simplePolicyManager.connect(lender).deletePolicyForPair(borrower.address, weth.address, usdc.address) + ).to.be.revertedWithCustomError(simplePolicyManager, 'UnregisteredVault') + + await expect( + simplePolicyManager.connect(team).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidSender') + + await simplePolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + + const currPolicyPostDelete = await simplePolicyManager.policies(lenderVault.address, weth.address, usdc.address) + + expect(currPolicyPostDelete.isSet).to.be.false }) }) }) From 8b0afeb85c1977b00574772dbdbcdb3d0b02592d Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Thu, 3 Aug 2023 23:19:46 -0400 Subject: [PATCH 28/51] edited propose functions, moved events, structs and errors into other places, edited tests --- contracts/Errors.sol | 3 + .../peer-to-peer/DataTypesPeerToPeer.sol | 28 +++ contracts/peer-to-peer/QuoteHandler.sol | 34 +-- .../peer-to-peer/interfaces/IQuoteHandler.sol | 11 +- .../interfaces/IQuotePolicyManager.sol | 34 +++ ...nager.sol => SimpleQuotePolicyManager.sol} | 83 ++----- .../interfaces/ISimpleQuotePolicyManager.sol | 28 +++ contracts/test/TestQuotePolicyManager.sol | 46 ---- test/peer-to-peer/local-tests.ts | 205 ++++++------------ 9 files changed, 191 insertions(+), 281 deletions(-) rename contracts/peer-to-peer/policies/{SimplePolicyManager.sol => SimpleQuotePolicyManager.sol} (74%) create mode 100644 contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol delete mode 100644 contracts/test/TestQuotePolicyManager.sol diff --git a/contracts/Errors.sol b/contracts/Errors.sol index 4d730165..7c044f5f 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -126,4 +126,7 @@ library Errors { error QuoteViolatesPolicy(); error RedundantOnChainQuoteProposed(); error InvalidProposedQuoteApproval(); + error PolicyNotSet(); + error InvalidTenors(); + error InvalidLoanPerCollOrLTV(); } diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index afdd1334..03c59236 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -160,6 +160,34 @@ library DataTypesPeerToPeer { uint256 validUntil; } + struct SimplePolicy { + // check if policy is set + bool isSet; + // requires oracle + bool requiresOracle; + // is policy for all quotes, on chain quotes only, or off chain quotes only + DataTypesPeerToPeer.PolicyType policyType; + // min signers for this policy if off chain quote + // if 0, then quote handler will use vault min num signers + // if > 0, then quote handler will use this value + // this is convenient for automated quotes or RFQs or easy third party handling. + // e.g., lender only wants 1 key for quotes covered by policy, + // but vault requires more signers for pairs without policies. + uint8 minNumSigners; + // min allowable tenor + uint40 minTenor; + // max allowable tenor + uint40 maxTenor; + // global min fee + uint64 minFee; + // global min apr + uint80 minAPR; + // min allowable loan per collateral amount or LTV + uint128 minAllowableLoanPerCollUnitOrLtv; + // max allowbale loan per collateral amount or LTV + uint128 maxAllowableLoanPerCollUnitOrLtv; + } + enum WhitelistState { // not whitelisted NOT_WHITELISTED, diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 43dd21b9..e4aedc5b 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -20,7 +20,7 @@ contract QuoteHandler is IQuoteHandler { mapping(address => mapping(bytes32 => bool)) public offChainQuoteIsInvalidated; mapping(address => mapping(bytes32 => bool)) public isOnChainQuote; - mapping(address => mapping(bytes32 => bool)) public isProposedOnChainQuote; + mapping(bytes32 => bool) public isProposedOnChainQuote; mapping(address => address) public quotePolicyManagerForVault; mapping(address => DataTypesPeerToPeer.OnChainQuoteInfo[]) internal onChainQuoteHistory; @@ -113,52 +113,30 @@ contract QuoteHandler is IQuoteHandler { bytes32 onChainQuoteHash ) external { _checkIsVaultAndSenderIsApproved(lenderVault, false); - mapping(bytes32 => bool) - storage isOnChainProposedForVault = isProposedOnChainQuote[ - lenderVault - ]; mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; if ( - !isOnChainProposedForVault[onChainQuoteHash] || + !isProposedOnChainQuote[onChainQuoteHash] || isOnChainQuoteFromVault[onChainQuoteHash] ) { revert Errors.InvalidProposedQuoteApproval(); } - delete isOnChainProposedForVault[onChainQuoteHash]; isOnChainQuoteFromVault[onChainQuoteHash] = true; emit ProposedOnChainQuoteApproved(lenderVault, onChainQuoteHash); } - function proposeOnChainQuoteForVault( - address lenderVault, + function proposeOnChainQuote( DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external { - // caller can be any address, so only check vault is registered - if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { - revert Errors.UnregisteredVault(); - } if (!_isValidOnChainQuote(onChainQuote)) { revert Errors.InvalidQuote(); } - mapping(bytes32 => bool) - storage isOnChainProposedForVault = isProposedOnChainQuote[ - lenderVault - ]; bytes32 onChainQuoteHash = _hashOnChainQuote(onChainQuote); - if ( - isOnChainQuote[lenderVault][onChainQuoteHash] || - isOnChainProposedForVault[onChainQuoteHash] - ) { + if (isProposedOnChainQuote[onChainQuoteHash]) { revert Errors.RedundantOnChainQuoteProposed(); } - isOnChainProposedForVault[onChainQuoteHash] = true; - emit OnChainQuoteProposed( - lenderVault, - onChainQuote, - onChainQuoteHash, - msg.sender - ); + isProposedOnChainQuote[onChainQuoteHash] = true; + emit OnChainQuoteProposed(onChainQuote, onChainQuoteHash, msg.sender); } function incrementOffChainQuoteNonce(address lenderVault) external { diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index a81cc6be..4ec227cc 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -45,7 +45,6 @@ interface IQuoteHandler { address indexed newPolicyManagerAddress ); event OnChainQuoteProposed( - address indexed lenderVault, DataTypesPeerToPeer.OnChainQuote onChainQuote, bytes32 indexed onChainQuoteHash, address indexed proposer @@ -102,13 +101,11 @@ interface IQuoteHandler { ) external; /** - * @notice function proposes on chain quote for vault - * @dev function can be called by anyone - * @param lenderVault address of the vault adding quote + * @notice function proposes on chain quote + * @dev function can be called by anyone and used by any vault * @param onChainQuote data for the onChain quote (See notes in DataTypesPeerToPeer.sol) */ - function proposeOnChainQuoteForVault( - address lenderVault, + function proposeOnChainQuote( DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external; @@ -213,12 +210,10 @@ interface IQuoteHandler { /** * @notice function returns if hash is for an on chain quote that has been proposed - * @param lenderVault address of vault * @param hashToCheck hash of the on chain quote * @return true if hash belongs to a valid on-chain quote, else false */ function isProposedOnChainQuote( - address lenderVault, bytes32 hashToCheck ) external view returns (bool); diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index 53c6c5f7..6c4d085a 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -5,6 +5,40 @@ pragma solidity 0.8.19; import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; interface IQuotePolicyManager { + event PolicyDeleted( + address indexed lenderVault, + address indexed collToken, + address indexed loanToken + ); + + event DefaultPolicySet( + address indexed lenderVault, + DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState + ); + + /** + * @notice deletes the policy for a pair of tokens + * @param lenderVault Address of the lender vault + * @param collToken Address of the collateral token + * @param loanToken Address of the loan token + */ + function deletePolicyForPair( + address lenderVault, + address collToken, + address loanToken + ) external; + + /** + * @notice sets the default policy for a vault + * this state will control access whenever a policy is not set for a pair of tokens + * @param lenderVault Address of the lender vault + * @param defaultPolicyState Default policy state to be set + */ + function setDefaultPolicy( + address lenderVault, + DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState + ) external; + /** * @notice Checks if a borrow violates the policy set by the lender * this function should always return 0 for _minSignersForThisPolicy if the policy is not set diff --git a/contracts/peer-to-peer/policies/SimplePolicyManager.sol b/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol similarity index 74% rename from contracts/peer-to-peer/policies/SimplePolicyManager.sol rename to contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol index 7e3576fa..90904d1d 100644 --- a/contracts/peer-to-peer/policies/SimplePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol @@ -8,64 +8,18 @@ import {Errors} from "../../Errors.sol"; import {IAddressRegistry} from "../interfaces/IAddressRegistry.sol"; import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; import {IQuotePolicyManager} from "../interfaces/IQuotePolicyManager.sol"; +import {ISimpleQuotePolicyManager} from "./interfaces/ISimpleQuotePolicyManager.sol"; -contract SimplePolicyManager is IQuotePolicyManager { - struct Policy { - // check if policy is set - bool isSet; - // requires oracle - bool requiresOracle; - // is policy for all quotes, on chain quotes only, or off chain quotes only - DataTypesPeerToPeer.PolicyType policyType; - // min signers for this policy if off chain quote - // if 0, then quote handler will use vault min num signers - // if > 0, then quote handler will use this value - // this is convenient for automated quotes or RFQs or easy third party handling. - // e.g., lender only wants 1 key for quotes covered by policy, - // but vault requires more signers for pairs without policies. - uint8 minNumSigners; - // min allowable tenor - uint40 minTenor; - // max allowable tenor - uint40 maxTenor; - // global min fee - uint64 minFee; - // global min apr - uint80 minAPR; - // min allowable loan per collateral amount or LTV - uint128 minAllowableLoanPerCollUnitOrLtv; - // max allowbale loan per collateral amount or LTV - uint128 maxAllowableLoanPerCollUnitOrLtv; - } - +contract SimpleQuotePolicyManager is + IQuotePolicyManager, + ISimpleQuotePolicyManager +{ mapping(address => DataTypesPeerToPeer.DefaultPolicyState) public defaultRulesWhenNoPolicySet; - mapping(address => mapping(address => mapping(address => Policy))) + mapping(address => mapping(address => mapping(address => DataTypesPeerToPeer.SimplePolicy))) public policies; address public immutable addressRegistry; - event PolicySet( - address indexed lenderVault, - address indexed collToken, - address indexed loanToken, - Policy policy - ); - - event PolicyDeleted( - address indexed lenderVault, - address indexed collToken, - address indexed loanToken - ); - - event DefaultPolicySet( - address indexed lenderVault, - DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState - ); - - error PolicyNotSet(); - error InvalidTenors(); - error InvalidLoanPerCollOrLTV(); - constructor(address _addressRegistry) { addressRegistry = _addressRegistry; } @@ -74,7 +28,7 @@ contract SimplePolicyManager is IQuotePolicyManager { address lenderVault, address collToken, address loanToken, - Policy calldata policy + DataTypesPeerToPeer.SimplePolicy calldata policy ) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); _isValidPolicy(policy); @@ -88,7 +42,14 @@ contract SimplePolicyManager is IQuotePolicyManager { address loanToken ) external { _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); - delete policies[lenderVault][collToken][loanToken]; + mapping(address => DataTypesPeerToPeer.SimplePolicy) + storage policiesForVaultAndCollToken = policies[lenderVault][ + collToken + ]; + if (!policiesForVaultAndCollToken[loanToken].isSet) { + revert Errors.PolicyNotSet(); + } + delete policiesForVaultAndCollToken[loanToken]; emit PolicyDeleted(lenderVault, collToken, loanToken); } @@ -112,7 +73,7 @@ contract SimplePolicyManager is IQuotePolicyManager { view returns (bool _borrowViolatesPolicy, uint256 _minSignersForThisPolicy) { - Policy memory policy = policies[lenderVault][ + DataTypesPeerToPeer.SimplePolicy memory policy = policies[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; // this will only affect off chain quotes once returned to the quote handler @@ -181,7 +142,7 @@ contract SimplePolicyManager is IQuotePolicyManager { // note: off chain quotes are allowed to leave _minNumSignersForThisPolicy as 0 // since the quote handler will just always use the vault min num signers in that case function _checkPolicy( - Policy memory policy, + DataTypesPeerToPeer.SimplePolicy memory policy, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple ) internal pure returns (bool _borrowViolatesPolicy) { @@ -224,18 +185,20 @@ contract SimplePolicyManager is IQuotePolicyManager { !isOnChainQuote); } - function _isValidPolicy(Policy calldata policy) internal pure { + function _isValidPolicy( + DataTypesPeerToPeer.SimplePolicy calldata policy + ) internal pure { if (!policy.isSet) { - revert PolicyNotSet(); + revert Errors.PolicyNotSet(); } if (policy.minTenor > policy.maxTenor) { - revert InvalidTenors(); + revert Errors.InvalidTenors(); } if ( policy.minAllowableLoanPerCollUnitOrLtv > policy.maxAllowableLoanPerCollUnitOrLtv ) { - revert InvalidLoanPerCollOrLTV(); + revert Errors.InvalidLoanPerCollOrLTV(); } } } diff --git a/contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol b/contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol new file mode 100644 index 00000000..a146de9e --- /dev/null +++ b/contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {DataTypesPeerToPeer} from "../../DataTypesPeerToPeer.sol"; + +interface ISimpleQuotePolicyManager { + event PolicySet( + address indexed lenderVault, + address indexed collToken, + address indexed loanToken, + DataTypesPeerToPeer.SimplePolicy policy + ); + + /** + * @notice sets the policy for a pair of tokens + * @param lenderVault Address of the lender vault + * @param collToken Address of the collateral token + * @param loanToken Address of the loan token + * @param policy Policy to be set + */ + function setPolicyForPair( + address lenderVault, + address collToken, + address loanToken, + DataTypesPeerToPeer.SimplePolicy calldata policy + ) external; +} diff --git a/contracts/test/TestQuotePolicyManager.sol b/contracts/test/TestQuotePolicyManager.sol deleted file mode 100644 index 611f2c5d..00000000 --- a/contracts/test/TestQuotePolicyManager.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.19; - -import {DataTypesPeerToPeer} from "../peer-to-peer/DataTypesPeerToPeer.sol"; -import {IQuotePolicyManager} from "../peer-to-peer/interfaces/IQuotePolicyManager.sol"; - -contract TestQuotePolicyManager is IQuotePolicyManager { - mapping(address => bool) public allow; - - // solhint-disable-next-line no-empty-blocks - constructor() {} - - function updatePolicy(address lenderVault, bool _allow) external { - allow[lenderVault] = _allow; - } - - function borrowViolatesPolicy( - address, - address lenderVault, - DataTypesPeerToPeer.GeneralQuoteInfo calldata, - DataTypesPeerToPeer.QuoteTuple calldata, - bool - ) - external - view - returns (bool _borrowViolatesPolicy, uint256 _minSignersForThisPolicy) - { - _borrowViolatesPolicy = !allow[lenderVault]; - _minSignersForThisPolicy = 0; - } - - function defaultRulesWhenNoPolicySet( - address - ) - external - pure - returns (DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState) - { - defaultPolicyState = DataTypesPeerToPeer.DefaultPolicyState.ALLOW_ALL; - } - - function addressRegistry() external pure returns (address) { - return address(0); - } -} diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 9bc60d48..ee501adf 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -3392,18 +3392,7 @@ describe('Peer-to-Peer: Local Tests', function () { }) it('Should process proposed on-chain quote correctly', async function () { - const { - addressRegistry, - borrowerGateway, - quoteHandler, - lender, - borrower, - whitelistAuthority, - usdc, - weth, - lenderVault, - testnetTokenManager - } = await setupTest() + const { quoteHandler, lender, borrower, whitelistAuthority, usdc, weth, lenderVault } = await setupTest() // lenderVault owner deposits usdc await usdc.connect(lender).transfer(lenderVault.address, ONE_USDC.mul(100000)) @@ -3444,19 +3433,13 @@ describe('Peer-to-Peer: Local Tests', function () { } await expect( - quoteHandler.connect(lender).proposeOnChainQuoteForVault(borrower.address, onChainQuote) - ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') - - await expect( - quoteHandler.connect(lender).proposeOnChainQuoteForVault(lenderVault.address, { + quoteHandler.connect(lender).proposeOnChainQuote({ ...onChainQuote, generalQuoteInfo: { ...onChainQuote.generalQuoteInfo, maxLoan: ethers.BigNumber.from(0) } }) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') - const proposedQuoteTransaction = await quoteHandler - .connect(borrower) - .proposeOnChainQuoteForVault(lenderVault.address, onChainQuote) + const proposedQuoteTransaction = await quoteHandler.connect(borrower).proposeOnChainQuote(onChainQuote) const proposedQuoteReceipt = await proposedQuoteTransaction.wait() @@ -3466,7 +3449,7 @@ describe('Peer-to-Peer: Local Tests', function () { const proposedOnChainQuoteHash = proposeQuoteEvent?.args?.onChainQuoteHash || ZERO_BYTES32 - expect(await quoteHandler.isProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash)).to.be.true + expect(await quoteHandler.isProposedOnChainQuote(proposedOnChainQuoteHash)).to.be.true await expect( quoteHandler.connect(borrower).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) @@ -3476,15 +3459,16 @@ describe('Peer-to-Peer: Local Tests', function () { quoteHandler.connect(lender).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) ).to.emit(quoteHandler, 'ProposedOnChainQuoteApproved') - expect(await quoteHandler.isProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash)).to.be.false + expect(await quoteHandler.isProposedOnChainQuote(proposedOnChainQuoteHash)).to.be.true await expect( quoteHandler.connect(lender).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidProposedQuoteApproval') - await expect( - quoteHandler.connect(borrower).proposeOnChainQuoteForVault(lenderVault.address, onChainQuote) - ).to.be.revertedWithCustomError(quoteHandler, 'RedundantOnChainQuoteProposed') + await expect(quoteHandler.connect(borrower).proposeOnChainQuote(onChainQuote)).to.be.revertedWithCustomError( + quoteHandler, + 'RedundantOnChainQuoteProposed' + ) await expect( quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote) @@ -5878,33 +5862,23 @@ describe('Peer-to-Peer: Local Tests', function () { describe('Quote Policy Manager', function () { it('Should handle checks on policy updates correctly', async function () { - const { - quoteHandler, - addressRegistry, - borrowerGateway, - lender, - delegateOnChainQuoting, - borrower, - team, - usdc, - weth, - lenderVault - } = await setupTest() + const { quoteHandler, addressRegistry, lender, delegateOnChainQuoting, borrower, team, usdc, weth, lenderVault } = + await setupTest() - const TestQuotePolicyManager = await ethers.getContractFactory('TestQuotePolicyManager') - const testQuotePolicyManager = await TestQuotePolicyManager.connect(team).deploy() - await testQuotePolicyManager.deployed() + const SimpleQuotePolicyManager = await ethers.getContractFactory('SimpleQuotePolicyManager') + const simpleQuotePolicyManager = await SimpleQuotePolicyManager.connect(team).deploy(addressRegistry.address) + await simpleQuotePolicyManager.deployed() - await addressRegistry.connect(team).setWhitelistState([testQuotePolicyManager.address], 10) + await addressRegistry.connect(team).setWhitelistState([simpleQuotePolicyManager.address], 10) // should revert if unregistered vault await expect( - quoteHandler.connect(team).updateQuotePolicyManagerForVault(borrower.address, testQuotePolicyManager.address) + quoteHandler.connect(team).updateQuotePolicyManagerForVault(borrower.address, simpleQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') // should revert if not vault owner await expect( - quoteHandler.connect(team).updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) + quoteHandler.connect(team).updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') await expect(lenderVault.connect(lender).setOnChainQuotingDelegate(delegateOnChainQuoting.address)) @@ -5915,7 +5889,7 @@ describe('Peer-to-Peer: Local Tests', function () { await expect( quoteHandler .connect(delegateOnChainQuoting) - .updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) + .updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') // should revert if not whitelisted quote policy manager @@ -5956,66 +5930,27 @@ describe('Peer-to-Peer: Local Tests', function () { // set policy manager address await expect( - quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) ) .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') - .withArgs(lenderVault.address, testQuotePolicyManager.address) + .withArgs(lenderVault.address, simpleQuotePolicyManager.address) // should revert if new address matches current policy manager await expect( - quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, testQuotePolicyManager.address) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidAddress') await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1)).to.emit( quoteHandler, 'OnChainQuoteAdded' ) - - // borrower approves gateway and executes quote - await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) - - const collSendAmount1 = ONE_WETH - const quoteTupleIdx1 = 0 - const borrowInstructions1 = { - collSendAmount: collSendAmount1, - expectedProtocolAndVaultTransferFee: 0, - expectedCompartmentTransferFee: 0, - deadline: MAX_UINT256, - minLoanAmount: 0, - callbackAddr: ZERO_ADDRESS, - callbackData: ZERO_BYTES32, - mysoTokenManagerData: ZERO_BYTES32 - } - - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // set policy to allow - await testQuotePolicyManager.connect(team).updatePolicy(lenderVault.address, true) - - await borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) }) it('Should handle simple policy implementation', async function () { - const { - quoteHandler, - addressRegistry, - borrowerGateway, - lender, - delegateOnChainQuoting, - borrower, - team, - usdc, - weth, - lenderVault - } = await setupTest() + const { quoteHandler, addressRegistry, borrowerGateway, lender, borrower, team, usdc, weth, lenderVault } = + await setupTest() - const SimplePolicyManager = await ethers.getContractFactory('SimplePolicyManager') + const SimplePolicyManager = await ethers.getContractFactory('SimpleQuotePolicyManager') const simplePolicyManager = await SimplePolicyManager.connect(team).deploy(addressRegistry.address) await simplePolicyManager.deployed() @@ -6211,13 +6146,11 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(25) - }) + await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(25) + }) // borrow fail if tenor greater than max tenor await expect( @@ -6226,14 +6159,12 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minFee: BASE.div(5) - }) + await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minFee: BASE.div(5) + }) // borrow should fail if upfront fee is below min fee await expect( @@ -6242,14 +6173,12 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAPR: BASE.div(5) - }) + await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAPR: BASE.div(5) + }) // borrow should fail if apr is below min apr await expect( @@ -6258,15 +6187,13 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAllowableLoanPerCollUnitOrLtv: BASE.mul(4).div(5), - maxAllowableLoanPerCollUnitOrLtv: BASE.mul(9).div(10) - }) + await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAllowableLoanPerCollUnitOrLtv: BASE.mul(4).div(5), + maxAllowableLoanPerCollUnitOrLtv: BASE.mul(9).div(10) + }) // borrow should fail if loan per coll unit is below min allowable loan per coll unit await expect( @@ -6275,15 +6202,13 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAllowableLoanPerCollUnitOrLtv: ONE_USDC, - maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(800) - }) + await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAllowableLoanPerCollUnitOrLtv: ONE_USDC, + maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(800) + }) // borrow should fail if loan per coll unit is above max allowable loan per coll unit await expect( @@ -6292,15 +6217,13 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAllowableLoanPerCollUnitOrLtv: ONE_USDC, - maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(1000) - }) + await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + ...policy2, + minTenor: ONE_DAY, + maxTenor: ONE_DAY.mul(35), + minAllowableLoanPerCollUnitOrLtv: ONE_USDC, + maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(1000) + }) // borrow should go through as all conditions are met await borrowerGateway @@ -6323,6 +6246,10 @@ describe('Peer-to-Peer: Local Tests', function () { const currPolicyPostDelete = await simplePolicyManager.policies(lenderVault.address, weth.address, usdc.address) expect(currPolicyPostDelete.isSet).to.be.false + + await expect( + simplePolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + ).to.be.revertedWithCustomError(simplePolicyManager, 'PolicyNotSet') }) }) }) From 36b82d6723a6f2f3dd1dd089d18b379f5a2db5f3 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Thu, 3 Aug 2023 23:41:33 -0400 Subject: [PATCH 29/51] edited check for annualizing with respect to tenor --- .../policies/SimpleQuotePolicyManager.sol | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol b/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol index 90904d1d..50b803b5 100644 --- a/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol @@ -2,8 +2,10 @@ pragma solidity 0.8.19; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; +import {Constants} from "../../Constants.sol"; import {Errors} from "../../Errors.sol"; import {IAddressRegistry} from "../interfaces/IAddressRegistry.sol"; import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; @@ -151,6 +153,7 @@ contract SimpleQuotePolicyManager is ) { _borrowViolatesPolicy = true; } else if ( + quoteTuple.tenor == 0 || quoteTuple.tenor < policy.minTenor || quoteTuple.tenor > policy.maxTenor ) { @@ -163,7 +166,13 @@ contract SimpleQuotePolicyManager is ) { _borrowViolatesPolicy = true; } else if ( - SafeCast.toUint256(quoteTuple.interestRatePctInBase) < policy.minAPR + quoteTuple.interestRatePctInBase < 0 || + Math.mulDiv( + SafeCast.toUint256(quoteTuple.interestRatePctInBase), + Constants.YEAR_IN_SECONDS, + quoteTuple.tenor + ) < + policy.minAPR ) { _borrowViolatesPolicy = true; } else if (quoteTuple.upfrontFeePctInBase < policy.minFee) { From 1f4a6c17af8e5b9dd85070aa22d9434fd6dcc4ad Mon Sep 17 00:00:00 2001 From: asardon Date: Sun, 6 Aug 2023 13:47:15 +0200 Subject: [PATCH 30/51] simplified quote policy manager --- contracts/Errors.sol | 3 +- .../peer-to-peer/DataTypesPeerToPeer.sol | 6 +- contracts/peer-to-peer/QuoteHandler.sol | 43 ++-- .../interfaces/IQuotePolicyManager.sol | 58 ++--- .../policies/SimpleQuotePolicyManager.sol | 213 ------------------ .../interfaces/ISimpleQuotePolicyManager.sol | 28 --- .../BasicQuotePolicyManager.sol | 149 ++++++++++++ 7 files changed, 189 insertions(+), 311 deletions(-) delete mode 100644 contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol delete mode 100644 contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol create mode 100644 contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol diff --git a/contracts/Errors.sol b/contracts/Errors.sol index 7c044f5f..40019c2c 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -126,7 +126,8 @@ library Errors { error QuoteViolatesPolicy(); error RedundantOnChainQuoteProposed(); error InvalidProposedQuoteApproval(); - error PolicyNotSet(); + error PolicyAlreadySet(); + error NoPolicyToDelete(); error InvalidTenors(); error InvalidLoanPerCollOrLTV(); } diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index 03c59236..7757bd70 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -160,13 +160,9 @@ library DataTypesPeerToPeer { uint256 validUntil; } - struct SimplePolicy { - // check if policy is set - bool isSet; + struct QuotingPolicy { // requires oracle bool requiresOracle; - // is policy for all quotes, on chain quotes only, or off chain quotes only - DataTypesPeerToPeer.PolicyType policyType; // min signers for this policy if off chain quote // if 0, then quote handler will use vault min num signers // if > 0, then quote handler will use this value diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index e4aedc5b..6ef6dfd9 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -169,8 +169,7 @@ contract QuoteHandler is IQuoteHandler { borrower, lenderVault, onChainQuote.generalQuoteInfo, - onChainQuote.quoteTuples[quoteTupleIdx], - true + onChainQuote.quoteTuples[quoteTupleIdx] ); mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; @@ -198,13 +197,11 @@ contract QuoteHandler is IQuoteHandler { DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bytes32[] calldata proof ) external { - // this returns 0 if no policy manager or policy is set - uint256 policyMinNumSigners = _checkSenderAndQuoteInfo( + _checkSenderAndQuoteInfo( borrower, lenderVault, offChainQuote.generalQuoteInfo, - quoteTuple, - false + quoteTuple ); if (offChainQuote.nonce < offChainQuoteNonce[lenderVault]) { revert Errors.InvalidQuote(); @@ -224,7 +221,6 @@ contract QuoteHandler is IQuoteHandler { !_areValidSignatures( lenderVault, offChainQuoteHash, - policyMinNumSigners, offChainQuote.compactSigs ) ) { @@ -313,16 +309,12 @@ contract QuoteHandler is IQuoteHandler { function _areValidSignatures( address lenderVault, bytes32 offChainQuoteHash, - uint256 policyMinNumSigners, bytes[] calldata compactSigs ) internal view returns (bool) { uint256 compactSigsLength = compactSigs.length; - // note: if policy min num signers returns 0, use the vault's min num signers - // this protects against case where for policy manager is set, but no policy is set for this pair - uint256 minNumSigners = policyMinNumSigners == 0 - ? ILenderVaultImpl(lenderVault).minNumOfSigners() - : policyMinNumSigners; - if (compactSigsLength < minNumSigners) { + if ( + compactSigsLength < ILenderVaultImpl(lenderVault).minNumOfSigners() + ) { return false; } bytes32 messageHash = ECDSA.toEthSignedMessageHash(offChainQuoteHash); @@ -365,25 +357,20 @@ contract QuoteHandler is IQuoteHandler { address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, - DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, - bool _isOnChainQuote - ) internal view returns (uint256 policyMinNumSigners) { + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple + ) internal view { if (msg.sender != IAddressRegistry(addressRegistry).borrowerGateway()) { revert Errors.InvalidSender(); } address quotePolicyManager = quotePolicyManagerForVault[lenderVault]; if (quotePolicyManager != address(0)) { - bool _violatesPolicy; - (_violatesPolicy, policyMinNumSigners) = IQuotePolicyManager( - quotePolicyManager - ).borrowViolatesPolicy( - borrower, - lenderVault, - generalQuoteInfo, - quoteTuple, - _isOnChainQuote - ); - if (_violatesPolicy) { + bool isAllowed = IQuotePolicyManager(quotePolicyManager).isAllowed( + borrower, + lenderVault, + generalQuoteInfo, + quoteTuple + ); + if (!isAllowed) { revert Errors.QuoteViolatesPolicy(); } } diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index 6c4d085a..02795ef4 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -5,72 +5,58 @@ pragma solidity 0.8.19; import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; interface IQuotePolicyManager { - event PolicyDeleted( + event PolicySet( address indexed lenderVault, address indexed collToken, - address indexed loanToken + address indexed loanToken, + bytes policyData ); - - event DefaultPolicySet( + event PolicyDeleted( address indexed lenderVault, - DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState + address indexed collToken, + address indexed loanToken ); /** - * @notice deletes the policy for a pair of tokens + * @notice sets the policy for a pair of tokens * @param lenderVault Address of the lender vault * @param collToken Address of the collateral token * @param loanToken Address of the loan token + * @param policyData Policy data to be set */ - function deletePolicyForPair( + function setAllowedPairAndPolicy( address lenderVault, address collToken, - address loanToken + address loanToken, + bytes calldata policyData ) external; /** - * @notice sets the default policy for a vault - * this state will control access whenever a policy is not set for a pair of tokens + * @notice deletes the policy for a pair of tokens * @param lenderVault Address of the lender vault - * @param defaultPolicyState Default policy state to be set + * @param collToken Address of the collateral token + * @param loanToken Address of the loan token */ - function setDefaultPolicy( + function deleteAllowedPairAndPolicy( address lenderVault, - DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState + address collToken, + address loanToken ) external; /** - * @notice Checks if a borrow violates the policy set by the lender - * this function should always return 0 for _minSignersForThisPolicy if the policy is not set + * @notice Checks if a borrow is allowed * @param borrower Address of the borrower * @param lenderVault Address of the lender vault * @param generalQuoteInfo General quote info (see DataTypesPeerToPeer.sol) * @param quoteTuple Quote tuple (see DataTypesPeerToPeer.sol) - * @param _isOnChainQuote Flag to indicate if the quote is on-chain or off-chain - * @return _borrowViolatesPolicy Flag to indicate if the borrow violates the policy - * @return _minSignersForThisPolicy Minimum number of signers required for this policy (if off-chain quote) + * @return _isAllowed Flag to indicate if the borrow is allowed */ - function borrowViolatesPolicy( + function isAllowed( address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, - DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, - bool _isOnChainQuote - ) - external - view - returns (bool _borrowViolatesPolicy, uint256 _minSignersForThisPolicy); - - /** - * @notice Gets the default policy state when no policy is set for a vault - * @param lenderVault Address of the lender vault - */ - function defaultRulesWhenNoPolicySet( - address lenderVault - ) - external - view - returns (DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState); + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple + ) external view returns (bool _isAllowed); /** * @notice Gets the address registry diff --git a/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol b/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol deleted file mode 100644 index 50b803b5..00000000 --- a/contracts/peer-to-peer/policies/SimpleQuotePolicyManager.sol +++ /dev/null @@ -1,213 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.19; - -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; -import {Constants} from "../../Constants.sol"; -import {Errors} from "../../Errors.sol"; -import {IAddressRegistry} from "../interfaces/IAddressRegistry.sol"; -import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; -import {IQuotePolicyManager} from "../interfaces/IQuotePolicyManager.sol"; -import {ISimpleQuotePolicyManager} from "./interfaces/ISimpleQuotePolicyManager.sol"; - -contract SimpleQuotePolicyManager is - IQuotePolicyManager, - ISimpleQuotePolicyManager -{ - mapping(address => DataTypesPeerToPeer.DefaultPolicyState) - public defaultRulesWhenNoPolicySet; - mapping(address => mapping(address => mapping(address => DataTypesPeerToPeer.SimplePolicy))) - public policies; - address public immutable addressRegistry; - - constructor(address _addressRegistry) { - addressRegistry = _addressRegistry; - } - - function setPolicyForPair( - address lenderVault, - address collToken, - address loanToken, - DataTypesPeerToPeer.SimplePolicy calldata policy - ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); - _isValidPolicy(policy); - policies[lenderVault][collToken][loanToken] = policy; - emit PolicySet(lenderVault, collToken, loanToken, policy); - } - - function deletePolicyForPair( - address lenderVault, - address collToken, - address loanToken - ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); - mapping(address => DataTypesPeerToPeer.SimplePolicy) - storage policiesForVaultAndCollToken = policies[lenderVault][ - collToken - ]; - if (!policiesForVaultAndCollToken[loanToken].isSet) { - revert Errors.PolicyNotSet(); - } - delete policiesForVaultAndCollToken[loanToken]; - emit PolicyDeleted(lenderVault, collToken, loanToken); - } - - function setDefaultPolicy( - address lenderVault, - DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState - ) external { - _checkIsRegisteredVaultAndSenderIsApproved(lenderVault); - defaultRulesWhenNoPolicySet[lenderVault] = defaultPolicyState; - emit DefaultPolicySet(lenderVault, defaultPolicyState); - } - - function borrowViolatesPolicy( - address, - address lenderVault, - DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, - DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, - bool isOnChainQuote - ) - external - view - returns (bool _borrowViolatesPolicy, uint256 _minSignersForThisPolicy) - { - DataTypesPeerToPeer.SimplePolicy memory policy = policies[lenderVault][ - generalQuoteInfo.collToken - ][generalQuoteInfo.loanToken]; - // this will only affect off chain quotes once returned to the quote handler - // @dev: quote handler will handle a return value of 0, so no need for special handling here - _minSignersForThisPolicy = policy.isSet && !isOnChainQuote - ? policy.minNumSigners - : 0; - bool doesPolicyApplyToThisQuote = policy.isSet - ? _doesPolicyApplyToThisQuote(policy.policyType, isOnChainQuote) - : false; - if (!doesPolicyApplyToThisQuote) { - _borrowViolatesPolicy = _checkDefaultPolicy( - lenderVault, - isOnChainQuote - ); - } else { - _borrowViolatesPolicy = _checkPolicy( - policy, - generalQuoteInfo, - quoteTuple - ); - } - } - - function _checkDefaultPolicy( - address lenderVault, - bool isOnChainQuote - ) internal view returns (bool _borrowViolatesPolicy) { - DataTypesPeerToPeer.DefaultPolicyState defaultPolicyState = defaultRulesWhenNoPolicySet[ - lenderVault - ]; - // check three cases where violations could occur, else by default no violation - if ( - defaultPolicyState == - DataTypesPeerToPeer.DefaultPolicyState.DISALLOW_ALL - ) { - _borrowViolatesPolicy = true; - } else if ( - defaultPolicyState == - DataTypesPeerToPeer.DefaultPolicyState.ALLOW_ONLY_ON_CHAIN_QUOTES && - !isOnChainQuote - ) { - _borrowViolatesPolicy = true; - } else if ( - defaultPolicyState == - DataTypesPeerToPeer - .DefaultPolicyState - .ALLOW_ONLY_OFF_CHAIN_QUOTES && - isOnChainQuote - ) { - _borrowViolatesPolicy = true; - } - } - - function _checkIsRegisteredVaultAndSenderIsApproved( - address lenderVault - ) internal view { - if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { - revert Errors.UnregisteredVault(); - } - if (ILenderVaultImpl(lenderVault).owner() != msg.sender) { - revert Errors.InvalidSender(); - } - } - - // note: off chain quotes are allowed to leave _minNumSignersForThisPolicy as 0 - // since the quote handler will just always use the vault min num signers in that case - function _checkPolicy( - DataTypesPeerToPeer.SimplePolicy memory policy, - DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, - DataTypesPeerToPeer.QuoteTuple calldata quoteTuple - ) internal pure returns (bool _borrowViolatesPolicy) { - if ( - policy.requiresOracle && generalQuoteInfo.oracleAddr == address(0) - ) { - _borrowViolatesPolicy = true; - } else if ( - quoteTuple.tenor == 0 || - quoteTuple.tenor < policy.minTenor || - quoteTuple.tenor > policy.maxTenor - ) { - _borrowViolatesPolicy = true; - } else if ( - quoteTuple.loanPerCollUnitOrLtv < - policy.minAllowableLoanPerCollUnitOrLtv || - quoteTuple.loanPerCollUnitOrLtv > - policy.maxAllowableLoanPerCollUnitOrLtv - ) { - _borrowViolatesPolicy = true; - } else if ( - quoteTuple.interestRatePctInBase < 0 || - Math.mulDiv( - SafeCast.toUint256(quoteTuple.interestRatePctInBase), - Constants.YEAR_IN_SECONDS, - quoteTuple.tenor - ) < - policy.minAPR - ) { - _borrowViolatesPolicy = true; - } else if (quoteTuple.upfrontFeePctInBase < policy.minFee) { - _borrowViolatesPolicy = true; - } - } - - function _doesPolicyApplyToThisQuote( - DataTypesPeerToPeer.PolicyType policyType, - bool isOnChainQuote - ) internal pure returns (bool) { - return - policyType == DataTypesPeerToPeer.PolicyType.ALL_QUOTES || - (policyType == - DataTypesPeerToPeer.PolicyType.ONLY_ON_CHAIN_QUOTES && - isOnChainQuote) || - (policyType == - DataTypesPeerToPeer.PolicyType.ONLY_OFF_CHAIN_QUOTES && - !isOnChainQuote); - } - - function _isValidPolicy( - DataTypesPeerToPeer.SimplePolicy calldata policy - ) internal pure { - if (!policy.isSet) { - revert Errors.PolicyNotSet(); - } - if (policy.minTenor > policy.maxTenor) { - revert Errors.InvalidTenors(); - } - if ( - policy.minAllowableLoanPerCollUnitOrLtv > - policy.maxAllowableLoanPerCollUnitOrLtv - ) { - revert Errors.InvalidLoanPerCollOrLTV(); - } - } -} diff --git a/contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol b/contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol deleted file mode 100644 index a146de9e..00000000 --- a/contracts/peer-to-peer/policies/interfaces/ISimpleQuotePolicyManager.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.19; - -import {DataTypesPeerToPeer} from "../../DataTypesPeerToPeer.sol"; - -interface ISimpleQuotePolicyManager { - event PolicySet( - address indexed lenderVault, - address indexed collToken, - address indexed loanToken, - DataTypesPeerToPeer.SimplePolicy policy - ); - - /** - * @notice sets the policy for a pair of tokens - * @param lenderVault Address of the lender vault - * @param collToken Address of the collateral token - * @param loanToken Address of the loan token - * @param policy Policy to be set - */ - function setPolicyForPair( - address lenderVault, - address collToken, - address loanToken, - DataTypesPeerToPeer.SimplePolicy calldata policy - ) external; -} diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol new file mode 100644 index 00000000..6e37c777 --- /dev/null +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; +import {Constants} from "../../Constants.sol"; +import {Errors} from "../../Errors.sol"; +import {IAddressRegistry} from "../interfaces/IAddressRegistry.sol"; +import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; +import {IQuotePolicyManager} from "../interfaces/IQuotePolicyManager.sol"; + +contract BasicQuotePolicyManager is IQuotePolicyManager { + mapping(address => mapping(address => mapping(address => bytes))) + public quotingPolicies; + mapping(address => mapping(address => mapping(address => bool))) + public isPairAllowed; + address public immutable addressRegistry; + + constructor(address _addressRegistry) { + addressRegistry = _addressRegistry; + } + + function setAllowedPairAndPolicy( + address lenderVault, + address collToken, + address loanToken, + bytes calldata policyData + ) external { + _checkIsVaultAndSenderIsOwner(lenderVault); + if (isPairAllowed[lenderVault][collToken][loanToken]) { + revert Errors.PolicyAlreadySet(); + } + ( + , + /*bool requiresOracle*/ uint40 minTenor, + uint40 maxTenor /*uint64 minFee*/ /*uint80 minAPR*/, + , + , + uint128 minLoanPerCollUnitOrLtv, + uint128 maxLoanPerCollUnitOrLtv + ) = abi.decode( + policyData, + (bool, uint40, uint40, uint64, uint80, uint128, uint128) + ); + if ( + minTenor < Constants.MIN_TIME_BETWEEN_EARLIEST_REPAY_AND_EXPIRY || + minTenor > maxTenor + ) { + revert Errors.InvalidTenors(); + } + if (minLoanPerCollUnitOrLtv > maxLoanPerCollUnitOrLtv) { + revert Errors.InvalidLoanPerCollOrLTV(); + } + isPairAllowed[lenderVault][collToken][loanToken] = true; + quotingPolicies[lenderVault][collToken][loanToken] = policyData; + emit PolicySet(lenderVault, collToken, loanToken, policyData); + } + + function deleteAllowedPairAndPolicy( + address lenderVault, + address collToken, + address loanToken + ) external { + _checkIsVaultAndSenderIsOwner(lenderVault); + if (!isPairAllowed[lenderVault][collToken][loanToken]) { + revert Errors.NoPolicyToDelete(); + } + delete isPairAllowed[lenderVault][collToken][loanToken]; + delete quotingPolicies[lenderVault][collToken][loanToken]; + emit PolicyDeleted(lenderVault, collToken, loanToken); + } + + function isAllowed( + address /*borrower*/, + address lenderVault, + DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple + ) external view returns (bool _isAllowed) { + if ( + !isPairAllowed[lenderVault][generalQuoteInfo.collToken][ + generalQuoteInfo.loanToken + ] + ) { + return false; + } + + ( + bool requiresOracle, + uint40 minTenor, + uint40 maxTenor, + uint64 minFee, + uint80 minAPR, + uint128 minLoanPerCollUnitOrLtv, + uint128 maxLoanPerCollUnitOrLtv + ) = abi.decode( + quotingPolicies[lenderVault][generalQuoteInfo.collToken][ + generalQuoteInfo.loanToken + ], + (bool, uint40, uint40, uint64, uint80, uint128, uint128) + ); + if (requiresOracle && generalQuoteInfo.oracleAddr == address(0)) { + return false; + } + + if ( + quoteTuple.tenor == 0 || + quoteTuple.tenor < minTenor || + quoteTuple.tenor > maxTenor + ) { + return false; + } + + if ( + quoteTuple.loanPerCollUnitOrLtv < minLoanPerCollUnitOrLtv || + quoteTuple.loanPerCollUnitOrLtv > maxLoanPerCollUnitOrLtv + ) { + return false; + } + + if ( + quoteTuple.interestRatePctInBase < 0 || + Math.mulDiv( + SafeCast.toUint256(quoteTuple.interestRatePctInBase), + Constants.YEAR_IN_SECONDS, + quoteTuple.tenor + ) < + minAPR + ) { + return false; + } + + if (quoteTuple.upfrontFeePctInBase < minFee) { + return false; + } + + return true; + } + + function _checkIsVaultAndSenderIsOwner(address lenderVault) internal view { + if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { + revert Errors.UnregisteredVault(); + } + if (ILenderVaultImpl(lenderVault).owner() != msg.sender) { + revert Errors.InvalidSender(); + } + } +} From 628c5ad62709aa1f6549242ba12254859c5e741e Mon Sep 17 00:00:00 2001 From: asardon Date: Sun, 6 Aug 2023 13:51:59 +0200 Subject: [PATCH 31/51] remove structs --- .../peer-to-peer/DataTypesPeerToPeer.sol | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/contracts/peer-to-peer/DataTypesPeerToPeer.sol b/contracts/peer-to-peer/DataTypesPeerToPeer.sol index 7757bd70..32ce596d 100644 --- a/contracts/peer-to-peer/DataTypesPeerToPeer.sol +++ b/contracts/peer-to-peer/DataTypesPeerToPeer.sol @@ -160,30 +160,6 @@ library DataTypesPeerToPeer { uint256 validUntil; } - struct QuotingPolicy { - // requires oracle - bool requiresOracle; - // min signers for this policy if off chain quote - // if 0, then quote handler will use vault min num signers - // if > 0, then quote handler will use this value - // this is convenient for automated quotes or RFQs or easy third party handling. - // e.g., lender only wants 1 key for quotes covered by policy, - // but vault requires more signers for pairs without policies. - uint8 minNumSigners; - // min allowable tenor - uint40 minTenor; - // max allowable tenor - uint40 maxTenor; - // global min fee - uint64 minFee; - // global min apr - uint80 minAPR; - // min allowable loan per collateral amount or LTV - uint128 minAllowableLoanPerCollUnitOrLtv; - // max allowbale loan per collateral amount or LTV - uint128 maxAllowableLoanPerCollUnitOrLtv; - } - enum WhitelistState { // not whitelisted NOT_WHITELISTED, @@ -210,24 +186,4 @@ library DataTypesPeerToPeer { // can be used as quote policy manager contract QUOTE_POLICY_MANAGER } - - enum DefaultPolicyState { - // if no explicit policy set, then default to allow all quotes - ALLOW_ALL, - // allow only on chain quotes when no policy - ALLOW_ONLY_ON_CHAIN_QUOTES, - // allow only off chain quotes when no policy - ALLOW_ONLY_OFF_CHAIN_QUOTES, - // only allow quotes when explicit policy is set - DISALLOW_ALL - } - - enum PolicyType { - // apply policy to both on chain and off chain quotes by default - ALL_QUOTES, - // apply policy only to on chain quotes - ONLY_ON_CHAIN_QUOTES, - // apply policy only to off chain quotes - ONLY_OFF_CHAIN_QUOTES - } } From 62649c4ebd1adaa0d24b27f34af5078527550cc4 Mon Sep 17 00:00:00 2001 From: asardon Date: Sun, 6 Aug 2023 14:21:51 +0200 Subject: [PATCH 32/51] minor renamings --- contracts/Errors.sol | 2 -- contracts/peer-to-peer/QuoteHandler.sol | 21 +++++++++---------- .../peer-to-peer/interfaces/IQuoteHandler.sol | 20 +++++++++--------- test/peer-to-peer/local-tests.ts | 20 +++++++++--------- 4 files changed, 30 insertions(+), 33 deletions(-) diff --git a/contracts/Errors.sol b/contracts/Errors.sol index 40019c2c..e3b819d1 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -124,8 +124,6 @@ library Errors { error TokenDoesNotBelongInWrapper(address tokenAddr, uint256 tokenId); error InvalidMintAmount(); error QuoteViolatesPolicy(); - error RedundantOnChainQuoteProposed(); - error InvalidProposedQuoteApproval(); error PolicyAlreadySet(); error NoPolicyToDelete(); error InvalidTenors(); diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 6ef6dfd9..e341d9c0 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -20,7 +20,7 @@ contract QuoteHandler is IQuoteHandler { mapping(address => mapping(bytes32 => bool)) public offChainQuoteIsInvalidated; mapping(address => mapping(bytes32 => bool)) public isOnChainQuote; - mapping(bytes32 => bool) public isProposedOnChainQuote; + mapping(bytes32 => bool) public isPublishedOnChainQuote; mapping(address => address) public quotePolicyManagerForVault; mapping(address => DataTypesPeerToPeer.OnChainQuoteInfo[]) internal onChainQuoteHistory; @@ -108,7 +108,7 @@ contract QuoteHandler is IQuoteHandler { emit OnChainQuoteDeleted(lenderVault, onChainQuoteHash); } - function approveProposedOnChainQuote( + function copyPublishedOnChainQuote( address lenderVault, bytes32 onChainQuoteHash ) external { @@ -116,27 +116,27 @@ contract QuoteHandler is IQuoteHandler { mapping(bytes32 => bool) storage isOnChainQuoteFromVault = isOnChainQuote[lenderVault]; if ( - !isProposedOnChainQuote[onChainQuoteHash] || + !isPublishedOnChainQuote[onChainQuoteHash] || isOnChainQuoteFromVault[onChainQuoteHash] ) { - revert Errors.InvalidProposedQuoteApproval(); + revert Errors.InvalidQuote(); } isOnChainQuoteFromVault[onChainQuoteHash] = true; - emit ProposedOnChainQuoteApproved(lenderVault, onChainQuoteHash); + emit OnChainQuoteCopied(lenderVault, onChainQuoteHash); } - function proposeOnChainQuote( + function publishOnChainQuote( DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external { if (!_isValidOnChainQuote(onChainQuote)) { revert Errors.InvalidQuote(); } bytes32 onChainQuoteHash = _hashOnChainQuote(onChainQuote); - if (isProposedOnChainQuote[onChainQuoteHash]) { - revert Errors.RedundantOnChainQuoteProposed(); + if (isPublishedOnChainQuote[onChainQuoteHash]) { + revert Errors.InvalidQuote(); } - isProposedOnChainQuote[onChainQuoteHash] = true; - emit OnChainQuoteProposed(onChainQuote, onChainQuoteHash, msg.sender); + isPublishedOnChainQuote[onChainQuoteHash] = true; + emit OnChainQuotePublished(onChainQuote, onChainQuoteHash, msg.sender); } function incrementOffChainQuoteNonce(address lenderVault) external { @@ -164,7 +164,6 @@ contract QuoteHandler is IQuoteHandler { if (quoteTupleIdx >= onChainQuote.quoteTuples.length) { revert Errors.InvalidArrayIndex(); } - // note: return value for minNumSigners not consumed for on-chain quotes _checkSenderAndQuoteInfo( borrower, lenderVault, diff --git a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol index 4ec227cc..b697c07e 100644 --- a/contracts/peer-to-peer/interfaces/IQuoteHandler.sol +++ b/contracts/peer-to-peer/interfaces/IQuoteHandler.sol @@ -44,12 +44,12 @@ interface IQuoteHandler { address indexed lenderVault, address indexed newPolicyManagerAddress ); - event OnChainQuoteProposed( + event OnChainQuotePublished( DataTypesPeerToPeer.OnChainQuote onChainQuote, bytes32 indexed onChainQuoteHash, address indexed proposer ); - event ProposedOnChainQuoteApproved( + event OnChainQuoteCopied( address indexed lenderVault, bytes32 indexed onChainQuoteHash ); @@ -90,22 +90,22 @@ interface IQuoteHandler { ) external; /** - * @notice function approves proposed on chain quote + * @notice function to copy a published on chain quote * @dev function can only be called by vault owner or on chain quote delegate * @param lenderVault address of the vault approving - * @param onChainQuoteHash quote hash for the onChain quote marked for approval + * @param onChainQuoteHash quote hash of a published onChain quote */ - function approveProposedOnChainQuote( + function copyPublishedOnChainQuote( address lenderVault, bytes32 onChainQuoteHash ) external; /** - * @notice function proposes on chain quote + * @notice function to publish an on chain quote * @dev function can be called by anyone and used by any vault * @param onChainQuote data for the onChain quote (See notes in DataTypesPeerToPeer.sol) */ - function proposeOnChainQuote( + function publishOnChainQuote( DataTypesPeerToPeer.OnChainQuote calldata onChainQuote ) external; @@ -209,11 +209,11 @@ interface IQuoteHandler { ) external view returns (bool); /** - * @notice function returns if hash is for an on chain quote that has been proposed + * @notice function returns if hash belongs to a published on chain quote * @param hashToCheck hash of the on chain quote - * @return true if hash belongs to a valid on-chain quote, else false + * @return true if hash belongs to a published on-chain quote, else false */ - function isProposedOnChainQuote( + function isPublishedOnChainQuote( bytes32 hashToCheck ) external view returns (bool); diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index ee501adf..cf10def5 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -3433,39 +3433,39 @@ describe('Peer-to-Peer: Local Tests', function () { } await expect( - quoteHandler.connect(lender).proposeOnChainQuote({ + quoteHandler.connect(lender).broadcastOnChainQuote({ ...onChainQuote, generalQuoteInfo: { ...onChainQuote.generalQuoteInfo, maxLoan: ethers.BigNumber.from(0) } }) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') - const proposedQuoteTransaction = await quoteHandler.connect(borrower).proposeOnChainQuote(onChainQuote) + const proposedQuoteTransaction = await quoteHandler.connect(borrower).broadcastOnChainQuote(onChainQuote) const proposedQuoteReceipt = await proposedQuoteTransaction.wait() const proposeQuoteEvent = proposedQuoteReceipt.events?.find(x => { - return x.event === 'OnChainQuoteProposed' + return x.event === 'OnChainQuotePublished' }) const proposedOnChainQuoteHash = proposeQuoteEvent?.args?.onChainQuoteHash || ZERO_BYTES32 - expect(await quoteHandler.isProposedOnChainQuote(proposedOnChainQuoteHash)).to.be.true + expect(await quoteHandler.isPublishedOnChainQuote(proposedOnChainQuoteHash)).to.be.true await expect( - quoteHandler.connect(borrower).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) + quoteHandler.connect(borrower).copyPublishedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') await expect( - quoteHandler.connect(lender).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) - ).to.emit(quoteHandler, 'ProposedOnChainQuoteApproved') + quoteHandler.connect(lender).copyPublishedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) + ).to.emit(quoteHandler, 'OnChainQuoteCopied') - expect(await quoteHandler.isProposedOnChainQuote(proposedOnChainQuoteHash)).to.be.true + expect(await quoteHandler.isPublishedOnChainQuote(proposedOnChainQuoteHash)).to.be.true await expect( - quoteHandler.connect(lender).approveProposedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) + quoteHandler.connect(lender).copyPublishedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidProposedQuoteApproval') - await expect(quoteHandler.connect(borrower).proposeOnChainQuote(onChainQuote)).to.be.revertedWithCustomError( + await expect(quoteHandler.connect(borrower).broadcastOnChainQuote(onChainQuote)).to.be.revertedWithCustomError( quoteHandler, 'RedundantOnChainQuoteProposed' ) From 82cfe0e5927b8a618b162b94eeb102d2117689c7 Mon Sep 17 00:00:00 2001 From: asardon Date: Sun, 6 Aug 2023 14:28:07 +0200 Subject: [PATCH 33/51] minor renaming --- contracts/Errors.sol | 1 + contracts/peer-to-peer/QuoteHandler.sol | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Errors.sol b/contracts/Errors.sol index e3b819d1..4ddf8fd0 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -124,6 +124,7 @@ library Errors { error TokenDoesNotBelongInWrapper(address tokenAddr, uint256 tokenId); error InvalidMintAmount(); error QuoteViolatesPolicy(); + error AlreadyPublished(); error PolicyAlreadySet(); error NoPolicyToDelete(); error InvalidTenors(); diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index e341d9c0..d85c5e17 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -133,7 +133,7 @@ contract QuoteHandler is IQuoteHandler { } bytes32 onChainQuoteHash = _hashOnChainQuote(onChainQuote); if (isPublishedOnChainQuote[onChainQuoteHash]) { - revert Errors.InvalidQuote(); + revert Errors.AlreadyPublished(); } isPublishedOnChainQuote[onChainQuoteHash] = true; emit OnChainQuotePublished(onChainQuote, onChainQuoteHash, msg.sender); From 6ad068bd6bc5913f8a85dff4490bf17d66062c2c Mon Sep 17 00:00:00 2001 From: asardon Date: Sun, 6 Aug 2023 14:30:36 +0200 Subject: [PATCH 34/51] minor renaming --- contracts/Errors.sol | 2 +- .../peer-to-peer/policyManagers/BasicQuotePolicyManager.sol | 2 +- test/peer-to-peer/local-tests.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/Errors.sol b/contracts/Errors.sol index 4ddf8fd0..67ffb031 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -128,5 +128,5 @@ library Errors { error PolicyAlreadySet(); error NoPolicyToDelete(); error InvalidTenors(); - error InvalidLoanPerCollOrLTV(); + error InvalidLoanPerCollOrLtv(); } diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 6e37c777..3a8b3dd3 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -51,7 +51,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { revert Errors.InvalidTenors(); } if (minLoanPerCollUnitOrLtv > maxLoanPerCollUnitOrLtv) { - revert Errors.InvalidLoanPerCollOrLTV(); + revert Errors.InvalidLoanPerCollOrLtv(); } isPairAllowed[lenderVault][collToken][loanToken] = true; quotingPolicies[lenderVault][collToken][loanToken] = policyData; diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index cf10def5..9635e799 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -6119,7 +6119,7 @@ describe('Peer-to-Peer: Local Tests', function () { ...policy2, maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) }) - ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidLoanPerCollOrLTV') + ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidLoanPerCollOrLtv') // policy set correctly await expect( From 87a1ec47059b44cd4a53258891c7c5fb7f66a0e4 Mon Sep 17 00:00:00 2001 From: asardon Date: Mon, 7 Aug 2023 20:37:59 +0200 Subject: [PATCH 35/51] incorporated discussion feedback --- contracts/peer-to-peer/QuoteHandler.sol | 25 ++- .../interfaces/IQuotePolicyManager.sol | 31 ++- .../BasicQuotePolicyManager.sol | 205 +++++++++++------- .../policyManagers/DataTypesBasicPolicies.sol | 24 ++ 4 files changed, 179 insertions(+), 106 deletions(-) create mode 100644 contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index d85c5e17..d74e3d37 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -164,6 +164,7 @@ contract QuoteHandler is IQuoteHandler { if (quoteTupleIdx >= onChainQuote.quoteTuples.length) { revert Errors.InvalidArrayIndex(); } + // @dev: ignore returned minNumOfSignersOverwrite for on-chain quotes _checkSenderAndQuoteInfo( borrower, lenderVault, @@ -196,7 +197,7 @@ contract QuoteHandler is IQuoteHandler { DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bytes32[] calldata proof ) external { - _checkSenderAndQuoteInfo( + uint256 minNumOfSignersOverwrite = _checkSenderAndQuoteInfo( borrower, lenderVault, offChainQuote.generalQuoteInfo, @@ -220,6 +221,7 @@ contract QuoteHandler is IQuoteHandler { !_areValidSignatures( lenderVault, offChainQuoteHash, + minNumOfSignersOverwrite, offChainQuote.compactSigs ) ) { @@ -308,12 +310,14 @@ contract QuoteHandler is IQuoteHandler { function _areValidSignatures( address lenderVault, bytes32 offChainQuoteHash, + uint256 minNumOfSignersOverwrite, bytes[] calldata compactSigs ) internal view returns (bool) { uint256 compactSigsLength = compactSigs.length; - if ( - compactSigsLength < ILenderVaultImpl(lenderVault).minNumOfSigners() - ) { + uint256 minNumOfSigners = minNumOfSignersOverwrite == 0 + ? ILenderVaultImpl(lenderVault).minNumOfSigners() + : minNumOfSignersOverwrite; + if (compactSigsLength < minNumOfSigners) { return false; } bytes32 messageHash = ECDSA.toEthSignedMessageHash(offChainQuoteHash); @@ -357,18 +361,16 @@ contract QuoteHandler is IQuoteHandler { address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple - ) internal view { + ) internal view returns (uint256 minNumOfSignersOverwrite) { if (msg.sender != IAddressRegistry(addressRegistry).borrowerGateway()) { revert Errors.InvalidSender(); } address quotePolicyManager = quotePolicyManagerForVault[lenderVault]; if (quotePolicyManager != address(0)) { - bool isAllowed = IQuotePolicyManager(quotePolicyManager).isAllowed( - borrower, - lenderVault, - generalQuoteInfo, - quoteTuple - ); + bool isAllowed; + (isAllowed, minNumOfSignersOverwrite) = IQuotePolicyManager( + quotePolicyManager + ).isAllowed(borrower, lenderVault, generalQuoteInfo, quoteTuple); if (!isAllowed) { revert Errors.QuoteViolatesPolicy(); } @@ -403,6 +405,7 @@ contract QuoteHandler is IQuoteHandler { ) { revert Errors.InvalidBorrower(); } + return minNumOfSignersOverwrite; } function _isValidOnChainQuote( diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index 02795ef4..62296ba0 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -5,42 +5,36 @@ pragma solidity 0.8.19; import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; interface IQuotePolicyManager { - event PolicySet( + event PairPolicySet( address indexed lenderVault, address indexed collToken, address indexed loanToken, - bytes policyData - ); - event PolicyDeleted( - address indexed lenderVault, - address indexed collToken, - address indexed loanToken + bytes singlePolicyData ); + event GlobalPolicySet(address indexed lenderVault, bytes globalPolicyData); /** * @notice sets the policy for a pair of tokens * @param lenderVault Address of the lender vault - * @param collToken Address of the collateral token - * @param loanToken Address of the loan token - * @param policyData Policy data to be set + * @param globalPolicyData Policy data to be set */ - function setAllowedPairAndPolicy( + function setGlobalPolicy( address lenderVault, - address collToken, - address loanToken, - bytes calldata policyData + bytes calldata globalPolicyData ) external; /** - * @notice deletes the policy for a pair of tokens + * @notice sets the policy for a pair of tokens * @param lenderVault Address of the lender vault * @param collToken Address of the collateral token * @param loanToken Address of the loan token + * @param pairPolicyData Policy data to be set */ - function deleteAllowedPairAndPolicy( + function setPairPolicy( address lenderVault, address collToken, - address loanToken + address loanToken, + bytes calldata pairPolicyData ) external; /** @@ -50,13 +44,14 @@ interface IQuotePolicyManager { * @param generalQuoteInfo General quote info (see DataTypesPeerToPeer.sol) * @param quoteTuple Quote tuple (see DataTypesPeerToPeer.sol) * @return _isAllowed Flag to indicate if the borrow is allowed + * @return minNumOfSignersOverwrite Overwrite of minimum number of signers (if zero ignored in quote handler) */ function isAllowed( address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple - ) external view returns (bool _isAllowed); + ) external view returns (bool _isAllowed, uint256 minNumOfSignersOverwrite); /** * @notice Gets the address registry diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 3a8b3dd3..116e5254 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.19; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; +import {DataTypesBasicPolicies} from "./DataTypesBasicPolicies.sol"; import {Constants} from "../../Constants.sol"; import {Errors} from "../../Errors.sol"; import {IAddressRegistry} from "../interfaces/IAddressRegistry.sol"; @@ -12,64 +13,71 @@ import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; import {IQuotePolicyManager} from "../interfaces/IQuotePolicyManager.sol"; contract BasicQuotePolicyManager is IQuotePolicyManager { - mapping(address => mapping(address => mapping(address => bytes))) - public quotingPolicies; + mapping(address => mapping(address => mapping(address => DataTypesBasicPolicies.SinglePolicy))) + public singleQuotingPolicies; + mapping(address => DataTypesBasicPolicies.GlobalPolicy) + public globalQuotingPolicies; mapping(address => mapping(address => mapping(address => bool))) - public isPairAllowed; + public hasSingleQuotingPolicy; address public immutable addressRegistry; constructor(address _addressRegistry) { addressRegistry = _addressRegistry; } - function setAllowedPairAndPolicy( + function setGlobalPolicy( address lenderVault, - address collToken, - address loanToken, - bytes calldata policyData + bytes calldata globalPolicyData ) external { + // @dev: global policy applies across all pairs _checkIsVaultAndSenderIsOwner(lenderVault); - if (isPairAllowed[lenderVault][collToken][loanToken]) { - revert Errors.PolicyAlreadySet(); - } - ( - , - /*bool requiresOracle*/ uint40 minTenor, - uint40 maxTenor /*uint64 minFee*/ /*uint80 minAPR*/, - , - , - uint128 minLoanPerCollUnitOrLtv, - uint128 maxLoanPerCollUnitOrLtv - ) = abi.decode( - policyData, - (bool, uint40, uint40, uint64, uint80, uint128, uint128) - ); - if ( - minTenor < Constants.MIN_TIME_BETWEEN_EARLIEST_REPAY_AND_EXPIRY || - minTenor > maxTenor - ) { - revert Errors.InvalidTenors(); - } - if (minLoanPerCollUnitOrLtv > maxLoanPerCollUnitOrLtv) { - revert Errors.InvalidLoanPerCollOrLtv(); + if (globalPolicyData.length > 0) { + DataTypesBasicPolicies.GlobalPolicy memory globalPolicy = abi + .decode( + globalPolicyData, + (DataTypesBasicPolicies.GlobalPolicy) + ); + _checkQuoteBounds(globalPolicy.quoteBounds); + globalQuotingPolicies[lenderVault] = globalPolicy; + } else { + delete globalQuotingPolicies[lenderVault]; } - isPairAllowed[lenderVault][collToken][loanToken] = true; - quotingPolicies[lenderVault][collToken][loanToken] = policyData; - emit PolicySet(lenderVault, collToken, loanToken, policyData); + emit GlobalPolicySet(lenderVault, globalPolicyData); } - function deleteAllowedPairAndPolicy( + function setPairPolicy( address lenderVault, address collToken, - address loanToken + address loanToken, + bytes calldata pairPolicyData ) external { _checkIsVaultAndSenderIsOwner(lenderVault); - if (!isPairAllowed[lenderVault][collToken][loanToken]) { - revert Errors.NoPolicyToDelete(); + if (collToken == address(0) || loanToken == address(0)) { + revert Errors.InvalidAddress(); + } + mapping(address => bool) + storage _hasSingleQuotingPolicy = hasSingleQuotingPolicy[ + lenderVault + ][collToken]; + if (pairPolicyData.length > 0) { + if (_hasSingleQuotingPolicy[loanToken]) { + revert Errors.PolicyAlreadySet(); + } + DataTypesBasicPolicies.SinglePolicy memory singlePolicy = abi + .decode(pairPolicyData, (DataTypesBasicPolicies.SinglePolicy)); + _checkQuoteBounds(singlePolicy.quoteBounds); + _hasSingleQuotingPolicy[loanToken] = true; + singleQuotingPolicies[lenderVault][collToken][ + loanToken + ] = singlePolicy; + } else { + if (!_hasSingleQuotingPolicy[loanToken]) { + revert Errors.NoPolicyToDelete(); + } + delete _hasSingleQuotingPolicy[loanToken]; + delete singleQuotingPolicies[lenderVault][collToken][loanToken]; } - delete isPairAllowed[lenderVault][collToken][loanToken]; - delete quotingPolicies[lenderVault][collToken][loanToken]; - emit PolicyDeleted(lenderVault, collToken, loanToken); + emit PairPolicySet(lenderVault, collToken, loanToken, pairPolicyData); } function isAllowed( @@ -77,44 +85,96 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple - ) external view returns (bool _isAllowed) { - if ( - !isPairAllowed[lenderVault][generalQuoteInfo.collToken][ - generalQuoteInfo.loanToken - ] - ) { - return false; + ) + external + view + returns (bool _isAllowed, uint256 minNumOfSignersOverwrite) + { + DataTypesBasicPolicies.GlobalPolicy + memory globalPolicy = globalQuotingPolicies[lenderVault]; + bool hasSinglePolicy = hasSingleQuotingPolicy[lenderVault][ + generalQuoteInfo.collToken + ][generalQuoteInfo.loanToken]; + if (!globalPolicy.allowAllPairs && !hasSinglePolicy) { + return (false, minNumOfSignersOverwrite); } - ( - bool requiresOracle, - uint40 minTenor, - uint40 maxTenor, - uint64 minFee, - uint80 minAPR, - uint128 minLoanPerCollUnitOrLtv, - uint128 maxLoanPerCollUnitOrLtv - ) = abi.decode( - quotingPolicies[lenderVault][generalQuoteInfo.collToken][ - generalQuoteInfo.loanToken - ], - (bool, uint40, uint40, uint64, uint80, uint128, uint128) + // @dev: single quoting policy takes precedence over global quoting policy + bool noOracle = generalQuoteInfo.oracleAddr == address(0); + if (hasSinglePolicy) { + DataTypesBasicPolicies.SinglePolicy + memory singlePolicy = singleQuotingPolicies[lenderVault][ + generalQuoteInfo.collToken + ][generalQuoteInfo.loanToken]; + if (singlePolicy.requiresOracle && noOracle) { + return (false, minNumOfSignersOverwrite); + } + return ( + _isQuoteTupleInBounds( + singlePolicy.quoteBounds, + quoteTuple, + true + ), + singlePolicy.minNumOfSignersOverwrite + ); + } else { + return ( + _isQuoteTupleInBounds( + globalPolicy.quoteBounds, + quoteTuple, + noOracle + ), + minNumOfSignersOverwrite ); - if (requiresOracle && generalQuoteInfo.oracleAddr == address(0)) { - return false; } + } + function _checkIsVaultAndSenderIsOwner(address lenderVault) internal view { + if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { + revert Errors.UnregisteredVault(); + } + if (ILenderVaultImpl(lenderVault).owner() != msg.sender) { + revert Errors.InvalidSender(); + } + } + + function _checkQuoteBounds( + DataTypesBasicPolicies.QuoteBounds memory quoteBounds + ) internal pure { + if ( + quoteBounds.minTenor < + Constants.MIN_TIME_BETWEEN_EARLIEST_REPAY_AND_EXPIRY || + quoteBounds.minTenor > quoteBounds.maxTenor + ) { + revert Errors.InvalidTenors(); + } + if ( + quoteBounds.minLoanPerCollUnitOrLtv > + quoteBounds.maxLoanPerCollUnitOrLtv + ) { + revert Errors.InvalidLoanPerCollOrLtv(); + } + } + + function _isQuoteTupleInBounds( + DataTypesBasicPolicies.QuoteBounds memory quoteBounds, + DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, + bool checkLoanPerCollUnitOrLtv + ) internal pure returns (bool) { if ( quoteTuple.tenor == 0 || - quoteTuple.tenor < minTenor || - quoteTuple.tenor > maxTenor + quoteTuple.tenor < quoteBounds.minTenor || + quoteTuple.tenor > quoteBounds.maxTenor ) { return false; } if ( - quoteTuple.loanPerCollUnitOrLtv < minLoanPerCollUnitOrLtv || - quoteTuple.loanPerCollUnitOrLtv > maxLoanPerCollUnitOrLtv + checkLoanPerCollUnitOrLtv && + (quoteTuple.loanPerCollUnitOrLtv < + quoteBounds.minLoanPerCollUnitOrLtv || + quoteTuple.loanPerCollUnitOrLtv > + quoteBounds.maxLoanPerCollUnitOrLtv) ) { return false; } @@ -126,24 +186,15 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { Constants.YEAR_IN_SECONDS, quoteTuple.tenor ) < - minAPR + quoteBounds.minAPR ) { return false; } - if (quoteTuple.upfrontFeePctInBase < minFee) { + if (quoteTuple.upfrontFeePctInBase < quoteBounds.minFee) { return false; } return true; } - - function _checkIsVaultAndSenderIsOwner(address lenderVault) internal view { - if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { - revert Errors.UnregisteredVault(); - } - if (ILenderVaultImpl(lenderVault).owner() != msg.sender) { - revert Errors.InvalidSender(); - } - } } diff --git a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol new file mode 100644 index 00000000..5763f307 --- /dev/null +++ b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +library DataTypesBasicPolicies { + struct QuoteBounds { + uint40 minTenor; + uint40 maxTenor; + uint80 minFee; + uint80 minAPR; + uint128 minLoanPerCollUnitOrLtv; + uint128 maxLoanPerCollUnitOrLtv; + } + + struct GlobalPolicy { + bool allowAllPairs; + QuoteBounds quoteBounds; + } + + struct SinglePolicy { + bool requiresOracle; + uint8 minNumOfSignersOverwrite; + QuoteBounds quoteBounds; + } +} From c78c7b1c243fba1aad80a601b76a229fc5303eff Mon Sep 17 00:00:00 2001 From: asardon Date: Mon, 7 Aug 2023 20:42:49 +0200 Subject: [PATCH 36/51] minor renaming --- contracts/peer-to-peer/QuoteHandler.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index d74e3d37..4368ec3a 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -165,7 +165,7 @@ contract QuoteHandler is IQuoteHandler { revert Errors.InvalidArrayIndex(); } // @dev: ignore returned minNumOfSignersOverwrite for on-chain quotes - _checkSenderAndQuoteInfo( + _checkSenderAndPolicyAndQuoteInfo( borrower, lenderVault, onChainQuote.generalQuoteInfo, @@ -197,7 +197,7 @@ contract QuoteHandler is IQuoteHandler { DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bytes32[] calldata proof ) external { - uint256 minNumOfSignersOverwrite = _checkSenderAndQuoteInfo( + uint256 minNumOfSignersOverwrite = _checkSenderAndPolicyAndQuoteInfo( borrower, lenderVault, offChainQuote.generalQuoteInfo, @@ -356,7 +356,7 @@ contract QuoteHandler is IQuoteHandler { ); } - function _checkSenderAndQuoteInfo( + function _checkSenderAndPolicyAndQuoteInfo( address borrower, address lenderVault, DataTypesPeerToPeer.GeneralQuoteInfo calldata generalQuoteInfo, From dc4d0409a5c9060d29e504c4177d72858dacf724 Mon Sep 17 00:00:00 2001 From: asardon Date: Mon, 7 Aug 2023 20:51:43 +0200 Subject: [PATCH 37/51] minor renaming and added inline comment --- contracts/peer-to-peer/QuoteHandler.sol | 1 + .../policyManagers/BasicQuotePolicyManager.sol | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 4368ec3a..335b166c 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -314,6 +314,7 @@ contract QuoteHandler is IQuoteHandler { bytes[] calldata compactSigs ) internal view returns (bool) { uint256 compactSigsLength = compactSigs.length; + // @dev: if defined in policy, allow overwriting of min number of signers (except zero) uint256 minNumOfSigners = minNumOfSignersOverwrite == 0 ? ILenderVaultImpl(lenderVault).minNumOfSigners() : minNumOfSignersOverwrite; diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 116e5254..e66f1add 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -96,18 +96,18 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; if (!globalPolicy.allowAllPairs && !hasSinglePolicy) { - return (false, minNumOfSignersOverwrite); + return (false, 0); } // @dev: single quoting policy takes precedence over global quoting policy - bool noOracle = generalQuoteInfo.oracleAddr == address(0); + bool hasOracle = generalQuoteInfo.oracleAddr != address(0); if (hasSinglePolicy) { DataTypesBasicPolicies.SinglePolicy memory singlePolicy = singleQuotingPolicies[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; - if (singlePolicy.requiresOracle && noOracle) { - return (false, minNumOfSignersOverwrite); + if (singlePolicy.requiresOracle && !hasOracle) { + return (false, 0); } return ( _isQuoteTupleInBounds( @@ -118,13 +118,14 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { singlePolicy.minNumOfSignersOverwrite ); } else { + // @dev: check against global minLoanPerCollUnitOrLtv only if pair has oracle return ( _isQuoteTupleInBounds( globalPolicy.quoteBounds, quoteTuple, - noOracle + hasOracle ), - minNumOfSignersOverwrite + 0 ); } } From b676db24a73d7fed398fedd3aeed6cc8c2525022 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 10:51:58 +0200 Subject: [PATCH 38/51] incorporated feedback --- contracts/peer-to-peer/QuoteHandler.sol | 1 - .../interfaces/IQuotePolicyManager.sol | 6 +- .../BasicQuotePolicyManager.sol | 104 +++++++++++++----- .../policyManagers/DataTypesBasicPolicies.sol | 15 ++- 4 files changed, 96 insertions(+), 30 deletions(-) diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index 335b166c..a238fff2 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -406,7 +406,6 @@ contract QuoteHandler is IQuoteHandler { ) { revert Errors.InvalidBorrower(); } - return minNumOfSignersOverwrite; } function _isValidOnChainQuote( diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol index 62296ba0..819fd2fe 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol @@ -14,9 +14,9 @@ interface IQuotePolicyManager { event GlobalPolicySet(address indexed lenderVault, bytes globalPolicyData); /** - * @notice sets the policy for a pair of tokens + * @notice sets the global policy * @param lenderVault Address of the lender vault - * @param globalPolicyData Policy data to be set + * @param globalPolicyData Global policy data to be set */ function setGlobalPolicy( address lenderVault, @@ -28,7 +28,7 @@ interface IQuotePolicyManager { * @param lenderVault Address of the lender vault * @param collToken Address of the collateral token * @param loanToken Address of the loan token - * @param pairPolicyData Policy data to be set + * @param pairPolicyData Pair policy data to be set */ function setPairPolicy( address lenderVault, diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index e66f1add..cabfaa87 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -13,12 +13,13 @@ import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; import {IQuotePolicyManager} from "../interfaces/IQuotePolicyManager.sol"; contract BasicQuotePolicyManager is IQuotePolicyManager { - mapping(address => mapping(address => mapping(address => DataTypesBasicPolicies.SinglePolicy))) - public singleQuotingPolicies; mapping(address => DataTypesBasicPolicies.GlobalPolicy) public globalQuotingPolicies; + mapping(address => mapping(address => mapping(address => DataTypesBasicPolicies.PairPolicy))) + public pairQuotingPolicies; + mapping(address => bool) public hasGlobalQuotingPolicy; mapping(address => mapping(address => mapping(address => bool))) - public hasSingleQuotingPolicy; + public hasPairQuotingPolicy; address public immutable addressRegistry; constructor(address _addressRegistry) { @@ -29,7 +30,8 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { address lenderVault, bytes calldata globalPolicyData ) external { - // @dev: global policy applies across all pairs + // @dev: global policy applies across all pairs; + // note: pair policies (if defined) take precedence over global policy _checkIsVaultAndSenderIsOwner(lenderVault); if (globalPolicyData.length > 0) { DataTypesBasicPolicies.GlobalPolicy memory globalPolicy = abi @@ -37,9 +39,26 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { globalPolicyData, (DataTypesBasicPolicies.GlobalPolicy) ); - _checkQuoteBounds(globalPolicy.quoteBounds); + DataTypesBasicPolicies.GlobalPolicy + memory currGlobalPolicy = globalQuotingPolicies[lenderVault]; + if ( + globalPolicy.allowAllPairs == currGlobalPolicy.allowAllPairs && + _equalQuoteBounds( + globalPolicy.quoteBounds, + currGlobalPolicy.quoteBounds + ) + ) { + revert Errors.PolicyAlreadySet(); + } + _checkNewQuoteBounds(globalPolicy.quoteBounds); + if (!hasGlobalQuotingPolicy[lenderVault]) { + hasGlobalQuotingPolicy[lenderVault] = true; + } globalQuotingPolicies[lenderVault] = globalPolicy; } else { + if (!hasGlobalQuotingPolicy[lenderVault]) { + revert Errors.NoPolicyToDelete(); + } delete globalQuotingPolicies[lenderVault]; } emit GlobalPolicySet(lenderVault, globalPolicyData); @@ -51,23 +70,41 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { address loanToken, bytes calldata pairPolicyData ) external { + // @dev: pair policies (if defined) take precedence over global policy _checkIsVaultAndSenderIsOwner(lenderVault); if (collToken == address(0) || loanToken == address(0)) { revert Errors.InvalidAddress(); } mapping(address => bool) - storage _hasSingleQuotingPolicy = hasSingleQuotingPolicy[ - lenderVault - ][collToken]; + storage _hasSingleQuotingPolicy = hasPairQuotingPolicy[lenderVault][ + collToken + ]; if (pairPolicyData.length > 0) { - if (_hasSingleQuotingPolicy[loanToken]) { + DataTypesBasicPolicies.PairPolicy memory singlePolicy = abi.decode( + pairPolicyData, + (DataTypesBasicPolicies.PairPolicy) + ); + DataTypesBasicPolicies.PairPolicy + memory currSinglePolicy = pairQuotingPolicies[lenderVault][ + collToken + ][loanToken]; + if ( + singlePolicy.requiresOracle == + currSinglePolicy.requiresOracle && + singlePolicy.minNumOfSignersOverwrite == + currSinglePolicy.minNumOfSignersOverwrite && + _equalQuoteBounds( + singlePolicy.quoteBounds, + currSinglePolicy.quoteBounds + ) + ) { revert Errors.PolicyAlreadySet(); } - DataTypesBasicPolicies.SinglePolicy memory singlePolicy = abi - .decode(pairPolicyData, (DataTypesBasicPolicies.SinglePolicy)); - _checkQuoteBounds(singlePolicy.quoteBounds); - _hasSingleQuotingPolicy[loanToken] = true; - singleQuotingPolicies[lenderVault][collToken][ + _checkNewQuoteBounds(singlePolicy.quoteBounds); + if (!_hasSingleQuotingPolicy[loanToken]) { + _hasSingleQuotingPolicy[loanToken] = true; + } + pairQuotingPolicies[lenderVault][collToken][ loanToken ] = singlePolicy; } else { @@ -75,7 +112,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { revert Errors.NoPolicyToDelete(); } delete _hasSingleQuotingPolicy[loanToken]; - delete singleQuotingPolicies[lenderVault][collToken][loanToken]; + delete pairQuotingPolicies[lenderVault][collToken][loanToken]; } emit PairPolicySet(lenderVault, collToken, loanToken, pairPolicyData); } @@ -92,25 +129,25 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { { DataTypesBasicPolicies.GlobalPolicy memory globalPolicy = globalQuotingPolicies[lenderVault]; - bool hasSinglePolicy = hasSingleQuotingPolicy[lenderVault][ + bool hasSinglePolicy = hasPairQuotingPolicy[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; if (!globalPolicy.allowAllPairs && !hasSinglePolicy) { return (false, 0); } - // @dev: single quoting policy takes precedence over global quoting policy + // @dev: pair policy (if defined) takes precedence over global policy bool hasOracle = generalQuoteInfo.oracleAddr != address(0); if (hasSinglePolicy) { - DataTypesBasicPolicies.SinglePolicy - memory singlePolicy = singleQuotingPolicies[lenderVault][ + DataTypesBasicPolicies.PairPolicy + memory singlePolicy = pairQuotingPolicies[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; if (singlePolicy.requiresOracle && !hasOracle) { return (false, 0); } return ( - _isQuoteTupleInBounds( + _isAllowedWithBounds( singlePolicy.quoteBounds, quoteTuple, true @@ -118,9 +155,9 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { singlePolicy.minNumOfSignersOverwrite ); } else { - // @dev: check against global minLoanPerCollUnitOrLtv only if pair has oracle + // @dev: check against global min/max loanPerCollUnitOrLtv only if pair has oracle return ( - _isQuoteTupleInBounds( + _isAllowedWithBounds( globalPolicy.quoteBounds, quoteTuple, hasOracle @@ -139,7 +176,25 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { } } - function _checkQuoteBounds( + function _equalQuoteBounds( + DataTypesBasicPolicies.QuoteBounds memory quoteBounds1, + DataTypesBasicPolicies.QuoteBounds memory quoteBounds2 + ) internal pure returns (bool isEqual) { + if ( + quoteBounds1.minTenor == quoteBounds2.minTenor && + quoteBounds1.maxTenor == quoteBounds2.maxTenor && + quoteBounds1.minFee == quoteBounds2.minFee && + quoteBounds1.minAPR == quoteBounds2.minAPR && + quoteBounds1.minLoanPerCollUnitOrLtv == + quoteBounds2.minLoanPerCollUnitOrLtv && + quoteBounds1.maxLoanPerCollUnitOrLtv == + quoteBounds2.maxLoanPerCollUnitOrLtv + ) { + isEqual = true; + } + } + + function _checkNewQuoteBounds( DataTypesBasicPolicies.QuoteBounds memory quoteBounds ) internal pure { if ( @@ -157,13 +212,12 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { } } - function _isQuoteTupleInBounds( + function _isAllowedWithBounds( DataTypesBasicPolicies.QuoteBounds memory quoteBounds, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, bool checkLoanPerCollUnitOrLtv ) internal pure returns (bool) { if ( - quoteTuple.tenor == 0 || quoteTuple.tenor < quoteBounds.minTenor || quoteTuple.tenor > quoteBounds.maxTenor ) { diff --git a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol index 5763f307..c6d2de1c 100644 --- a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol +++ b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol @@ -3,22 +3,35 @@ pragma solidity 0.8.19; library DataTypesBasicPolicies { struct QuoteBounds { + // Allowed minimum tenor for the quote (in seconds) uint40 minTenor; + // Allowed maximum tenor for the quote (in seconds) uint40 maxTenor; + // Allowed minimum fee for the quote (in BASE) uint80 minFee; + // Allowed minimum APR for the quote (in BASE) uint80 minAPR; + // Allowed minimum loan per collateral unit or LTV for the quote uint128 minLoanPerCollUnitOrLtv; + // Allowed maximum loan per collateral unit or LTV for the quote uint128 maxLoanPerCollUnitOrLtv; } struct GlobalPolicy { + // Flag indicating if all pairs are allowed (=true) or + // only pairs with explicitly defined pair policy (=false), default case bool allowAllPairs; + // Applicable global bounds QuoteBounds quoteBounds; } - struct SinglePolicy { + struct PairPolicy { + // Flag indicating if an oracle is required for the pair bool requiresOracle; + // Minimum number of signers required for the pair (if zero ignored, otherwise overwrites vault min signers) + // @dev: can overwrite signer threshold to be lower or higher than vault min signers uint8 minNumOfSignersOverwrite; + // Applicable global bounds QuoteBounds quoteBounds; } } From 68f6a3f365ead857e3b3248ec1e4d72b5592cb59 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 14:46:12 +0200 Subject: [PATCH 39/51] delete global quoting policy flag on delete --- .../peer-to-peer/policyManagers/BasicQuotePolicyManager.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index cabfaa87..1850c9dd 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -59,6 +59,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { if (!hasGlobalQuotingPolicy[lenderVault]) { revert Errors.NoPolicyToDelete(); } + delete hasGlobalQuotingPolicy[lenderVault]; delete globalQuotingPolicies[lenderVault]; } emit GlobalPolicySet(lenderVault, globalPolicyData); From e4a637b49aac7ad6fad3e005718aab4c86749506 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 17:12:23 +0200 Subject: [PATCH 40/51] incorporated feedback --- contracts/Errors.sol | 1 + .../BasicQuotePolicyManager.sol | 29 +++++++++---------- .../policyManagers/DataTypesBasicPolicies.sol | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/Errors.sol b/contracts/Errors.sol index 67ffb031..46860a09 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -129,4 +129,5 @@ library Errors { error NoPolicyToDelete(); error InvalidTenors(); error InvalidLoanPerCollOrLtv(); + error InvalidMinApr(); } diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 1850c9dd..105bd8c6 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -151,6 +151,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { _isAllowedWithBounds( singlePolicy.quoteBounds, quoteTuple, + generalQuoteInfo.earliestRepayTenor, true ), singlePolicy.minNumOfSignersOverwrite @@ -161,6 +162,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { _isAllowedWithBounds( globalPolicy.quoteBounds, quoteTuple, + generalQuoteInfo.earliestRepayTenor, hasOracle ), 0 @@ -185,7 +187,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { quoteBounds1.minTenor == quoteBounds2.minTenor && quoteBounds1.maxTenor == quoteBounds2.maxTenor && quoteBounds1.minFee == quoteBounds2.minFee && - quoteBounds1.minAPR == quoteBounds2.minAPR && + quoteBounds1.minApr == quoteBounds2.minApr && quoteBounds1.minLoanPerCollUnitOrLtv == quoteBounds2.minLoanPerCollUnitOrLtv && quoteBounds1.maxLoanPerCollUnitOrLtv == @@ -198,11 +200,8 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { function _checkNewQuoteBounds( DataTypesBasicPolicies.QuoteBounds memory quoteBounds ) internal pure { - if ( - quoteBounds.minTenor < - Constants.MIN_TIME_BETWEEN_EARLIEST_REPAY_AND_EXPIRY || - quoteBounds.minTenor > quoteBounds.maxTenor - ) { + // @dev: allow minTenor == 0 to enable swaps + if (quoteBounds.minTenor > quoteBounds.maxTenor) { revert Errors.InvalidTenors(); } if ( @@ -211,11 +210,15 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { ) { revert Errors.InvalidLoanPerCollOrLtv(); } + if (quoteBounds.minApr + int(Constants.BASE) <= 0) { + revert Errors.InvalidMinApr(); + } } function _isAllowedWithBounds( DataTypesBasicPolicies.QuoteBounds memory quoteBounds, DataTypesPeerToPeer.QuoteTuple calldata quoteTuple, + uint256 earliestRepayTenor, bool checkLoanPerCollUnitOrLtv ) internal pure returns (bool) { if ( @@ -235,15 +238,11 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { return false; } - if ( - quoteTuple.interestRatePctInBase < 0 || - Math.mulDiv( - SafeCast.toUint256(quoteTuple.interestRatePctInBase), - Constants.YEAR_IN_SECONDS, - quoteTuple.tenor - ) < - quoteBounds.minAPR - ) { + int256 apr = (quoteTuple.interestRatePctInBase * + SafeCast.toInt256(Constants.YEAR_IN_SECONDS)) / + SafeCast.toInt256(quoteTuple.tenor); + // @dev: disallow negative apr and where earliest repay is zero + if ((apr < 0 && earliestRepayTenor == 0) || apr < quoteBounds.minApr) { return false; } diff --git a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol index c6d2de1c..0635f7a6 100644 --- a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol +++ b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol @@ -10,7 +10,7 @@ library DataTypesBasicPolicies { // Allowed minimum fee for the quote (in BASE) uint80 minFee; // Allowed minimum APR for the quote (in BASE) - uint80 minAPR; + int80 minApr; // Allowed minimum loan per collateral unit or LTV for the quote uint128 minLoanPerCollUnitOrLtv; // Allowed maximum loan per collateral unit or LTV for the quote From 58820b679a5609b0f8cc5ee2dbac482279aedb31 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 17:25:53 +0200 Subject: [PATCH 41/51] added check for earliest repay > 0 when comparing apr --- .../policyManagers/BasicQuotePolicyManager.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 105bd8c6..6eb5d03d 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -238,12 +238,14 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { return false; } - int256 apr = (quoteTuple.interestRatePctInBase * - SafeCast.toInt256(Constants.YEAR_IN_SECONDS)) / - SafeCast.toInt256(quoteTuple.tenor); - // @dev: disallow negative apr and where earliest repay is zero - if ((apr < 0 && earliestRepayTenor == 0) || apr < quoteBounds.minApr) { - return false; + // @dev: if earliest repay is zero no need to check apr + if (earliestRepayTenor > 0) { + int256 apr = (quoteTuple.interestRatePctInBase * + SafeCast.toInt256(Constants.YEAR_IN_SECONDS)) / + SafeCast.toInt256(quoteTuple.tenor); + if (apr < quoteBounds.minApr) { + return false; + } } if (quoteTuple.upfrontFeePctInBase < quoteBounds.minFee) { From 28c1c91c09805369abb69937916c04cd65d4d571 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 17:30:25 +0200 Subject: [PATCH 42/51] added check for earliest repay == 0 and apr < 0 --- .../policyManagers/BasicQuotePolicyManager.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 6eb5d03d..68fc2c11 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -238,14 +238,18 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { return false; } - // @dev: if earliest repay is zero no need to check apr - if (earliestRepayTenor > 0) { + // @dev: if tenor is zero tx is swap and no need to check apr + if (quoteTuple.tenor > 0) { int256 apr = (quoteTuple.interestRatePctInBase * SafeCast.toInt256(Constants.YEAR_IN_SECONDS)) / SafeCast.toInt256(quoteTuple.tenor); if (apr < quoteBounds.minApr) { return false; } + // @dev: disallow if negative apr and earliest repay is zero + if (apr < 0 && earliestRepayTenor == 0) { + return false; + } } if (quoteTuple.upfrontFeePctInBase < quoteBounds.minFee) { From c977e102833361009b4bc258e7281653df3fe7ee Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 18:04:09 +0200 Subject: [PATCH 43/51] incorporated feedback --- .../policyManagers/BasicQuotePolicyManager.sol | 7 +++++-- .../peer-to-peer/policyManagers/DataTypesBasicPolicies.sol | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 68fc2c11..6df06a36 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -246,8 +246,11 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { if (apr < quoteBounds.minApr) { return false; } - // @dev: disallow if negative apr and earliest repay is zero - if (apr < 0 && earliestRepayTenor == 0) { + // @dev: disallow if negative apr and earliest repay is below bound + if ( + apr < 0 && + earliestRepayTenor < quoteBounds.minEarliestRepayTenor + ) { return false; } } diff --git a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol index 0635f7a6..42ef20cb 100644 --- a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol +++ b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol @@ -4,13 +4,15 @@ pragma solidity 0.8.19; library DataTypesBasicPolicies { struct QuoteBounds { // Allowed minimum tenor for the quote (in seconds) - uint40 minTenor; + uint32 minTenor; // Allowed maximum tenor for the quote (in seconds) - uint40 maxTenor; + uint32 maxTenor; // Allowed minimum fee for the quote (in BASE) uint80 minFee; // Allowed minimum APR for the quote (in BASE) int80 minApr; + // Allowed minimum earliest repay tenor + uint32 minEarliestRepayTenor; // Allowed minimum loan per collateral unit or LTV for the quote uint128 minLoanPerCollUnitOrLtv; // Allowed maximum loan per collateral unit or LTV for the quote From aac27ad993fc33af0510d9dcf4ba996169b488f3 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Tue, 8 Aug 2023 12:28:54 -0400 Subject: [PATCH 44/51] added in fix for first quote manager test renaming --- test/peer-to-peer/local-tests.ts | 102 +++++++++++++++---------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 9635e799..73f38435 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -5865,20 +5865,20 @@ describe('Peer-to-Peer: Local Tests', function () { const { quoteHandler, addressRegistry, lender, delegateOnChainQuoting, borrower, team, usdc, weth, lenderVault } = await setupTest() - const SimpleQuotePolicyManager = await ethers.getContractFactory('SimpleQuotePolicyManager') - const simpleQuotePolicyManager = await SimpleQuotePolicyManager.connect(team).deploy(addressRegistry.address) - await simpleQuotePolicyManager.deployed() + const BasicQuotePolicyManager = await ethers.getContractFactory('BasicQuotePolicyManager') + const basicQuotePolicyManager = await BasicQuotePolicyManager.connect(team).deploy(addressRegistry.address) + await basicQuotePolicyManager.deployed() - await addressRegistry.connect(team).setWhitelistState([simpleQuotePolicyManager.address], 10) + await addressRegistry.connect(team).setWhitelistState([basicQuotePolicyManager.address], 10) // should revert if unregistered vault await expect( - quoteHandler.connect(team).updateQuotePolicyManagerForVault(borrower.address, simpleQuotePolicyManager.address) + quoteHandler.connect(team).updateQuotePolicyManagerForVault(borrower.address, basicQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'UnregisteredVault') // should revert if not vault owner await expect( - quoteHandler.connect(team).updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) + quoteHandler.connect(team).updateQuotePolicyManagerForVault(lenderVault.address, basicQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') await expect(lenderVault.connect(lender).setOnChainQuotingDelegate(delegateOnChainQuoting.address)) @@ -5889,7 +5889,7 @@ describe('Peer-to-Peer: Local Tests', function () { await expect( quoteHandler .connect(delegateOnChainQuoting) - .updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) + .updateQuotePolicyManagerForVault(lenderVault.address, basicQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidSender') // should revert if not whitelisted quote policy manager @@ -5930,14 +5930,14 @@ describe('Peer-to-Peer: Local Tests', function () { // set policy manager address await expect( - quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, basicQuotePolicyManager.address) ) .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') - .withArgs(lenderVault.address, simpleQuotePolicyManager.address) + .withArgs(lenderVault.address, basicQuotePolicyManager.address) // should revert if new address matches current policy manager await expect( - quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, simpleQuotePolicyManager.address) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, basicQuotePolicyManager.address) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidAddress') await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote1)).to.emit( @@ -5950,11 +5950,11 @@ describe('Peer-to-Peer: Local Tests', function () { const { quoteHandler, addressRegistry, borrowerGateway, lender, borrower, team, usdc, weth, lenderVault } = await setupTest() - const SimplePolicyManager = await ethers.getContractFactory('SimpleQuotePolicyManager') - const simplePolicyManager = await SimplePolicyManager.connect(team).deploy(addressRegistry.address) - await simplePolicyManager.deployed() + const BasicPolicyManager = await ethers.getContractFactory('BasicQuotePolicyManager') + const basicPolicyManager = await BasicPolicyManager.connect(team).deploy(addressRegistry.address) + await basicPolicyManager.deployed() - await addressRegistry.connect(team).setWhitelistState([simplePolicyManager.address], 10) + await addressRegistry.connect(team).setWhitelistState([basicPolicyManager.address], 10) const reth = '0xae78736Cd615f374D3085123A210448E74Fc6393' @@ -5962,10 +5962,10 @@ describe('Peer-to-Peer: Local Tests', function () { // set policy manager address await expect( - quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, simplePolicyManager.address) + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, basicPolicyManager.address) ) .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') - .withArgs(lenderVault.address, simplePolicyManager.address) + .withArgs(lenderVault.address, basicPolicyManager.address) // lenderVault owner deposits usdc await usdc.connect(lender).transfer(lenderVault.address, ONE_USDC.mul(100000)) @@ -6018,24 +6018,24 @@ describe('Peer-to-Peer: Local Tests', function () { // should revert if unregistered vault await expect( - simplePolicyManager.connect(team).setPolicyForPair(borrower.address, weth.address, usdc.address, policy) - ).to.be.revertedWithCustomError(simplePolicyManager, 'UnregisteredVault') + basicPolicyManager.connect(team).setPolicyForPair(borrower.address, weth.address, usdc.address, policy) + ).to.be.revertedWithCustomError(basicPolicyManager, 'UnregisteredVault') // should revert if sender is not vault owner await expect( - simplePolicyManager.connect(borrower).setPolicyForPair(lenderVault.address, weth.address, usdc.address, policy) - ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidSender') + basicPolicyManager.connect(borrower).setPolicyForPair(lenderVault.address, weth.address, usdc.address, policy) + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') // should revert if unregistered vault - await expect(simplePolicyManager.connect(team).setDefaultPolicy(borrower.address, 1)).to.be.revertedWithCustomError( - simplePolicyManager, + await expect(basicPolicyManager.connect(team).setDefaultPolicy(borrower.address, 1)).to.be.revertedWithCustomError( + basicPolicyManager, 'UnregisteredVault' ) // should revert if sender is not vault owner await expect( - simplePolicyManager.connect(borrower).setDefaultPolicy(lenderVault.address, 1) - ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidSender') + basicPolicyManager.connect(borrower).setDefaultPolicy(lenderVault.address, 1) + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') // borrower approves gateway and executes quote await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) @@ -6061,7 +6061,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) // set default policy to only allow off-chain quotes - await simplePolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 2) + await basicPolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 2) await expect( borrowerGateway @@ -6070,7 +6070,7 @@ describe('Peer-to-Peer: Local Tests', function () { ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') // set policy to allow no quotes without a policy - await simplePolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 3) + await basicPolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 3) await expect( borrowerGateway @@ -6079,7 +6079,7 @@ describe('Peer-to-Peer: Local Tests', function () { ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') // set policy to allow on chain quotes without a policy and borrow should go through again - await simplePolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 1) + await basicPolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 1) await borrowerGateway .connect(borrower) @@ -6101,32 +6101,32 @@ describe('Peer-to-Peer: Local Tests', function () { // should default if not set await expect( - simplePolicyManager + basicPolicyManager .connect(lender) .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, isSet: false }) - ).to.be.revertedWithCustomError(simplePolicyManager, 'PolicyNotSet') + ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') // should default if min tenor < max tenor await expect( - simplePolicyManager + basicPolicyManager .connect(lender) .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, maxTenor: ONE_DAY.div(2) }) - ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidTenors') + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidTenors') // should default if min ltv > max ltv await expect( - simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) }) - ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidLoanPerCollOrLtv') + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidLoanPerCollOrLtv') // policy set correctly await expect( - simplePolicyManager + basicPolicyManager .connect(lender) .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: true }) - ).to.emit(simplePolicyManager, 'PolicySet') + ).to.emit(basicPolicyManager, 'PolicySet') // borrow should fail without oracle address await expect( @@ -6135,7 +6135,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager + await basicPolicyManager .connect(lender) .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: false }) @@ -6146,7 +6146,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, minTenor: ONE_DAY, maxTenor: ONE_DAY.mul(25) @@ -6159,7 +6159,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, minTenor: ONE_DAY, maxTenor: ONE_DAY.mul(35), @@ -6173,7 +6173,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, minTenor: ONE_DAY, maxTenor: ONE_DAY.mul(35), @@ -6187,7 +6187,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, minTenor: ONE_DAY, maxTenor: ONE_DAY.mul(35), @@ -6202,7 +6202,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, minTenor: ONE_DAY, maxTenor: ONE_DAY.mul(35), @@ -6217,7 +6217,7 @@ describe('Peer-to-Peer: Local Tests', function () { .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - await simplePolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, minTenor: ONE_DAY, maxTenor: ONE_DAY.mul(35), @@ -6230,26 +6230,26 @@ describe('Peer-to-Peer: Local Tests', function () { .connect(borrower) .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - const currPolicyPreDelete = await simplePolicyManager.policies(lenderVault.address, weth.address, usdc.address) + const currPolicyPreDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) expect(currPolicyPreDelete.isSet).to.be.true await expect( - simplePolicyManager.connect(lender).deletePolicyForPair(borrower.address, weth.address, usdc.address) - ).to.be.revertedWithCustomError(simplePolicyManager, 'UnregisteredVault') + basicPolicyManager.connect(lender).deletePolicyForPair(borrower.address, weth.address, usdc.address) + ).to.be.revertedWithCustomError(basicPolicyManager, 'UnregisteredVault') await expect( - simplePolicyManager.connect(team).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - ).to.be.revertedWithCustomError(simplePolicyManager, 'InvalidSender') + basicPolicyManager.connect(team).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') - await simplePolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + await basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - const currPolicyPostDelete = await simplePolicyManager.policies(lenderVault.address, weth.address, usdc.address) + const currPolicyPostDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) expect(currPolicyPostDelete.isSet).to.be.false await expect( - simplePolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - ).to.be.revertedWithCustomError(simplePolicyManager, 'PolicyNotSet') + basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') }) }) }) From c301bb21ab3d93d49c9c85b3dfb7cbf8e026d3ca Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Tue, 8 Aug 2023 14:42:39 -0400 Subject: [PATCH 45/51] added global requires oracle flag --- .../BasicQuotePolicyManager.sol | 3 + .../policyManagers/DataTypesBasicPolicies.sol | 2 + test/peer-to-peer/local-tests.ts | 381 +++++++++--------- 3 files changed, 199 insertions(+), 187 deletions(-) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 6df06a36..7af75626 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -157,6 +157,9 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { singlePolicy.minNumOfSignersOverwrite ); } else { + if (globalPolicy.requiresOracle && !hasOracle) { + return (false, 0); + } // @dev: check against global min/max loanPerCollUnitOrLtv only if pair has oracle return ( _isAllowedWithBounds( diff --git a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol index 42ef20cb..d6dbaf16 100644 --- a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol +++ b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol @@ -23,6 +23,8 @@ library DataTypesBasicPolicies { // Flag indicating if all pairs are allowed (=true) or // only pairs with explicitly defined pair policy (=false), default case bool allowAllPairs; + //Flag indicating if an oracle is required for the pair + bool requiresOracle; // Applicable global bounds QuoteBounds quoteBounds; } diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 73f38435..11f8e10b 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -5946,7 +5946,7 @@ describe('Peer-to-Peer: Local Tests', function () { ) }) - it('Should handle simple policy implementation', async function () { + it('Should handle basic policy implementation', async function () { const { quoteHandler, addressRegistry, borrowerGateway, lender, borrower, team, usdc, weth, lenderVault } = await setupTest() @@ -6003,38 +6003,52 @@ describe('Peer-to-Peer: Local Tests', function () { 'OnChainQuoteAdded' ) - const policy = { - isSet: true, - requiresOracle: false, - policyType: 0, - minNumSigners: 0, + const quoteBounds = { minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(25), + maxTenor: ONE_DAY.mul(35), minFee: 0, - minAPR: 0, - minAllowableLoanPerCollUnitOrLtv: BASE.div(10), - maxAllowableLoanPerCollUnitOrLtv: BASE.div(2) + minApr: 0, + minEarliestRepayTenor: 0, + minLoanPerCollUnitOrLtv: BASE.div(10), + maxLoanPerCollUnitOrLtv: BASE.div(2) } - // should revert if unregistered vault + const globalPolicyData = ethers.utils.defaultAbiCoder.encode( + [ + 'bool allowAllPairs', + 'bool requiresOracle', + 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' + ], + [true, false, quoteBounds] + ) + + const pairPolicyData = ethers.utils.defaultAbiCoder.encode( + [ + 'bool requiresOracle', + 'uint8 minNumOfSignersOverwrite', + 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' + ], + [true, 1, quoteBounds] + ) + + //should revert if unregistered vault await expect( - basicPolicyManager.connect(team).setPolicyForPair(borrower.address, weth.address, usdc.address, policy) + basicPolicyManager.connect(team).setPairPolicy(borrower.address, weth.address, usdc.address, pairPolicyData) ).to.be.revertedWithCustomError(basicPolicyManager, 'UnregisteredVault') // should revert if sender is not vault owner await expect( - basicPolicyManager.connect(borrower).setPolicyForPair(lenderVault.address, weth.address, usdc.address, policy) + basicPolicyManager.connect(borrower).setPairPolicy(lenderVault.address, weth.address, usdc.address, pairPolicyData) ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') // should revert if unregistered vault - await expect(basicPolicyManager.connect(team).setDefaultPolicy(borrower.address, 1)).to.be.revertedWithCustomError( - basicPolicyManager, - 'UnregisteredVault' - ) + await expect( + basicPolicyManager.connect(team).setGlobalPolicy(borrower.address, globalPolicyData) + ).to.be.revertedWithCustomError(basicPolicyManager, 'UnregisteredVault') // should revert if sender is not vault owner await expect( - basicPolicyManager.connect(borrower).setDefaultPolicy(lenderVault.address, 1) + basicPolicyManager.connect(borrower).setGlobalPolicy(lenderVault.address, globalPolicyData) ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') // borrower approves gateway and executes quote @@ -6053,33 +6067,26 @@ describe('Peer-to-Peer: Local Tests', function () { mysoTokenManagerData: ZERO_BYTES32 } - /*** Test default policy ***/ - - // before default policy or policy is set this should go through as default policy is by default, allow all - await borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - - // set default policy to only allow off-chain quotes - await basicPolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 2) + /*** Test global policy ***/ + // before global policy or pair policy is set this should revert await expect( borrowerGateway .connect(borrower) .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - // set policy to allow no quotes without a policy - await basicPolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 3) + expect(await basicPolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.false - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + // set global policy + await basicPolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, globalPolicyData) + + expect(await basicPolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.true - // set policy to allow on chain quotes without a policy and borrow should go through again - await basicPolicyManager.connect(lender).setDefaultPolicy(lenderVault.address, 1) + // should revert if global policy is set again + await expect( + basicPolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, globalPolicyData) + ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyAlreadySet') await borrowerGateway .connect(borrower) @@ -6099,157 +6106,157 @@ describe('Peer-to-Peer: Local Tests', function () { maxAllowableLoanPerCollUnitOrLtv: BASE.div(2) } - // should default if not set - await expect( - basicPolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, isSet: false }) - ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') - - // should default if min tenor < max tenor - await expect( - basicPolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, maxTenor: ONE_DAY.div(2) }) - ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidTenors') - - // should default if min ltv > max ltv - await expect( - basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) - }) - ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidLoanPerCollOrLtv') - - // policy set correctly - await expect( - basicPolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: true }) - ).to.emit(basicPolicyManager, 'PolicySet') - - // borrow should fail without oracle address - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - await basicPolicyManager - .connect(lender) - .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: false }) - - // borrow fail if tenor less than min tenor - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(25) - }) - - // borrow fail if tenor greater than max tenor - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minFee: BASE.div(5) - }) - - // borrow should fail if upfront fee is below min fee - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAPR: BASE.div(5) - }) - - // borrow should fail if apr is below min apr - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAllowableLoanPerCollUnitOrLtv: BASE.mul(4).div(5), - maxAllowableLoanPerCollUnitOrLtv: BASE.mul(9).div(10) - }) - - // borrow should fail if loan per coll unit is below min allowable loan per coll unit - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAllowableLoanPerCollUnitOrLtv: ONE_USDC, - maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(800) - }) - - // borrow should fail if loan per coll unit is above max allowable loan per coll unit - await expect( - borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - ...policy2, - minTenor: ONE_DAY, - maxTenor: ONE_DAY.mul(35), - minAllowableLoanPerCollUnitOrLtv: ONE_USDC, - maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(1000) - }) - - // borrow should go through as all conditions are met - await borrowerGateway - .connect(borrower) - .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - - const currPolicyPreDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) - expect(currPolicyPreDelete.isSet).to.be.true - - await expect( - basicPolicyManager.connect(lender).deletePolicyForPair(borrower.address, weth.address, usdc.address) - ).to.be.revertedWithCustomError(basicPolicyManager, 'UnregisteredVault') - - await expect( - basicPolicyManager.connect(team).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') - - await basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - - const currPolicyPostDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) - - expect(currPolicyPostDelete.isSet).to.be.false - - await expect( - basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') + // // should default if not set + // await expect( + // basicPolicyManager + // .connect(lender) + // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, isSet: false }) + // ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') + + // // should default if min tenor < max tenor + // await expect( + // basicPolicyManager + // .connect(lender) + // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, maxTenor: ONE_DAY.div(2) }) + // ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidTenors') + + // // should default if min ltv > max ltv + // await expect( + // basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + // ...policy2, + // maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) + // }) + // ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidLoanPerCollOrLtv') + + // // policy set correctly + // await expect( + // basicPolicyManager + // .connect(lender) + // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: true }) + // ).to.emit(basicPolicyManager, 'PolicySet') + + // // borrow should fail without oracle address + // await expect( + // borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // await basicPolicyManager + // .connect(lender) + // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: false }) + + // // borrow fail if tenor less than min tenor + // await expect( + // borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + // ...policy2, + // minTenor: ONE_DAY, + // maxTenor: ONE_DAY.mul(25) + // }) + + // // borrow fail if tenor greater than max tenor + // await expect( + // borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + // ...policy2, + // minTenor: ONE_DAY, + // maxTenor: ONE_DAY.mul(35), + // minFee: BASE.div(5) + // }) + + // // borrow should fail if upfront fee is below min fee + // await expect( + // borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + // ...policy2, + // minTenor: ONE_DAY, + // maxTenor: ONE_DAY.mul(35), + // minAPR: BASE.div(5) + // }) + + // // borrow should fail if apr is below min apr + // await expect( + // borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + // ...policy2, + // minTenor: ONE_DAY, + // maxTenor: ONE_DAY.mul(35), + // minAllowableLoanPerCollUnitOrLtv: BASE.mul(4).div(5), + // maxAllowableLoanPerCollUnitOrLtv: BASE.mul(9).div(10) + // }) + + // // borrow should fail if loan per coll unit is below min allowable loan per coll unit + // await expect( + // borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + // ...policy2, + // minTenor: ONE_DAY, + // maxTenor: ONE_DAY.mul(35), + // minAllowableLoanPerCollUnitOrLtv: ONE_USDC, + // maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(800) + // }) + + // // borrow should fail if loan per coll unit is above max allowable loan per coll unit + // await expect( + // borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { + // ...policy2, + // minTenor: ONE_DAY, + // maxTenor: ONE_DAY.mul(35), + // minAllowableLoanPerCollUnitOrLtv: ONE_USDC, + // maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(1000) + // }) + + // // borrow should go through as all conditions are met + // await borrowerGateway + // .connect(borrower) + // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + + // const currPolicyPreDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) + // expect(currPolicyPreDelete.isSet).to.be.true + + // await expect( + // basicPolicyManager.connect(lender).deletePolicyForPair(borrower.address, weth.address, usdc.address) + // ).to.be.revertedWithCustomError(basicPolicyManager, 'UnregisteredVault') + + // await expect( + // basicPolicyManager.connect(team).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + // ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') + + // await basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + + // const currPolicyPostDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) + + // expect(currPolicyPostDelete.isSet).to.be.false + + // await expect( + // basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) + // ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') }) }) }) From da873c4ac121d520ef6a2c3054fe29a0cc7b29c7 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 20:49:04 +0200 Subject: [PATCH 46/51] added getters --- contracts/Errors.sol | 1 + contracts/peer-to-peer/QuoteHandler.sol | 2 +- .../IBasicQuotePolicyManager.sol | 52 +++++++++++++ .../IQuotePolicyManager.sol | 2 +- .../BasicQuotePolicyManager.sol | 76 ++++++++++++++----- 5 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 contracts/peer-to-peer/interfaces/policyManagers/IBasicQuotePolicyManager.sol rename contracts/peer-to-peer/interfaces/{ => policyManagers}/IQuotePolicyManager.sol (96%) diff --git a/contracts/Errors.sol b/contracts/Errors.sol index 46860a09..751b8efe 100644 --- a/contracts/Errors.sol +++ b/contracts/Errors.sol @@ -130,4 +130,5 @@ library Errors { error InvalidTenors(); error InvalidLoanPerCollOrLtv(); error InvalidMinApr(); + error NoPolicy(); } diff --git a/contracts/peer-to-peer/QuoteHandler.sol b/contracts/peer-to-peer/QuoteHandler.sol index a238fff2..54141f6d 100644 --- a/contracts/peer-to-peer/QuoteHandler.sol +++ b/contracts/peer-to-peer/QuoteHandler.sol @@ -10,7 +10,7 @@ import {Helpers} from "../Helpers.sol"; import {IAddressRegistry} from "./interfaces/IAddressRegistry.sol"; import {ILenderVaultImpl} from "./interfaces/ILenderVaultImpl.sol"; import {IQuoteHandler} from "./interfaces/IQuoteHandler.sol"; -import {IQuotePolicyManager} from "./interfaces/IQuotePolicyManager.sol"; +import {IQuotePolicyManager} from "./interfaces/policyManagers/IQuotePolicyManager.sol"; contract QuoteHandler is IQuoteHandler { using ECDSA for bytes32; diff --git a/contracts/peer-to-peer/interfaces/policyManagers/IBasicQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/policyManagers/IBasicQuotePolicyManager.sol new file mode 100644 index 00000000..7e73add4 --- /dev/null +++ b/contracts/peer-to-peer/interfaces/policyManagers/IBasicQuotePolicyManager.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {DataTypesBasicPolicies} from "../../policyManagers/DataTypesBasicPolicies.sol"; +import {IQuotePolicyManager} from "./IQuotePolicyManager.sol"; + +interface IBasicQuotePolicyManager is IQuotePolicyManager { + /** + * @notice Retrieve the global quoting policy for a specific lender's vault + * @param lenderVault The address of the lender's vault + * @return The global quoting policy for the specified lender's vault + */ + function globalQuotingPolicy( + address lenderVault + ) external view returns (DataTypesBasicPolicies.GlobalPolicy memory); + + /** + * @notice Retrieve the quoting policy for a specific lending pair involving collateral and loan tokens + * @param lenderVault The address of the lender's vault + * @param collToken The address of the collateral token + * @param loanToken The address of the loan token + * @return The quoting policy for the specified lender's vault, collateral, and loan tokens + */ + function pairQuotingPolicy( + address lenderVault, + address collToken, + address loanToken + ) external view returns (DataTypesBasicPolicies.PairPolicy memory); + + /** + * @notice Check if there is a global quoting policy for a specific lender's vault + * @param lenderVault The address of the lender's vault + * @return True if there is a global quoting policy, false otherwise + */ + function hasGlobalQuotingPolicy( + address lenderVault + ) external view returns (bool); + + /** + * @notice Check if there is a quoting policy for a specific lending pair involving collateral and loan tokens + * @param lenderVault The address of the lender's vault + * @param collToken The address of the collateral token + * @param loanToken The address of the loan token + * @return True if there is a quoting policy for the specified lender's vault, collateral, and loan tokens, false otherwise + */ + function hasPairQuotingPolicy( + address lenderVault, + address collToken, + address loanToken + ) external view returns (bool); +} diff --git a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol b/contracts/peer-to-peer/interfaces/policyManagers/IQuotePolicyManager.sol similarity index 96% rename from contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol rename to contracts/peer-to-peer/interfaces/policyManagers/IQuotePolicyManager.sol index 819fd2fe..40fcc89d 100644 --- a/contracts/peer-to-peer/interfaces/IQuotePolicyManager.sol +++ b/contracts/peer-to-peer/interfaces/policyManagers/IQuotePolicyManager.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; -import {DataTypesPeerToPeer} from "../DataTypesPeerToPeer.sol"; +import {DataTypesPeerToPeer} from "../../DataTypesPeerToPeer.sol"; interface IQuotePolicyManager { event PairPolicySet( diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 6df06a36..67db9906 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -10,16 +10,16 @@ import {Constants} from "../../Constants.sol"; import {Errors} from "../../Errors.sol"; import {IAddressRegistry} from "../interfaces/IAddressRegistry.sol"; import {ILenderVaultImpl} from "../interfaces/ILenderVaultImpl.sol"; -import {IQuotePolicyManager} from "../interfaces/IQuotePolicyManager.sol"; +import {IQuotePolicyManager} from "../interfaces/policyManagers/IQuotePolicyManager.sol"; contract BasicQuotePolicyManager is IQuotePolicyManager { mapping(address => DataTypesBasicPolicies.GlobalPolicy) - public globalQuotingPolicies; + internal _globalQuotingPolicies; mapping(address => mapping(address => mapping(address => DataTypesBasicPolicies.PairPolicy))) - public pairQuotingPolicies; - mapping(address => bool) public hasGlobalQuotingPolicy; + internal _pairQuotingPolicies; + mapping(address => bool) internal _hasGlobalQuotingPolicy; mapping(address => mapping(address => mapping(address => bool))) - public hasPairQuotingPolicy; + internal _hasPairQuotingPolicy; address public immutable addressRegistry; constructor(address _addressRegistry) { @@ -40,7 +40,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { (DataTypesBasicPolicies.GlobalPolicy) ); DataTypesBasicPolicies.GlobalPolicy - memory currGlobalPolicy = globalQuotingPolicies[lenderVault]; + memory currGlobalPolicy = _globalQuotingPolicies[lenderVault]; if ( globalPolicy.allowAllPairs == currGlobalPolicy.allowAllPairs && _equalQuoteBounds( @@ -51,16 +51,16 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { revert Errors.PolicyAlreadySet(); } _checkNewQuoteBounds(globalPolicy.quoteBounds); - if (!hasGlobalQuotingPolicy[lenderVault]) { - hasGlobalQuotingPolicy[lenderVault] = true; + if (!_hasGlobalQuotingPolicy[lenderVault]) { + _hasGlobalQuotingPolicy[lenderVault] = true; } - globalQuotingPolicies[lenderVault] = globalPolicy; + _globalQuotingPolicies[lenderVault] = globalPolicy; } else { - if (!hasGlobalQuotingPolicy[lenderVault]) { + if (!_hasGlobalQuotingPolicy[lenderVault]) { revert Errors.NoPolicyToDelete(); } - delete hasGlobalQuotingPolicy[lenderVault]; - delete globalQuotingPolicies[lenderVault]; + delete _hasGlobalQuotingPolicy[lenderVault]; + delete _globalQuotingPolicies[lenderVault]; } emit GlobalPolicySet(lenderVault, globalPolicyData); } @@ -77,16 +77,16 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { revert Errors.InvalidAddress(); } mapping(address => bool) - storage _hasSingleQuotingPolicy = hasPairQuotingPolicy[lenderVault][ - collToken - ]; + storage _hasSingleQuotingPolicy = _hasPairQuotingPolicy[ + lenderVault + ][collToken]; if (pairPolicyData.length > 0) { DataTypesBasicPolicies.PairPolicy memory singlePolicy = abi.decode( pairPolicyData, (DataTypesBasicPolicies.PairPolicy) ); DataTypesBasicPolicies.PairPolicy - memory currSinglePolicy = pairQuotingPolicies[lenderVault][ + memory currSinglePolicy = _pairQuotingPolicies[lenderVault][ collToken ][loanToken]; if ( @@ -105,7 +105,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { if (!_hasSingleQuotingPolicy[loanToken]) { _hasSingleQuotingPolicy[loanToken] = true; } - pairQuotingPolicies[lenderVault][collToken][ + _pairQuotingPolicies[lenderVault][collToken][ loanToken ] = singlePolicy; } else { @@ -113,7 +113,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { revert Errors.NoPolicyToDelete(); } delete _hasSingleQuotingPolicy[loanToken]; - delete pairQuotingPolicies[lenderVault][collToken][loanToken]; + delete _pairQuotingPolicies[lenderVault][collToken][loanToken]; } emit PairPolicySet(lenderVault, collToken, loanToken, pairPolicyData); } @@ -129,8 +129,8 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { returns (bool _isAllowed, uint256 minNumOfSignersOverwrite) { DataTypesBasicPolicies.GlobalPolicy - memory globalPolicy = globalQuotingPolicies[lenderVault]; - bool hasSinglePolicy = hasPairQuotingPolicy[lenderVault][ + memory globalPolicy = _globalQuotingPolicies[lenderVault]; + bool hasSinglePolicy = _hasPairQuotingPolicy[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; if (!globalPolicy.allowAllPairs && !hasSinglePolicy) { @@ -141,7 +141,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { bool hasOracle = generalQuoteInfo.oracleAddr != address(0); if (hasSinglePolicy) { DataTypesBasicPolicies.PairPolicy - memory singlePolicy = pairQuotingPolicies[lenderVault][ + memory singlePolicy = _pairQuotingPolicies[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; if (singlePolicy.requiresOracle && !hasOracle) { @@ -170,6 +170,40 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { } } + function globalQuotingPolicy( + address lenderVault + ) external view returns (DataTypesBasicPolicies.GlobalPolicy memory) { + if (!_hasGlobalQuotingPolicy[lenderVault]) { + revert Errors.NoPolicy(); + } + return _globalQuotingPolicies[lenderVault]; + } + + function pairQuotingPolicy( + address lenderVault, + address collToken, + address loanToken + ) external view returns (DataTypesBasicPolicies.PairPolicy memory) { + if (!_hasPairQuotingPolicy[lenderVault][collToken][loanToken]) { + revert Errors.NoPolicy(); + } + return _pairQuotingPolicies[lenderVault][collToken][loanToken]; + } + + function hasGlobalQuotingPolicy( + address lenderVault + ) external view returns (bool) { + return _hasGlobalQuotingPolicy[lenderVault]; + } + + function hasPairQuotingPolicy( + address lenderVault, + address collToken, + address loanToken + ) external view returns (bool) { + return _hasPairQuotingPolicy[lenderVault][collToken][loanToken]; + } + function _checkIsVaultAndSenderIsOwner(address lenderVault) internal view { if (!IAddressRegistry(addressRegistry).isRegisteredVault(lenderVault)) { revert Errors.UnregisteredVault(); From bbc888eca35a79e24f1129024defa3e13304e3b4 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 21:04:22 +0200 Subject: [PATCH 47/51] slighlty simplified global vs pair policy checks --- .../BasicQuotePolicyManager.sol | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 1bca2111..51e58540 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -130,47 +130,49 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { { DataTypesBasicPolicies.GlobalPolicy memory globalPolicy = _globalQuotingPolicies[lenderVault]; - bool hasSinglePolicy = _hasPairQuotingPolicy[lenderVault][ + bool hasPairPolicy = _hasPairQuotingPolicy[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; - if (!globalPolicy.allowAllPairs && !hasSinglePolicy) { + if (!globalPolicy.allowAllPairs && !hasPairPolicy) { return (false, 0); } // @dev: pair policy (if defined) takes precedence over global policy bool hasOracle = generalQuoteInfo.oracleAddr != address(0); - if (hasSinglePolicy) { + bool requiresOracle; + DataTypesBasicPolicies.QuoteBounds memory quoteBounds; + bool checkLoanPerCollUnitOrLtv; + if (hasPairPolicy) { DataTypesBasicPolicies.PairPolicy memory singlePolicy = _pairQuotingPolicies[lenderVault][ generalQuoteInfo.collToken ][generalQuoteInfo.loanToken]; - if (singlePolicy.requiresOracle && !hasOracle) { - return (false, 0); - } - return ( - _isAllowedWithBounds( - singlePolicy.quoteBounds, - quoteTuple, - generalQuoteInfo.earliestRepayTenor, - true - ), - singlePolicy.minNumOfSignersOverwrite - ); + requiresOracle = singlePolicy.requiresOracle; + quoteBounds = singlePolicy.quoteBounds; + // @dev: in case of pair policy always check against min/max loanPerCollUnitOrLtv + checkLoanPerCollUnitOrLtv = true; + minNumOfSignersOverwrite = singlePolicy.minNumOfSignersOverwrite; } else { - if (globalPolicy.requiresOracle && !hasOracle) { - return (false, 0); - } - // @dev: check against global min/max loanPerCollUnitOrLtv only if pair has oracle - return ( - _isAllowedWithBounds( - globalPolicy.quoteBounds, - quoteTuple, - generalQuoteInfo.earliestRepayTenor, - hasOracle - ), - 0 - ); + requiresOracle = globalPolicy.requiresOracle; + quoteBounds = globalPolicy.quoteBounds; + // @dev: in case of global policy, only check against global min/max loanPerCollUnitOrLtv if + // pair has oracle + checkLoanPerCollUnitOrLtv = hasOracle; } + + if (requiresOracle && !hasOracle) { + return (false, 0); + } + + return ( + _isAllowedWithBounds( + quoteBounds, + quoteTuple, + generalQuoteInfo.earliestRepayTenor, + checkLoanPerCollUnitOrLtv + ), + minNumOfSignersOverwrite + ); } function globalQuotingPolicy( From 630b96f6cd18ae17d8d7f5e863f98569c59bfc88 Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Tue, 8 Aug 2023 16:58:35 -0400 Subject: [PATCH 48/51] added tests for global policy --- .../BasicQuotePolicyManager.sol | 2 + test/peer-to-peer/helpers/misc.ts | 36 ++ test/peer-to-peer/local-tests.ts | 374 +++++++++--------- 3 files changed, 228 insertions(+), 184 deletions(-) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 51e58540..f590ad5f 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -43,6 +43,8 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { memory currGlobalPolicy = _globalQuotingPolicies[lenderVault]; if ( globalPolicy.allowAllPairs == currGlobalPolicy.allowAllPairs && + globalPolicy.requiresOracle == + currGlobalPolicy.requiresOracle && _equalQuoteBounds( globalPolicy.quoteBounds, currGlobalPolicy.quoteBounds diff --git a/test/peer-to-peer/helpers/misc.ts b/test/peer-to-peer/helpers/misc.ts index a95d4dd3..54a02cea 100644 --- a/test/peer-to-peer/helpers/misc.ts +++ b/test/peer-to-peer/helpers/misc.ts @@ -324,3 +324,39 @@ export const findBalanceSlot = async (erc20: any) => { await ethers.provider.send('evm_revert', [snapshot]) } } + +export type QuoteBounds = { + minTenor: BigNumber + maxTenor: BigNumber + minFee: BigNumber + minApr: BigNumber + minEarliestRepayTenor: BigNumber + minLoanPerCollUnitOrLtv: BigNumber + maxLoanPerCollUnitOrLtv: BigNumber +} + +export const encodeGlobalPolicy = (allowAllPairs: boolean, requiresOracle: boolean, quoteBounds: QuoteBounds): string => { + return ethers.utils.defaultAbiCoder.encode( + [ + 'bool allowAllPairs', + 'bool requiresOracle', + 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' + ], + [allowAllPairs, requiresOracle, quoteBounds] + ) +} + +export const encodePairPolicy = ( + requiresOracle: boolean, + minNumOfSignersOverwrite: number, + quoteBounds: QuoteBounds +): string => { + return ethers.utils.defaultAbiCoder.encode( + [ + 'bool requiresOracle', + 'uint8 minNumOfSignersOverwrite', + 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' + ], + [requiresOracle, minNumOfSignersOverwrite, quoteBounds] + ) +} diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 11f8e10b..25612174 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -4,7 +4,7 @@ import { StandardMerkleTree } from '@openzeppelin/merkle-tree' import { LenderVaultImpl, MyERC20 } from '../../typechain-types' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { payloadScheme } from './helpers/abi' -import { setupBorrowerWhitelist, getSlot, findBalanceSlot } from './helpers/misc' +import { setupBorrowerWhitelist, getSlot, findBalanceSlot, encodeGlobalPolicy, encodePairPolicy } from './helpers/misc' import { HARDHAT_CHAIN_ID_AND_FORKING_CONFIG } from '../../hardhat.config' import { erc20 } from '../../typechain-types/@openzeppelin/contracts/token' @@ -5946,7 +5946,7 @@ describe('Peer-to-Peer: Local Tests', function () { ) }) - it('Should handle basic policy implementation', async function () { + it('Should handle basic global policy implementation', async function () { const { quoteHandler, addressRegistry, borrowerGateway, lender, borrower, team, usdc, weth, lenderVault } = await setupTest() @@ -6006,30 +6006,16 @@ describe('Peer-to-Peer: Local Tests', function () { const quoteBounds = { minTenor: ONE_DAY, maxTenor: ONE_DAY.mul(35), - minFee: 0, - minApr: 0, - minEarliestRepayTenor: 0, + minFee: ethers.BigNumber.from(0), + minApr: ethers.BigNumber.from(0), + minEarliestRepayTenor: ethers.BigNumber.from(0), minLoanPerCollUnitOrLtv: BASE.div(10), maxLoanPerCollUnitOrLtv: BASE.div(2) } - const globalPolicyData = ethers.utils.defaultAbiCoder.encode( - [ - 'bool allowAllPairs', - 'bool requiresOracle', - 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' - ], - [true, false, quoteBounds] - ) + const globalPolicyData = encodeGlobalPolicy(true, false, quoteBounds) - const pairPolicyData = ethers.utils.defaultAbiCoder.encode( - [ - 'bool requiresOracle', - 'uint8 minNumOfSignersOverwrite', - 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' - ], - [true, 1, quoteBounds] - ) + const pairPolicyData = encodePairPolicy(true, 1, quoteBounds) //should revert if unregistered vault await expect( @@ -6078,6 +6064,10 @@ describe('Peer-to-Peer: Local Tests', function () { expect(await basicPolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.false + await expect( + basicPolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, '0x') + ).to.be.revertedWithCustomError(basicPolicyManager, 'NoPolicyToDelete') + // set global policy await basicPolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, globalPolicyData) @@ -6092,171 +6082,187 @@ describe('Peer-to-Peer: Local Tests', function () { .connect(borrower) .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - /*** Test specific policy ***/ - const policy2 = { - isSet: true, - requiresOracle: false, - policyType: 0, - minNumSigners: 0, - minTenor: ONE_DAY.mul(35), - maxTenor: ONE_DAY.mul(35), - minFee: 0, - minAPR: 0, - minAllowableLoanPerCollUnitOrLtv: BASE.div(10), - maxAllowableLoanPerCollUnitOrLtv: BASE.div(2) + await expect( + basicPolicyManager + .connect(lender) + .setGlobalPolicy( + lenderVault.address, + encodeGlobalPolicy(true, false, { ...quoteBounds, minTenor: ONE_DAY.mul(40) }) + ) + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidTenors') + + await expect( + basicPolicyManager + .connect(lender) + .setGlobalPolicy( + lenderVault.address, + encodeGlobalPolicy(true, false, { ...quoteBounds, minLoanPerCollUnitOrLtv: BASE }) + ) + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidLoanPerCollOrLtv') + + await expect( + basicPolicyManager + .connect(lender) + .setGlobalPolicy(lenderVault.address, encodeGlobalPolicy(true, false, { ...quoteBounds, minApr: BASE.mul(-2) })) + ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidMinApr') + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy(lenderVault.address, encodeGlobalPolicy(true, true, quoteBounds)) + + // borrow should fail without oracle address + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy(lenderVault.address, encodeGlobalPolicy(true, false, { ...quoteBounds, minTenor: ONE_DAY.mul(35) })) + + // borrow fail if tenor less than min tenor + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy(lenderVault.address, encodeGlobalPolicy(true, false, { ...quoteBounds, maxTenor: ONE_DAY.mul(25) })) + + // borrow fail if tenor greater than max tenor + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy( + lenderVault.address, + encodeGlobalPolicy(true, false, { ...quoteBounds, minFee: BASE.mul(9).div(10) }) + ) + + // borrow should fail if upfront fee is below min fee + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy(lenderVault.address, encodeGlobalPolicy(true, false, { ...quoteBounds, minApr: BASE.mul(10) })) + + // borrow should fail if apr is below min apr + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy( + lenderVault.address, + encodeGlobalPolicy(true, false, { + ...quoteBounds, + minLoanPerCollUnitOrLtv: BASE.mul(4).div(5), + maxLoanPerCollUnitOrLtv: BASE.mul(9).div(10) + }) + ) + + // borrow should go through even if loanPerCollUnitOrLtv is below minLoanPerCollUnitOrLtv if no oracle required + await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy( + lenderVault.address, + encodeGlobalPolicy(true, false, { + ...quoteBounds, + minLoanPerCollUnitOrLtv: ethers.BigNumber.from(0), + maxLoanPerCollUnitOrLtv: ethers.BigNumber.from(0) + }) + ) + + // borrow should go through even if loanPerCollUnitOrLtv is above maxLoanPerCollUnitOrLtv if no oracle required + await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) + + let quoteTuples2 = [ + { + loanPerCollUnitOrLtv: loanPerCollUnit, + interestRatePctInBase: BASE.div(100).mul(-1), + upfrontFeePctInBase: BASE.div(10), + tenor: ONE_DAY.mul(30) + } + ] + + let onChainQuote2 = { + generalQuoteInfo: { + collToken: weth.address, + loanToken: usdc.address, + oracleAddr: ZERO_ADDRESS, + minLoan: 1, + maxLoan: MAX_UINT256, + validUntil: MAX_UINT256, + earliestRepayTenor: 0, + borrowerCompartmentImplementation: ZERO_ADDRESS, + isSingleUse: false, + whitelistAddr: ZERO_ADDRESS, + isWhitelistAddrSingleBorrower: false + }, + quoteTuples: quoteTuples2, + salt: ZERO_BYTES32 } - // // should default if not set - // await expect( - // basicPolicyManager - // .connect(lender) - // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, isSet: false }) - // ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') - - // // should default if min tenor < max tenor - // await expect( - // basicPolicyManager - // .connect(lender) - // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, maxTenor: ONE_DAY.div(2) }) - // ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidTenors') - - // // should default if min ltv > max ltv - // await expect( - // basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - // ...policy2, - // maxAllowableLoanPerCollUnitOrLtv: BASE.div(100) - // }) - // ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidLoanPerCollOrLtv') - - // // policy set correctly - // await expect( - // basicPolicyManager - // .connect(lender) - // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: true }) - // ).to.emit(basicPolicyManager, 'PolicySet') - - // // borrow should fail without oracle address - // await expect( - // borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // await basicPolicyManager - // .connect(lender) - // .setPolicyForPair(lenderVault.address, weth.address, usdc.address, { ...policy2, requiresOracle: false }) - - // // borrow fail if tenor less than min tenor - // await expect( - // borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - // ...policy2, - // minTenor: ONE_DAY, - // maxTenor: ONE_DAY.mul(25) - // }) - - // // borrow fail if tenor greater than max tenor - // await expect( - // borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - // ...policy2, - // minTenor: ONE_DAY, - // maxTenor: ONE_DAY.mul(35), - // minFee: BASE.div(5) - // }) - - // // borrow should fail if upfront fee is below min fee - // await expect( - // borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - // ...policy2, - // minTenor: ONE_DAY, - // maxTenor: ONE_DAY.mul(35), - // minAPR: BASE.div(5) - // }) - - // // borrow should fail if apr is below min apr - // await expect( - // borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - // ...policy2, - // minTenor: ONE_DAY, - // maxTenor: ONE_DAY.mul(35), - // minAllowableLoanPerCollUnitOrLtv: BASE.mul(4).div(5), - // maxAllowableLoanPerCollUnitOrLtv: BASE.mul(9).div(10) - // }) - - // // borrow should fail if loan per coll unit is below min allowable loan per coll unit - // await expect( - // borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - // ...policy2, - // minTenor: ONE_DAY, - // maxTenor: ONE_DAY.mul(35), - // minAllowableLoanPerCollUnitOrLtv: ONE_USDC, - // maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(800) - // }) - - // // borrow should fail if loan per coll unit is above max allowable loan per coll unit - // await expect( - // borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - // ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') - - // await basicPolicyManager.connect(lender).setPolicyForPair(lenderVault.address, weth.address, usdc.address, { - // ...policy2, - // minTenor: ONE_DAY, - // maxTenor: ONE_DAY.mul(35), - // minAllowableLoanPerCollUnitOrLtv: ONE_USDC, - // maxAllowableLoanPerCollUnitOrLtv: ONE_USDC.mul(1000) - // }) - - // // borrow should go through as all conditions are met - // await borrowerGateway - // .connect(borrower) - // .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote1, quoteTupleIdx1) - - // const currPolicyPreDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) - // expect(currPolicyPreDelete.isSet).to.be.true - - // await expect( - // basicPolicyManager.connect(lender).deletePolicyForPair(borrower.address, weth.address, usdc.address) - // ).to.be.revertedWithCustomError(basicPolicyManager, 'UnregisteredVault') - - // await expect( - // basicPolicyManager.connect(team).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - // ).to.be.revertedWithCustomError(basicPolicyManager, 'InvalidSender') - - // await basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - - // const currPolicyPostDelete = await basicPolicyManager.policies(lenderVault.address, weth.address, usdc.address) - - // expect(currPolicyPostDelete.isSet).to.be.false - - // await expect( - // basicPolicyManager.connect(lender).deletePolicyForPair(lenderVault.address, weth.address, usdc.address) - // ).to.be.revertedWithCustomError(basicPolicyManager, 'PolicyNotSet') + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote2)).to.emit( + quoteHandler, + 'OnChainQuoteAdded' + ) + + await basicPolicyManager + .connect(lender) + .setGlobalPolicy( + lenderVault.address, + encodeGlobalPolicy(true, false, { + ...quoteBounds, + minApr: BASE.mul(-99).div(100), + minEarliestRepayTenor: ONE_DAY.mul(20) + }) + ) + + // borrow with negative interest rate should fail if earliest min repay is too short + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote2, quoteTupleIdx1) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + let onChainQuote3 = { + ...onChainQuote2, + generalQuoteInfo: { + ...onChainQuote2.generalQuoteInfo, + earliestRepayTenor: ONE_DAY.mul(20) + } + } + + await expect(quoteHandler.connect(lender).addOnChainQuote(lenderVault.address, onChainQuote3)).to.emit( + quoteHandler, + 'OnChainQuoteAdded' + ) + + // borrow with negative interest rate should go through if earliest min repay is long enough + await borrowerGateway + .connect(borrower) + .borrowWithOnChainQuote(lenderVault.address, borrowInstructions1, onChainQuote3, quoteTupleIdx1) }) }) }) From 2869ef553369bbac242cdfc5246cd1bb752494e2 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 23:31:40 +0200 Subject: [PATCH 49/51] added tests for policy with oracles --- .../BasicQuotePolicyManager.sol | 2 +- .../policyManagers/DataTypesBasicPolicies.sol | 4 +- test/peer-to-peer/local-tests.ts | 11 +- test/peer-to-peer/mainnet-forked-tests.ts | 438 ++++++++++++++++++ 4 files changed, 446 insertions(+), 9 deletions(-) diff --git a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol index 51e58540..302fd3f3 100644 --- a/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol +++ b/contracts/peer-to-peer/policyManagers/BasicQuotePolicyManager.sol @@ -277,7 +277,7 @@ contract BasicQuotePolicyManager is IQuotePolicyManager { return false; } - // @dev: if tenor is zero tx is swap and no need to check apr + // @dev: if tenor is zero then tx is swap and no need to check apr if (quoteTuple.tenor > 0) { int256 apr = (quoteTuple.interestRatePctInBase * SafeCast.toInt256(Constants.YEAR_IN_SECONDS)) / diff --git a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol index d6dbaf16..cd2028ce 100644 --- a/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol +++ b/contracts/peer-to-peer/policyManagers/DataTypesBasicPolicies.sol @@ -21,9 +21,9 @@ library DataTypesBasicPolicies { struct GlobalPolicy { // Flag indicating if all pairs are allowed (=true) or - // only pairs with explicitly defined pair policy (=false), default case + // per default only pairs with explicitly defined pair policy (=false) bool allowAllPairs; - //Flag indicating if an oracle is required for the pair + // Flag indicating if an oracle is required for the pair bool requiresOracle; // Applicable global bounds QuoteBounds quoteBounds; diff --git a/test/peer-to-peer/local-tests.ts b/test/peer-to-peer/local-tests.ts index 11f8e10b..ceac9705 100644 --- a/test/peer-to-peer/local-tests.ts +++ b/test/peer-to-peer/local-tests.ts @@ -6,7 +6,6 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { payloadScheme } from './helpers/abi' import { setupBorrowerWhitelist, getSlot, findBalanceSlot } from './helpers/misc' import { HARDHAT_CHAIN_ID_AND_FORKING_CONFIG } from '../../hardhat.config' -import { erc20 } from '../../typechain-types/@openzeppelin/contracts/token' // test config vars let snapshotId: String // use snapshot id to reset state before each test @@ -3433,13 +3432,13 @@ describe('Peer-to-Peer: Local Tests', function () { } await expect( - quoteHandler.connect(lender).broadcastOnChainQuote({ + quoteHandler.connect(lender).publishOnChainQuote({ ...onChainQuote, generalQuoteInfo: { ...onChainQuote.generalQuoteInfo, maxLoan: ethers.BigNumber.from(0) } }) ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') - const proposedQuoteTransaction = await quoteHandler.connect(borrower).broadcastOnChainQuote(onChainQuote) + const proposedQuoteTransaction = await quoteHandler.connect(borrower).publishOnChainQuote(onChainQuote) const proposedQuoteReceipt = await proposedQuoteTransaction.wait() @@ -3463,11 +3462,11 @@ describe('Peer-to-Peer: Local Tests', function () { await expect( quoteHandler.connect(lender).copyPublishedOnChainQuote(lenderVault.address, proposedOnChainQuoteHash) - ).to.be.revertedWithCustomError(quoteHandler, 'InvalidProposedQuoteApproval') + ).to.be.revertedWithCustomError(quoteHandler, 'InvalidQuote') - await expect(quoteHandler.connect(borrower).broadcastOnChainQuote(onChainQuote)).to.be.revertedWithCustomError( + await expect(quoteHandler.connect(borrower).publishOnChainQuote(onChainQuote)).to.be.revertedWithCustomError( quoteHandler, - 'RedundantOnChainQuoteProposed' + 'AlreadyPublished' ) await expect( diff --git a/test/peer-to-peer/mainnet-forked-tests.ts b/test/peer-to-peer/mainnet-forked-tests.ts index b6eadf4d..3adc1d93 100644 --- a/test/peer-to-peer/mainnet-forked-tests.ts +++ b/test/peer-to-peer/mainnet-forked-tests.ts @@ -59,6 +59,88 @@ function getLoopingSendAmount( return collTokenReceivedFromDex + collTokenFromBorrower } +async function generateOffChainQuote({ + lenderVault, + signer, + collTokenAddr, + loanTokenAddr, + oracleAddr, + quoteTuples, + whitelistAuthority = ZERO_ADDR, + offChainQuoteBodyInfo = {}, + generalQuoteInfo = {}, + customSignatures = [], + earliestRepayTenor = 0, + minLoan = 1, + maxLoan = MAX_UINT256 +}: { + lenderVault: any + signer: any + collTokenAddr: any + loanTokenAddr: any + oracleAddr: any + quoteTuples: any[] + whitelistAuthority?: any + offChainQuoteBodyInfo?: any + generalQuoteInfo?: any + customSignatures?: any[] + earliestRepayTenor?: any + minLoan?: any + maxLoan?: any +}) { + // lenderVault owner gives quote + const blocknum = await ethers.provider.getBlockNumber() + const timestamp = (await ethers.provider.getBlock(blocknum)).timestamp + + const quoteTuplesTree = StandardMerkleTree.of( + quoteTuples.map(quoteTuple => Object.values(quoteTuple)), + ['uint256', 'uint256', 'uint256', 'uint256'] + ) + const quoteTuplesRoot = quoteTuplesTree.root + let offChainQuote = { + generalQuoteInfo: { + collToken: collTokenAddr, + loanToken: loanTokenAddr, + oracleAddr: oracleAddr, + minLoan: minLoan, + maxLoan: maxLoan, + validUntil: timestamp + 60, + earliestRepayTenor: earliestRepayTenor, + borrowerCompartmentImplementation: ZERO_ADDR, + isSingleUse: false, + whitelistAddr: whitelistAuthority == ZERO_ADDR ? ZERO_ADDR : whitelistAuthority.address, + isWhitelistAddrSingleBorrower: false, + ...generalQuoteInfo + }, + quoteTuplesRoot: quoteTuplesRoot, + salt: ZERO_BYTES32, + nonce: 0, + compactSigs: [], + ...offChainQuoteBodyInfo + } + + const payload = ethers.utils.defaultAbiCoder.encode(payloadScheme as any, [ + offChainQuote.generalQuoteInfo, + offChainQuote.quoteTuplesRoot, + offChainQuote.salt, + offChainQuote.nonce, + lenderVault.address, + HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId + ]) + + const payloadHash = ethers.utils.keccak256(payload) + const signature = await signer.signMessage(ethers.utils.arrayify(payloadHash)) + const sig = ethers.utils.splitSignature(signature) + const compactSig = sig.compact + + const recoveredAddr = ethers.utils.verifyMessage(ethers.utils.arrayify(payloadHash), sig) + expect(recoveredAddr).to.equal(signer.address) + + // add sig to quote and pass to borrower + offChainQuote.compactSigs = customSignatures.length != 0 ? customSignatures : [compactSig] + return { offChainQuote, quoteTuplesTree, payloadHash } +} + describe('Peer-to-Peer: Forked Mainnet Tests', function () { before(async () => { console.log('Note: Running mainnet tests with the following forking config:') @@ -5174,6 +5256,362 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { expect(vaultWethBalPre.sub(vaultWethBalPost).sub(maxLoanPerColl)).to.equal(0) }) + it('Should handle pair policies correctly', async function () { + const { + borrowerGateway, + quoteHandler, + lender, + signer, + borrower, + weth, + paxg, + ldo, + usdc, + team, + lenderVault, + addressRegistry + } = await setupTest() + + // borrower mints weth + await weth.connect(borrower).deposit({ value: ONE_WETH.mul(100) }) + + // whitelist tokens + await addressRegistry.connect(team).setWhitelistState([weth.address, usdc.address, paxg.address, ldo.address], 1) + + // deploy and set basic quote policy manager + const BasicQuotePolicyManager = await ethers.getContractFactory('BasicQuotePolicyManager') + const basicQuotePolicyManager = await BasicQuotePolicyManager.connect(team).deploy(addressRegistry.address) + await basicQuotePolicyManager.deployed() + await addressRegistry.connect(team).setWhitelistState([basicQuotePolicyManager.address], 10) + + // deploy chainlinkOracleContract + const ChainlinkBasicImplementation = await ethers.getContractFactory('ChainlinkBasic') + const chainlinkBasicImplementation = await ChainlinkBasicImplementation.connect(team).deploy( + [usdc.address, paxg.address], + [usdcEthChainlinkAddr, paxgEthChainlinkAddr], + weth.address, + BASE + ) + await chainlinkBasicImplementation.deployed() + await addressRegistry.connect(team).setWhitelistState([chainlinkBasicImplementation.address], 2) + + // lender deposits + await usdc.connect(lender).transfer(lenderVault.address, MAX_UINT128) + + // lender sets high min num of signers + await lenderVault.connect(lender).setMinNumOfSigners(100) + await lenderVault.connect(lender).addSigners([signer.address]) + + // lender sets policy manager for his vault + await expect( + quoteHandler.connect(lender).updateQuotePolicyManagerForVault(lenderVault.address, basicQuotePolicyManager.address) + ) + .to.emit(quoteHandler, 'QuotePolicyManagerUpdated') + .withArgs(lenderVault.address, basicQuotePolicyManager.address) + + // check initial state + expect(await basicQuotePolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.false + await expect(basicQuotePolicyManager.globalQuotingPolicy(lenderVault.address)).to.be.revertedWithCustomError( + basicQuotePolicyManager, + 'NoPolicy' + ) + expect(await basicQuotePolicyManager.hasPairQuotingPolicy(lenderVault.address, weth.address, usdc.address)).to.be.false + await expect( + basicQuotePolicyManager.pairQuotingPolicy(lenderVault.address, weth.address, usdc.address) + ).to.be.revertedWithCustomError(basicQuotePolicyManager, 'NoPolicy') + + // lender defines global policy + const globalQuoteBounds = { + minTenor: ONE_DAY.mul(11), + maxTenor: ONE_DAY.mul(11), + minFee: BASE.mul(11).div(100), + minApr: BASE.mul(11).div(100), + minEarliestRepayTenor: 0, + minLoanPerCollUnitOrLtv: BASE.mul(11).div(100), + maxLoanPerCollUnitOrLtv: BASE.mul(11).div(100) + } + let allowAllPairs = false + let globalRequiresOracle = false + let globalPolicyData = ethers.utils.defaultAbiCoder.encode( + [ + 'bool allowAllPairs', + 'bool requiresOracle', + 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' + ], + [allowAllPairs, globalRequiresOracle, globalQuoteBounds] + ) + + // set global policy + await basicQuotePolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, globalPolicyData) + expect(await basicQuotePolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.true + const actGlobalPolicyData = await basicQuotePolicyManager.globalQuotingPolicy(lenderVault.address) + expect(actGlobalPolicyData.allowAllPairs).to.be.equal(allowAllPairs) + expect(actGlobalPolicyData.requiresOracle).to.be.equal(globalRequiresOracle) + expect(actGlobalPolicyData.quoteBounds.minTenor).to.be.equal(globalQuoteBounds.minTenor) + expect(actGlobalPolicyData.quoteBounds.maxTenor).to.be.equal(globalQuoteBounds.maxTenor) + expect(actGlobalPolicyData.quoteBounds.minFee).to.be.equal(globalQuoteBounds.minFee) + expect(actGlobalPolicyData.quoteBounds.minApr).to.be.equal(globalQuoteBounds.minApr) + expect(actGlobalPolicyData.quoteBounds.minEarliestRepayTenor).to.be.equal(globalQuoteBounds.minEarliestRepayTenor) + expect(actGlobalPolicyData.quoteBounds.minLoanPerCollUnitOrLtv).to.be.equal(globalQuoteBounds.minLoanPerCollUnitOrLtv) + expect(actGlobalPolicyData.quoteBounds.maxLoanPerCollUnitOrLtv).to.be.equal(globalQuoteBounds.maxLoanPerCollUnitOrLtv) + + // lender produces off-chain quote + let quoteTuples = [ + { + loanPerCollUnitOrLtv: BASE, + interestRatePctInBase: globalQuoteBounds.minApr.mul(ONE_DAY).div(ONE_DAY.mul(365)), + upfrontFeePctInBase: BASE.mul(11).div(100), + tenor: ONE_DAY + }, + { + loanPerCollUnitOrLtv: BASE, + interestRatePctInBase: 0, + upfrontFeePctInBase: BASE.mul(11).div(100), + tenor: ONE_DAY.mul(11) + }, + { + loanPerCollUnitOrLtv: BASE, + interestRatePctInBase: globalQuoteBounds.minApr.mul(ONE_DAY).div(ONE_DAY.mul(365)).add(1), + upfrontFeePctInBase: 0, + tenor: ONE_DAY.mul(11) + }, + { + loanPerCollUnitOrLtv: BASE.mul(1).div(100), + interestRatePctInBase: BASE.mul(22).div(100), + upfrontFeePctInBase: BASE.mul(22).div(100), + tenor: ONE_DAY.mul(11) + }, + { + loanPerCollUnitOrLtv: BASE.mul(33).div(100), + interestRatePctInBase: BASE.mul(22).div(100), + upfrontFeePctInBase: BASE.mul(22).div(100), + tenor: ONE_DAY.mul(11) + }, + { + loanPerCollUnitOrLtv: BASE.mul(11).div(100), + interestRatePctInBase: BASE.mul(22).div(100), + upfrontFeePctInBase: BASE.mul(22).div(100), + tenor: ONE_DAY.mul(11) + } + ] + const { offChainQuote, quoteTuplesTree } = await generateOffChainQuote({ + lenderVault: lenderVault, + signer: signer, + collTokenAddr: weth.address, + loanTokenAddr: usdc.address, + oracleAddr: chainlinkBasicImplementation.address, + quoteTuples: quoteTuples + }) + + // borrower obtains proof for chosen quote tuple + let quoteTupleIdx = 0 + let selectedQuoteTuple = quoteTuples[quoteTupleIdx] + let proof = quoteTuplesTree.getProof(quoteTupleIdx) + const collSendAmount = ONE_WETH + const expectedProtocolAndVaultTransferFee = 0 + const expectedCompartmentTransferFee = 0 + const callbackAddr = ZERO_ADDR + const callbackData = ZERO_BYTES32 + const borrowInstructions = { + collSendAmount, + expectedProtocolAndVaultTransferFee, + expectedCompartmentTransferFee, + deadline: MAX_UINT256, + minLoanAmount: 0, + callbackAddr, + callbackData, + mysoTokenManagerData: ZERO_BYTES32 + } + + // borrower approves gateway + await weth.connect(borrower).approve(borrowerGateway.address, MAX_UINT256) + + // check revert if borrow violates policy (here because allow all pairs is false and no explicit pair policy set) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // update global policy to allow all pairs + allowAllPairs = true + globalPolicyData = ethers.utils.defaultAbiCoder.encode( + [ + 'bool allowAllPairs', + 'bool requiresOracle', + 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' + ], + [allowAllPairs, globalRequiresOracle, globalQuoteBounds] + ) + await basicQuotePolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, globalPolicyData) + + // check revert if borrow violates policy (here because min signer threshold is breached) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // lender sets lower min num of signers + await lenderVault.connect(lender).setMinNumOfSigners(1) + // check revert if borrow violates policy (here because tenor is too short) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // use other tuple + quoteTupleIdx = 1 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + // check revert if borrow violates policy (here because interest too low) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // use other tuple + quoteTupleIdx = 2 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + // check revert if borrow violates policy (here because upfront fee is too low) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // use other tuple + quoteTupleIdx = 3 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + // check revert if borrow violates policy (here because LTV too low) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // use other tuple + quoteTupleIdx = 4 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + // check revert if borrow violates policy (here because LTV too high) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // use other tuple + quoteTupleIdx = 5 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + // borrow should go through + await borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + + // lender sets higher min num of signers again (test that overwrite works) + await lenderVault.connect(lender).setMinNumOfSigners(100) + + // lender defines less strict pair policy + const pairQuoteBounds = { + minTenor: ONE_DAY.mul(10), + maxTenor: ONE_DAY.mul(365), + minFee: 0, + minApr: 0, + minEarliestRepayTenor: 0, + minLoanPerCollUnitOrLtv: BASE.mul(1).div(100), + maxLoanPerCollUnitOrLtv: BASE.sub(1) + } + const pairRequiresOracle = true + const pairMinNumOfSignersOverwrite = 1 + const pairPolicyData = ethers.utils.defaultAbiCoder.encode( + [ + 'bool requiresOracle', + 'uint8 minNumOfSignersOverwrite', + 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' + ], + [pairRequiresOracle, pairMinNumOfSignersOverwrite, pairQuoteBounds] + ) + + // set pair policy + await basicQuotePolicyManager + .connect(lender) + .setPairPolicy(lenderVault.address, weth.address, usdc.address, pairPolicyData) + const actPairPolicyData = await basicQuotePolicyManager.pairQuotingPolicy( + lenderVault.address, + weth.address, + usdc.address + ) + expect(actPairPolicyData.requiresOracle).to.be.equal(pairRequiresOracle) + expect(actPairPolicyData.minNumOfSignersOverwrite).to.be.equal(pairMinNumOfSignersOverwrite) + expect(actPairPolicyData.quoteBounds.minTenor).to.be.equal(pairQuoteBounds.minTenor) + expect(actPairPolicyData.quoteBounds.maxTenor).to.be.equal(pairQuoteBounds.maxTenor) + expect(actPairPolicyData.quoteBounds.minFee).to.be.equal(pairQuoteBounds.minFee) + expect(actPairPolicyData.quoteBounds.minApr).to.be.equal(pairQuoteBounds.minApr) + expect(actPairPolicyData.quoteBounds.minEarliestRepayTenor).to.be.equal(pairQuoteBounds.minEarliestRepayTenor) + expect(actPairPolicyData.quoteBounds.minLoanPerCollUnitOrLtv).to.be.equal(pairQuoteBounds.minLoanPerCollUnitOrLtv) + expect(actPairPolicyData.quoteBounds.maxLoanPerCollUnitOrLtv).to.be.equal(pairQuoteBounds.maxLoanPerCollUnitOrLtv) + + // check revert if borrow violates policy (here because LTV too high) + quoteTupleIdx = 0 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // check revert if borrow violates policy (here because LTV too high) + quoteTupleIdx = 1 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // check revert if borrow violates policy (here because LTV too high) + quoteTupleIdx = 2 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // borrow should go through + quoteTupleIdx = 3 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + + // borrow should go through + quoteTupleIdx = 4 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + + // borrow should go through + quoteTupleIdx = 5 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + }) + it('Should process off-chain quote with too high ltv or negative rate correctly', async function () { const { borrowerGateway, lender, signer, borrower, team, usdc, weth, lenderVault, addressRegistry } = await setupTest() From 1d214947c3e1e48a94388f5a78fae79935316929 Mon Sep 17 00:00:00 2001 From: asardon Date: Tue, 8 Aug 2023 23:56:33 +0200 Subject: [PATCH 50/51] added tests for deleting policies --- test/peer-to-peer/mainnet-forked-tests.ts | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/peer-to-peer/mainnet-forked-tests.ts b/test/peer-to-peer/mainnet-forked-tests.ts index 3adc1d93..2283d2da 100644 --- a/test/peer-to-peer/mainnet-forked-tests.ts +++ b/test/peer-to-peer/mainnet-forked-tests.ts @@ -5341,6 +5341,11 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { [allowAllPairs, globalRequiresOracle, globalQuoteBounds] ) + // check revert when trying to delete global policy although not yet set + await expect( + basicQuotePolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, '0x') + ).to.be.revertedWithCustomError(basicQuotePolicyManager, 'NoPolicyToDelete') + // set global policy await basicQuotePolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, globalPolicyData) expect(await basicQuotePolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.true @@ -5538,6 +5543,22 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { [pairRequiresOracle, pairMinNumOfSignersOverwrite, pairQuoteBounds] ) + // check revert when trying to delete pair policy although not yet set + await expect( + basicQuotePolicyManager.connect(lender).setPairPolicy(lenderVault.address, weth.address, usdc.address, '0x') + ).to.be.revertedWithCustomError(basicQuotePolicyManager, 'NoPolicyToDelete') + + // check revert for pair policy with either token being zero address + await expect( + basicQuotePolicyManager.connect(lender).setPairPolicy(lenderVault.address, ZERO_ADDR, usdc.address, pairPolicyData) + ).to.be.revertedWithCustomError(basicQuotePolicyManager, 'InvalidAddress') + await expect( + basicQuotePolicyManager.connect(lender).setPairPolicy(lenderVault.address, weth.address, ZERO_ADDR, pairPolicyData) + ).to.be.revertedWithCustomError(basicQuotePolicyManager, 'InvalidAddress') + await expect( + basicQuotePolicyManager.connect(lender).setPairPolicy(lenderVault.address, ZERO_ADDR, ZERO_ADDR, pairPolicyData) + ).to.be.revertedWithCustomError(basicQuotePolicyManager, 'InvalidAddress') + // set pair policy await basicQuotePolicyManager .connect(lender) @@ -5610,6 +5631,12 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { await borrowerGateway .connect(borrower) .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + + // delete policies + await basicQuotePolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, '0x') + expect(await basicQuotePolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.false + await basicQuotePolicyManager.connect(lender).setPairPolicy(lenderVault.address, weth.address, usdc.address, '0x') + expect(await basicQuotePolicyManager.hasPairQuotingPolicy(lenderVault.address, weth.address, usdc.address)).to.be.false }) it('Should process off-chain quote with too high ltv or negative rate correctly', async function () { From 5a3d5d3290588b63c6940b8cbcb49a83ad98a7ea Mon Sep 17 00:00:00 2001 From: asardon Date: Wed, 9 Aug 2023 01:01:01 +0200 Subject: [PATCH 51/51] added swap case and updated readme --- README.md | 22 ++++-- test/peer-to-peer/mainnet-forked-tests.ts | 93 ++++++++++++++++------- 2 files changed, 84 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 07ac52d4..89ec85ec 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ contracts/ ┃ ┃ ┃ ┣ IDSETH.sol ┃ ┃ ┃ ┣ IOlympus.sol ┃ ┃ ┃ ┗ IUniV2.sol +┃ ┃ ┣ policyManagers/ +┃ ┃ ┃ ┣ IBasicQuotePolicyManager.sol +┃ ┃ ┃ ┗ IQuotePolicyManager.sol ┃ ┃ ┣ wrappers/ ┃ ┃ ┃ ┣ ERC20/ ┃ ┃ ┃ ┃ ┣ IERC20Wrapper.sol @@ -81,6 +84,9 @@ contracts/ ┃ ┃ ┣ OracleLibrary.sol ┃ ┃ ┣ TickMath.sol ┃ ┃ ┗ TwapGetter.sol +┃ ┣ policyManagers/ +┃ ┃ ┣ BasicQuotePolicyManager.sol +┃ ┃ ┗ DataTypesBasicPolicies.sol ┃ ┣ wrappers/ ┃ ┃ ┣ ERC20/ ┃ ┃ ┃ ┣ ERC20Wrapper.sol @@ -164,13 +170,13 @@ File | % Stmts | % Branch | Helpers.sol | 100 | 50 | 100 | 100 | | contracts\interfaces\ | 100 | 100 | 100 | 100 | | IMysoTokenManager.sol | 100 | 100 | 100 | 100 | | - contracts\peer-to-peer\ | 99.73 | 94.74 | 98.78 | 98.76 | | - AddressRegistry.sol | 100 | 96.74 | 100 | 99.17 | 116 | + contracts\peer-to-peer\ | 99.74 | 94.61 | 98.84 | 98.66 | | + AddressRegistry.sol | 100 | 96.74 | 100 | 99.17 | 117 | BorrowerGateway.sol | 98.57 | 90.91 | 90.91 | 96.97 | 241,317,358 | DataTypesPeerToPeer.sol | 100 | 100 | 100 | 100 | | LenderVaultFactory.sol | 100 | 87.5 | 100 | 100 | | - LenderVaultImpl.sol | 100 | 92.86 | 100 | 98.88 | 63,206 | - QuoteHandler.sol | 100 | 98.04 | 100 | 99.35 | 412 | + LenderVaultImpl.sol | 100 | 93.1 | 100 | 98.92 | 64,207 | + QuoteHandler.sol | 100 | 96.83 | 100 | 98.91 | 266,478 | contracts\peer-to-peer\callbacks\ | 100 | 75 | 88.89 | 96.88 | | BalancerV2Looping.sol | 100 | 100 | 100 | 100 | | UniV3Looping.sol | 100 | 100 | 100 | 100 | | @@ -209,6 +215,9 @@ File | % Stmts | % Branch | AggregatorV3Interface.sol | 100 | 100 | 100 | 100 | | contracts\peer-to-peer\interfaces\oracles\uniswap\ | 100 | 100 | 100 | 100 | | ITwapGetter.sol | 100 | 100 | 100 | 100 | | + contracts\peer-to-peer\interfaces\policyManagers\ | 100 | 100 | 100 | 100 | | + IBasicQuotePolicyManager.sol | 100 | 100 | 100 | 100 | | + IQuotePolicyManager.sol | 100 | 100 | 100 | 100 | | contracts\peer-to-peer\interfaces\wrappers\ERC20\ | 100 | 100 | 100 | 100 | | IERC20Wrapper.sol | 100 | 100 | 100 | 100 | | IWrappedERC20Impl.sol | 100 | 100 | 100 | 100 | | @@ -229,6 +238,9 @@ File | % Stmts | % Branch | OracleLibrary.sol | 100 | 66.67 | 100 | 100 | | TickMath.sol | 100 | 80.43 | 100 | 84.09 |... 63,65,67,73 | TwapGetter.sol | 100 | 62.5 | 100 | 93.33 | 57 | + contracts\peer-to-peer\policyManagers\ | 100 | 100 | 100 | 100 | | + BasicQuotePolicyManager.sol | 100 | 100 | 100 | 100 | | + DataTypesBasicPolicies.sol | 100 | 100 | 100 | 100 | | contracts\peer-to-peer\wrappers\ERC20\ | 100 | 83.33 | 100 | 98.88 | | ERC20Wrapper.sol | 100 | 75 | 100 | 96.88 | 45 | WrappedERC20Impl.sol | 100 | 89.29 | 100 | 100 | | @@ -245,6 +257,6 @@ File | % Stmts | % Branch | IFundingPoolImpl.sol | 100 | 100 | 100 | 100 | | ILoanProposalImpl.sol | 100 | 100 | 100 | 100 | | ---------------------------------------------------------|----------|----------|----------|----------|----------------| -All files | 98.99 | 88.83 | 98.65 | 96.81 | | +All files | 99.07 | 89.56 | 98.74 | 96.97 | | ---------------------------------------------------------|----------|----------|----------|----------|----------------| ``` diff --git a/test/peer-to-peer/mainnet-forked-tests.ts b/test/peer-to-peer/mainnet-forked-tests.ts index 2283d2da..1a3231de 100644 --- a/test/peer-to-peer/mainnet-forked-tests.ts +++ b/test/peer-to-peer/mainnet-forked-tests.ts @@ -24,7 +24,9 @@ import { getExactLpTokenPriceInEth, getFairReservesPriceAndEthValue, getDeltaBNComparison, - setupBorrowerWhitelist + setupBorrowerWhitelist, + encodeGlobalPolicy, + encodePairPolicy } from './helpers/misc' // test config constants & vars @@ -5332,14 +5334,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { } let allowAllPairs = false let globalRequiresOracle = false - let globalPolicyData = ethers.utils.defaultAbiCoder.encode( - [ - 'bool allowAllPairs', - 'bool requiresOracle', - 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' - ], - [allowAllPairs, globalRequiresOracle, globalQuoteBounds] - ) + let globalPolicyData = encodeGlobalPolicy(allowAllPairs, globalRequiresOracle, globalQuoteBounds) // check revert when trying to delete global policy although not yet set await expect( @@ -5397,6 +5392,12 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { interestRatePctInBase: BASE.mul(22).div(100), upfrontFeePctInBase: BASE.mul(22).div(100), tenor: ONE_DAY.mul(11) + }, + { + loanPerCollUnitOrLtv: BASE, + interestRatePctInBase: 0, + upfrontFeePctInBase: BASE, + tenor: 0 } ] const { offChainQuote, quoteTuplesTree } = await generateOffChainQuote({ @@ -5440,14 +5441,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { // update global policy to allow all pairs allowAllPairs = true - globalPolicyData = ethers.utils.defaultAbiCoder.encode( - [ - 'bool allowAllPairs', - 'bool requiresOracle', - 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' - ], - [allowAllPairs, globalRequiresOracle, globalQuoteBounds] - ) + globalPolicyData = encodeGlobalPolicy(allowAllPairs, globalRequiresOracle, globalQuoteBounds) await basicQuotePolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, globalPolicyData) // check revert if borrow violates policy (here because min signer threshold is breached) @@ -5534,14 +5528,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { } const pairRequiresOracle = true const pairMinNumOfSignersOverwrite = 1 - const pairPolicyData = ethers.utils.defaultAbiCoder.encode( - [ - 'bool requiresOracle', - 'uint8 minNumOfSignersOverwrite', - 'tuple(uint32 minTenor, uint32 maxTenor, uint80 minFee, int80 minApr, uint32 minEarliestRepayTenor, uint128 minLoanPerCollUnitOrLtv, uint128 maxLoanPerCollUnitOrLtv) quoteBounds' - ], - [pairRequiresOracle, pairMinNumOfSignersOverwrite, pairQuoteBounds] - ) + let pairPolicyData = encodePairPolicy(pairRequiresOracle, pairMinNumOfSignersOverwrite, pairQuoteBounds) // check revert when trying to delete pair policy although not yet set await expect( @@ -5563,6 +5550,7 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { await basicQuotePolicyManager .connect(lender) .setPairPolicy(lenderVault.address, weth.address, usdc.address, pairPolicyData) + const actPairPolicyData = await basicQuotePolicyManager.pairQuotingPolicy( lenderVault.address, weth.address, @@ -5578,6 +5566,13 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { expect(actPairPolicyData.quoteBounds.minLoanPerCollUnitOrLtv).to.be.equal(pairQuoteBounds.minLoanPerCollUnitOrLtv) expect(actPairPolicyData.quoteBounds.maxLoanPerCollUnitOrLtv).to.be.equal(pairQuoteBounds.maxLoanPerCollUnitOrLtv) + // check revert when trying to set same policy again + await expect( + basicQuotePolicyManager + .connect(lender) + .setPairPolicy(lenderVault.address, weth.address, usdc.address, pairPolicyData) + ).to.be.revertedWithCustomError(basicQuotePolicyManager, 'PolicyAlreadySet') + // check revert if borrow violates policy (here because LTV too high) quoteTupleIdx = 0 selectedQuoteTuple = quoteTuples[quoteTupleIdx] @@ -5632,11 +5627,57 @@ describe('Peer-to-Peer: Forked Mainnet Tests', function () { .connect(borrower) .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) - // delete policies + // delete global policy await basicQuotePolicyManager.connect(lender).setGlobalPolicy(lenderVault.address, '0x') expect(await basicQuotePolicyManager.hasGlobalQuotingPolicy(lenderVault.address)).to.be.false + + // check borrow should still go through + quoteTupleIdx = 5 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + + // swap should initially revert + quoteTupleIdx = 6 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') + + // update pair policy + pairQuoteBounds.minTenor = ethers.BigNumber.from(0) + pairQuoteBounds.maxLoanPerCollUnitOrLtv = BASE + pairPolicyData = encodePairPolicy(pairRequiresOracle, pairMinNumOfSignersOverwrite, pairQuoteBounds) + await basicQuotePolicyManager + .connect(lender) + .setPairPolicy(lenderVault.address, weth.address, usdc.address, pairPolicyData) + + // swap should now go through + quoteTupleIdx = 6 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + + // delete pair policy await basicQuotePolicyManager.connect(lender).setPairPolicy(lenderVault.address, weth.address, usdc.address, '0x') expect(await basicQuotePolicyManager.hasPairQuotingPolicy(lenderVault.address, weth.address, usdc.address)).to.be.false + + // check borrow shouldn't go through anymore + quoteTupleIdx = 5 + selectedQuoteTuple = quoteTuples[quoteTupleIdx] + proof = quoteTuplesTree.getProof(quoteTupleIdx) + await expect( + borrowerGateway + .connect(borrower) + .borrowWithOffChainQuote(lenderVault.address, borrowInstructions, offChainQuote, selectedQuoteTuple, proof) + ).to.be.revertedWithCustomError(quoteHandler, 'QuoteViolatesPolicy') }) it('Should process off-chain quote with too high ltv or negative rate correctly', async function () {