From 3f75cc6d3f048b542413261bff242cbe71f99691 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Wed, 26 Oct 2022 14:50:45 -0400 Subject: [PATCH 1/4] Remove contract size contraint (#64) --- hardhat.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hardhat.config.ts b/hardhat.config.ts index d79655cb..da682af4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -11,6 +11,11 @@ const config: HardhatUserConfig = { runs: 10 } } + }, + networks: { + hardhat: { + allowUnlimitedContractSize: true + } } }; From 518dd837c87076256eb0a8cd22d2cd467632ed7f Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 26 Oct 2022 14:56:10 -0400 Subject: [PATCH 2/4] VAL-59 Allow cancelling requests (#60) * Add cancellation fee parameter * Add cancellation methods * Add tests for calculateMaxCancellation * Add calculateCancellationFee tests * Add tests for calculateWithdrawStateForCancellation * Add scenario tests * Update ceil call to be divideCeil --- contracts/Pool.sol | 104 +++++++++++++++-- contracts/interfaces/IPool.sol | 10 ++ contracts/libraries/PoolLib.sol | 87 +++++++++++--- contracts/mocks/PoolLibTestWrapper.sol | 35 +++++- test/libraries/PoolLib.test.ts | 115 ++++++++++++++++++- test/scenarios/pool/withdraw-request.test.ts | 12 +- test/support/pool.ts | 3 +- 7 files changed, 333 insertions(+), 33 deletions(-) diff --git a/contracts/Pool.sol b/contracts/Pool.sol index 7eb3f228..2ea02350 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -511,8 +511,21 @@ contract Pool is IPool, ERC20 { } /** - * @dev + * @dev Returns the maximum number of `shares` that can be + * cancelled from being requested for a redemption. + * + * Note: This is equivalent of EIP-4626 `maxRedeem` */ + function maxRequestCancellation(address owner) + public + view + returns (uint256 maxShares) + { + maxShares = PoolLib.calculateMaxCancellation( + _withdrawState[owner], + _poolSettings.requestCancellationFeeBps + ); + } /** * @dev Returns the maximum number of `shares` that can be @@ -558,7 +571,7 @@ contract Pool is IPool, ERC20 { * returns an estimated amount of underlying that will be received if this * were immeidately executed. * - * Emits a {RedeemRequested} event. + * Emits a {WithdrawRequested} event. */ function requestRedeem(uint256 shares) external @@ -570,6 +583,23 @@ contract Pool is IPool, ERC20 { _requestWithdraw(msg.sender, assets, shares); } + /** + * @dev Cancels a redeem request for a specific number of `shares` from + * owner and returns an estimated amnount of underlying that equates to + * this number of shares. + * + * Emits a {WithdrawRequestCancelled} event. + */ + function cancelRedeemRequest(uint256 shares) + external + onlyActivatedPool + onlyLender + returns (uint256 assets) + { + assets = convertToAssets(shares); + _cancelWithdraw(msg.sender, assets, shares); + } + /** * @dev Returns the maximum amount of underlying `assets` that can be * requested to be withdrawn from the owner balance with a single @@ -622,6 +652,23 @@ contract Pool is IPool, ERC20 { _requestWithdraw(msg.sender, assets, shares); } + /** + * @dev Cancels a withdraw request for a specific values of `assets` from + * owner and returns an estimated number of shares that equates to + * this number of assets. + * + * Emits a {WithdrawRequestCancelled} event. + */ + function cancelWithdrawRequest(uint256 assets) + external + onlyActivatedPool + onlyLender + returns (uint256 shares) + { + shares = convertToShares(assets); + _cancelWithdraw(msg.sender, assets, shares); + } + /** * @dev Returns the amount of shares that should be considered interest * bearing for a given owner. This number is their balance, minus their @@ -720,7 +767,7 @@ contract Pool is IPool, ERC20 { } /** - * @dev Performs a redeem request for the owner, including paying any fees. + * @dev Performs a withdraw request for the owner, including paying any fees. */ function _requestWithdraw( address owner, @@ -730,7 +777,6 @@ contract Pool is IPool, ERC20 { require(maxRedeemRequest(owner) >= shares, "Pool: InsufficientBalance"); uint256 currentPeriod = withdrawPeriod(); - uint256 nextPeriod = withdrawPeriod().add(1); uint256 feeShares = PoolLib.calculateRequestFee( shares, _poolSettings.requestFeeBps @@ -740,10 +786,9 @@ contract Pool is IPool, ERC20 { _burn(owner, feeShares); // Update the requested amount from the user - _withdrawState[owner] = PoolLib.caclulateWithdrawState( + _withdrawState[owner] = PoolLib.calculateWithdrawStateForRequest( _withdrawState[owner], currentPeriod, - nextPeriod, shares ); @@ -751,16 +796,59 @@ contract Pool is IPool, ERC20 { _withdrawAddresses.add(owner); // Update the global amount - _globalWithdrawState = PoolLib.caclulateWithdrawState( + _globalWithdrawState = PoolLib.calculateWithdrawStateForRequest( _globalWithdrawState, currentPeriod, - nextPeriod, shares ); emit WithdrawRequested(msg.sender, assets, shares); } + /** + * @dev Cancels a withdraw request for the owner, including paying any fees. + * A cancellation can only occur before the + */ + function _cancelWithdraw( + address owner, + uint256 assets, + uint256 shares + ) internal { + // TODO: If we move to a lighter crank, we must run it here before this method continues + require( + maxRequestCancellation(owner) >= shares, + "Pool: InsufficientBalance" + ); + + uint256 currentPeriod = withdrawPeriod(); + uint256 feeShares = PoolLib.calculateRequestFee( + shares, + _poolSettings.requestFeeBps + ); + + // Pay the Fee + _burn(owner, feeShares); + + // Update the requested amount from the user + _withdrawState[owner] = PoolLib.calculateWithdrawStateForCancellation( + _withdrawState[owner], + currentPeriod, + shares + ); + + // Add the address to the addresslist + _withdrawAddresses.add(owner); + + // Update the global amount + _globalWithdrawState = PoolLib.calculateWithdrawStateForCancellation( + _globalWithdrawState, + currentPeriod, + shares + ); + + emit WithdrawRequestCancelled(msg.sender, assets, shares); + } + /** * @dev Set the pool lifecycle state. If the state changes, this method * will also update the poolActivatedAt variable diff --git a/contracts/interfaces/IPool.sol b/contracts/interfaces/IPool.sol index 7c37d7ae..4fbfc377 100644 --- a/contracts/interfaces/IPool.sol +++ b/contracts/interfaces/IPool.sol @@ -29,6 +29,7 @@ struct IPoolConfigurableSettings { uint256 maxCapacity; // amount uint256 endDate; // epoch seconds uint256 requestFeeBps; // bips + uint256 requestCancellationFeeBps; // bips uint256 withdrawGateBps; // Percent of liquidity pool available to withdraw, represented in BPS uint256 firstLossInitialMinimum; // amount uint256 withdrawRequestPeriodDuration; // seconds (e.g. 30 days) @@ -86,6 +87,15 @@ interface IPool is IERC4626 { uint256 shares ); + /** + * @dev Emitted when a withdrawal is requested. + */ + event WithdrawRequestCancelled( + address indexed lender, + uint256 assets, + uint256 shares + ); + /** * @dev Emitted when pool settings are updated. */ diff --git a/contracts/libraries/PoolLib.sol b/contracts/libraries/PoolLib.sol index 872c2686..9328897e 100644 --- a/contracts/libraries/PoolLib.sol +++ b/contracts/libraries/PoolLib.sol @@ -431,9 +431,7 @@ library PoolLib { // given request period, we need to move "requested" shares over // to be "eligible". if (state.latestRequestPeriod <= currentPeriod) { - state.eligibleShares = state.eligibleShares.add( - state.requestedShares - ); + state.eligibleShares = state.eligibleShares + state.requestedShares; state.requestedShares = 0; } @@ -444,22 +442,52 @@ library PoolLib { * @dev Calculate the current IPoolWithdrawState based on the existing * request state and the current request period. */ - function caclulateWithdrawState( + function calculateWithdrawStateForRequest( IPoolWithdrawState memory state, uint256 currentPeriod, - uint256 requestedPeriod, uint256 requestedShares ) public pure returns (IPoolWithdrawState memory updatedState) { - require(requestedPeriod > 0, "Pool: Invalid request period"); + require(currentPeriod >= 0, "Pool: Invalid request period"); updatedState = progressWithdrawState(state, currentPeriod); // Increment the requested shares count, and ensure the "latestRequestPeriod" // is set to the current request period. - updatedState.requestedShares = state.requestedShares.add( - requestedShares - ); - updatedState.latestRequestPeriod = requestedPeriod; + updatedState.requestedShares = state.requestedShares + requestedShares; + updatedState.latestRequestPeriod = currentPeriod + 1; + } + + /** + * @dev Calculate the current IPoolWithdrawState based on the existing + * request state and the current request period. + */ + function calculateWithdrawStateForCancellation( + IPoolWithdrawState memory state, + uint256 currentPeriod, + uint256 cancelledShares + ) public pure returns (IPoolWithdrawState memory updatedState) { + updatedState = progressWithdrawState(state, currentPeriod); + + // Decrease the requested, eligible shares count, and ensure the "latestRequestPeriod" + // is set to the current request period. + if (updatedState.requestedShares > cancelledShares) { + updatedState.requestedShares -= cancelledShares; + cancelledShares = 0; + } else { + cancelledShares -= updatedState.requestedShares; + updatedState.requestedShares = 0; + } + + if (updatedState.eligibleShares > cancelledShares) { + updatedState.eligibleShares -= cancelledShares; + cancelledShares = 0; + } else { + cancelledShares -= updatedState.eligibleShares; + updatedState.eligibleShares = 0; + } + + // Sanity check that we've cancelled all shares. + require(cancelledShares == 0, "Pool: Invalid cancelled shares"); } /** @@ -474,6 +502,17 @@ library PoolLib { return divideCeil(shares * requestFeeBps, 10_000); } + /** + * @dev Calculate the fee for cancelling a withdrawRequest or a redeemRequest. + * Per the EIP-4626 spec, this method rounds up. + */ + function calculateCancellationFee( + uint256 shares, + uint256 requestCancellationFeeBps + ) public pure returns (uint256) { + return divideCeil(shares * requestCancellationFeeBps, 10_000); + } + /** * @dev Calculates the Maximum amount of shares that can be requested */ @@ -482,14 +521,32 @@ library PoolLib { uint256 shareBalance, uint256 requestFeeBps ) public pure returns (uint256) { - uint256 sharesRemaining = shareBalance - .sub(state.requestedShares) - .sub(state.eligibleShares) - .sub(state.redeemableShares); + uint256 sharesRemaining = shareBalance - + state.requestedShares - + state.eligibleShares - + state.redeemableShares; uint256 sharesFee = calculateRequestFee(sharesRemaining, requestFeeBps); - return Math.max(sharesRemaining.sub(sharesFee), 0); + return Math.max(sharesRemaining - sharesFee, 0); + } + + /** + * @dev Calculates the Maximum amount of shares that can be cancelled + * from the current withdraw request. + */ + function calculateMaxCancellation( + IPoolWithdrawState memory state, + uint256 requestCancellationFeeBps + ) public pure returns (uint256) { + uint256 sharesRemaining = state.requestedShares + state.eligibleShares; + + uint256 sharesFee = calculateCancellationFee( + sharesRemaining, + requestCancellationFeeBps + ); + + return Math.max(sharesRemaining - sharesFee, 0); } /** diff --git a/contracts/mocks/PoolLibTestWrapper.sol b/contracts/mocks/PoolLibTestWrapper.sol index 0d3de588..321595e4 100644 --- a/contracts/mocks/PoolLibTestWrapper.sol +++ b/contracts/mocks/PoolLibTestWrapper.sol @@ -188,21 +188,32 @@ contract PoolLibTestWrapper is ERC20("PoolLibTest", "PLT") { ); } - function caclulateWithdrawState( + function calculateWithdrawStateForRequest( IPoolWithdrawState memory state, uint256 currentPeriod, - uint256 requestPeriod, uint256 requestedShares ) public pure returns (IPoolWithdrawState memory) { return - PoolLib.caclulateWithdrawState( + PoolLib.calculateWithdrawStateForRequest( state, currentPeriod, - requestPeriod, requestedShares ); } + function calculateWithdrawStateForCancellation( + IPoolWithdrawState memory state, + uint256 currentPeriod, + uint256 cancelledShares + ) public pure returns (IPoolWithdrawState memory) { + return + PoolLib.calculateWithdrawStateForCancellation( + state, + currentPeriod, + cancelledShares + ); + } + function calculateRequestFee(uint256 shares, uint256 requestFeeBps) external pure @@ -211,6 +222,14 @@ contract PoolLibTestWrapper is ERC20("PoolLibTest", "PLT") { return PoolLib.calculateRequestFee(shares, requestFeeBps); } + function calculateCancellationFee( + uint256 shares, + uint256 requestCancellationFeeBps + ) external pure returns (uint256) { + return + PoolLib.calculateCancellationFee(shares, requestCancellationFeeBps); + } + function calculateMaxRedeemRequest( IPoolWithdrawState memory state, uint256 shareBalance, @@ -223,4 +242,12 @@ contract PoolLibTestWrapper is ERC20("PoolLibTest", "PLT") { requestFeeBps ); } + + function calculateMaxCancellation( + IPoolWithdrawState memory state, + uint256 requestCancellationFeeBps + ) public pure returns (uint256) { + return + PoolLib.calculateMaxCancellation(state, requestCancellationFeeBps); + } } diff --git a/test/libraries/PoolLib.test.ts b/test/libraries/PoolLib.test.ts index 892fa0d1..f82275cc 100644 --- a/test/libraries/PoolLib.test.ts +++ b/test/libraries/PoolLib.test.ts @@ -676,14 +676,18 @@ describe("PoolLib", () => { }); }); - describe("caclulateWithdrawState", () => { + describe("calculateWithdrawStateForRequest", () => { it("increments the requested shares of the lender", async () => { const { poolLibWrapper } = await loadFixture(deployFixture); const withdrawState = buildWithdrawState(); expect( - await poolLibWrapper.caclulateWithdrawState(withdrawState, 0, 1, 22) + await poolLibWrapper.calculateWithdrawStateForRequest( + withdrawState, + 0, + 22 + ) ).to.deep.equal( Object.values( buildWithdrawState({ @@ -703,7 +707,11 @@ describe("PoolLib", () => { }); expect( - await poolLibWrapper.caclulateWithdrawState(withdrawState, 1, 2, 33) + await poolLibWrapper.calculateWithdrawStateForRequest( + withdrawState, + 1, + 33 + ) ).to.deep.equal( Object.values( buildWithdrawState({ @@ -716,6 +724,49 @@ describe("PoolLib", () => { }); }); + describe("calculateWithdrawStateForCancellation", () => { + it("subtracts the requested shares of the lender, followed by eligible shares", async () => { + const { poolLibWrapper } = await loadFixture(deployFixture); + + const withdrawState = buildWithdrawState({ + requestedShares: 10, + eligibleShares: 20 + }); + + expect( + await poolLibWrapper.calculateWithdrawStateForCancellation( + withdrawState, + 0, + 22 + ) + ).to.deep.equal( + Object.values( + buildWithdrawState({ + requestedShares: 0, + eligibleShares: 8 + }) + ) + ); + }); + + it("returns an error if not enough shares available to cancel", async () => { + const { poolLibWrapper } = await loadFixture(deployFixture); + + const withdrawState = buildWithdrawState({ + requestedShares: 20, + latestRequestPeriod: 1 + }); + + await expect( + poolLibWrapper.calculateWithdrawStateForCancellation( + withdrawState, + 1, + 33 + ) + ).to.be.revertedWith("Pool: Invalid cancelled shares"); + }); + }); + describe("calculateRequestFee()", () => { it("calculates the fee for a request", async () => { const { poolLibWrapper } = await loadFixture(deployFixture); @@ -738,6 +789,30 @@ describe("PoolLib", () => { }); }); + describe("calculateCancellationFee()", () => { + it("calculates the fee for a cancellation", async () => { + const { poolLibWrapper } = await loadFixture(deployFixture); + + const shares = 500; + const bps = 127; // 1.27% + + expect( + await poolLibWrapper.calculateCancellationFee(shares, bps) + ).to.equal(7); + }); + + it("rounds the fee up", async () => { + const { poolLibWrapper } = await loadFixture(deployFixture); + + const shares = 101; + const bps = 900; // 9% + + expect( + await poolLibWrapper.calculateCancellationFee(shares, bps) + ).to.equal(10); // 9.09 rounded up + }); + }); + describe("calculateMaxRedeemRequest()", () => { it("returns the number of shares the owner has not yet requested if no fees", async () => { const { poolLibWrapper } = await loadFixture(deployFixture); @@ -779,4 +854,38 @@ describe("PoolLib", () => { ).to.equal(26); }); }); + + describe("calculateMaxCancellation()", () => { + it("returns the number of shares the owner can cancel from a request", async () => { + const { poolLibWrapper } = await loadFixture(deployFixture); + + const fees = 0; + const withdrawState = buildWithdrawState({ + requestedShares: 50, + eligibleShares: 22, + redeemableShares: 28, + latestRequestPeriod: 2 + }); + + expect( + await poolLibWrapper.calculateMaxCancellation(withdrawState, fees) + ).to.equal(72); + }); + + it("returns the number of shares minus fees", async () => { + const { poolLibWrapper } = await loadFixture(deployFixture); + + const fees = 1200; // 12% + const withdrawState = buildWithdrawState({ + requestedShares: 50, + eligibleShares: 22, + redeemableShares: 28, + latestRequestPeriod: 2 + }); + + expect( + await poolLibWrapper.calculateMaxCancellation(withdrawState, fees) + ).to.equal(63); + }); + }); }); diff --git a/test/scenarios/pool/withdraw-request.test.ts b/test/scenarios/pool/withdraw-request.test.ts index 8d1b1627..4dd67f01 100644 --- a/test/scenarios/pool/withdraw-request.test.ts +++ b/test/scenarios/pool/withdraw-request.test.ts @@ -111,9 +111,9 @@ describe("Withdraw Requests", () => { // crank it await pool.crank(); - // 170 assets, 25% withdraw gate = 42 assets. - expect(await pool.totalWithdrawableAssets()).to.equal(41); // 42? + // 170 assets = 160 shares. 25% withdraw gate = 40 expect(await pool.totalRedeemableShares()).to.equal(40); + expect(await pool.totalWithdrawableAssets()).to.equal(41); // verify the global state is updated expect(await pool.totalRequestedBalance()).to.equal(0); @@ -130,5 +130,13 @@ describe("Withdraw Requests", () => { 6 ); /* 10 * (41/60) */ expect(await pool.maxWithdraw(bobLender.address)).to.equal(6); + + // Cancel a request + expect(await pool.maxRequestCancellation(aliceLender.address)).to.equal(16); + expect(await pool.maxRequestCancellation(bobLender.address)).to.equal(3); + + // Cancel Bob's request + expect(await pool.connect(bobLender).cancelRedeemRequest(3)); + expect(await pool.maxRequestCancellation(bobLender.address)).to.equal(0); }); }); diff --git a/test/support/pool.ts b/test/support/pool.ts index c2bfef01..d6d17325 100644 --- a/test/support/pool.ts +++ b/test/support/pool.ts @@ -6,7 +6,8 @@ import { deployServiceConfiguration } from "./serviceconfiguration"; export const DEFAULT_POOL_SETTINGS = { maxCapacity: 10_000_000, endDate: 2524611601, // Jan 1, 2050 - requestFeeBps: 500, // bps (1%) + requestFeeBps: 500, // bps (5%) + requestCancellationFeeBps: 100, // bps (1%) withdrawGateBps: 10_000, // bps (100%) firstLossInitialMinimum: 100_000, withdrawRequestPeriodDuration: 30 * 24 * 60 * 60, // 30 days From 599a166801c34a715791c532e00105460e917440 Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Wed, 26 Oct 2022 15:26:39 -0400 Subject: [PATCH 3/4] Add late payment when making a late payment (#63) * Add support for late payment fees * Combine pool payment and late payment --- contracts/Loan.sol | 13 ++++++++--- contracts/LoanFactory.sol | 6 +++-- contracts/libraries/LoanLib.sol | 15 +++++++++--- test/Loan.test.ts | 41 ++++++++++++++++++++++++++++++++- test/support/loan.ts | 3 ++- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/contracts/Loan.sol b/contracts/Loan.sol index 655a68b8..8af08276 100644 --- a/contracts/Loan.sol +++ b/contracts/Loan.sol @@ -38,6 +38,7 @@ contract Loan is ILoan { uint256 public immutable payment; uint256 public paymentsRemaining; uint256 public paymentDueDate; + uint256 public latePaymentFee; /** * @dev Modifier that requires the Loan be in the given `state_` @@ -96,7 +97,8 @@ contract Loan is ILoan { uint256 apr_, address liquidityAsset_, uint256 principal_, - uint256 dropDeadTimestamp + uint256 dropDeadTimestamp, + uint256 latePaymentFee_ ) { _serviceConfiguration = serviceConfiguration; _factory = factory; @@ -111,6 +113,7 @@ contract Loan is ILoan { apr = apr_; liquidityAsset = liquidityAsset_; principal = principal_; + latePaymentFee = latePaymentFee_; LoanLib.validateLoan( serviceConfiguration, @@ -308,7 +311,9 @@ contract Loan is ILoan { .previewFees( payment, _serviceConfiguration.firstLossFeeBps(), - IPool(_pool).poolFeePercentOfInterest() + IPool(_pool).poolFeePercentOfInterest(), + latePaymentFee, + paymentDueDate ); LoanLib.payFees( @@ -336,7 +341,9 @@ contract Loan is ILoan { .previewFees( amount, _serviceConfiguration.firstLossFeeBps(), - IPool(_pool).poolFeePercentOfInterest() + IPool(_pool).poolFeePercentOfInterest(), + latePaymentFee, + paymentDueDate ); LoanLib.payFees( diff --git a/contracts/LoanFactory.sol b/contracts/LoanFactory.sol index 69e368f3..383acf38 100644 --- a/contracts/LoanFactory.sol +++ b/contracts/LoanFactory.sol @@ -40,7 +40,8 @@ contract LoanFactory { uint256 apr, address liquidityAsset, uint256 principal, - uint256 dropDeadDate + uint256 dropDeadDate, + uint256 latePaymentFee ) public virtual returns (address LoanAddress) { require( _serviceConfiguration.paused() == false, @@ -57,7 +58,8 @@ contract LoanFactory { apr, liquidityAsset, principal, - dropDeadDate + dropDeadDate, + latePaymentFee ); address addr = address(loan); emit LoanCreated(addr); diff --git a/contracts/libraries/LoanLib.sol b/contracts/libraries/LoanLib.sol index a80b8fa1..dcfa8d40 100644 --- a/contracts/libraries/LoanLib.sol +++ b/contracts/libraries/LoanLib.sol @@ -234,10 +234,12 @@ library LoanLib { function previewFees( uint256 payment, uint256 firstLoss, - uint256 poolFeePercentOfInterest + uint256 poolFeePercentOfInterest, + uint256 latePaymentFee, + uint256 paymentDueDate ) public - pure + view returns ( uint256, uint256, @@ -252,7 +254,14 @@ library LoanLib { .mul(payment) .div(10000) .div(RAY); - uint256 poolPayment = payment - poolFee - firstLossFee; + + // Late fee is applied on top of interest payment + uint256 lateFee; + if (block.timestamp > paymentDueDate) { + lateFee = latePaymentFee; + } + + uint256 poolPayment = payment - poolFee - firstLossFee + lateFee; return (poolPayment, firstLossFee, poolFee); } diff --git a/test/Loan.test.ts b/test/Loan.test.ts index 97478007..2b4b9ff0 100644 --- a/test/Loan.test.ts +++ b/test/Loan.test.ts @@ -99,7 +99,8 @@ describe("Loan", () => { 500, liquidityAsset.address, 500_000, - Math.floor(Date.now() / 1000) + SEVEN_DAYS + Math.floor(Date.now() / 1000) + SEVEN_DAYS, + 1_000 ); const tx2Receipt = await tx2.wait(); @@ -941,6 +942,44 @@ describe("Loan", () => { expect(newDueDate).to.equal(dueDate.add(THIRTY_DAYS)); }); + it("can complete the next payment if late", async () => { + const fixture = await loadFixture(deployFixture); + const { + borrower, + collateralAsset, + liquidityAsset, + loan, + pool, + poolManager + } = fixture; + + // Setup + await collateralAsset.connect(borrower).approve(loan.address, 100); + await loan + .connect(borrower) + .postFungibleCollateral(collateralAsset.address, 100); + await pool.connect(poolManager).fundLoan(loan.address); + await loan.connect(borrower).drawdown(); + + // Advance time to drop dead timestamp + const foo = await loan.paymentDueDate(); + await time.increaseTo(foo.add(100)); + + // Make payment + const firstLoss = await pool.firstLossVault(); + const dueDate = await loan.paymentDueDate(); + expect(await loan.paymentsRemaining()).to.equal(6); + await liquidityAsset.connect(borrower).approve(loan.address, 3083); + const tx = loan.connect(borrower).completeNextPayment(); + await expect(tx).to.not.be.reverted; + await expect(tx).to.changeTokenBalance(liquidityAsset, borrower, -3083); + await expect(tx).to.changeTokenBalance(liquidityAsset, pool, 1979 + 1000); + await expect(tx).to.changeTokenBalance(liquidityAsset, firstLoss, 104); + expect(await loan.paymentsRemaining()).to.equal(5); + const newDueDate = await loan.paymentDueDate(); + expect(newDueDate).to.equal(dueDate.add(THIRTY_DAYS)); + }); + it("can payoff the entire loan at once", async () => { const fixture = await loadFixture(deployFixture); const { diff --git a/test/support/loan.ts b/test/support/loan.ts index acf2fdaf..8985a4e7 100644 --- a/test/support/loan.ts +++ b/test/support/loan.ts @@ -42,7 +42,8 @@ export async function deployLoan( 500, liquidityAsset, 1_000_000, - Math.floor(Date.now() / 1000) + SEVEN_DAYS + Math.floor(Date.now() / 1000) + SEVEN_DAYS, + 0 ); const txnReceipt = await txn.wait(); From 838c973da859778f96ce32e93544c2231670c8eb Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Wed, 26 Oct 2022 15:52:48 -0400 Subject: [PATCH 4/4] VAL-8 Add ToS acceptance criteria to permissioned pools & factory (#66) --- contracts/ServiceConfiguration.sol | 19 ++++ .../interfaces/IServiceConfiguration.sol | 8 ++ contracts/permissioned/PermissionedPool.sol | 5 +- .../permissioned/PermissionedPoolFactory.sol | 2 +- contracts/permissioned/PoolAccessControl.sol | 18 ++- .../permissioned/PoolManagerAccessControl.sol | 18 +++ .../permissioned/ToSAcceptanceRegistry.sol | 69 +++++++++++ .../interfaces/IToSAcceptanceRegistry.sol | 37 ++++++ test/ToSConsentRegistry.test.ts | 107 ++++++++++++++++++ .../PermissionedPoolFactory.test.ts | 37 +++++- .../PoolManagerAccessControl.test.ts | 61 +++++++--- test/support/permissionedpool.ts | 9 +- test/support/tosacceptanceregistry.ts | 18 +++ 13 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 contracts/permissioned/ToSAcceptanceRegistry.sol create mode 100644 contracts/permissioned/interfaces/IToSAcceptanceRegistry.sol create mode 100644 test/ToSConsentRegistry.test.ts create mode 100644 test/support/tosacceptanceregistry.ts diff --git a/contracts/ServiceConfiguration.sol b/contracts/ServiceConfiguration.sol index c1d59856..859f46cf 100644 --- a/contracts/ServiceConfiguration.sol +++ b/contracts/ServiceConfiguration.sol @@ -23,6 +23,8 @@ contract ServiceConfiguration is AccessControl, IServiceConfiguration { uint256 public firstLossFeeBps = 500; + address public tosAcceptanceRegistry; + /** * @dev Holds a reference to valid LoanFactories */ @@ -53,6 +55,11 @@ contract ServiceConfiguration is AccessControl, IServiceConfiguration { */ event LoanFactorySet(address indexed factory, bool isValid); + /** + * @dev Emitted when the TermsOfServiceRegistry is set + */ + event TermsOfServiceRegistrySet(address indexed registry); + /** * @dev Constructor for the contract, which sets up the default roles and * owners. @@ -107,4 +114,16 @@ contract ServiceConfiguration is AccessControl, IServiceConfiguration { isLoanFactory[addr] = isValid; emit LoanFactorySet(addr, isValid); } + + /** + * @inheritdoc IServiceConfiguration + */ + function setToSAcceptanceRegistry(address addr) + external + override + onlyOperator + { + tosAcceptanceRegistry = addr; + emit TermsOfServiceRegistrySet(addr); + } } diff --git a/contracts/interfaces/IServiceConfiguration.sol b/contracts/interfaces/IServiceConfiguration.sol index 632b6213..92f0cad0 100644 --- a/contracts/interfaces/IServiceConfiguration.sol +++ b/contracts/interfaces/IServiceConfiguration.sol @@ -18,6 +18,8 @@ interface IServiceConfiguration is IAccessControl { function isLiquidityAsset(address addr) external view returns (bool); + function tosAcceptanceRegistry() external view returns (address); + /** * @dev checks if an address is a valid loan factory * @param addr Address of loan factory @@ -31,4 +33,10 @@ interface IServiceConfiguration is IAccessControl { * @param isValid Whether the loan factory is valid */ function setLoanFactory(address addr, bool isValid) external; + + /** + * @dev Sets the ToSAcceptanceRegistry for the protocol + * @param addr Address of registry + */ + function setToSAcceptanceRegistry(address addr) external; } diff --git a/contracts/permissioned/PermissionedPool.sol b/contracts/permissioned/PermissionedPool.sol index 9f8773d5..faa099aa 100644 --- a/contracts/permissioned/PermissionedPool.sol +++ b/contracts/permissioned/PermissionedPool.sol @@ -58,7 +58,10 @@ contract PermissionedPool is Pool { tokenSymbol ) { - _poolAccessControl = new PoolAccessControl(address(this)); + _poolAccessControl = new PoolAccessControl( + address(this), + IServiceConfiguration(serviceConfiguration).tosAcceptanceRegistry() + ); } /** diff --git a/contracts/permissioned/PermissionedPoolFactory.sol b/contracts/permissioned/PermissionedPoolFactory.sol index 4abe8e9f..ca96afa2 100644 --- a/contracts/permissioned/PermissionedPoolFactory.sol +++ b/contracts/permissioned/PermissionedPoolFactory.sol @@ -31,7 +31,7 @@ contract PermissionedPoolFactory is PoolFactory { _serviceConfiguration.poolManagerAccessControl().isAllowed( msg.sender ), - "caller is not a pool manager" + "caller is not allowed pool manager" ); _; } diff --git a/contracts/permissioned/PoolAccessControl.sol b/contracts/permissioned/PoolAccessControl.sol index 2df8d352..6a12385e 100644 --- a/contracts/permissioned/PoolAccessControl.sol +++ b/contracts/permissioned/PoolAccessControl.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.16; import "./interfaces/IPoolAccessControl.sol"; import "./interfaces/IPermissionedServiceConfiguration.sol"; +import "./interfaces/IToSAcceptanceRegistry.sol"; import "../interfaces/IPool.sol"; /** @@ -18,6 +19,11 @@ contract PoolAccessControl is IPoolAccessControl { */ IPool private _pool; + /** + * @dev Reference to the ToS Acceptance Registry + */ + IToSAcceptanceRegistry private _tosRegistry; + /** * @dev A mapping of addresses to whether they are allowed as a Lender */ @@ -39,8 +45,14 @@ contract PoolAccessControl is IPoolAccessControl { /** * The constructor for the PoolAccessControl contract */ - constructor(address pool) { + constructor(address pool, address tosAcceptanceRegistry) { + require( + tosAcceptanceRegistry != address(0), + "Pool: invalid ToS registry" + ); + _pool = IPool(pool); + _tosRegistry = IToSAcceptanceRegistry(tosAcceptanceRegistry); } /** @@ -57,6 +69,10 @@ contract PoolAccessControl is IPoolAccessControl { * Emits an {AllowedLenderListUpdated} event. */ function allowLender(address addr) external onlyManagerOfPool { + require( + _tosRegistry.hasAccepted(addr), + "Pool: lender not accepted ToS" + ); _allowedLenders[addr] = true; emit AllowedLenderListUpdated(addr, true); diff --git a/contracts/permissioned/PoolManagerAccessControl.sol b/contracts/permissioned/PoolManagerAccessControl.sol index 44e7ee5e..74646ad3 100644 --- a/contracts/permissioned/PoolManagerAccessControl.sol +++ b/contracts/permissioned/PoolManagerAccessControl.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.16; import "./interfaces/IPoolManagerAccessControl.sol"; import "./interfaces/IPermissionedServiceConfiguration.sol"; +import "./interfaces/IToSAcceptanceRegistry.sol"; /** * @title The PoolManagerAccessControl contract @@ -17,6 +18,11 @@ contract PoolManagerAccessControl is IPoolManagerAccessControl { */ IPermissionedServiceConfiguration private _serviceConfiguration; + /** + * @dev Reference to the ToS Acceptance Registry + */ + IToSAcceptanceRegistry private _tosRegistry; + /** * @dev A mapping of addresses to whether they are allowed as a Pool Manager */ @@ -45,6 +51,14 @@ contract PoolManagerAccessControl is IPoolManagerAccessControl { _serviceConfiguration = IPermissionedServiceConfiguration( serviceConfiguration ); + _tosRegistry = IToSAcceptanceRegistry( + _serviceConfiguration.tosAcceptanceRegistry() + ); + + require( + address(_tosRegistry) != address(0), + "Pool: invalid ToS registry" + ); } /** @@ -62,6 +76,10 @@ contract PoolManagerAccessControl is IPoolManagerAccessControl { * Emits an {AllowListUpdated} event. */ function allow(address addr) external onlyOperator { + require( + _tosRegistry.hasAccepted(addr), + "Pool: no ToS acceptance recorded" + ); _allowList[addr] = true; emit AllowListUpdated(addr, true); diff --git a/contracts/permissioned/ToSAcceptanceRegistry.sol b/contracts/permissioned/ToSAcceptanceRegistry.sol new file mode 100644 index 00000000..2be877df --- /dev/null +++ b/contracts/permissioned/ToSAcceptanceRegistry.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.16; + +import "./interfaces/IToSAcceptanceRegistry.sol"; +import "../interfaces/IServiceConfiguration.sol"; + +contract ToSAcceptanceRegistry is IToSAcceptanceRegistry { + /** + * @inheritdoc IToSAcceptanceRegistry + */ + mapping(address => bool) public hasAccepted; + + /** + * @dev ToS URL. + */ + string private _termsOfService; + + /** + * @dev Flag to track when the ToS are "initialized" + */ + bool private _termsSet; + + /** + * @dev ServiceConfiguration + */ + IServiceConfiguration private _serviceConfig; + + /** + * @dev Restricts caller to ServiceOperator + */ + modifier onlyOperator() { + require(_serviceConfig.isOperator(msg.sender), "ToS: not operator"); + _; + } + + constructor(address serviceConfiguration) { + _serviceConfig = IServiceConfiguration(serviceConfiguration); + } + + /** + * @inheritdoc IToSAcceptanceRegistry + */ + function acceptTermsOfService() external override { + require(_termsSet, "ToS: not set"); + + hasAccepted[msg.sender] = true; + emit AcceptanceRecorded(msg.sender); + } + + /** + * @inheritdoc IToSAcceptanceRegistry + */ + function updateTermsOfService(string memory url) + external + override + onlyOperator + { + _termsOfService = url; + _termsSet = true; + emit TermsOfServiceUpdated(); + } + + /** + * @inheritdoc IToSAcceptanceRegistry + */ + function termsOfService() external view override returns (string memory) { + return _termsOfService; + } +} diff --git a/contracts/permissioned/interfaces/IToSAcceptanceRegistry.sol b/contracts/permissioned/interfaces/IToSAcceptanceRegistry.sol new file mode 100644 index 00000000..b98144a8 --- /dev/null +++ b/contracts/permissioned/interfaces/IToSAcceptanceRegistry.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.16; + +/** + * @title The interface for interacting with Terms of Service Acceptance Registry. + */ +interface IToSAcceptanceRegistry { + /** + * @dev Emitted when someone accepts the ToS. + */ + event AcceptanceRecorded(address indexed accepter); + + /** + * @dev Emitted when the Terms of Service is updated. + */ + event TermsOfServiceUpdated(); + + /** + * @dev Returns the current TermsOfService URL + */ + function termsOfService() external view returns (string memory); + + /** + * @dev Updates the TermsOfService. + */ + function updateTermsOfService(string memory url) external; + + /** + * @dev Records that msg.sender has accepted the TermsOfService. + */ + function acceptTermsOfService() external; + + /** + * @dev Returns whether an address has accepted the TermsOfService. + */ + function hasAccepted(address addr) external view returns (bool); +} diff --git a/test/ToSConsentRegistry.test.ts b/test/ToSConsentRegistry.test.ts new file mode 100644 index 00000000..09d7136b --- /dev/null +++ b/test/ToSConsentRegistry.test.ts @@ -0,0 +1,107 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { deployServiceConfiguration } from "./support/serviceconfiguration"; +import { deployToSAcceptanceRegistry } from "./support/tosacceptanceregistry"; + +describe("ToSAcceptanceRegistry", () => { + const TOS_URL = "https://example.xyz/tos"; + + async function deployFixture() { + const [operator, otherAccount] = await ethers.getSigners(); + + const { serviceConfiguration } = await deployServiceConfiguration(); + const { tosAcceptanceRegistry } = await deployToSAcceptanceRegistry( + serviceConfiguration + ); + + return { + tosAcceptanceRegistry, + operator, + otherAccount, + serviceConfiguration + }; + } + + describe("Deployment", async () => { + it("terms of service URL is empty", async () => { + const { tosAcceptanceRegistry } = await loadFixture(deployFixture); + expect(await tosAcceptanceRegistry.termsOfService()).to.be.empty; + }); + }); + + describe("updateTermsOfService()", async () => { + describe("Permissions", () => { + it("can only be called by service operator", async () => { + const { tosAcceptanceRegistry, operator, otherAccount } = + await loadFixture(deployFixture); + + await expect( + tosAcceptanceRegistry + .connect(otherAccount) + .updateTermsOfService(TOS_URL) + ).to.be.revertedWith("ToS: not operator"); + + await expect( + tosAcceptanceRegistry.connect(operator).updateTermsOfService(TOS_URL) + ).to.not.be.reverted; + }); + }); + + it("updates the stored terms of service", async () => { + const { tosAcceptanceRegistry, operator } = await loadFixture( + deployFixture + ); + + expect(await tosAcceptanceRegistry.termsOfService()).to.be.empty; + await expect( + tosAcceptanceRegistry.connect(operator).updateTermsOfService(TOS_URL) + ).to.not.be.reverted; + expect(await tosAcceptanceRegistry.termsOfService()).to.equal(TOS_URL); + }); + + it("emits and event when the ToS URL is updated", async () => { + const { tosAcceptanceRegistry, operator } = await loadFixture( + deployFixture + ); + + await expect( + tosAcceptanceRegistry.connect(operator).updateTermsOfService("test1") + ).to.emit(tosAcceptanceRegistry, "TermsOfServiceUpdated"); + }); + }); + + describe("acceptTermsOfService()", async () => { + it("reverts if ToS haven't been set", async () => { + const { tosAcceptanceRegistry, otherAccount } = await loadFixture( + deployFixture + ); + + await expect( + tosAcceptanceRegistry.connect(otherAccount).acceptTermsOfService() + ).to.be.revertedWith("ToS: not set"); + }); + + it("records acceptance record", async () => { + const { tosAcceptanceRegistry, operator, otherAccount } = + await loadFixture(deployFixture); + expect(await tosAcceptanceRegistry.hasAccepted(otherAccount.address)).to + .be.false; + + // Set the terms, so we can start recording acceptances + await tosAcceptanceRegistry + .connect(operator) + .updateTermsOfService("test1"); + + await expect( + tosAcceptanceRegistry.connect(otherAccount).acceptTermsOfService() + ) + .to.emit(tosAcceptanceRegistry, "AcceptanceRecorded") + .withArgs(otherAccount.address); + + // Acceptance should be recorded + expect(await tosAcceptanceRegistry.hasAccepted(otherAccount.address)).to + .be.true; + }); + }); +}); diff --git a/test/permissioned/PermissionedPoolFactory.test.ts b/test/permissioned/PermissionedPoolFactory.test.ts index 7dc80f93..7bd6b7a3 100644 --- a/test/permissioned/PermissionedPoolFactory.test.ts +++ b/test/permissioned/PermissionedPoolFactory.test.ts @@ -3,6 +3,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { deployMockERC20 } from "../support/erc20"; import { DEFAULT_POOL_SETTINGS } from "../support/pool"; +import { deployToSAcceptanceRegistry } from "../support/tosacceptanceregistry"; describe("PermissionedPoolFactory", () => { async function deployFixture() { @@ -21,6 +22,19 @@ describe("PermissionedPoolFactory", () => { await PermissionedServiceConfiguration.deploy(); await permissionedServiceConfiguration.deployed(); + // Deploy ToS Registry + const { tosAcceptanceRegistry } = await deployToSAcceptanceRegistry( + permissionedServiceConfiguration + ); + + // Configure ToS + await permissionedServiceConfiguration + .connect(operator) + .setToSAcceptanceRegistry(tosAcceptanceRegistry.address); + await tosAcceptanceRegistry + .connect(operator) + .updateTermsOfService("https://terms.example"); + // Deploy the PoolManagerAccessControl contract const PoolManagerAccessControl = await ethers.getContractFactory( "PoolManagerAccessControl" @@ -60,7 +74,8 @@ describe("PermissionedPoolFactory", () => { poolManagerAccessControl, operator, otherAccount, - liquidityAsset + liquidityAsset, + tosAcceptanceRegistry }; } @@ -69,9 +84,11 @@ describe("PermissionedPoolFactory", () => { poolFactory, poolManagerAccessControl, otherAccount, - liquidityAsset + liquidityAsset, + tosAcceptanceRegistry } = await loadFixture(deployFixture); + await tosAcceptanceRegistry.connect(otherAccount).acceptTermsOfService(); await poolManagerAccessControl.allow(otherAccount.getAddress()); await expect( @@ -89,9 +106,11 @@ describe("PermissionedPoolFactory", () => { poolFactory, poolManagerAccessControl, otherAccount, - liquidityAsset + liquidityAsset, + tosAcceptanceRegistry } = await loadFixture(deployFixture); + await tosAcceptanceRegistry.connect(otherAccount).acceptTermsOfService(); await poolManagerAccessControl.allow(otherAccount.getAddress()); await expect( @@ -99,6 +118,16 @@ describe("PermissionedPoolFactory", () => { /* liquidityAsset */ liquidityAsset.address, DEFAULT_POOL_SETTINGS ) - ).to.be.revertedWith("caller is not a pool manager"); + ).to.be.revertedWith("caller is not allowed pool manager"); + }); + + it("access control reverts if PM hasn't accepted ToS", async () => { + const { poolManagerAccessControl, otherAccount } = await loadFixture( + deployFixture + ); + + await expect( + poolManagerAccessControl.allow(otherAccount.getAddress()) + ).to.be.revertedWith("Pool: no ToS acceptance recorded"); }); }); diff --git a/test/permissioned/PoolManagerAccessControl.test.ts b/test/permissioned/PoolManagerAccessControl.test.ts index ffa57d0d..de6eea68 100644 --- a/test/permissioned/PoolManagerAccessControl.test.ts +++ b/test/permissioned/PoolManagerAccessControl.test.ts @@ -1,6 +1,7 @@ import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import { ethers } from "hardhat"; +import { deployToSAcceptanceRegistry } from "../support/tosacceptanceregistry"; describe("PoolManagerAccessControl", () => { // We define a fixture to reuse the same setup in every test. @@ -18,6 +19,14 @@ describe("PoolManagerAccessControl", () => { const serviceConfiguration = await ServiceConfiguration.deploy(); await serviceConfiguration.deployed(); + const { tosAcceptanceRegistry } = await deployToSAcceptanceRegistry( + serviceConfiguration + ); + await tosAcceptanceRegistry.updateTermsOfService("https://terms.xyz"); + await serviceConfiguration.setToSAcceptanceRegistry( + tosAcceptanceRegistry.address + ); + // Deploy the PoolManagerAccessControl contract const PoolManagerAccessControl = await ethers.getContractFactory( "PoolManagerAccessControl" @@ -29,7 +38,8 @@ describe("PoolManagerAccessControl", () => { return { poolManagerAccessControl, - otherAccount + otherAccount, + tosAcceptanceRegistry }; } @@ -45,10 +55,10 @@ describe("PoolManagerAccessControl", () => { }); it("returns true if the address is on the allow list", async () => { - const { poolManagerAccessControl, otherAccount } = await loadFixture( - deployFixture - ); + const { poolManagerAccessControl, otherAccount, tosAcceptanceRegistry } = + await loadFixture(deployFixture); + await tosAcceptanceRegistry.connect(otherAccount).acceptTermsOfService(); await poolManagerAccessControl.allow(otherAccount.address); expect( @@ -58,10 +68,26 @@ describe("PoolManagerAccessControl", () => { }); describe("allow()", () => { - it("adds an address to the allowList", async () => { - const { poolManagerAccessControl, otherAccount } = await loadFixture( - deployFixture - ); + it("reverts when adding an address to the allowList if they haven't accepted ToS", async () => { + const { poolManagerAccessControl, otherAccount, tosAcceptanceRegistry } = + await loadFixture(deployFixture); + + // No ToS acceptance + expect(await tosAcceptanceRegistry.hasAccepted(otherAccount.address)).to + .be.false; + + await expect( + poolManagerAccessControl.allow(otherAccount.address) + ).to.be.revertedWith("Pool: no ToS acceptance recorded"); + }); + + it("adds an address to the allowList if they have accepted the ToS", async () => { + const { poolManagerAccessControl, otherAccount, tosAcceptanceRegistry } = + await loadFixture(deployFixture); + + await tosAcceptanceRegistry.connect(otherAccount).acceptTermsOfService(); + expect(await tosAcceptanceRegistry.hasAccepted(otherAccount.address)).to + .be.true; await poolManagerAccessControl.allow(otherAccount.address); @@ -71,9 +97,9 @@ describe("PoolManagerAccessControl", () => { }); it("succeeds if the address is already in the allowList", async () => { - const { poolManagerAccessControl, otherAccount } = await loadFixture( - deployFixture - ); + const { poolManagerAccessControl, otherAccount, tosAcceptanceRegistry } = + await loadFixture(deployFixture); + await tosAcceptanceRegistry.connect(otherAccount).acceptTermsOfService(); await poolManagerAccessControl.allow(otherAccount.address); await poolManagerAccessControl.allow(otherAccount.address); @@ -99,9 +125,14 @@ describe("PoolManagerAccessControl", () => { describe("events", () => { it("emits an AllowListUpdated event upon adding an address", async () => { - const { poolManagerAccessControl, otherAccount } = await loadFixture( - deployFixture - ); + const { + poolManagerAccessControl, + otherAccount, + tosAcceptanceRegistry + } = await loadFixture(deployFixture); + await tosAcceptanceRegistry + .connect(otherAccount) + .acceptTermsOfService(); expect(await poolManagerAccessControl.allow(otherAccount.address)) .to.emit(poolManagerAccessControl, "AllowListUpdated") @@ -124,7 +155,7 @@ describe("PoolManagerAccessControl", () => { ).to.equal(false); }); - it("succeeds if the address is not in the allowList", async () => { + it("returns false if the address is not in the allowList", async () => { const { poolManagerAccessControl, otherAccount } = await loadFixture( deployFixture ); diff --git a/test/support/permissionedpool.ts b/test/support/permissionedpool.ts index 1b0f0078..1a6ffc7f 100644 --- a/test/support/permissionedpool.ts +++ b/test/support/permissionedpool.ts @@ -2,6 +2,7 @@ import { ethers } from "hardhat"; import { deployMockERC20 } from "./erc20"; import { DEFAULT_POOL_SETTINGS } from "./pool"; import { deployServiceConfiguration } from "./serviceconfiguration"; +import { deployToSAcceptanceRegistry } from "./tosacceptanceregistry"; /** * Deploy an "Initialized" Pool @@ -13,6 +14,12 @@ export async function deployPermissionedPool( const { mockERC20: liquidityAsset } = await deployMockERC20(); const { serviceConfiguration } = await deployServiceConfiguration(); + const { tosAcceptanceRegistry } = await deployToSAcceptanceRegistry( + serviceConfiguration + ); + await serviceConfiguration.setToSAcceptanceRegistry( + tosAcceptanceRegistry.address + ); const PoolLib = await ethers.getContractFactory("PoolLib"); const poolLib = await PoolLib.deploy(); @@ -38,5 +45,5 @@ export async function deployPermissionedPool( poolSettings.firstLossInitialMinimum ); - return { pool, liquidityAsset }; + return { pool, liquidityAsset, tosAcceptanceRegistry }; } diff --git a/test/support/tosacceptanceregistry.ts b/test/support/tosacceptanceregistry.ts new file mode 100644 index 00000000..de43f732 --- /dev/null +++ b/test/support/tosacceptanceregistry.ts @@ -0,0 +1,18 @@ +import { ethers } from "hardhat"; + +/** + * Deploy ToServiceAcceptanceRegistry + */ +export async function deployToSAcceptanceRegistry(serviceConfig: any) { + const ToSAcceptanceRegistry = await ethers.getContractFactory( + "ToSAcceptanceRegistry" + ); + const tosAcceptanceRegistry = await ToSAcceptanceRegistry.deploy( + serviceConfig.address + ); + await tosAcceptanceRegistry.deployed(); + + return { + tosAcceptanceRegistry + }; +}