diff --git a/contracts/DebtLocker.sol b/contracts/DebtLocker.sol index 5cec781..ccc3c8f 100644 --- a/contracts/DebtLocker.sol +++ b/contracts/DebtLocker.sol @@ -12,7 +12,7 @@ import { IERC20Like, IMapleGlobalsLike, IMapleLoanLike, IPoolLike, IPoolFactoryL import { DebtLockerStorage } from "./DebtLockerStorage.sol"; -/// @title DebtLocker holds custody of LoanFDT tokens. +/// @title DebtLocker interacts with Loans on behalf of PoolV1 contract DebtLocker is IDebtLocker, DebtLockerStorage, MapleProxied { /********************************/ diff --git a/contracts/test/DebtLocker.t.sol b/contracts/test/DebtLocker.t.sol index 25bbe79..012a5b8 100644 --- a/contracts/test/DebtLocker.t.sol +++ b/contracts/test/DebtLocker.t.sol @@ -15,7 +15,16 @@ import { PoolDelegate } from "./accounts/PoolDelegate.sol"; import { ILiquidatorLike } from "../interfaces/Interfaces.sol"; -import { MockGlobals, MockLiquidationStrategy, MockPool, MockPoolFactory } from "./mocks/Mocks.sol"; +import { DebtLockerHarness } from "./mocks/DebtLockerHarness.sol"; +import { ManipulatableDebtLocker } from "./mocks/ManipulatableDebtLocker.sol"; +import { + MockGlobals, + MockLiquidationStrategy, + MockLoan, + MockMigrator, + MockPool, + MockPoolFactory +} from "./mocks/Mocks.sol"; interface Hevm { @@ -26,9 +35,7 @@ interface Hevm { contract DebtLockerTests is TestUtils { - ConstructableMapleLoan internal loan; DebtLockerFactory internal dlFactory; - DebtLocker internal debtLocker; Governor internal governor; MockERC20 internal collateralAsset; MockERC20 internal fundsAsset; @@ -70,36 +77,54 @@ contract DebtLockerTests is TestUtils { globals.setPrice(address(fundsAsset), 1 * 10 ** 8); // 1 USD } - function _createLoan(uint256 principalRequested_) internal returns (ConstructableMapleLoan loan_) { + function _createLoan(uint256 principalRequested_, uint256 collateralRequired_) internal returns (ConstructableMapleLoan loan_) { address[2] memory assets = [address(collateralAsset), address(fundsAsset)]; uint256[3] memory termDetails = [uint256(10 days), uint256(30 days), 6]; - uint256[3] memory amounts = [uint256(0), principalRequested_, 0]; + uint256[3] memory amounts = [collateralRequired_, principalRequested_, 0]; uint256[4] memory rates = [uint256(0.10e18), uint256(0), uint256(0), uint256(0)]; loan_ = new ConstructableMapleLoan(address(this), assets, termDetails, amounts, rates); } - function _createFundAndDrawdownLoan(uint256 principalRequested_) internal returns (ConstructableMapleLoan loan_, DebtLocker debtLocker_) { - loan_ = _createLoan(principalRequested_); + function _fundAndDrawdownLoan(address loan_, address debtLocker_) internal { + ConstructableMapleLoan loan = ConstructableMapleLoan(loan_); - debtLocker_ = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan_))); + uint256 principalRequested = loan.principalRequested(); + uint256 collateralRequired = loan.collateralRequired(); + + fundsAsset.mint(address(this), principalRequested); + fundsAsset.approve(loan_, principalRequested); + + loan.fundLoan(debtLocker_, principalRequested); + + collateralAsset.mint(address(this), collateralRequired); + collateralAsset.approve(loan_, collateralRequired); + + loan.drawdownFunds(loan.drawableFunds(), address(1)); // Drawdown to empty funds from loan + } + + function _createFundAndDrawdownLoan(uint256 principalRequested_, uint256 collateralRequired_) internal returns (ConstructableMapleLoan loan_, DebtLocker debtLocker_) { + loan_ = _createLoan(principalRequested_, collateralRequired_); - fundsAsset.mint(address(this), principalRequested_); - fundsAsset.approve(address(loan_), principalRequested_); + debtLocker_ = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan_))); - loan_.fundLoan(address(debtLocker_), principalRequested_); - loan_.drawdownFunds(loan_.drawableFunds(), address(1)); // Drawdown to empty funds from loan (account for estab fees) + _fundAndDrawdownLoan(address(loan_), address(debtLocker_)); } - function test_claim(uint256 principalRequested_) public { + /*******************/ + /*** Claim Tests ***/ + /*******************/ + + function test_claim(uint256 principalRequested_, uint256 collateralRequired_) public { /**********************************/ /*** Create Loan and DebtLocker ***/ /**********************************/ principalRequested_ = constrictToRange(principalRequested_, 1_000_000, MAX_TOKEN_AMOUNT); + collateralRequired_ = constrictToRange(collateralRequired_, 0, MAX_TOKEN_AMOUNT); - ( loan, debtLocker ) = _createFundAndDrawdownLoan(principalRequested_); + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(principalRequested_, collateralRequired_); /*************************/ /*** Make two payments ***/ @@ -174,7 +199,7 @@ contract DebtLockerTests is TestUtils { } function test_initialize_invalidCollateralAsset() public { - loan = _createLoan(1_000_000); + ConstructableMapleLoan loan = _createLoan(1_000_000, 300_000); assertTrue(globals.isValidCollateralAsset(loan.collateralAsset())); @@ -190,7 +215,7 @@ contract DebtLockerTests is TestUtils { } function test_initialize_invalidLiquidityAsset() public { - loan = _createLoan(1_000_000); + ConstructableMapleLoan loan = _createLoan(1_000_000, 300_000); assertTrue(globals.isValidLiquidityAsset(loan.fundsAsset())); @@ -205,7 +230,7 @@ contract DebtLockerTests is TestUtils { assertTrue(pool.createDebtLocker(address(dlFactory), address(loan)) != address(0)); } - function test_liquidation_shortfall(uint256 principalRequested_, uint256 collateralRequired_) public { + function test_claim_liquidation_shortfall(uint256 principalRequested_, uint256 collateralRequired_) public { /**********************************/ /*** Create Loan and DebtLocker ***/ @@ -214,10 +239,7 @@ contract DebtLockerTests is TestUtils { principalRequested_ = constrictToRange(principalRequested_, 1_000_000, MAX_TOKEN_AMOUNT); collateralRequired_ = constrictToRange(collateralRequired_, 0, principalRequested_ / 12); - ( loan, debtLocker ) = _createFundAndDrawdownLoan(principalRequested_); - - // Mint collateral into loan, representing 10x value since market value is $10 - collateralAsset.mint(address(loan), collateralRequired_); + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(principalRequested_, collateralRequired_); /**********************/ /*** Make a payment ***/ @@ -300,7 +322,7 @@ contract DebtLockerTests is TestUtils { assertEq(details[6], principalToCover - amountRecovered); // Shortfall to be covered by burning BPTs } - function test_liquidation_equalToPrincipal(uint256 principalRequested_) public { + function test_claim_liquidation_equalToPrincipal(uint256 principalRequested_) public { /*************************/ /*** Set up parameters ***/ @@ -313,10 +335,8 @@ contract DebtLockerTests is TestUtils { /**********************************/ /*** Create Loan and DebtLocker ***/ /**********************************/ - ( loan, debtLocker ) = _createFundAndDrawdownLoan(principalRequested_); - // Mint collateral into loan, representing 10x value since market value is $10 - collateralAsset.mint(address(loan), collateralRequired); + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(principalRequested_, collateralRequired); /*************************************/ /*** Trigger default and liquidate ***/ @@ -383,7 +403,7 @@ contract DebtLockerTests is TestUtils { assertEq(details[6], 0); // Zero shortfall since principalToCover == amountRecovered } - function test_liquidation_greaterThanPrincipal(uint256 principalRequested_, uint256 excessRecovered_) public { + function test_claim_liquidation_greaterThanPrincipal(uint256 principalRequested_, uint256 excessRecovered_) public { /*************************/ /*** Set up parameters ***/ @@ -397,10 +417,8 @@ contract DebtLockerTests is TestUtils { /**********************************/ /*** Create Loan and DebtLocker ***/ /**********************************/ - ( loan, debtLocker ) = _createFundAndDrawdownLoan(principalRequested_); - - // Mint collateral into loan, representing 10x value since market value is $10 - collateralAsset.mint(address(loan), collateralRequired); + + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(principalRequested_, collateralRequired); /*************************************/ /*** Trigger default and liquidate ***/ @@ -474,11 +492,7 @@ contract DebtLockerTests is TestUtils { principalRequested_ = constrictToRange(principalRequested_, 1_000_000, MAX_TOKEN_AMOUNT); collateralRequired_ = constrictToRange(collateralRequired_, 1, principalRequested_ / 10); // Need collateral for liquidator deployment - ( loan, debtLocker ) = _createFundAndDrawdownLoan(principalRequested_); - - // Mint collateral into loan, representing 10x value since market value is $10 - // TODO: Change this when DL unit testing PR gets merged - collateralAsset.mint(address(loan), collateralRequired_); + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(principalRequested_, collateralRequired_); /*************************************/ /*** Trigger default and liquidate ***/ @@ -492,7 +506,6 @@ contract DebtLockerTests is TestUtils { if (collateralRequired_ > 0) { MockLiquidationStrategy mockLiquidationStrategy = new MockLiquidationStrategy(); - mockLiquidationStrategy.flashBorrowLiquidation(liquidator, collateralRequired_, address(collateralAsset), address(fundsAsset)); } @@ -515,11 +528,50 @@ contract DebtLockerTests is TestUtils { pool.claim(address(debtLocker)); // Can successfully claim } + + /****************************/ + /*** Access Control Tests ***/ + /****************************/ + + function test_acl_factory_migrate() external { + MockLoan mockLoan = new MockLoan(); + + ManipulatableDebtLocker debtLocker = new ManipulatableDebtLocker(address(mockLoan), address(pool), address(dlFactory)); - function test_setAllowedSlippage() external { - loan = _createLoan(1_000_000); + address migrator = address(new MockMigrator()); - debtLocker = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan))); + try debtLocker.migrate(address(migrator), abi.encode(0)) { assertTrue(false, "Non-factory calling migrate"); } catch { } + + assertEq(debtLocker.factory(), address(dlFactory)); + + debtLocker.setFactory(address(this)); + + assertEq(debtLocker.factory(), address(this)); + + debtLocker.migrate(address(migrator), abi.encode(0)); + } + + function test_acl_factory_setImplementation() external { + MockLoan mockLoan = new MockLoan(); + + ManipulatableDebtLocker debtLocker = new ManipulatableDebtLocker(address(mockLoan), address(pool), address(dlFactory)); + + try debtLocker.setImplementation(address(1)) { assertTrue(false, "Non-factory calling setImplementation"); } catch { } + + assertEq(debtLocker.factory(), address(dlFactory)); + + debtLocker.setFactory(address(this)); + + assertEq(debtLocker.factory(), address(this)); + assertEq(debtLocker.implementation(), address(0)); + + debtLocker.setImplementation(address(1)); + } + + function test_acl_poolDelegate_setAllowedSlippage() external { + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); + + DebtLocker debtLocker = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan))); assertEq(debtLocker.allowedSlippage(), 0); @@ -529,8 +581,8 @@ contract DebtLockerTests is TestUtils { assertEq(debtLocker.allowedSlippage(), 100); } - function test_setAuctioneer() external { - ( loan, debtLocker ) = _createFundAndDrawdownLoan(1_000_000); + function test_acl_poolDelegate_setAuctioneer() external { + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(1_000_000, 30_000); // Mint collateral into loan so that liquidator gets deployed collateralAsset.mint(address(loan), 1000); @@ -547,10 +599,23 @@ contract DebtLockerTests is TestUtils { assertEq(ILiquidatorLike(debtLocker.liquidator()).auctioneer(), address(1)); } - function test_setMinRatio() external { - loan = _createLoan(1_000_000); + function test_acl_poolDelegate_setFundsToCapture() external { + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); + + DebtLocker debtLocker = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan))); - debtLocker = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan))); + assertEq(debtLocker.fundsToCapture(), 0); + + assertTrue(!notPoolDelegate.try_debtLocker_setFundsToCapture(address(debtLocker), 100 * 10 ** 6)); // Non-PD can't set + assertTrue( poolDelegate.try_debtLocker_setFundsToCapture(address(debtLocker), 100 * 10 ** 6)); // PD can set + + assertEq(debtLocker.fundsToCapture(), 100 * 10 ** 6); + } + + function test_acl_poolDelegate_setMinRatio() external { + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); + + DebtLocker debtLocker = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan))); assertEq(debtLocker.minRatio(), 0); @@ -560,21 +625,101 @@ contract DebtLockerTests is TestUtils { assertEq(debtLocker.minRatio(), 100 * 10 ** 6); } - function test_refinance_withAmountIncrease(uint256 principalRequested_, uint256 principalIncrease_) external { + function test_acl_poolDelegate_upgrade() external { + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); + + DebtLocker debtLocker = DebtLocker(pool.createDebtLocker(address(dlFactory), address(loan))); + + // Deploying and registering DebtLocker implementation and initializer + address implementationV2 = address(new DebtLocker()); + address initializerV2 = address(new DebtLockerInitializer()); + + governor.mapleProxyFactory_registerImplementation(address(dlFactory), 2, implementationV2, initializerV2); + governor.mapleProxyFactory_enableUpgradePath(address(dlFactory), 1, 2, address(0)); + + assertTrue(dlFactory.implementationOf(1) != dlFactory.implementationOf(2)); + + assertEq(debtLocker.implementation(), dlFactory.implementationOf(1)); + + bytes memory arguments = new bytes(0); + + assertTrue(!notPoolDelegate.try_debtLocker_upgrade(address(debtLocker), 2, arguments)); // Non-PD can't set + assertTrue( poolDelegate.try_debtLocker_upgrade(address(debtLocker), 2, arguments)); // PD can set + + assertEq(debtLocker.implementation(), dlFactory.implementationOf(2)); + } + + function test_acl_poolDelegate_acceptNewTerms() external { + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(1_000_000, 30_000); + + address refinancer = address(new Refinancer()); + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("setEarlyFeeRate(uint256)", 100); + + loan.proposeNewTerms(refinancer, data); // address(this) is borrower + + assertEq(loan.earlyFeeRate(), 0); + + assertTrue(!notPoolDelegate.try_debtLocker_acceptNewTerms(address(debtLocker), refinancer, data, 0)); // Non-PD can't set + assertTrue( poolDelegate.try_debtLocker_acceptNewTerms(address(debtLocker), refinancer, data, 0)); // PD can set + + assertEq(loan.earlyFeeRate(), 100); + } + + function test_acl_pool_claim() external { + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); + + ManipulatableDebtLocker debtLocker = new ManipulatableDebtLocker(address(loan), address(pool), address(dlFactory)); + + _fundAndDrawdownLoan(address(loan), address(debtLocker)); + + ( uint256 principal1, uint256 interest1 ) = loan.getNextPaymentBreakdown(); + + uint256 total1 = principal1 + interest1; + + // Make a payment amount with interest and principal + fundsAsset.mint(address(this), total1); + fundsAsset.approve(address(loan), total1); // Mock payment amount + + loan.makePayment(total1); + + try debtLocker.claim() { assertTrue(false, "Non-pool able to claim"); } catch { } + + debtLocker.setPool(address(this)); + + debtLocker.claim(); + } + + function test_acl_pool_triggerDefault() external { + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); + + ManipulatableDebtLocker debtLocker = new ManipulatableDebtLocker(address(loan), address(pool), address(dlFactory)); + + _fundAndDrawdownLoan(address(loan), address(debtLocker)); + + hevm.warp(loan.nextPaymentDueDate() + loan.gracePeriod() + 1); + + try debtLocker.triggerDefault() { assertTrue(false, "Non-pool able to triggerDefault"); } catch { } + + debtLocker.setPool(address(this)); + + debtLocker.triggerDefault(); + } + + /***********************/ + /*** Refinance Tests ***/ + /***********************/ + + function test_refinance_withAmountIncrease(uint256 principalRequested_, uint256 collateralRequired_, uint256 principalIncrease_) external { principalRequested_ = constrictToRange(principalRequested_, 1_000_000, MAX_TOKEN_AMOUNT); + collateralRequired_ = constrictToRange(collateralRequired_, 0, principalRequested_ / 10); principalIncrease_ = constrictToRange(principalIncrease_, 1, MAX_TOKEN_AMOUNT); /**********************************/ /*** Create Loan and DebtLocker ***/ /**********************************/ - ( loan, debtLocker ) = _createFundAndDrawdownLoan(principalRequested_); - - fundsAsset.mint(address(this), principalRequested_); - fundsAsset.approve(address(loan), principalRequested_); - - loan.fundLoan(address(debtLocker), principalRequested_); - loan.drawdownFunds(loan.drawableFunds(), address(1)); // Drawdown to empty funds from loan (account for estab fees) + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(principalRequested_, collateralRequired_); /**********************/ /*** Make a payment ***/ @@ -602,12 +747,12 @@ contract DebtLockerTests is TestUtils { fundsAsset.mint(address(debtLocker), principalIncrease_); - // should fail due to pending claim + // Should fail due to pending claim try debtLocker.acceptNewTerms(refinancer, data, principalIncrease_) { fail(); } catch { } pool.claim(address(debtLocker)); - // should fail for not pool delegate + // Should fail for not pool delegate try notPoolDelegate.debtLocker_acceptNewTerms(address(debtLocker), refinancer, data, principalIncrease_) { fail(); } catch { } // Note: More state changes in real loan that are asserted in integration tests @@ -621,10 +766,14 @@ contract DebtLockerTests is TestUtils { assertEq(debtLocker.principalRemainingAtLastClaim(), principalAfter); } - // TODO: test_refinance_withExcessAmount + // TODO: test_refinance_withExcessAmount (Will leave in until DoS loan PR is merged and updated as submodule) + + /***************************/ + /*** Funds Capture Tests ***/ + /***************************/ function test_fundsToCaptureForNextClaim() public { - ( loan, debtLocker ) = _createFundAndDrawdownLoan(1_000_000); + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(1_000_000, 30_000); // Make a payment amount with interest and principal ( uint256 principal, uint256 interest ) = loan.getNextPaymentBreakdown(); @@ -656,8 +805,49 @@ contract DebtLockerTests is TestUtils { assertEq(details[6], 0); } + function test_fundsToCaptureWhileInDefault() public { + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(1_000_000, 30_000); + + // Prepare additional amount to be captured + fundsAsset.mint(address(debtLocker), 500_000); + + assertEq(fundsAsset.balanceOf(address(debtLocker)), 500_000); + assertEq(fundsAsset.balanceOf(address(pool)), 0); + assertEq(debtLocker.principalRemainingAtLastClaim(), loan.principalRequested()); + assertEq(debtLocker.fundsToCapture(), 0); + + // Trigger default + hevm.warp(loan.nextPaymentDueDate() + loan.gracePeriod() + 1); + + pool.triggerDefault(address(debtLocker)); // ACL not done in mock pool + + // After triggering default, set funds to capture + poolDelegate.debtLocker_setFundsToCapture(address(debtLocker), 500_000); + + // Claim + try pool.claim(address(debtLocker)) { assertTrue(false, "Able to claim during active liquidation"); } catch { } + + MockLiquidationStrategy mockLiquidationStrategy = new MockLiquidationStrategy(); + + mockLiquidationStrategy.flashBorrowLiquidation(debtLocker.liquidator(), loan.collateralRequired(), address(collateralAsset), address(fundsAsset)); + + uint256[7] memory details = pool.claim(address(debtLocker)); + + assertEq(fundsAsset.balanceOf(address(debtLocker)), 0); + assertEq(fundsAsset.balanceOf(address(pool)), 800_000); + assertEq(debtLocker.fundsToCapture(), 0); + + assertEq(details[0], 500_000 + 300_000); // Funds to capture included, with recovered funds (30k at $10) + assertEq(details[1], 0); + assertEq(details[2], 500_000); // Funds to capture accounted as principal + assertEq(details[3], 0); + assertEq(details[4], 0); + assertEq(details[5], 300_000); // Recovered funds (30k at $10) + assertEq(details[6], 700_000); // 300k recovered on a 1m loan + } + function testFail_fundsToCaptureForNextClaim() public { - ( loan, debtLocker ) = _createFundAndDrawdownLoan(1_000_000); + ( ConstructableMapleLoan loan, DebtLocker debtLocker ) = _createFundAndDrawdownLoan(1_000_000, 30_000); fundsAsset.mint(address(loan), 1_000_000); loan.fundLoan(address(debtLocker), 1_000_000); @@ -678,61 +868,78 @@ contract DebtLockerTests is TestUtils { pool.claim(address(debtLocker)); } - function test_fundsToCaptureWhileInDefault() public { - ( loan, debtLocker ) = _createFundAndDrawdownLoan(1_000_000); + /************************************/ + /*** Internal View Function Tests ***/ + /************************************/ - // Prepare additional amount to be captured - fundsAsset.mint(address(debtLocker), 500_000); + function _registerDebtLockerHarnesss() internal { + // Deploying and registering DebtLocker implementation and initializer + address implementation = address(new DebtLockerHarness()); + address initializer = address(new DebtLockerInitializer()); - assertEq(fundsAsset.balanceOf(address(debtLocker)), 500_000); - assertEq(fundsAsset.balanceOf(address(pool)), 0); - assertEq(debtLocker.principalRemainingAtLastClaim(), loan.principalRequested()); - assertEq(debtLocker.fundsToCapture(), 0); + governor.mapleProxyFactory_registerImplementation(address(dlFactory), 2, implementation, initializer); + governor.mapleProxyFactory_setDefaultVersion(address(dlFactory), 2); + } - // Trigger default - hevm.warp(loan.nextPaymentDueDate() + loan.gracePeriod() + 1); + function test_getGlobals() public { + _registerDebtLockerHarnesss(); - pool.triggerDefault(address(debtLocker)); + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); - // After triggering default, set funds to capture - poolDelegate.debtLocker_setFundsToCapture(address(debtLocker), 500_000); + DebtLockerHarness debtLocker = DebtLockerHarness(pool.createDebtLocker(address(dlFactory), address(loan))); - // Claim - uint256[7] memory details = pool.claim(address(debtLocker)); + assertEq(debtLocker.getGlobals(), address(globals)); + } - assertEq(fundsAsset.balanceOf(address(debtLocker)), 0); - assertEq(fundsAsset.balanceOf(address(pool)), 500_000); - assertEq(debtLocker.fundsToCapture(), 0); + function test_getPoolDelegate() public { + _registerDebtLockerHarnesss(); - assertEq(details[0], 500_000); - assertEq(details[1], 0); - assertEq(details[2], 500_000); - assertEq(details[3], 0); - assertEq(details[4], 0); - assertEq(details[5], 0); - assertEq(details[6], loan.principalRequested()); // No principal was recovered + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); + + DebtLockerHarness debtLocker = DebtLockerHarness(pool.createDebtLocker(address(dlFactory), address(loan))); + + assertEq(debtLocker.poolDelegate(), address(poolDelegate)); } - // TODO: test that only factory can call migrate + function test_isLiquidationActive() public { + _registerDebtLockerHarnesss(); - // TODO: test that only factory can call setImplementation + ConstructableMapleLoan loan = _createLoan(1_000_000, 30_000); - // TODO: test that only poolDelegate can call upgrade + DebtLockerHarness debtLocker = DebtLockerHarness(pool.createDebtLocker(address(dlFactory), address(loan))); - // TODO: test that only poolDelegate can call acceptNewTerms + // No liquidator deployed, liquidation not active + assertTrue(!(debtLocker.liquidator() != address(0))); + assertTrue(!(collateralAsset.balanceOf(debtLocker.liquidator()) > 0)); + assertTrue(!debtLocker.isLiquidationActive()); - // TODO: test that only pool can call claim + collateralAsset.mint(address(0), 100); // address(0) can have a balance of collateralAsset on mainnet - // TODO: test that only poolDelegate can call setFundsToCapture + // Zero address has balance of collateralAsset, liquidation not active + assertTrue(!(debtLocker.liquidator() != address(0))); + assertTrue( (collateralAsset.balanceOf(debtLocker.liquidator()) > 0)); // address(0) can have a balance of collateralAsset on mainnet + assertTrue(!debtLocker.isLiquidationActive()); - // TODO: test that only pool can call triggerDefault + _fundAndDrawdownLoan(address(loan), address(debtLocker)); - // TODO: test that triggerDefault can only be called if claim was called + hevm.warp(loan.nextPaymentDueDate() + loan.gracePeriod() + 1); - // TODO: test _getGlobals + pool.triggerDefault(address(debtLocker)); + + // Liquidator is deployed, and new liquidator address has a balance, liquidation active + assertTrue((debtLocker.liquidator() != address(0))); + assertTrue((collateralAsset.balanceOf(debtLocker.liquidator()) > 0)); + assertTrue(debtLocker.isLiquidationActive()); - // TODO: test _getPoolDelegate + // Perform fake liquidation + MockLiquidationStrategy mockLiquidationStrategy = new MockLiquidationStrategy(); - // TODO: test _isLiquidationActive + mockLiquidationStrategy.flashBorrowLiquidation(debtLocker.liquidator(), loan.collateralRequired(), address(collateralAsset), address(fundsAsset)); + + // Liquidator is deployed, liquidator has no balance, liquidation finished + assertTrue( (debtLocker.liquidator() != address(0))); + assertTrue(!(collateralAsset.balanceOf(debtLocker.liquidator()) > 0)); + assertTrue(!debtLocker.isLiquidationActive()); + } } diff --git a/contracts/test/accounts/PoolDelegate.sol b/contracts/test/accounts/PoolDelegate.sol index 7853160..4a06abd 100644 --- a/contracts/test/accounts/PoolDelegate.sol +++ b/contracts/test/accounts/PoolDelegate.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.7; import { User as ProxyUser } from "../../../modules/maple-proxy-factory/contracts/test/accounts/User.sol"; -import { IDebtLocker } from "../../interfaces/IDebtLocker.sol"; +import { IDebtLocker, IMapleProxied } from "../../interfaces/IDebtLocker.sol"; contract PoolDelegate is ProxyUser { @@ -34,6 +34,10 @@ contract PoolDelegate is ProxyUser { function debtLocker_stopLiquidation(address debtLocker_) external { IDebtLocker(debtLocker_).stopLiquidation(); } + + function debtLocker_upgrade(address debtLocker_, uint256 toVersion_, bytes memory arguments_) external { + IDebtLocker(debtLocker_).upgrade(toVersion_, arguments_); + } /*********************/ /*** Try Functions ***/ @@ -67,5 +71,9 @@ contract PoolDelegate is ProxyUser { function try_debtLocker_stopLiquidation(address debtLocker_) external returns (bool ok_) { ( ok_, ) = debtLocker_.call(abi.encodeWithSelector(IDebtLocker.stopLiquidation.selector)); } + + function try_debtLocker_upgrade(address debtLocker_, uint256 toVersion_, bytes memory arguments_) external returns (bool ok_) { + ( ok_, ) = debtLocker_.call(abi.encodeWithSelector(IMapleProxied.upgrade.selector, toVersion_, arguments_)); + } } diff --git a/contracts/test/mocks/DebtLockerHarness.sol b/contracts/test/mocks/DebtLockerHarness.sol new file mode 100644 index 0000000..8850316 --- /dev/null +++ b/contracts/test/mocks/DebtLockerHarness.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.7; + +import { DebtLocker } from "../../DebtLocker.sol"; + +contract DebtLockerHarness is DebtLocker { + + /*************************/ + /*** Harness Functions ***/ + /*************************/ + + function getGlobals() external view returns (address) { + return _getGlobals(); + } + + function getPoolDelegate() external view returns(address) { + return _getPoolDelegate(); + } + + function isLiquidationActive() external view returns (bool) { + return _isLiquidationActive(); + } + +} diff --git a/contracts/test/mocks/ManipulatableDebtLocker.sol b/contracts/test/mocks/ManipulatableDebtLocker.sol new file mode 100644 index 0000000..8066077 --- /dev/null +++ b/contracts/test/mocks/ManipulatableDebtLocker.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.7; + +import { IMapleLoanLike } from "../../interfaces/Interfaces.sol"; + +import { DebtLocker } from "../../DebtLocker.sol"; + +contract ManipulatableDebtLocker is DebtLocker { + + bytes32 constant FACTORY_SLOT = bytes32(0x7a45a402e4cb6e08ebc196f20f66d5d30e67285a2a8aa80503fa409e727a4af1); + + constructor(address loan_, address pool_, address factory_) public { + _loan = loan_; + _pool = pool_; + + _principalRemainingAtLastClaim = IMapleLoanLike(loan_).principalRequested(); + + setFactory(factory_); + } + + /**************************************/ + /*** Storage Manipulation Functions ***/ + /**************************************/ + + function setFactory(address factory_) public { + _setSlotValue(FACTORY_SLOT, bytes32(uint256(uint160(factory_)))); + } + + function setPool(address pool_) external { + _pool = pool_; + } + +} diff --git a/contracts/test/mocks/Mocks.sol b/contracts/test/mocks/Mocks.sol index 2b436b6..d8a5361 100644 --- a/contracts/test/mocks/Mocks.sol +++ b/contracts/test/mocks/Mocks.sol @@ -68,6 +68,18 @@ contract MockLiquidationStrategy { } +contract MockLoan { + + function principalRequested() external view returns (uint256 principalRequested_) { + return 0; + } + + function acceptNewTerms(address refinancer_, bytes[] calldata calls_, uint256 amount_) external { + // Empty, just testing ACL + } + +} + contract MockGlobals { address public governor; @@ -110,3 +122,9 @@ contract MockGlobals { } } + +contract MockMigrator { + + fallback() external { } + +}