diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index ca5c5a7c..296ae521 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -426,9 +426,18 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { address[] memory allowList = new address[](2); allowList[0] = address(msg.sender); allowList[1] = delegate; + RouterStorage storage s = _loadRouterSlot(); return - _newVault(uint256(0), delegate, uint256(0), true, allowList, uint256(0)); + _newVault( + s, + uint256(0), + delegate, + uint256(0), + true, + allowList, + uint256(0) + ); } function newPublicVault( @@ -439,8 +448,20 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { address[] calldata allowList, uint256 depositCap ) public whenNotPaused returns (address) { + RouterStorage storage s = _loadRouterSlot(); + if (s.minEpochLength > epochLength) { + revert IPublicVault.InvalidState( + IPublicVault.InvalidStates.EPOCH_TOO_LOW + ); + } + if (s.maxEpochLength < epochLength) { + revert IPublicVault.InvalidState( + IPublicVault.InvalidStates.EPOCH_TOO_HIGH + ); + } return _newVault( + s, epochLength, delegate, vaultFee, @@ -598,6 +619,7 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { * @return vaultAddr The address for the new Vault. */ function _newVault( + RouterStorage storage s, uint256 epochLength, address delegate, uint256 vaultFee, @@ -607,11 +629,7 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { ) internal returns (address vaultAddr) { uint8 vaultType; - RouterStorage storage s = _loadRouterSlot(); if (epochLength > uint256(0)) { - if (s.minEpochLength > epochLength || epochLength > s.maxEpochLength) { - revert InvalidEpochLength(epochLength); - } vaultType = uint8(ImplementationType.PublicVault); } else { vaultType = uint8(ImplementationType.PrivateVault); diff --git a/src/LienToken.sol b/src/LienToken.sol index df8f6aa1..ee624e6e 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -225,8 +225,8 @@ contract LienToken is ERC721, ILienToken, Auth { modifier validateStack(uint256 collateralId, Stack[] memory stack) { LienStorage storage s = _loadLienStorageSlot(); bytes32 stateHash = s.collateralStateHash[collateralId]; - if (stateHash != bytes32(0)) { - require(keccak256(abi.encode(stack)) == stateHash, "invalid hash"); + if (stateHash != bytes32(0) && keccak256(abi.encode(stack)) != stateHash) { + revert InvalidState(InvalidStates.INVALID_HASH); } _; } @@ -410,28 +410,37 @@ contract LienToken is ERC721, ILienToken, Auth { LienStorage storage s, Stack[] memory stack, Stack memory newSlot - ) internal view returns (Stack[] memory newStack) { - uint256 n = stack.length; - newStack = new Stack[](n + 1); - - uint256 maxPotentialDebt = 0; - for (uint256 i; i < n; ) { - if (block.timestamp > stack[i].point.end) { + ) internal returns (Stack[] memory newStack) { + newStack = new Stack[](stack.length + 1); + newStack[stack.length] = newSlot; + + uint256 potentialDebt = _getOwed(newSlot, newSlot.point.end); + for (uint256 i = stack.length; i > 0; ) { + uint256 j = i - 1; + newStack[j] = stack[j]; + if (block.timestamp > newStack[j].point.end) { revert InvalidState(InvalidStates.EXPIRED_LIEN); } - newStack[i] = stack[i]; unchecked { - maxPotentialDebt += _getOwed(stack[i], stack[i].point.end); - ++i; + potentialDebt += _getOwed(newStack[j], newStack[j].point.end); + } + if (potentialDebt > newStack[j].lien.details.liquidationInitialAsk) { + revert InvalidState(InvalidStates.INITIAL_ASK_EXCEEDED); } - } - if (maxPotentialDebt > newSlot.lien.details.maxPotentialDebt) { + unchecked { + --i; + } + } + if ( + stack.length > 0 && potentialDebt > newSlot.lien.details.maxPotentialDebt + ) { revert InvalidState(InvalidStates.DEBT_LIMIT); } - newStack[n] = newSlot; } + event log_named_uint(string name, uint256 value); + function payDebtViaClearingHouse(uint256 collateralId, uint256 payment) external { diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index 34ab48a0..4a35993e 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -319,8 +319,9 @@ interface ILienToken is IERC721 { LIEN_NO_DEBT, EXPIRED_LIEN, DEBT_LIMIT, - MAX_LIENS, - INVALID_LIQUIDATION_INITIAL_ASK + MAX_LIENS, + INVALID_HASH, + INITIAL_ASK_EXCEEDED } error InvalidState(InvalidStates); diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index dba36425..1ed08a74 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -46,14 +46,31 @@ contract RevertTesting is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; - function testFailRandomAccountIncrementNonce() public { + enum InvalidStates { + NO_AUTHORITY, + NOT_ENOUGH_FUNDS, + INVALID_LIEN_ID, + COLLATERAL_AUCTION, + COLLATERAL_NOT_DEPOSITED, + LIEN_NO_DEBT, + EXPIRED_LIEN, + DEBT_LIMIT, + MAX_LIENS + } + + function testCannotRandomAccountIncrementNonce() public { address privateVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 10 days }); - vm.expectRevert(abi.encodePacked("InvalidRequest(0)")); + vm.expectRevert( + abi.encodeWithSelector( + IVaultImplementation.InvalidRequest.selector, + IVaultImplementation.InvalidRequestReason.NO_AUTHORITY + ) + ); VaultImplementation(privateVault).incrementNonce(); assertEq( VaultImplementation(privateVault).getStrategistNonce(), @@ -125,7 +142,7 @@ contract RevertTesting is TestHelpers { ); } - function testFailBorrowMoreThanMaxAmount() public { + function testCannotBorrowMoreThanMaxAmount() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -148,8 +165,9 @@ contract RevertTesting is TestHelpers { ILienToken.Details memory details = standardLienDetails; details.maxAmount = 10 ether; + ILienToken.Stack[] memory stack; // borrow 10 eth against the dummy NFT - (, ILienToken.Stack[] memory stack) = _commitToLien({ + (, stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, @@ -157,12 +175,17 @@ contract RevertTesting is TestHelpers { tokenId: tokenId, lienDetails: details, amount: 11 ether, - isFirstLien: true + isFirstLien: true, + stack: stack, + revertMessage: abi.encodeWithSelector( + IAstariaRouter.InvalidCommitmentState.selector, + IAstariaRouter.CommitmentState.INVALID_AMOUNT + ) }); } // PublicVaults should not be able to progress to the next epoch unless all liens that are able to be liquidated have been liquidated - function testFailProcessEpochWithUnliquidatedLien() public { + function testCannotProcessEpochWithUnliquidatedLien() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); uint256 tokenId = uint256(1); @@ -195,10 +218,17 @@ contract RevertTesting is TestHelpers { }); vm.warp(block.timestamp + 15 days); + + vm.expectRevert( + abi.encodeWithSelector( + IPublicVault.InvalidState.selector, + IPublicVault.InvalidStates.LIENS_OPEN_FOR_EPOCH_NOT_ZERO + ) + ); PublicVault(publicVault).processEpoch(); } - function testFailBorrowMoreThanMaxPotentialDebt() public { + function testCannotBorrowMoreThanMaxPotentialDebt() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -214,20 +244,23 @@ contract RevertTesting is TestHelpers { // lend 50 ether to the PublicVault as address(1) _lendToVault( - Lender({addr: address(1), amountToLend: 50 ether}), + Lender({addr: address(1), amountToLend: 100 ether}), publicVault ); + ILienToken.Stack[] memory stack; + // borrow 10 eth against the dummy NFT - _commitToLien({ + (, stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, - amount: 10 ether, - isFirstLien: true + amount: 50 ether, + isFirstLien: true, + stack: stack }); _commitToLien({ @@ -236,13 +269,18 @@ contract RevertTesting is TestHelpers { strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, - lienDetails: standardLienDetails, + lienDetails: standardLienDetails2, amount: 10 ether, - isFirstLien: false + isFirstLien: false, + stack: stack, + revertMessage: abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.DEBT_LIMIT + ) }); } - function testFailMinMaxPublicVaultEpochLength() public { + function testCannotExceedMinMaxPublicVaultEpochLength() public { vm.expectRevert( abi.encodeWithSelector( IPublicVault.InvalidState.selector, @@ -303,7 +341,7 @@ contract RevertTesting is TestHelpers { }); } - function testFailLienRateZero() public { + function testCannotLienRateZero() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -326,8 +364,9 @@ contract RevertTesting is TestHelpers { ILienToken.Details memory zeroRate = standardLienDetails; zeroRate.rate = 0; + ILienToken.Stack[] memory stack; // borrow 10 eth against the dummy NFT - (, ILienToken.Stack[] memory stack) = _commitToLien({ + (, stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, @@ -335,7 +374,12 @@ contract RevertTesting is TestHelpers { tokenId: tokenId, lienDetails: zeroRate, amount: 10 ether, - isFirstLien: true + isFirstLien: true, + stack: stack, + revertMessage: abi.encodeWithSelector( + IAstariaRouter.InvalidCommitmentState.selector, + IAstariaRouter.CommitmentState.INVALID_RATE + ) }); } function testCannotLiquidationInitialAskExceedsAmountBorrowed() public { @@ -423,7 +467,7 @@ contract RevertTesting is TestHelpers { function testFailPayLienAfterLiquidate() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); - uint256 tokenId = uint256(1); + uint256 tokenId = uint256(0); address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, @@ -435,8 +479,7 @@ contract RevertTesting is TestHelpers { publicVault ); - ILienToken.Stack[][] memory stack = new ILienToken.Stack[][](1); - (, stack[0]) = _commitToLien({ + (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, @@ -451,8 +494,72 @@ contract RevertTesting is TestHelpers { vm.warp(block.timestamp + 14 days); - ASTARIA_ROUTER.liquidate(stack[0], uint8(0)); + ASTARIA_ROUTER.liquidate(stack, uint8(0)); + + _repay(stack, 0, 10 ether, address(this)); + } + + function testCannotCommitToLienPotentialDebtExceedsLiquidationInitialAsk() + public + { + TestNFT nft = new TestNFT(1); + address tokenContract = address(nft); + uint256 tokenId = uint256(0); + + uint256 initialBalance = WETH9.balanceOf(address(this)); + + // create a PublicVault with a 14-day epoch + address publicVault = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 30 days + }); + + _lendToVault( + Lender({addr: address(1), amountToLend: 500 ether}), + publicVault + ); + + ILienToken.Details memory details1 = standardLienDetails; + details1.duration = 14 days; + details1.liquidationInitialAsk = 100 ether; + details1.maxPotentialDebt = 1000 ether; + + ILienToken.Details memory details2 = standardLienDetails; + details2.duration = 25 days; + details2.liquidationInitialAsk = 100 ether; + details2.maxPotentialDebt = 1000 ether; + + IAstariaRouter.Commitment[] + memory commitments = new IAstariaRouter.Commitment[](2); + ILienToken.Stack[] memory stack; - _repay(stack[0], 0, 10 ether, address(this)); + (, stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: details1, + amount: 50 ether, + isFirstLien: true, + stack: stack + }); + + _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: details2, + amount: 50 ether, + isFirstLien: false, + stack: stack, + revertMessage: abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.INITIAL_ASK_EXCEEDED + ) + }); } } diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 514c4632..cbc1ec77 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -175,6 +175,14 @@ contract TestHelpers is ConsiderationTester { maxPotentialDebt: 0 ether, liquidationInitialAsk: 500 ether }); + ILienToken.Details public standardLienDetails2 = + ILienToken.Details({ + maxAmount: 50 ether, + rate: (uint256(1e16) * 150) / (365 days), + duration: 11 days, + maxPotentialDebt: 0 ether, + liquidationInitialAsk: 500 ether + }); ILienToken.Details public refinanceLienDetails = ILienToken.Details({