diff --git a/README.md b/README.md index 61e2368..cf9564e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,54 @@ -## Debt Locker Contracts +# DebtLocker -This readme describes what Collateral Lockers and their Factories are, and how they fit into the architecture. +[![CircleCI](https://circleci.com/gh/maple-labs/debt-locker/tree/main.svg?style=svg)](https://circleci.com/gh/maple-labs/debt-locker/tree/main) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) + +**DISCLAIMER: This code has NOT been externally audited and is actively being developed. Please do not use in production without taking the appropriate steps to ensure maximum security.** + +DebtLocker is a smart contract that allows Pools to interact with different versions of Loans. + +This contract has the following capabilities: +1. Claim funds from a loan, accounting for interest and principal respectively. +2. Accept terms of refinancing. +3. Perform repossession of funds and collateral from a Loan that is in default, transferring the funds to a Liquidator contract. +4. Set the allowed slippage and minimum price of collateral to be used by the liquidator contract. +4. Claim recovered funds from a liquidation, accounting for the amount that was recovered as principal in the context of the Pool, and registering the shortfall. + +### Dependencies/Inheritance +The `DebtLocker` contract is deployed using the `MapleProxyFactory`, which can be found in the modules or on GitHub [here](https://github.com/maple-labs/maple-proxy-factory). + +`MapleProxyFactory` inherits from the generic `ProxyFactory` contract which can be found [here](https://github.com/maple-labs/proxy-factory). + +## Testing and Development +#### Setup +```sh +git clone git@github.com:maple-labs/debt-locker.git +cd debt-locker +dapp update +``` +#### Running Tests +- To run all tests: `make test` (runs `./test.sh`) +- To run a specific test function: `./test.sh -t ` (e.g., `./test.sh -t test_setAllowedSlippage`) +- To run tests with a specified number of fuzz runs: `./test.sh -r ` (e.g., `./test.sh -t test_setAllowedSlippage -r 10000`) + +This project was built using [dapptools](https://github.com/dapphub/dapptools). + +## Roles and Permissions +- **Governor**: Controls all implementation-related logic in the DebtLocker, allowing for new versions of proxies to be deployed from the same factory and upgrade paths between versions to be allowed. +- **Pool Delegate**: Can perform the following actions: +- Claim funds +- Set allowed slippage and minimum price for liquidations +- Trigger default +- Set the auctioneer (dictates price for liquidations) to another contract +- Accept refinance terms +- Set `fundsToCapture`, a variable that represents extra funds in the DebtLocker that should be sent to the Pool and registered as interest. + +## About Maple +[Maple Finance](https://maple.finance) is a decentralized corporate credit market. Maple provides capital to institutional borrowers through globally accessible fixed-income yield opportunities. + +For all technical documentation related to the currently deployed Maple protocol, please refer to the maple-core GitHub [wiki](https://github.com/maple-labs/maple-core/wiki). + +--- + +

+ +

diff --git a/contracts/DebtLocker.sol b/contracts/DebtLocker.sol index 9f8154a..1da2dd6 100644 --- a/contracts/DebtLocker.sol +++ b/contracts/DebtLocker.sol @@ -30,7 +30,7 @@ contract DebtLocker is IDebtLocker, DebtLockerStorage, MapleProxied { } function upgrade(uint256 toVersion_, bytes calldata arguments_) external override { - require(msg.sender == IPoolLike(_pool).poolDelegate(), "DL:U:NOT_POOL_DELEGATE"); + require(msg.sender == _getPoolDelegate(), "DL:U:NOT_POOL_DELEGATE"); IMapleProxyFactory(_factory()).upgradeInstance(toVersion_, arguments_); } @@ -53,6 +53,8 @@ contract DebtLocker is IDebtLocker, DebtLockerStorage, MapleProxied { require(amount_ == uint256(0) || ERC20Helper.transfer(loan_.fundsAsset(), address(_loan), amount_)); loan_.acceptNewTerms(refinancer_, calls_, uint256(0)); + + _principalRemainingAtLastClaim = loan_.principal(); } function setFundsToCapture(uint256 amount_) override external { @@ -119,8 +121,6 @@ contract DebtLocker is IDebtLocker, DebtLockerStorage, MapleProxied { ); } - // TODO: Add setters for allowed slippage and minRatio - /**********************/ /*** View Functions ***/ /**********************/ @@ -211,19 +211,24 @@ contract DebtLocker is IDebtLocker, DebtLockerStorage, MapleProxied { require(!_isLiquidationActive(), "DL:HCOR:LIQ_NOT_FINISHED"); address fundsAsset = IMapleLoanLike(_loan).fundsAsset(); - uint256 recoveredFunds = IERC20Like(fundsAsset).balanceOf(address(this)); // Funds recovered from liquidation and any unclaimed previous payment amounts - uint256 principalToCover = _principalRemainingAtLastClaim; // Principal remaining at time of liquidation + uint256 principalToCover = _principalRemainingAtLastClaim; // Principal remaining at time of liquidation + uint256 fundsCaptured = _fundsToCapture; + + // Funds recovered from liquidation and any unclaimed previous payment amounts + uint256 recoveredFunds = IERC20Like(fundsAsset).balanceOf(address(this)) - fundsCaptured; // If `recoveredFunds` is greater than `principalToCover`, the remaining amount is treated as interest in the context of the pool. // If `recoveredFunds` is less than `principalToCover`, the difference is registered as a shortfall. - details_[0] = recoveredFunds; + details_[0] = recoveredFunds + fundsCaptured; details_[1] = recoveredFunds > principalToCover ? recoveredFunds - principalToCover : 0; + details_[2] = fundsCaptured; details_[5] = recoveredFunds > principalToCover ? principalToCover : recoveredFunds; details_[6] = principalToCover > recoveredFunds ? principalToCover - recoveredFunds : 0; - require(ERC20Helper.transfer(fundsAsset, _pool, recoveredFunds), "DL:HCOR:TRANSFER"); + require(ERC20Helper.transfer(fundsAsset, _pool, recoveredFunds + fundsCaptured), "DL:HCOR:TRANSFER"); - _repossessed = false; + _fundsToCapture = uint256(0); + _repossessed = false; } function _handleClaim() internal returns (uint256[7] memory details_) { diff --git a/contracts/test/DebtLocker.t.sol b/contracts/test/DebtLocker.t.sol index 3a20cf3..f106187 100644 --- a/contracts/test/DebtLocker.t.sol +++ b/contracts/test/DebtLocker.t.sol @@ -532,7 +532,8 @@ contract DebtLockerTest is TestUtils { uint256 principalAfter = loan.principal(); - assertEq(principalBefore + principalIncrease_, principalAfter); + assertEq(principalBefore + principalIncrease_, principalAfter); + assertEq(debtLocker.principalRemainingAtLastClaim(), principalAfter); } function test_fundsToCaptureForNextClaim() public { @@ -590,4 +591,39 @@ contract DebtLockerTest is TestUtils { pool.claim(address(debtLocker)); } + function test_fundsToCaptureWhileInDefault() public { + ( loan, debtLocker ) = _createFundAndDrawdownLoan(1_000_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)); + + // After triggering default, set funds to capture + poolDelegate.debtLocker_setFundsToCapture(address(debtLocker), 500_000); + + // Claim + uint256[7] memory details = pool.claim(address(debtLocker)); + + assertEq(fundsAsset.balanceOf(address(debtLocker)), 0); + assertEq(fundsAsset.balanceOf(address(pool)), 500_000); + assertEq(debtLocker.fundsToCapture(), 0); + + 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 + } + }