From ea5d5bd41b802d80c7eaad731024a3c1299d60d5 Mon Sep 17 00:00:00 2001 From: Santiago Lisa Date: Tue, 15 Nov 2022 16:05:42 -0500 Subject: [PATCH] Feat/buyout test (#92) --- .gitignore | 1 + src/AstariaRouter.sol | 14 +----- src/LienToken.sol | 72 +++++++++++++++++-------------- src/PublicVault.sol | 15 +++++++ src/interfaces/IAstariaRouter.sol | 5 +-- src/interfaces/IPublicVault.sol | 8 ++++ src/test/AstariaTest.t.sol | 65 +++++++++++++++++++++------- 7 files changed, 115 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index fdc245aa..b3d13a81 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ out lcov.info +scripts/loanProofGenerator.js \ No newline at end of file diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index ec136435..f52ace7d 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -96,10 +96,9 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { s.maxInterestRate = ((uint256(1e16) * 200) / (365 days)).safeCastTo88(); //63419583966; // 200% apy / second s.strategistFeeNumerator = uint32(200); s.strategistFeeDenominator = uint32(1000); - s.buyoutFeeNumerator = uint32(200); + s.buyoutFeeNumerator = uint32(100); s.buyoutFeeDenominator = uint32(1000); s.minDurationIncrease = uint32(5 days); - s.buyoutInterestWindow = uint32(60 days); s.guardian = address(msg.sender); } @@ -259,9 +258,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { } else if (what == FileType.FeeTo) { address addr = abi.decode(data, (address)); s.feeTo = addr; - } else if (what == FileType.BuyoutInterestWindow) { - uint256 value = abi.decode(data, (uint256)); - s.buyoutInterestWindow = value.safeCastTo32(); } else if (what == FileType.StrategyValidator) { (uint8 TYPE, address addr) = abi.decode(data, (uint8, address)); s.strategyValidators[TYPE] = addr; @@ -618,14 +614,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { ); } - /** - * @notice Retrieves the time window for computing maxbuyout costs - * @return The numerator and denominator used to compute the percentage fee taken by the protocol - */ - function getBuyoutInterestWindow() external view returns (uint32) { - return _loadRouterSlot().buyoutInterestWindow; - } - /** * @notice Returns whether a given address is that of a Vault. * @param vault The Vault address. diff --git a/src/LienToken.sol b/src/LienToken.sol index 8d41f257..c2d2adef 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -145,25 +145,34 @@ contract LienToken is ERC721, ILienToken, Auth { ) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); } - (uint256 owed, uint256 buyout) = getBuyout( + (uint256 owed, uint256 buyout) = _getBuyout( + s, params.encumber.stack[params.position] ); if (params.encumber.lien.details.maxAmount < owed) { revert InvalidBuyoutDetails(params.encumber.lien.details.maxAmount, owed); } - if ( - msg.sender != - s.COLLATERAL_TOKEN.ownerOf( - params.encumber.stack[params.position].lien.collateralId - ) && - msg.sender != ownerOf(params.encumber.stack[params.position].point.lienId) - ) { - s.TRANSFER_PROXY.tokenTransferFrom( - s.WETH, - address(msg.sender), - _getPayee(s, params.encumber.stack[params.position].point.lienId), - buyout + + s.TRANSFER_PROXY.tokenTransferFrom( + s.WETH, + address(msg.sender), + _getPayee(s, params.encumber.stack[params.position].point.lienId), + buyout + ); + + address payee = _getPayee( + s, + params.encumber.stack[params.position].point.lienId + ); + if (_isPublicVault(s, payee)) { + IPublicVault(payee).handleBuyoutLien( + IPublicVault.BuyoutLienParams({ + lienSlope: calculateSlope(params.encumber.stack[params.position]), + lienEnd: params.encumber.stack[params.position].point.end, + increaseYIntercept: buyout - + params.encumber.stack[params.position].point.amount + }) ); } @@ -194,11 +203,6 @@ contract LienToken is ERC721, ILienToken, Auth { newStack[i] = newLien; _burn(oldLienId); delete s.lienMeta[oldLienId]; - if (s.ASTARIA_ROUTER.isValidVault(stack[i].lien.vault)) { - IPublicVault(stack[i].lien.vault).decreaseEpochLienCount( - stack[i].point.end - ); - } } else { newStack[i] = stack[i]; } @@ -479,11 +483,20 @@ contract LienToken is ERC721, ILienToken, Auth { { LienStorage storage s = _loadLienStorageSlot(); - uint256 remainingInterest = _getRemainingInterest(s, stack, true); - uint256 buyoutTotal = stack.point.amount + + return _getBuyout(s, stack); + } + + function _getBuyout(LienStorage storage s, Stack calldata stack) + internal + view + returns (uint256, uint256) + { + uint256 remainingInterest = _getRemainingInterest(s, stack); + uint256 owed = _getOwed(stack, block.timestamp); + uint256 buyoutTotal = owed + s.ASTARIA_ROUTER.getBuyoutFee(remainingInterest); - return (_getOwed(stack, block.timestamp), buyoutTotal); + return (owed, buyoutTotal); } function makePayment(Stack[] calldata stack, uint256 amount) @@ -662,21 +675,14 @@ contract LienToken is ERC721, ILienToken, Auth { * @dev Computes the interest still owed to a Lien. * @param s active storage slot * @param stack the lien - * @param buyout compute with a ceiling based on the buyout interest window * @return The WETH still owed in interest to the Lien. */ - function _getRemainingInterest( - LienStorage storage s, - Stack memory stack, - bool buyout - ) internal view returns (uint256) { + function _getRemainingInterest(LienStorage storage s, Stack memory stack) + internal + view + returns (uint256) + { uint256 end = stack.point.end; - if (buyout) { - uint32 buyoutInterestWindow = s.ASTARIA_ROUTER.getBuyoutInterestWindow(); - if (end >= block.timestamp + buyoutInterestWindow) { - end = block.timestamp + buyoutInterestWindow; - } - } uint256 delta_t = end - block.timestamp; diff --git a/src/PublicVault.sol b/src/PublicVault.sol index af0cd9f8..68515593 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -593,6 +593,21 @@ contract PublicVault is return ROUTER().LIEN_TOKEN(); } + function handleBuyoutLien(BuyoutLienParams calldata params) public { + require(msg.sender == address(LIEN_TOKEN())); + VaultData storage s = _loadStorageSlot(); + + unchecked { + s.slope -= params.lienSlope.safeCastTo48(); + s.yIntercept += params.increaseYIntercept.safeCastTo88(); + s.last = block.timestamp.safeCastTo40(); + } + + uint64 lienEpoch = getLienEpoch(params.lienEnd.safeCastTo64()); + _decreaseEpochLienCount(s, lienEpoch); + emit YInterceptChanged(s.yIntercept); + } + /** * @notice * @param maxAuctionWindow The max possible auction duration. diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index bbe5f742..beb413ff 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -73,7 +73,6 @@ interface IAstariaRouter is IPausable, IBeacon { address BEACON_PROXY_IMPLEMENTATION; //20 uint88 maxInterestRate; //6 uint32 minInterestBPS; // was uint64 - mapping(uint8 => address) strategyValidators; //slot 3 + address guardian; //20 uint32 buyoutFeeNumerator; @@ -81,7 +80,7 @@ interface IAstariaRouter is IPausable, IBeacon { uint32 strategistFeeDenominator; uint32 strategistFeeNumerator; //4 uint32 minDurationIncrease; - uint32 buyoutInterestWindow; + mapping(uint32 => address) strategyValidators; mapping(uint8 => address) implementations; //A strategist can have many deployed vaults mapping(address => address) vaults; @@ -211,8 +210,6 @@ interface IAstariaRouter is IPausable, IBeacon { function getLiquidatorFee(uint256) external view returns (uint256); - function getBuyoutInterestWindow() external view returns (uint32); - /** * @notice Liquidate a CollateralToken that has defaulted on one of its liens. * @param collateralId The ID of the CollateralToken. diff --git a/src/interfaces/IPublicVault.sol b/src/interfaces/IPublicVault.sol index 16e4041c..57091e9c 100644 --- a/src/interfaces/IPublicVault.sol +++ b/src/interfaces/IPublicVault.sol @@ -36,6 +36,12 @@ interface IPublicVault is IVaultImplementation { uint256 interestOwed; } + struct BuyoutLienParams { + uint256 lienSlope; + uint256 lienEnd; + uint256 increaseYIntercept; + } + struct AfterLiquidationParams { uint256 lienSlope; uint256 newAmount; @@ -104,6 +110,8 @@ interface IPublicVault is IVaultImplementation { function decreaseYIntercept(uint256 amount) external; + function handleBuyoutLien(BuyoutLienParams calldata params) external; + function updateVaultAfterLiquidation( uint256 auctionWindow, AfterLiquidationParams calldata params diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index d3328aea..7e71ed76 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -381,7 +381,12 @@ contract AstariaTest is TestHelpers { isFirstLien: true }); - // buyout liens + vm.warp(block.timestamp + 3 days); + + uint256 accruedInterest = uint256(LIEN_TOKEN.getOwed(stack[0])); + uint256 tenthOfRemaining = (uint256( + LIEN_TOKEN.getOwed(stack[0], block.timestamp + 7 days) + ) - accruedInterest).mulDivDown(1, 10); address privateVault = _createPrivateVault({ strategist: strategistOne, @@ -403,6 +408,7 @@ contract AstariaTest is TestHelpers { Lender({addr: strategistOne, amountToLend: 50 ether}), privateVault ); + VaultImplementation(privateVault).buyoutLien( tokenContract.computeId(tokenId), uint8(0), @@ -410,20 +416,49 @@ contract AstariaTest is TestHelpers { stack ); - // LIEN_TOKEN.buyoutLien(liens[0], 10 ether, address(1), address(1)); - - // uint256 collateralId = tokenContract.computeId(tokenId); - // - // // make sure the borrow was successful - // assertEq(WETH9.balanceOf(address(this)), initialBalance + 10 ether); - // - // vm.warp(block.timestamp + 9 days); - // - // _repay(collateralId, 50 ether, address(this)); - // - // COLLATERAL_TOKEN.releaseToAddress(collateralId, address(this)); - // - // assertEq(ERC721(tokenContract).ownerOf(tokenId), address(this)); + assertEq( + WETH9.balanceOf(privateVault), + 40 ether - tenthOfRemaining - (accruedInterest - stack[0].point.amount), + "Incorrect PrivateVault balance" + ); + assertEq( + WETH9.balanceOf(publicVault), + 50 ether + tenthOfRemaining + ((accruedInterest - stack[0].point.amount)), + "Incorrect PublicVault balance" + ); + assertEq( + PublicVault(publicVault).getYIntercept(), + 50 ether + tenthOfRemaining + ((accruedInterest - stack[0].point.amount)), + "Incorrect PublicVault YIntercept" + ); + assertEq( + PublicVault(publicVault).totalAssets(), + 50 ether + tenthOfRemaining + (accruedInterest - stack[0].point.amount), + "Incorrect PublicVault YIntercept" + ); + assertEq( + PublicVault(publicVault).getSlope(), + 0, + "Incorrect PublicVault slope" + ); + + _signalWithdraw(address(1), publicVault); + _warpToEpochEnd(publicVault); + PublicVault(publicVault).processEpoch(); + PublicVault(publicVault).transferWithdrawReserve(); + + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); + + withdrawProxy.redeem( + withdrawProxy.balanceOf(address(1)), + address(1), + address(1) + ); + assertEq( + WETH9.balanceOf(address(1)), + 50 ether + tenthOfRemaining + (accruedInterest - stack[0].point.amount), + "Incorrect withdrawer balance" + ); } function testReleaseToAddress() public {