From 80e1ccd26e1f495ce6bffba6caa1b56b7717cc2f Mon Sep 17 00:00:00 2001 From: = Date: Sun, 22 Jan 2023 21:34:55 -0500 Subject: [PATCH 01/39] add coverage --- scripts/coverage.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 scripts/coverage.py diff --git a/scripts/coverage.py b/scripts/coverage.py new file mode 100644 index 00000000..153f1207 --- /dev/null +++ b/scripts/coverage.py @@ -0,0 +1,48 @@ +import json +import subprocess + +coverage_contracts = ["IAstariaRouter", "ClearingHouse", "ICollateralToken", "ILienToken", "IPublicVault", "IVaultImplementation", "WithdrawProxy"] +excluded_parent_contracts = ["AuthInitializable", "Initializable", "AmountDeriver", "Clone", "IERC1155", "IERC721Receiver", "ERC721", "ZoneInterface", "IERC4626"] + +tests = subprocess.run(["forge", "test", "--ffi", "--no-match-contract", "ForkedTest", "-vvvvv"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + +with open("coverage.txt", "w") as file: + pass + +excluded_parent_fns = [] +for contract in excluded_parent_contracts: + with open("out/" + contract + ".sol/" + contract + ".json") as file: + abi = json.load(file) + for fn in abi["methodIdentifiers"].keys(): + excluded_parent_fns.append(fn.split("(")[0])\ + +# remove duplicates +excluded_parent_fns = list(dict.fromkeys(excluded_parent_fns)) + +for contract in coverage_contracts: + with open("out/" + contract + ".sol/" + contract + ".json") as file: + abi = json.load(file) + covered_fns = abi["methodIdentifiers"] + coverage = len(covered_fns) + excluded_fns = 0 + uncovered_fns = [] + for fn in covered_fns.keys(): + fn_name = fn.split("(")[0] + if fn_name in excluded_parent_fns or fn_name.startswith("get") or fn_name.startswith("is") or fn_name == fn_name.upper(): + excluded_fns += 1 + elif fn_name not in tests.stdout.decode("utf-8"): + uncovered_fns.append(fn_name) + coverage -= 1 + # store the contents of uncovered_fns in a text file + with open("coverage.txt", "a") as file: + file.write("\n" + contract + ": ") + for fn_name in uncovered_fns: + file.write(fn_name + ", ") + print(contract + ": " + str(coverage - excluded_fns) + "/" + str(len(covered_fns) - excluded_fns)) + + + + + + + From 74d4e3086814464f62c930d1f86a602366fdf5e1 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 2 Feb 2023 22:41:25 -0500 Subject: [PATCH 02/39] fix edge case where totalAssets * withdrawRatio > yIntercept, https://github.com/code-423n4/2023-01-astaria-findings/issues/408 --- src/PublicVault.sol | 73 +++++++++++++++++--------------------- src/test/AstariaTest.t.sol | 51 ++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 40 deletions(-) diff --git a/src/PublicVault.sol b/src/PublicVault.sol index 16247ce8..cdb620f5 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -103,7 +103,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { if (ERC20(asset()).decimals() == uint8(18)) { return 100 gwei; } else { - return 10**(ERC20(asset()).decimals() - 1); + return 10 ** (ERC20(asset()).decimals() - 1); } } @@ -213,9 +213,10 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { return uint256(_loadStorageSlot().yIntercept); } - function _deployWithdrawProxyIfNotDeployed(VaultData storage s, uint64 epoch) - internal - { + function _deployWithdrawProxyIfNotDeployed( + VaultData storage s, + uint64 epoch + ) internal { if (s.epochData[epoch].withdrawProxy == address(0)) { s.epochData[epoch].withdrawProxy = ClonesWithImmutableArgs.clone( IAstariaRouter(ROUTER()).BEACON_PROXY_IMPLEMENTATION(), @@ -230,12 +231,10 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { } } - function mint(uint256 shares, address receiver) - public - override(ERC4626Cloned) - whenNotPaused - returns (uint256) - { + function mint( + uint256 shares, + address receiver + ) public override(ERC4626Cloned) whenNotPaused returns (uint256) { VIData storage s = _loadVISlot(); if (s.allowListEnabled) { require(s.allowList[receiver]); @@ -248,12 +247,10 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { * @param amount The amount of funds to deposit. * @param receiver The receiver of the resulting VaultToken shares. */ - function deposit(uint256 amount, address receiver) - public - override(ERC4626Cloned) - whenNotPaused - returns (uint256) - { + function deposit( + uint256 amount, + address receiver + ) public override(ERC4626Cloned) whenNotPaused returns (uint256) { VIData storage s = _loadVISlot(); if (s.allowListEnabled) { require(s.allowList[receiver]); @@ -327,6 +324,10 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { s.withdrawReserve = 0; } } + + s.yIntercept = totalAssets().safeCastTo88(); + s.last = block.timestamp.safeCastTo40(); + _setYIntercept( s, s.yIntercept - @@ -342,12 +343,9 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { } } - function supportsInterface(bytes4 interfaceId) - public - pure - override(IERC165) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public pure override(IERC165) returns (bool) { return interfaceId == type(IPublicVault).interfaceId || interfaceId == type(ERC4626Cloned).interfaceId || @@ -410,11 +408,9 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { } } - function _beforeCommitToLien(IAstariaRouter.Commitment calldata params) - internal - virtual - override(VaultImplementation) - { + function _beforeCommitToLien( + IAstariaRouter.Commitment calldata params + ) internal virtual override(VaultImplementation) { VaultData storage s = _loadStorageSlot(); if (s.withdrawReserve > uint256(0)) { @@ -512,10 +508,9 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { _mint(msg.sender, unclaimed); } - function beforePayment(BeforePaymentParams calldata params) - external - onlyLienToken - { + function beforePayment( + BeforePaymentParams calldata params + ) external onlyLienToken { VaultData storage s = _loadStorageSlot(); _accrue(s); @@ -572,11 +567,10 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { * @param assets The amount of assets deposited to the PublicVault. * @param shares The resulting amount of VaultToken shares that were issued. */ - function afterDeposit(uint256 assets, uint256 shares) - internal - virtual - override - { + function afterDeposit( + uint256 assets, + uint256 shares + ) internal virtual override { VaultData storage s = _loadStorageSlot(); unchecked { @@ -612,10 +606,9 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { return ROUTER().LIEN_TOKEN(); } - function handleBuyoutLien(BuyoutLienParams calldata params) - public - onlyLienToken - { + function handleBuyoutLien( + BuyoutLienParams calldata params + ) public onlyLienToken { VaultData storage s = _loadStorageSlot(); unchecked { diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index c7ce1624..444a1e68 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -896,6 +896,57 @@ contract AstariaTest is TestHelpers { _; } + // From C4 #408 + function testCompleteWithdrawAfterOneEpoch() public { + TestNFT nft = new TestNFT(1); + address tokenContract = address(nft); + uint256 tokenId = uint256(0); + + address publicVault = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 7 days + }); + _lendToVault( + Lender({addr: address(1), amountToLend: 60 ether}), + publicVault + ); + + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + vm.warp(block.timestamp + 3 days); + + _signalWithdraw(address(1), publicVault); + _warpToEpochEnd(publicVault); + PublicVault(publicVault).processEpoch(); + PublicVault(publicVault).transferWithdrawReserve(); + + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); + + vm.startPrank(address(1)); + withdrawProxy.redeem( + withdrawProxy.balanceOf(address(1)), + address(1), + address(1) + ); + vm.stopPrank(); + + assertEq( + WETH9.balanceOf(address(1)), + 50 ether, + "LP did not receive all WETH not lent out" + ); + } + function testFinalAuctionEnd() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); From 39078c7b07f057de917493b64a05a78a214ca682 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 14:37:47 -0400 Subject: [PATCH 03/39] C4:158 - prevent from deploying vaults with an underlying erc20 that has no code --- src/AstariaRouter.sol | 61 +++++++++++++------------------ src/interfaces/IAstariaRouter.sol | 29 +++++++-------- src/test/RevertTesting.t.sol | 14 +++++++ src/test/TestHelpers.t.sol | 58 ++++++++++++++--------------- 4 files changed, 82 insertions(+), 80 deletions(-) diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index c797baaf..02e6dff1 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -404,11 +404,10 @@ contract AstariaRouter is return s.auctionWindow + (includeBuffer ? s.auctionWindowBuffer : 0); } - function _sliceUint(bytes memory bs, uint256 start) - internal - pure - returns (uint256 x) - { + function _sliceUint( + bytes memory bs, + uint256 start + ) internal pure returns (uint256 x) { uint256 length = bs.length; assembly { @@ -487,7 +486,9 @@ contract AstariaRouter is }); } - function commitToLiens(IAstariaRouter.Commitment[] memory commitments) + function commitToLiens( + IAstariaRouter.Commitment[] memory commitments + ) public whenNotPaused returns (uint256[] memory lienIds, ILienToken.Stack[] memory stack) @@ -519,11 +520,10 @@ contract AstariaRouter is .safeTransfer(msg.sender, totalBorrowed); } - function newVault(address delegate, address underlying) - external - whenNotPaused - returns (address) - { + function newVault( + address delegate, + address underlying + ) external whenNotPaused returns (address) { address[] memory allowList = new address[](1); allowList[0] = msg.sender; RouterStorage storage s = _loadRouterSlot(); @@ -581,11 +581,7 @@ contract AstariaRouter is external whenNotPaused validVault(msg.sender) - returns ( - uint256, - ILienToken.Stack[] memory, - uint256 - ) + returns (uint256, ILienToken.Stack[] memory, uint256) { RouterStorage storage s = _loadRouterSlot(); @@ -608,20 +604,18 @@ contract AstariaRouter is ); } - function canLiquidate(ILienToken.Stack memory stack) - public - view - returns (bool) - { + function canLiquidate( + ILienToken.Stack memory stack + ) public view returns (bool) { RouterStorage storage s = _loadRouterSlot(); return (stack.point.end <= block.timestamp || msg.sender == s.COLLATERAL_TOKEN.ownerOf(stack.lien.collateralId)); } - function liquidate(ILienToken.Stack[] memory stack, uint8 position) - public - returns (OrderParameters memory listedOrder) - { + function liquidate( + ILienToken.Stack[] memory stack, + uint8 position + ) public returns (OrderParameters memory listedOrder) { if (!canLiquidate(stack[position])) { revert InvalidLienState(LienState.HEALTHY); } @@ -664,11 +658,9 @@ contract AstariaRouter is ); } - function getBuyoutFee(uint256 remainingInterestIn) - external - view - returns (uint256) - { + function getBuyoutFee( + uint256 remainingInterestIn + ) external view returns (uint256) { RouterStorage storage s = _loadRouterSlot(); return remainingInterestIn.mulDivDown( @@ -721,6 +713,9 @@ contract AstariaRouter is ) internal returns (address vaultAddr) { uint8 vaultType; + if (underlying.code.length == 0) { + revert InvalidUnderlying(underlying); + } if (epochLength > uint256(0)) { vaultType = uint8(ImplementationType.PublicVault); } else { @@ -763,11 +758,7 @@ contract AstariaRouter is IAstariaRouter.Commitment memory c ) internal - returns ( - uint256, - ILienToken.Stack[] memory stack, - uint256 payout - ) + returns (uint256, ILienToken.Stack[] memory stack, uint256 payout) { uint256 collateralId = c.tokenContract.computeId(c.tokenId); diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index 2ae1431b..e890d1c7 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -162,9 +162,10 @@ interface IAstariaRouter is IPausable, IBeacon { * @param underlying The address of the underlying token. * @return The address of the new PrivateVault. */ - function newVault(address delegate, address underlying) - external - returns (address); + function newVault( + address delegate, + address underlying + ) external returns (address); /** * @notice Retrieves the address that collects protocol-level fees. @@ -176,9 +177,9 @@ interface IAstariaRouter is IPausable, IBeacon { * @param commitments The commitment proofs and requested loan data for each loan. * @return lienIds the lienIds for each loan. */ - function commitToLiens(Commitment[] memory commitments) - external - returns (uint256[] memory, ILienToken.Stack[] memory); + function commitToLiens( + Commitment[] memory commitments + ) external returns (uint256[] memory, ILienToken.Stack[] memory); /** * @notice Create a new lien against a CollateralToken. @@ -188,13 +189,7 @@ interface IAstariaRouter is IPausable, IBeacon { function requestLienPosition( IAstariaRouter.Commitment calldata params, address recipient - ) - external - returns ( - uint256, - ILienToken.Stack[] memory, - uint256 - ); + ) external returns (uint256, ILienToken.Stack[] memory, uint256); function LIEN_TOKEN() external view returns (ILienToken); @@ -231,9 +226,10 @@ interface IAstariaRouter is IPausable, IBeacon { * @param position The position of the defaulted lien. * @return reserve The amount owed on all liens for against the collateral being liquidated, including accrued interest. */ - function liquidate(ILienToken.Stack[] calldata stack, uint8 position) - external - returns (OrderParameters memory); + function liquidate( + ILienToken.Stack[] calldata stack, + uint8 position + ) external returns (OrderParameters memory); /** * @notice Returns whether a specified lien can be liquidated. @@ -311,6 +307,7 @@ interface IAstariaRouter is IPausable, IBeacon { error InvalidCommitmentState(CommitmentState); error InvalidStrategy(uint16); error InvalidVault(address); + error InvalidUnderlying(address); error UnsupportedFile(); enum LienState { diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..f7b0ee14 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -61,6 +61,20 @@ contract RevertTesting is TestHelpers { MAX_LIENS } + function testCannotDeployUnderlyingWithNoCode() public { + vm.expectRevert( + abi.encodeWithSelector( + IAstariaRouter.InvalidUnderlying.selector, + address(3) + ) + ); + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo, + token: address(3) + }); + } + function testCannotRandomAccountIncrementNonce() public { address privateVault = _createPublicVault({ strategist: strategistOne, diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 36314db3..55bd5780 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -323,10 +323,9 @@ contract TestHelpers is Deploy, ConsiderationTester { (rate * amount * duration).mulDivDown(1, 365 days).mulDivDown(1, 1e18); } - function setupLiquidation(address borrower) - public - returns (address publicVault, ILienToken.Stack[] memory stack) - { + function setupLiquidation( + address borrower + ) public returns (address publicVault, ILienToken.Stack[] memory stack) { TestNFT nft = new TestNFT(0); _mintNoDepositApproveRouterSpecific(borrower, address(nft), 99); address tokenContract = address(nft); @@ -412,9 +411,10 @@ contract TestHelpers is Deploy, ConsiderationTester { ); } - function _mintNoDepositApproveRouter(address tokenContract, uint256 tokenId) - internal - { + function _mintNoDepositApproveRouter( + address tokenContract, + uint256 tokenId + ) internal { TestNFT(tokenContract).mint(address(this), tokenId); TestNFT(tokenContract).approve(address(ASTARIA_ROUTER), tokenId); } @@ -448,15 +448,23 @@ contract TestHelpers is Deploy, ConsiderationTester { ); } - function _createPrivateVault(address strategist, address delegate) - internal - returns (address privateVault) - { + function _createPrivateVault( + address strategist, + address delegate, + address token + ) internal returns (address privateVault) { vm.startPrank(strategist); - privateVault = ASTARIA_ROUTER.newVault(delegate, address(WETH9)); + privateVault = ASTARIA_ROUTER.newVault(delegate, token); vm.stopPrank(); } + function _createPrivateVault( + address strategist, + address delegate + ) internal returns (address) { + return _createPrivateVault(strategist, delegate, address(WETH9)); + } + function _createPublicVault( address strategist, address delegate, @@ -875,14 +883,9 @@ contract TestHelpers is Deploy, ConsiderationTester { uint256 amount; } - function _toVRS(bytes memory signature) - internal - returns ( - uint8 v, - bytes32 r, - bytes32 s - ) - { + function _toVRS( + bytes memory signature + ) internal returns (uint8 v, bytes32 r, bytes32 s) { emit log_bytes(signature); emit log_named_uint("signature length", signature.length); if (signature.length == 65) { @@ -899,10 +902,9 @@ contract TestHelpers is Deploy, ConsiderationTester { } } - function _generateTerms(GenTerms memory params) - internal - returns (IAstariaRouter.Commitment memory terms) - { + function _generateTerms( + GenTerms memory params + ) internal returns (IAstariaRouter.Commitment memory terms) { (uint8 v, bytes32 r, bytes32 s) = _toVRS(params.signature); return @@ -1249,11 +1251,9 @@ contract TestHelpers is Deploy, ConsiderationTester { return _mirrorOrderParameters; } - function _toOfferItems(ConsiderationItem[] memory _considerationItems) - internal - pure - returns (OfferItem[] memory) - { + function _toOfferItems( + ConsiderationItem[] memory _considerationItems + ) internal pure returns (OfferItem[] memory) { OfferItem[] memory _offerItems = new OfferItem[]( _considerationItems.length ); From fbda49387144cc8b9d55038a7f3e1ff61c65afe9 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 14:49:47 -0400 Subject: [PATCH 04/39] C4: 25 - private vaults cannot be deposited into if shut down or protocol is paused --- src/Vault.sol | 48 ++++++++++++++++-------------------- src/test/RevertTesting.t.sol | 13 ++++++++++ 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/Vault.sol b/src/Vault.sol index cee62cc9..dfb91849 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -/** -* █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ -* ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ -* ███████║███████╗ ██║ ███████║██████╔╝██║███████║ -* ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ -* ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ -* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ -* -* Astaria Labs, Inc -*/ +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ pragma solidity =0.8.17; @@ -46,21 +46,16 @@ contract Vault is VaultImplementation { string(abi.encodePacked("AST-V", owner(), "-", ERC20(asset()).symbol())); } - function supportsInterface(bytes4) - public - pure - virtual - override(IERC165) - returns (bool) - { + function supportsInterface( + bytes4 + ) public pure virtual override(IERC165) returns (bool) { return false; } - function deposit(uint256 amount, address receiver) - public - virtual - returns (uint256) - { + function deposit( + uint256 amount, + address receiver + ) public virtual whenNotPaused returns (uint256) { VIData storage s = _loadVISlot(); require(s.allowList[msg.sender] && receiver == owner()); ERC20(asset()).safeTransferFrom(msg.sender, address(this), amount); @@ -82,11 +77,10 @@ contract Vault is VaultImplementation { revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } - function modifyAllowList(address, bool) - external - pure - override(VaultImplementation) - { + function modifyAllowList( + address, + bool + ) external pure override(VaultImplementation) { //invalid action private vautls can only be the owner or strategist revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..1e6b14af 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -94,6 +94,19 @@ contract RevertTesting is TestHelpers { ); } + function testFailDepositWhenProtocolPaused() public { + address privateVault = _createPrivateVault({ + delegate: strategistOne, + strategist: strategistTwo + }); + ASTARIA_ROUTER.__emergencyPause(); + + _lendToPrivateVault( + Lender({addr: strategistTwo, amountToLend: 50 ether}), + privateVault + ); + } + function testFailInvalidSignature() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); From b0d449af2f4e2825eae7d6e9c5b3c0223eb55a92 Mon Sep 17 00:00:00 2001 From: Joseph Delong Date: Thu, 9 Feb 2023 15:11:47 -0600 Subject: [PATCH 05/39] 2Develop: 2Furious --- package.json | 2 +- src/AstariaRouter.sol | 58 +++----- src/AuthInitializable.sol | 10 +- src/ClearingHouse.sol | 33 ++--- src/CollateralToken.sol | 61 ++++---- src/LienToken.sol | 182 +++++++++++------------- src/TransferProxy.sol | 20 +-- src/Vault.sol | 48 +++---- src/VaultImplementation.sol | 27 ++-- src/WithdrawProxy.sol | 60 ++++---- src/WithdrawVaultBase.sol | 20 +-- src/interfaces/IAstariaRouter.sol | 28 ++-- src/interfaces/ICollateralToken.sol | 13 +- src/interfaces/IERC1155.sol | 24 ++-- src/interfaces/IERC20.sol | 8 +- src/interfaces/IERC4626.sol | 49 +++---- src/interfaces/IERC721.sol | 12 +- src/interfaces/IFlashAction.sol | 7 +- src/interfaces/ILienToken.sol | 80 +++++------ src/interfaces/IV3PositionManager.sol | 32 ++--- src/interfaces/IVaultImplementation.sol | 6 +- src/interfaces/IWithdrawProxy.sol | 7 +- src/libraries/CollateralLookup.sol | 9 +- src/security/V3SecurityHook.sol | 9 +- src/strategies/CollectionValidator.sol | 16 +-- src/strategies/UNI_V3Validator.sol | 16 +-- src/strategies/UniqueValidator.sol | 16 +-- src/test/TestHelpers.t.sol | 48 +++---- src/utils/Initializable.sol | 33 +++-- 29 files changed, 409 insertions(+), 525 deletions(-) diff --git a/package.json b/package.json index 3298b85d..dbcc5d85 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "lint": "eslint src/**/*.sol", - "lint:fix": "prettier --write src/**/*.sol", + "lint:fix": "prettier --write src/**/*.sol src/*.sol", "postinstall": "husky install" }, "lint-staged": { diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index c797baaf..22a73297 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -404,11 +404,10 @@ contract AstariaRouter is return s.auctionWindow + (includeBuffer ? s.auctionWindowBuffer : 0); } - function _sliceUint(bytes memory bs, uint256 start) - internal - pure - returns (uint256 x) - { + function _sliceUint( + bytes memory bs, + uint256 start + ) internal pure returns (uint256 x) { uint256 length = bs.length; assembly { @@ -487,7 +486,9 @@ contract AstariaRouter is }); } - function commitToLiens(IAstariaRouter.Commitment[] memory commitments) + function commitToLiens( + IAstariaRouter.Commitment[] memory commitments + ) public whenNotPaused returns (uint256[] memory lienIds, ILienToken.Stack[] memory stack) @@ -519,11 +520,10 @@ contract AstariaRouter is .safeTransfer(msg.sender, totalBorrowed); } - function newVault(address delegate, address underlying) - external - whenNotPaused - returns (address) - { + function newVault( + address delegate, + address underlying + ) external whenNotPaused returns (address) { address[] memory allowList = new address[](1); allowList[0] = msg.sender; RouterStorage storage s = _loadRouterSlot(); @@ -581,11 +581,7 @@ contract AstariaRouter is external whenNotPaused validVault(msg.sender) - returns ( - uint256, - ILienToken.Stack[] memory, - uint256 - ) + returns (uint256, ILienToken.Stack[] memory, uint256) { RouterStorage storage s = _loadRouterSlot(); @@ -608,20 +604,18 @@ contract AstariaRouter is ); } - function canLiquidate(ILienToken.Stack memory stack) - public - view - returns (bool) - { + function canLiquidate( + ILienToken.Stack memory stack + ) public view returns (bool) { RouterStorage storage s = _loadRouterSlot(); return (stack.point.end <= block.timestamp || msg.sender == s.COLLATERAL_TOKEN.ownerOf(stack.lien.collateralId)); } - function liquidate(ILienToken.Stack[] memory stack, uint8 position) - public - returns (OrderParameters memory listedOrder) - { + function liquidate( + ILienToken.Stack[] memory stack, + uint8 position + ) public returns (OrderParameters memory listedOrder) { if (!canLiquidate(stack[position])) { revert InvalidLienState(LienState.HEALTHY); } @@ -664,11 +658,9 @@ contract AstariaRouter is ); } - function getBuyoutFee(uint256 remainingInterestIn) - external - view - returns (uint256) - { + function getBuyoutFee( + uint256 remainingInterestIn + ) external view returns (uint256) { RouterStorage storage s = _loadRouterSlot(); return remainingInterestIn.mulDivDown( @@ -763,11 +755,7 @@ contract AstariaRouter is IAstariaRouter.Commitment memory c ) internal - returns ( - uint256, - ILienToken.Stack[] memory stack, - uint256 payout - ) + returns (uint256, ILienToken.Stack[] memory stack, uint256 payout) { uint256 collateralId = c.tokenContract.computeId(c.tokenId); diff --git a/src/AuthInitializable.sol b/src/AuthInitializable.sol index 61353854..2883bfca 100644 --- a/src/AuthInitializable.sol +++ b/src/AuthInitializable.sol @@ -62,12 +62,10 @@ abstract contract AuthInitializable { return _getAuthSlot().authority; } - function isAuthorized(address user, bytes4 functionSig) - internal - view - virtual - returns (bool) - { + function isAuthorized( + address user, + bytes4 functionSig + ) internal view virtual returns (bool) { AuthStorage storage s = _getAuthSlot(); Authority auth = s.authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index 5c2a400c..b6e1a792 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -63,9 +63,9 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { } } - function setAuctionData(ILienToken.AuctionData calldata auctionData) - external - { + function setAuctionData( + ILienToken.AuctionData calldata auctionData + ) external { IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); // get the router from the immutable arg //only execute from the conduit @@ -79,19 +79,17 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { return interfaceId == type(IERC1155).interfaceId; } - function balanceOf(address account, uint256 id) - external - view - returns (uint256) - { + function balanceOf( + address account, + uint256 id + ) external view returns (uint256) { return type(uint256).max; } - function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) - external - view - returns (uint256[] memory output) - { + function balanceOfBatch( + address[] calldata accounts, + uint256[] calldata ids + ) external view returns (uint256[] memory output) { output = new uint256[](accounts.length); for (uint256 i; i < accounts.length; ) { output[i] = type(uint256).max; @@ -103,11 +101,10 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { function setApprovalForAll(address operator, bool approved) external {} - function isApprovedForAll(address account, address operator) - external - view - returns (bool) - { + function isApprovedForAll( + address account, + address operator + ) external view returns (bool) { return true; } diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index c82b400e..9d270b33 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -182,12 +182,9 @@ contract CollateralToken is : bytes4(0xffffffff); } - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC721, IERC165) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721, IERC165) returns (bool) { return interfaceId == type(ICollateralToken).interfaceId || super.supportsInterface(interfaceId); @@ -328,11 +325,10 @@ contract CollateralToken is } } - function releaseToAddress(uint256 collateralId, address releaseTo) - public - releaseCheck(collateralId) - onlyOwner(collateralId) - { + function releaseToAddress( + uint256 collateralId, + address releaseTo + ) public releaseCheck(collateralId) onlyOwner(collateralId) { CollateralStorage storage s = _loadCollateralSlot(); if (msg.sender != ownerOf(collateralId)) { @@ -382,11 +378,9 @@ contract CollateralToken is * @param collateralId The ID of the CollateralToken wrapping the NFT. * @return The address and tokenId of the underlying NFT. */ - function getUnderlying(uint256 collateralId) - public - view - returns (address, uint256) - { + function getUnderlying( + uint256 collateralId + ) public view returns (address, uint256) { Asset memory underlying = _loadCollateralSlot().idToUnderlying[ collateralId ]; @@ -398,13 +392,9 @@ contract CollateralToken is * @param collateralId The ID of the CollateralToken. * @return the URI of the CollateralToken. */ - function tokenURI(uint256 collateralId) - public - view - virtual - override(ERC721, IERC721) - returns (string memory) - { + function tokenURI( + uint256 collateralId + ) public view virtual override(ERC721, IERC721) returns (string memory) { (address underlyingAsset, uint256 assetId) = getUnderlying(collateralId); return ERC721(underlyingAsset).tokenURI(assetId); } @@ -413,11 +403,9 @@ contract CollateralToken is return _loadCollateralSlot().securityHooks[target]; } - function getClearingHouse(uint256 collateralId) - external - view - returns (ClearingHouse) - { + function getClearingHouse( + uint256 collateralId + ) external view returns (ClearingHouse) { return ClearingHouse(payable(_loadCollateralSlot().clearingHouse[collateralId])); } @@ -474,11 +462,9 @@ contract CollateralToken is }); } - function auctionVault(AuctionVaultParams calldata params) - external - requiresAuth - returns (OrderParameters memory orderParameters) - { + function auctionVault( + AuctionVaultParams calldata params + ) external requiresAuth returns (OrderParameters memory orderParameters) { CollateralStorage storage s = _loadCollateralSlot(); uint256[] memory prices = new uint256[](2); @@ -538,9 +524,10 @@ contract CollateralToken is _burn(collateralId); } - function _settleAuction(CollateralStorage storage s, uint256 collateralId) - internal - { + function _settleAuction( + CollateralStorage storage s, + uint256 collateralId + ) internal { delete s.collateralIdToAuction[collateralId]; } @@ -551,7 +538,7 @@ contract CollateralToken is * @return a static return of the receive signature */ function onERC721Received( - address, /* operator_ */ + address /* operator_ */, address from_, uint256 tokenId_, bytes calldata // calldata data_ diff --git a/src/LienToken.sol b/src/LienToken.sol index 631ac02c..28559f60 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -/** -* █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ -* ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ -* ███████║███████╗ ██║ ███████║██████╔╝██║███████║ -* ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ -* ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ -* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ -* -* Astaria Labs, Inc -*/ +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ pragma solidity =0.8.17; @@ -56,10 +56,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { _disableInitializers(); } - function initialize(Authority _AUTHORITY, ITransferProxy _TRANSFER_PROXY) - public - initializer - { + function initialize( + Authority _AUTHORITY, + ITransferProxy _TRANSFER_PROXY + ) public initializer { __initAuth(msg.sender, address(_AUTHORITY)); __initERC721("Astaria Lien Token", "ALT"); LienStorage storage s = _loadLienStorageSlot(); @@ -93,18 +93,17 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { emit FileUpdated(what, data); } - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC721, IERC165) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721, IERC165) returns (bool) { return interfaceId == type(ILienToken).interfaceId || super.supportsInterface(interfaceId); } - function buyoutLien(ILienToken.LienActionBuyout calldata params) + function buyoutLien( + ILienToken.LienActionBuyout calldata params + ) external validateStack(params.encumber.lien.collateralId, params.encumber.stack) returns (Stack[] memory, Stack memory newStack) @@ -252,11 +251,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack The Lien for the loan to calculate interest for. * @param timestamp The timestamp at which to compute interest for. */ - function _getInterest(Stack memory stack, uint256 timestamp) - internal - pure - returns (uint256) - { + function _getInterest( + Stack memory stack, + uint256 timestamp + ) internal pure returns (uint256) { uint256 delta_t = timestamp - stack.point.last; return (delta_t * stack.lien.details.rate).mulWadDown(stack.point.amount); @@ -345,12 +343,9 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { ); } - function tokenURI(uint256 tokenId) - public - view - override(ERC721, IERC721) - returns (string memory) - { + function tokenURI( + uint256 tokenId + ) public view override(ERC721, IERC721) returns (string memory) { if (!_exists(tokenId)) { revert InvalidTokenId(tokenId); } @@ -386,15 +381,13 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _loadERC721Slot()._ownerOf[tokenId] != address(0); } - function createLien(ILienToken.LienActionEncumber memory params) + function createLien( + ILienToken.LienActionEncumber memory params + ) external requiresAuth validateStack(params.lien.collateralId, params.stack) - returns ( - uint256 lienId, - Stack[] memory newStack, - uint256 lienSlope - ) + returns (uint256 lienId, Stack[] memory newStack, uint256 lienSlope) { LienStorage storage s = _loadLienStorageSlot(); //0 - 4 are valid @@ -517,7 +510,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { AuctionStack[] memory stack ) internal returns (uint256 totalSpent) { uint256 i; - for (; i < stack.length;) { + for (; i < stack.length; ) { uint256 spent; unchecked { spent = _paymentAH(s, token, stack, i, payment, payer); @@ -528,30 +521,24 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getAuctionData(uint256 collateralId) - external - view - returns (AuctionData memory) - { + function getAuctionData( + uint256 collateralId + ) external view returns (AuctionData memory) { return _loadLienStorageSlot().auctionData[collateralId]; } - function getAuctionLiquidator(uint256 collateralId) - external - view - returns (address liquidator) - { + function getAuctionLiquidator( + uint256 collateralId + ) external view returns (address liquidator) { liquidator = _loadLienStorageSlot().auctionData[collateralId].liquidator; if (liquidator == address(0)) { revert InvalidState(InvalidStates.COLLATERAL_NOT_LIQUIDATED); } } - function getAmountOwingAtLiquidation(ILienToken.Stack calldata stack) - public - view - returns (uint256) - { + function getAmountOwingAtLiquidation( + ILienToken.Stack calldata stack + ) public view returns (uint256) { return _loadLienStorageSlot() .auctionData[stack.lien.collateralId] @@ -566,27 +553,22 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getCollateralState(uint256 collateralId) - external - view - returns (bytes32) - { + function getCollateralState( + uint256 collateralId + ) external view returns (bytes32) { return _loadLienStorageSlot().collateralStateHash[collateralId]; } - function getBuyout(Stack calldata stack) - public - view - returns (uint256 owed, uint256 buyout) - { + function getBuyout( + Stack calldata stack + ) public view returns (uint256 owed, uint256 buyout) { return _getBuyout(_loadLienStorageSlot(), stack); } - function _getBuyout(LienStorage storage s, Stack calldata stack) - internal - view - returns (uint256 owed, uint256 buyout) - { + function _getBuyout( + LienStorage storage s, + Stack calldata stack + ) internal view returns (uint256 owed, uint256 buyout) { owed = _getOwed(stack, block.timestamp); buyout = owed + @@ -703,7 +685,9 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return stack.lien.details.rate.mulWadDown(stack.point.amount); } - function getMaxPotentialDebtForCollateral(Stack[] memory stack) + function getMaxPotentialDebtForCollateral( + Stack[] memory stack + ) public view validateStack(stack[0].lien.collateralId, stack) @@ -724,7 +708,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getMaxPotentialDebtForCollateral(Stack[] memory stack, uint256 end) + function getMaxPotentialDebtForCollateral( + Stack[] memory stack, + uint256 end + ) public view validateStack(stack[0].lien.collateralId, stack) @@ -744,11 +731,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _getOwed(stack, block.timestamp); } - function getOwed(Stack memory stack, uint256 timestamp) - external - view - returns (uint88) - { + function getOwed( + Stack memory stack, + uint256 timestamp + ) external view returns (uint88) { validateLien(stack.lien); return _getOwed(stack, timestamp); } @@ -758,11 +744,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack The specified Lien. * @return The amount owed to the Lien at the specified timestamp. */ - function _getOwed(Stack memory stack, uint256 timestamp) - internal - pure - returns (uint88) - { + function _getOwed( + Stack memory stack, + uint256 timestamp + ) internal pure returns (uint88) { return stack.point.amount + _getInterest(stack, timestamp).safeCastTo88(); } @@ -772,11 +757,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack the lien * @return The WETH still owed in interest to the Lien. */ - function _getRemainingInterest(LienStorage storage s, Stack memory stack) - internal - view - returns (uint256) - { + function _getRemainingInterest( + LienStorage storage s, + Stack memory stack + ) internal view returns (uint256) { uint256 delta_t = stack.point.end - block.timestamp; return (delta_t * stack.lien.details.rate).mulWadDown(stack.point.amount); } @@ -852,10 +836,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return (activeStack, amount); } - function _removeStackPosition(Stack[] memory stack, uint8 position) - internal - returns (Stack[] memory newStack) - { + function _removeStackPosition( + Stack[] memory stack, + uint8 position + ) internal returns (Stack[] memory newStack) { uint256 length = stack.length; require(position < length); newStack = new ILienToken.Stack[](length - 1); @@ -880,11 +864,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { ); } - function _isPublicVault(LienStorage storage s, address account) - internal - view - returns (bool) - { + function _isPublicVault( + LienStorage storage s, + address account + ) internal view returns (bool) { return s.ASTARIA_ROUTER.isValidVault(account) && IPublicVault(account).supportsInterface(type(IPublicVault).interfaceId); @@ -897,11 +880,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _getPayee(_loadLienStorageSlot(), lienId); } - function _getPayee(LienStorage storage s, uint256 lienId) - internal - view - returns (address) - { + function _getPayee( + LienStorage storage s, + uint256 lienId + ) internal view returns (address) { return s.lienMeta[lienId].payee != address(0) ? s.lienMeta[lienId].payee diff --git a/src/TransferProxy.sol b/src/TransferProxy.sol index a8abb111..d998a28b 100644 --- a/src/TransferProxy.sol +++ b/src/TransferProxy.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -/** -* █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ -* ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ -* ███████║███████╗ ██║ ███████║██████╔╝██║███████║ -* ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ -* ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ -* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ -* -* Astaria Labs, Inc -*/ +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ pragma solidity =0.8.17; diff --git a/src/Vault.sol b/src/Vault.sol index cee62cc9..42733213 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -/** -* █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ -* ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ -* ███████║███████╗ ██║ ███████║██████╔╝██║███████║ -* ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ -* ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ -* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ -* -* Astaria Labs, Inc -*/ +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ pragma solidity =0.8.17; @@ -46,21 +46,16 @@ contract Vault is VaultImplementation { string(abi.encodePacked("AST-V", owner(), "-", ERC20(asset()).symbol())); } - function supportsInterface(bytes4) - public - pure - virtual - override(IERC165) - returns (bool) - { + function supportsInterface( + bytes4 + ) public pure virtual override(IERC165) returns (bool) { return false; } - function deposit(uint256 amount, address receiver) - public - virtual - returns (uint256) - { + function deposit( + uint256 amount, + address receiver + ) public virtual returns (uint256) { VIData storage s = _loadVISlot(); require(s.allowList[msg.sender] && receiver == owner()); ERC20(asset()).safeTransferFrom(msg.sender, address(this), amount); @@ -82,11 +77,10 @@ contract Vault is VaultImplementation { revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } - function modifyAllowList(address, bool) - external - pure - override(VaultImplementation) - { + function modifyAllowList( + address, + bool + ) external pure override(VaultImplementation) { //invalid action private vautls can only be the owner or strategist revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index b5ff5d76..86a9e010 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -/** -* █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ -* ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ -* ███████║███████╗ ██║ ███████║██████╔╝██║███████║ -* ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ -* ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ -* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ -* -* Astaria Labs, Inc -*/ +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ pragma solidity =0.8.17; @@ -271,10 +271,9 @@ abstract contract VaultImplementation is uint256 slope ) internal virtual {} - function _beforeCommitToLien(IAstariaRouter.Commitment calldata) - internal - virtual - {} + function _beforeCommitToLien( + IAstariaRouter.Commitment calldata + ) internal virtual {} /** * @notice Pipeline for lifecycle of new loan origination. diff --git a/src/WithdrawProxy.sol b/src/WithdrawProxy.sol index 9906ec75..61b32aa9 100644 --- a/src/WithdrawProxy.sol +++ b/src/WithdrawProxy.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -/** -* █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ -* ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ -* ███████║███████╗ ██║ ███████║██████╔╝██║███████║ -* ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ -* ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ -* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ -* -* Astaria Labs, Inc -*/ +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ pragma solidity =0.8.17; @@ -129,23 +129,19 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { * @param receiver The receiver of the Withdraw Tokens. * @param shares The number of shares to mint. */ - function mint(uint256 shares, address receiver) - public - virtual - override(ERC4626Cloned, IERC4626) - returns (uint256 assets) - { + function mint( + uint256 shares, + address receiver + ) public virtual override(ERC4626Cloned, IERC4626) returns (uint256 assets) { require(msg.sender == VAULT(), "only vault can mint"); _mint(receiver, shares); return shares; } - function deposit(uint256 assets, address receiver) - public - virtual - override(ERC4626Cloned, IERC4626) - returns (uint256 shares) - { + function deposit( + uint256 assets, + address receiver + ) public virtual override(ERC4626Cloned, IERC4626) returns (uint256 shares) { revert NotSupported(); } @@ -195,12 +191,9 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { return super.redeem(shares, receiver, owner); } - function supportsInterface(bytes4 interfaceId) - external - view - virtual - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) external view virtual returns (bool) { return interfaceId == type(IWithdrawProxy).interfaceId; } @@ -270,7 +263,7 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { } else { transferAmount = uint256(s.withdrawRatio).mulDivDown( balance, - 10**ERC20(asset()).decimals() + 10 ** ERC20(asset()).decimals() ); unchecked { @@ -286,11 +279,10 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { emit Claimed(address(this), transferAmount, VAULT(), balance); } - function drain(uint256 amount, address withdrawProxy) - public - onlyVault - returns (uint256) - { + function drain( + uint256 amount, + address withdrawProxy + ) public onlyVault returns (uint256) { uint256 balance = ERC20(asset()).balanceOf(address(this)); if (amount > balance) { amount = balance; diff --git a/src/WithdrawVaultBase.sol b/src/WithdrawVaultBase.sol index fae8a527..cce15ff7 100644 --- a/src/WithdrawVaultBase.sol +++ b/src/WithdrawVaultBase.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -/** -* █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ -* ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ -* ███████║███████╗ ██║ ███████║██████╔╝██║███████║ -* ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ -* ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ -* ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ -* -* Astaria Labs, Inc -*/ +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ pragma solidity =0.8.17; import {IRouterBase} from "core/interfaces/IRouterBase.sol"; diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index 2ae1431b..c4ea03e4 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -162,9 +162,10 @@ interface IAstariaRouter is IPausable, IBeacon { * @param underlying The address of the underlying token. * @return The address of the new PrivateVault. */ - function newVault(address delegate, address underlying) - external - returns (address); + function newVault( + address delegate, + address underlying + ) external returns (address); /** * @notice Retrieves the address that collects protocol-level fees. @@ -176,9 +177,9 @@ interface IAstariaRouter is IPausable, IBeacon { * @param commitments The commitment proofs and requested loan data for each loan. * @return lienIds the lienIds for each loan. */ - function commitToLiens(Commitment[] memory commitments) - external - returns (uint256[] memory, ILienToken.Stack[] memory); + function commitToLiens( + Commitment[] memory commitments + ) external returns (uint256[] memory, ILienToken.Stack[] memory); /** * @notice Create a new lien against a CollateralToken. @@ -188,13 +189,7 @@ interface IAstariaRouter is IPausable, IBeacon { function requestLienPosition( IAstariaRouter.Commitment calldata params, address recipient - ) - external - returns ( - uint256, - ILienToken.Stack[] memory, - uint256 - ); + ) external returns (uint256, ILienToken.Stack[] memory, uint256); function LIEN_TOKEN() external view returns (ILienToken); @@ -231,9 +226,10 @@ interface IAstariaRouter is IPausable, IBeacon { * @param position The position of the defaulted lien. * @return reserve The amount owed on all liens for against the collateral being liquidated, including accrued interest. */ - function liquidate(ILienToken.Stack[] calldata stack, uint8 position) - external - returns (OrderParameters memory); + function liquidate( + ILienToken.Stack[] calldata stack, + uint8 position + ) external returns (OrderParameters memory); /** * @notice Returns whether a specified lien can be liquidated. diff --git a/src/interfaces/ICollateralToken.sol b/src/interfaces/ICollateralToken.sol index 8d9acfed..03ef8d06 100644 --- a/src/interfaces/ICollateralToken.sol +++ b/src/interfaces/ICollateralToken.sol @@ -128,9 +128,9 @@ interface ICollateralToken is IERC721 { * @notice Send a CollateralToken to a Seaport auction on liquidation. * @param params The auction data. */ - function auctionVault(AuctionVaultParams calldata params) - external - returns (OrderParameters memory); + function auctionVault( + AuctionVaultParams calldata params + ) external returns (OrderParameters memory); /** * @notice Clears the auction for a CollateralToken. @@ -145,10 +145,9 @@ interface ICollateralToken is IERC721 { * @param collateralId The ID of the CollateralToken wrapping the NFT. * @return The address and tokenId of the underlying NFT. */ - function getUnderlying(uint256 collateralId) - external - view - returns (address, uint256); + function getUnderlying( + uint256 collateralId + ) external view returns (address, uint256); /** * @notice Unlocks the NFT for a CollateralToken and sends it to a specified address. diff --git a/src/interfaces/IERC1155.sol b/src/interfaces/IERC1155.sol index 7de6a3a8..2a955d72 100644 --- a/src/interfaces/IERC1155.sol +++ b/src/interfaces/IERC1155.sol @@ -61,10 +61,10 @@ interface IERC1155 is IERC165 { * * - `account` cannot be the zero address. */ - function balanceOf(address account, uint256 id) - external - view - returns (uint256); + function balanceOf( + address account, + uint256 id + ) external view returns (uint256); /** * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. @@ -73,10 +73,10 @@ interface IERC1155 is IERC165 { * * - `accounts` and `ids` must have the same length. */ - function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) - external - view - returns (uint256[] memory); + function balanceOfBatch( + address[] calldata accounts, + uint256[] calldata ids + ) external view returns (uint256[] memory); /** * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, @@ -94,10 +94,10 @@ interface IERC1155 is IERC165 { * * See {setApprovalForAll}. */ - function isApprovedForAll(address account, address operator) - external - view - returns (bool); + function isApprovedForAll( + address account, + address operator + ) external view returns (bool); /** * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol index 60224a4a..cb389c03 100644 --- a/src/interfaces/IERC20.sol +++ b/src/interfaces/IERC20.sol @@ -47,10 +47,10 @@ interface IERC20 { * * This value changes when {approve} or {transferFrom} are called. */ - function allowance(address owner, address spender) - external - view - returns (uint256); + function allowance( + address owner, + address spender + ) external view returns (uint256); /** * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. diff --git a/src/interfaces/IERC4626.sol b/src/interfaces/IERC4626.sol index 0c7f3336..94ad9178 100644 --- a/src/interfaces/IERC4626.sol +++ b/src/interfaces/IERC4626.sol @@ -57,10 +57,9 @@ interface IERC4626 is IERC20, IERC20Metadata { * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and * from. */ - function convertToShares(uint256 assets) - external - view - returns (uint256 shares); + function convertToShares( + uint256 assets + ) external view returns (uint256 shares); /** * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal @@ -75,10 +74,9 @@ interface IERC4626 is IERC20, IERC20Metadata { * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and * from. */ - function convertToAssets(uint256 shares) - external - view - returns (uint256 assets); + function convertToAssets( + uint256 shares + ) external view returns (uint256 assets); /** * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver, @@ -88,10 +86,9 @@ interface IERC4626 is IERC20, IERC20Metadata { * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. * - MUST NOT revert. */ - function maxDeposit(address receiver) - external - view - returns (uint256 maxAssets); + function maxDeposit( + address receiver + ) external view returns (uint256 maxAssets); /** * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given @@ -108,10 +105,9 @@ interface IERC4626 is IERC20, IERC20Metadata { * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in * share price or some other type of condition, meaning the depositor will lose assets by depositing. */ - function previewDeposit(uint256 assets) - external - view - returns (uint256 shares); + function previewDeposit( + uint256 assets + ) external view returns (uint256 shares); /** * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. @@ -124,9 +120,10 @@ interface IERC4626 is IERC20, IERC20Metadata { * * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. */ - function deposit(uint256 assets, address receiver) - external - returns (uint256 shares); + function deposit( + uint256 assets, + address receiver + ) external returns (uint256 shares); /** * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call. @@ -164,9 +161,10 @@ interface IERC4626 is IERC20, IERC20Metadata { * * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. */ - function mint(uint256 shares, address receiver) - external - returns (uint256 assets); + function mint( + uint256 shares, + address receiver + ) external returns (uint256 assets); /** * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the @@ -193,10 +191,9 @@ interface IERC4626 is IERC20, IERC20Metadata { * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in * share price or some other type of condition, meaning the depositor will lose assets by depositing. */ - function previewWithdraw(uint256 assets) - external - view - returns (uint256 shares); + function previewWithdraw( + uint256 assets + ) external view returns (uint256 shares); /** * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. diff --git a/src/interfaces/IERC721.sol b/src/interfaces/IERC721.sol index 9103cd3a..cfe363dc 100644 --- a/src/interfaces/IERC721.sol +++ b/src/interfaces/IERC721.sol @@ -27,17 +27,9 @@ interface IERC721 is IERC165 { function setApprovalForAll(address operator, bool approved) external; - function transferFrom( - address from, - address to, - uint256 id - ) external; + function transferFrom(address from, address to, uint256 id) external; - function safeTransferFrom( - address from, - address to, - uint256 id - ) external; + function safeTransferFrom(address from, address to, uint256 id) external; function safeTransferFrom( address from, diff --git a/src/interfaces/IFlashAction.sol b/src/interfaces/IFlashAction.sol index 99926d07..a4f537b4 100644 --- a/src/interfaces/IFlashAction.sol +++ b/src/interfaces/IFlashAction.sol @@ -20,7 +20,8 @@ interface IFlashAction { uint256 tokenId; } - function onFlashAction(Underlying calldata, bytes calldata) - external - returns (bytes32); + function onFlashAction( + Underlying calldata, + bytes calldata + ) external returns (bytes32); } diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index 964caa23..f009e878 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -95,10 +95,9 @@ interface ILienToken is IERC721 { * @param lien The Lien. * @return lienId The lienId of the requested Lien, if valid (otherwise, reverts). */ - function validateLien(Lien calldata lien) - external - view - returns (uint256 lienId); + function validateLien( + Lien calldata lien + ) external view returns (uint256 lienId); function ASTARIA_ROUTER() external view returns (IAstariaRouter); @@ -109,10 +108,9 @@ interface ILienToken is IERC721 { * @param stack The Lien to compute the slope for. * @return slope The rate for the specified lien, in WETH per second. */ - function calculateSlope(Stack calldata stack) - external - pure - returns (uint256 slope); + function calculateSlope( + Stack calldata stack + ) external pure returns (uint256 slope); /** * @notice Stops accruing interest for all liens against a single CollateralToken. @@ -129,10 +127,9 @@ interface ILienToken is IERC721 { * @notice Computes and returns the buyout amount for a Lien. * @param stack the lien */ - function getBuyout(Stack calldata stack) - external - view - returns (uint256 owed, uint256 buyout); + function getBuyout( + Stack calldata stack + ) external view returns (uint256 owed, uint256 buyout); /** * @notice Removes all liens for a given CollateralToken. @@ -147,10 +144,10 @@ interface ILienToken is IERC721 { * @param timestamp the timestamp you want to inquire about * @return the amount owed in uint192 */ - function getOwed(Stack calldata stack, uint256 timestamp) - external - view - returns (uint88); + function getOwed( + Stack calldata stack, + uint256 timestamp + ) external view returns (uint88); /** * @notice Public view function that computes the interest for a LienToken since its last payment. @@ -162,39 +159,33 @@ interface ILienToken is IERC721 { * @notice Retrieves a lienCount for specific collateral * @param collateralId the Lien to compute a point for */ - function getCollateralState(uint256 collateralId) - external - view - returns (bytes32); + function getCollateralState( + uint256 collateralId + ) external view returns (bytes32); /** * @notice Retrieves a specific point by its lienId. * @param stack the Lien to compute a point for */ - function getAmountOwingAtLiquidation(ILienToken.Stack calldata stack) - external - view - returns (uint256); + function getAmountOwingAtLiquidation( + ILienToken.Stack calldata stack + ) external view returns (uint256); /** * @notice Creates a new lien against a CollateralToken. * @param params LienActionEncumber data containing CollateralToken information and lien parameters (rate, duration, and amount, rate, and debt caps). */ - function createLien(LienActionEncumber memory params) - external - returns ( - uint256 lienId, - Stack[] memory stack, - uint256 slope - ); + function createLien( + LienActionEncumber memory params + ) external returns (uint256 lienId, Stack[] memory stack, uint256 slope); /** * @notice Purchase a LienToken for its buyout price. * @param params The LienActionBuyout data specifying the lien position, receiver address, and underlying CollateralToken information of the lien. */ - function buyoutLien(LienActionBuyout memory params) - external - returns (Stack[] memory, Stack memory); + function buyoutLien( + LienActionBuyout memory params + ) external returns (Stack[] memory, Stack memory); /** * @notice Called by the ClearingHouse (through Seaport) to pay back debt with auction funds. @@ -245,28 +236,25 @@ interface ILienToken is IERC721 { * @notice Retrieves the AuctionData for a CollateralToken (The liquidator address and the AuctionStack). * @param collateralId The ID of the CollateralToken. */ - function getAuctionData(uint256 collateralId) - external - view - returns (AuctionData memory); + function getAuctionData( + uint256 collateralId + ) external view returns (AuctionData memory); /** * @notice Retrieves the liquidator for a CollateralToken. * @param collateralId The ID of the CollateralToken. */ - function getAuctionLiquidator(uint256 collateralId) - external - view - returns (address liquidator); + function getAuctionLiquidator( + uint256 collateralId + ) external view returns (address liquidator); /** * Calculates the debt accrued by all liens against a CollateralToken, assuming no payments are made until the end timestamp in the stack. * @param stack The stack data for active liens against the CollateralToken. */ - function getMaxPotentialDebtForCollateral(ILienToken.Stack[] memory stack) - external - view - returns (uint256); + function getMaxPotentialDebtForCollateral( + ILienToken.Stack[] memory stack + ) external view returns (uint256); /** * Calculates the debt accrued by all liens against a CollateralToken, assuming no payments are made until the provided timestamp. diff --git a/src/interfaces/IV3PositionManager.sol b/src/interfaces/IV3PositionManager.sol index 90cf779a..7118cbe4 100644 --- a/src/interfaces/IV3PositionManager.sol +++ b/src/interfaces/IV3PositionManager.sol @@ -52,7 +52,9 @@ interface IV3PositionManager { /// @return feeGrowthInside1LastX128 The fee growth of token1 as of the last action on the individual position /// @return tokensOwed0 The uncollected amount of token0 owed to the position as of the last computation /// @return tokensOwed1 The uncollected amount of token1 owed to the position as of the last computation - function positions(uint256 tokenId) + function positions( + uint256 tokenId + ) external view returns ( @@ -92,7 +94,9 @@ interface IV3PositionManager { /// @return liquidity The amount of liquidity for this position /// @return amount0 The amount of token0 /// @return amount1 The amount of token1 - function mint(MintParams calldata params) + function mint( + MintParams calldata params + ) external payable returns ( @@ -121,14 +125,12 @@ interface IV3PositionManager { /// @return liquidity The new liquidity amount as a result of the increase /// @return amount0 The amount of token0 to acheive resulting liquidity /// @return amount1 The amount of token1 to acheive resulting liquidity - function increaseLiquidity(IncreaseLiquidityParams calldata params) + function increaseLiquidity( + IncreaseLiquidityParams calldata params + ) external payable - returns ( - uint128 liquidity, - uint256 amount0, - uint256 amount1 - ); + returns (uint128 liquidity, uint256 amount0, uint256 amount1); struct DecreaseLiquidityParams { uint256 tokenId; @@ -146,10 +148,9 @@ interface IV3PositionManager { /// deadline The time by which the transaction must be included to effect the change /// @return amount0 The amount of token0 accounted to the position's tokens owed /// @return amount1 The amount of token1 accounted to the position's tokens owed - function decreaseLiquidity(DecreaseLiquidityParams calldata params) - external - payable - returns (uint256 amount0, uint256 amount1); + function decreaseLiquidity( + DecreaseLiquidityParams calldata params + ) external payable returns (uint256 amount0, uint256 amount1); struct CollectParams { uint256 tokenId; @@ -165,10 +166,9 @@ interface IV3PositionManager { /// amount1Max The maximum amount of token1 to collect /// @return amount0 The amount of fees collected in token0 /// @return amount1 The amount of fees collected in token1 - function collect(CollectParams calldata params) - external - payable - returns (uint256 amount0, uint256 amount1); + function collect( + CollectParams calldata params + ) external payable returns (uint256 amount0, uint256 amount1); /// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity and all tokens /// must be collected first. diff --git a/src/interfaces/IVaultImplementation.sol b/src/interfaces/IVaultImplementation.sol index 17e5d467..09ea327e 100644 --- a/src/interfaces/IVaultImplementation.sol +++ b/src/interfaces/IVaultImplementation.sol @@ -72,11 +72,7 @@ interface IVaultImplementation is IAstariaVaultBase, IERC165 { address receiver ) external - returns ( - uint256 lienId, - ILienToken.Stack[] memory stack, - uint256 payout - ); + returns (uint256 lienId, ILienToken.Stack[] memory stack, uint256 payout); function buyoutLien( ILienToken.Stack[] calldata stack, diff --git a/src/interfaces/IWithdrawProxy.sol b/src/interfaces/IWithdrawProxy.sol index e1bd4efc..0c6fcc1b 100644 --- a/src/interfaces/IWithdrawProxy.sol +++ b/src/interfaces/IWithdrawProxy.sol @@ -44,9 +44,10 @@ interface IWithdrawProxy is IRouterBase, IERC165, IERC4626 { * @param amount The amount to attempt to drain from the WithdrawProxy. * @param withdrawProxy The address of the withdrawProxy to drain to. */ - function drain(uint256 amount, address withdrawProxy) - external - returns (uint256); + function drain( + uint256 amount, + address withdrawProxy + ) external returns (uint256); /** * @notice Return any excess funds to the PublicVault, according to the withdrawRatio between withdrawing and remaining LPs. diff --git a/src/libraries/CollateralLookup.sol b/src/libraries/CollateralLookup.sol index e089f702..c33ad65f 100644 --- a/src/libraries/CollateralLookup.sol +++ b/src/libraries/CollateralLookup.sol @@ -16,11 +16,10 @@ pragma solidity =0.8.17; import {IERC721} from "core/interfaces/IERC721.sol"; library CollateralLookup { - function computeId(address token, uint256 tokenId) - internal - pure - returns (uint256 hash) - { + function computeId( + address token, + uint256 tokenId + ) internal pure returns (uint256 hash) { assembly { mstore(0, token) // sets the right most 20 bytes in the first memory slot. mstore(0x20, tokenId) // stores tokenId in the second memory slot. diff --git a/src/security/V3SecurityHook.sol b/src/security/V3SecurityHook.sol index 0ac2a510..a8d218a8 100644 --- a/src/security/V3SecurityHook.sol +++ b/src/security/V3SecurityHook.sol @@ -22,11 +22,10 @@ contract V3SecurityHook is ISecurityHook { positionManager = nftManager_; } - function getState(address tokenContract, uint256 tokenId) - external - view - returns (bytes32) - { + function getState( + address tokenContract, + uint256 tokenId + ) external view returns (bytes32) { ( uint96 nonce, address operator, diff --git a/src/strategies/CollectionValidator.sol b/src/strategies/CollectionValidator.sol index 93cc8028..726b6193 100644 --- a/src/strategies/CollectionValidator.sol +++ b/src/strategies/CollectionValidator.sol @@ -31,19 +31,15 @@ interface ICollectionValidator is IStrategyValidator { contract CollectionValidator is ICollectionValidator { uint8 public constant VERSION_TYPE = uint8(2); - function getLeafDetails(bytes memory nlrDetails) - public - pure - returns (ICollectionValidator.Details memory) - { + function getLeafDetails( + bytes memory nlrDetails + ) public pure returns (ICollectionValidator.Details memory) { return abi.decode(nlrDetails, (ICollectionValidator.Details)); } - function assembleLeaf(ICollectionValidator.Details memory details) - public - pure - returns (bytes memory) - { + function assembleLeaf( + ICollectionValidator.Details memory details + ) public pure returns (bytes memory) { return abi.encode(details); } diff --git a/src/strategies/UNI_V3Validator.sol b/src/strategies/UNI_V3Validator.sol index 2b044a34..e0fae9f1 100644 --- a/src/strategies/UNI_V3Validator.sol +++ b/src/strategies/UNI_V3Validator.sol @@ -61,19 +61,15 @@ contract UNI_V3Validator is IUNI_V3Validator { IUniswapV3Factory public V3_FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - function assembleLeaf(IUNI_V3Validator.Details memory details) - public - pure - returns (bytes memory) - { + function assembleLeaf( + IUNI_V3Validator.Details memory details + ) public pure returns (bytes memory) { return abi.encode(details); } - function getLeafDetails(bytes memory nlrDetails) - public - pure - returns (IUNI_V3Validator.Details memory) - { + function getLeafDetails( + bytes memory nlrDetails + ) public pure returns (IUNI_V3Validator.Details memory) { return abi.decode(nlrDetails, (IUNI_V3Validator.Details)); } diff --git a/src/strategies/UniqueValidator.sol b/src/strategies/UniqueValidator.sol index 2403cb9a..fb7f1c7e 100644 --- a/src/strategies/UniqueValidator.sol +++ b/src/strategies/UniqueValidator.sol @@ -32,19 +32,15 @@ interface IUniqueValidator is IStrategyValidator { contract UniqueValidator is IUniqueValidator { uint8 public constant VERSION_TYPE = uint8(1); - function getLeafDetails(bytes memory nlrDetails) - public - pure - returns (Details memory) - { + function getLeafDetails( + bytes memory nlrDetails + ) public pure returns (Details memory) { return abi.decode(nlrDetails, (Details)); } - function assembleLeaf(Details memory details) - public - pure - returns (bytes memory) - { + function assembleLeaf( + Details memory details + ) public pure returns (bytes memory) { return abi.encode(details); } diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 36314db3..9c170b6f 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -323,10 +323,9 @@ contract TestHelpers is Deploy, ConsiderationTester { (rate * amount * duration).mulDivDown(1, 365 days).mulDivDown(1, 1e18); } - function setupLiquidation(address borrower) - public - returns (address publicVault, ILienToken.Stack[] memory stack) - { + function setupLiquidation( + address borrower + ) public returns (address publicVault, ILienToken.Stack[] memory stack) { TestNFT nft = new TestNFT(0); _mintNoDepositApproveRouterSpecific(borrower, address(nft), 99); address tokenContract = address(nft); @@ -412,9 +411,10 @@ contract TestHelpers is Deploy, ConsiderationTester { ); } - function _mintNoDepositApproveRouter(address tokenContract, uint256 tokenId) - internal - { + function _mintNoDepositApproveRouter( + address tokenContract, + uint256 tokenId + ) internal { TestNFT(tokenContract).mint(address(this), tokenId); TestNFT(tokenContract).approve(address(ASTARIA_ROUTER), tokenId); } @@ -448,10 +448,10 @@ contract TestHelpers is Deploy, ConsiderationTester { ); } - function _createPrivateVault(address strategist, address delegate) - internal - returns (address privateVault) - { + function _createPrivateVault( + address strategist, + address delegate + ) internal returns (address privateVault) { vm.startPrank(strategist); privateVault = ASTARIA_ROUTER.newVault(delegate, address(WETH9)); vm.stopPrank(); @@ -875,14 +875,9 @@ contract TestHelpers is Deploy, ConsiderationTester { uint256 amount; } - function _toVRS(bytes memory signature) - internal - returns ( - uint8 v, - bytes32 r, - bytes32 s - ) - { + function _toVRS( + bytes memory signature + ) internal returns (uint8 v, bytes32 r, bytes32 s) { emit log_bytes(signature); emit log_named_uint("signature length", signature.length); if (signature.length == 65) { @@ -899,10 +894,9 @@ contract TestHelpers is Deploy, ConsiderationTester { } } - function _generateTerms(GenTerms memory params) - internal - returns (IAstariaRouter.Commitment memory terms) - { + function _generateTerms( + GenTerms memory params + ) internal returns (IAstariaRouter.Commitment memory terms) { (uint8 v, bytes32 r, bytes32 s) = _toVRS(params.signature); return @@ -1249,11 +1243,9 @@ contract TestHelpers is Deploy, ConsiderationTester { return _mirrorOrderParameters; } - function _toOfferItems(ConsiderationItem[] memory _considerationItems) - internal - pure - returns (OfferItem[] memory) - { + function _toOfferItems( + ConsiderationItem[] memory _considerationItems + ) internal pure returns (OfferItem[] memory) { OfferItem[] memory _offerItems = new OfferItem[]( _considerationItems.length ); diff --git a/src/utils/Initializable.sol b/src/utils/Initializable.sol index ca9c76da..0b25aab3 100644 --- a/src/utils/Initializable.sol +++ b/src/utils/Initializable.sol @@ -85,10 +85,10 @@ library Address { * * _Available since v3.1._ */ - function functionCall(address target, bytes memory data) - internal - returns (bytes memory) - { + function functionCall( + address target, + bytes memory data + ) internal returns (bytes memory) { return functionCallWithValue(target, data, 0, "Address: low-level call failed"); } @@ -159,11 +159,10 @@ library Address { * * _Available since v3.3._ */ - function functionStaticCall(address target, bytes memory data) - internal - view - returns (bytes memory) - { + function functionStaticCall( + address target, + bytes memory data + ) internal view returns (bytes memory) { return functionStaticCall(target, data, "Address: low-level static call failed"); } @@ -190,10 +189,10 @@ library Address { * * _Available since v3.4._ */ - function functionDelegateCall(address target, bytes memory data) - internal - returns (bytes memory) - { + function functionDelegateCall( + address target, + bytes memory data + ) internal returns (bytes memory) { return functionDelegateCall( target, @@ -260,10 +259,10 @@ library Address { } } - function _revert(bytes memory returndata, string memory errorMessage) - private - pure - { + function _revert( + bytes memory returndata, + string memory errorMessage + ) private pure { // Look for revert reason and bubble it up if present if (returndata.length > 0) { // The easiest way to bubble the revert reason is using memory via assembly From d8ede5fdb2f9d42a514d740e7e39d37584647150 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Wed, 8 Feb 2023 12:57:29 -0400 Subject: [PATCH 06/39] C4:332 fix use of blockhash --- src/CollateralToken.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index 9d270b33..6c1f25cd 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -456,7 +456,11 @@ contract CollateralToken is startTime: uint256(block.timestamp), endTime: uint256(block.timestamp + maxDuration), zoneHash: bytes32(collateralId), - salt: uint256(blockhash(block.number)), + salt: uint256( + keccak256( + abi.encodePacked(collateralId, uint256(blockhash(block.number - 1))) + ) + ), conduitKey: s.CONDUIT_KEY, // 0x120 totalOriginalConsiderationItems: considerationItems.length }); From 4d5a9fe9f15c279e6b53233ad6344e6bb4c26594 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Mon, 6 Feb 2023 15:38:59 -0400 Subject: [PATCH 07/39] C4:122 - moved strategy deadline validation into the vault contract from the router - added tests to show proper enforcement https://github.com/code-423n4/2023-01-astaria-findings/issues/122 AST-303 --- src/VaultImplementation.sol | 3 ++ src/interfaces/IVaultImplementation.sol | 3 +- src/test/ForkedTesting.t.sol | 3 +- src/test/RevertTesting.t.sol | 38 ++++++++++++++++++++++ src/test/TestHelpers.t.sol | 43 +++++++------------------ 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 86a9e010..905cc82e 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -230,6 +230,9 @@ abstract contract VaultImplementation is IAstariaRouter.Commitment calldata params, address receiver ) internal view { + if (block.timestamp > params.lienRequest.strategy.deadline) { + revert IVaultImplementation.InvalidRequest(InvalidRequestReason.EXPIRED); + } uint256 collateralId = params.tokenContract.computeId(params.tokenId); ERC721 CT = ERC721(address(COLLATERAL_TOKEN())); address holder = CT.ownerOf(collateralId); diff --git a/src/interfaces/IVaultImplementation.sol b/src/interfaces/IVaultImplementation.sol index 09ea327e..2855bb8d 100644 --- a/src/interfaces/IVaultImplementation.sol +++ b/src/interfaces/IVaultImplementation.sol @@ -28,7 +28,8 @@ interface IVaultImplementation is IAstariaVaultBase, IERC165 { INVALID_RATE, INVALID_POTENTIAL_DEBT, SHUTDOWN, - PAUSED + PAUSED, + EXPIRED } error InvalidRequest(InvalidRequestReason reason); diff --git a/src/test/ForkedTesting.t.sol b/src/test/ForkedTesting.t.sol index 87e6e9c0..54f33c60 100644 --- a/src/test/ForkedTesting.t.sol +++ b/src/test/ForkedTesting.t.sol @@ -119,8 +119,7 @@ contract ForkedTesting is TestHelpers { vault: privateVault, amount: 10 ether, stack: new ILienToken.Stack[](0), - isFirstLien: true, - broadcast: false + isFirstLien: true }); } diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..35cd41e3 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -481,6 +481,44 @@ contract RevertTesting is TestHelpers { }); } + function testCannotCommitToLienAfterStrategyDeadline() public { + TestNFT nft = new TestNFT(1); + address tokenContract = address(nft); + uint256 tokenId = uint256(0); + address publicVault = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 14 days + }); + + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + uint256 balanceBefore = WETH9.balanceOf(address(this)); + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true, + stack: new ILienToken.Stack[](0), + revertMessage: abi.encodeWithSelector( + IVaultImplementation.InvalidRequest.selector, + IVaultImplementation.InvalidRequestReason.EXPIRED + ), + beforeExecution: this._skip11DaysToFailStrategyDeadlineCheck + }); + } + + function _skip11DaysToFailStrategyDeadlineCheck() public { + skip(11 days); + } + function testFailPayLienAfterLiquidate() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 9c170b6f..14cee485 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -533,30 +533,22 @@ contract TestHelpers is Deploy, ConsiderationTester { amount: amount, isFirstLien: isFirstLien, stack: new ILienToken.Stack[](0), - revertMessage: new bytes(0), - broadcast: false + revertMessage: new bytes(0) }); } function _executeCommitments( IAstariaRouter.Commitment[] memory commitments, - bytes memory revertMessage, - bool broadcast + bytes memory revertMessage ) internal returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) { - if (broadcast) { - vm.startBroadcast(msg.sender); - } COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); if (revertMessage.length > 0) { vm.expectRevert(revertMessage); } (lienIds, newStack) = ASTARIA_ROUTER.commitToLiens(commitments); - if (broadcast) { - vm.stopBroadcast(); - } } struct V3LienParams { @@ -580,8 +572,7 @@ contract TestHelpers is Deploy, ConsiderationTester { address vault, uint256 amount, ILienToken.Stack[] memory stack, - bool isFirstLien, - bool broadcast + bool isFirstLien ) internal returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) @@ -594,16 +585,10 @@ contract TestHelpers is Deploy, ConsiderationTester { }); if (isFirstLien) { - if (broadcast) { - vm.startBroadcast(msg.sender); - } ERC721(params.tokenContract).setApprovalForAll( address(ASTARIA_ROUTER), true ); - if (broadcast) { - vm.stopBroadcast(); - } } IAstariaRouter.Commitment[] memory commitments = new IAstariaRouter.Commitment[](1); @@ -611,8 +596,7 @@ contract TestHelpers is Deploy, ConsiderationTester { return _executeCommitments({ commitments: commitments, - revertMessage: new bytes(0), - broadcast: broadcast + revertMessage: new bytes(0) }); } @@ -641,8 +625,7 @@ contract TestHelpers is Deploy, ConsiderationTester { amount: amount, isFirstLien: isFirstLien, stack: stack, - revertMessage: new bytes(0), - broadcast: false + revertMessage: new bytes(0) }); } @@ -673,10 +656,12 @@ contract TestHelpers is Deploy, ConsiderationTester { isFirstLien: isFirstLien, stack: stack, revertMessage: revertMessage, - broadcast: false + beforeExecution: this.beforeExecutionMock }); } + function beforeExecutionMock() public {} + function _commitToLien( address vault, // address of deployed Vault address strategist, @@ -688,7 +673,7 @@ contract TestHelpers is Deploy, ConsiderationTester { bool isFirstLien, ILienToken.Stack[] memory stack, bytes memory revertMessage, - bool broadcast + function() external beforeExecution ) internal returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) @@ -705,22 +690,16 @@ contract TestHelpers is Deploy, ConsiderationTester { }); if (isFirstLien) { - if (broadcast) { - vm.startBroadcast(msg.sender); - } ERC721(tokenContract).setApprovalForAll(address(ASTARIA_ROUTER), true); - if (broadcast) { - vm.stopBroadcast(); - } } IAstariaRouter.Commitment[] memory commitments = new IAstariaRouter.Commitment[](1); commitments[0] = terms; + beforeExecution(); return _executeCommitments({ commitments: commitments, - revertMessage: revertMessage, - broadcast: broadcast + revertMessage: revertMessage }); } From b77746f5f1488d2a40a284b6969e63fa963f1c07 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 5 Feb 2023 20:52:42 -0500 Subject: [PATCH 08/39] remove publicvault unchecked slope update and changed slope from uint48 to uint256, https://github.com/code-423n4/2023-01-astaria-findings/issues/418 --- src/PublicVault.sol | 16 ++++++---------- src/interfaces/IPublicVault.sol | 3 ++- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/PublicVault.sol b/src/PublicVault.sol index cdb620f5..726bcb13 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -442,7 +442,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { // increment slope for the new lien _accrue(s); unchecked { - uint48 newSlope = s.slope + lienSlope.safeCastTo48(); + uint256 newSlope = s.slope + lienSlope; _setSlope(s, newSlope); } @@ -452,8 +452,6 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { emit LienOpen(lienId, epoch); } - event SlopeUpdated(uint48 newSlope); - function accrue() public returns (uint256) { return _accrue(_loadStorageSlot()); } @@ -515,13 +513,13 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { _accrue(s); unchecked { - uint48 newSlope = s.slope - params.lienSlope.safeCastTo48(); + uint256 newSlope = s.slope - params.lienSlope; _setSlope(s, newSlope); } _handleStrategistInterestReward(s, params.interestOwed, params.amount); } - function _setSlope(VaultData storage s, uint48 newSlope) internal { + function _setSlope(VaultData storage s, uint256 newSlope) internal { s.slope = newSlope; emit SlopeUpdated(newSlope); } @@ -556,9 +554,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { function afterPayment(uint256 computedSlope) public onlyLienToken { VaultData storage s = _loadStorageSlot(); - unchecked { - s.slope += computedSlope.safeCastTo48(); - } + s.slope += computedSlope; emit SlopeUpdated(s.slope); } @@ -612,7 +608,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { VaultData storage s = _loadStorageSlot(); unchecked { - uint48 newSlope = s.slope - params.lienSlope.safeCastTo48(); + uint256 newSlope = s.slope - params.lienSlope; _setSlope(s, newSlope); s.yIntercept += params.increaseYIntercept.safeCastTo88(); s.last = block.timestamp.safeCastTo40(); @@ -638,7 +634,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { _accrue(s); unchecked { - _setSlope(s, s.slope - params.lienSlope.safeCastTo48()); + _setSlope(s, s.slope - params.lienSlope); } if (s.currentEpoch != 0) { diff --git a/src/interfaces/IPublicVault.sol b/src/interfaces/IPublicVault.sol index b653eb3b..8f8384a4 100644 --- a/src/interfaces/IPublicVault.sol +++ b/src/interfaces/IPublicVault.sol @@ -24,7 +24,7 @@ interface IPublicVault is IVaultImplementation { struct VaultData { uint88 yIntercept; - uint48 slope; + uint256 slope; uint40 last; uint64 currentEpoch; uint88 withdrawReserve; @@ -170,4 +170,5 @@ interface IPublicVault is IVaultImplementation { event YInterceptChanged(uint88 newYintercept); event WithdrawReserveTransferred(uint256 amount); event LienOpen(uint256 lienId, uint256 epoch); + event SlopeUpdated(uint256 newSlope); } From cb5a61c0bbbe09dbcd57a5cf55c5507d560de4f7 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 20:59:16 -0400 Subject: [PATCH 09/39] C4:281 - removed self liquidate feature, https://github.com/code-423n4/2023-01-astaria-findings/issues/281 --- src/AstariaRouter.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index 22a73297..1f9d7707 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -608,8 +608,7 @@ contract AstariaRouter is ILienToken.Stack memory stack ) public view returns (bool) { RouterStorage storage s = _loadRouterSlot(); - return (stack.point.end <= block.timestamp || - msg.sender == s.COLLATERAL_TOKEN.ownerOf(stack.lien.collateralId)); + return (stack.point.end <= block.timestamp); } function liquidate( From 79c094d7316c79ee422f3bfcb0efcf1266f15171 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 14:37:47 -0400 Subject: [PATCH 10/39] C4:158 - prevent from deploying vaults with an underlying erc20 that has no code --- src/AstariaRouter.sol | 3 +++ src/interfaces/IAstariaRouter.sol | 1 + src/test/RevertTesting.t.sol | 14 ++++++++++++++ src/test/TestHelpers.t.sol | 12 ++++++++++-- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index 22a73297..02e6dff1 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -713,6 +713,9 @@ contract AstariaRouter is ) internal returns (address vaultAddr) { uint8 vaultType; + if (underlying.code.length == 0) { + revert InvalidUnderlying(underlying); + } if (epochLength > uint256(0)) { vaultType = uint8(ImplementationType.PublicVault); } else { diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index c4ea03e4..e890d1c7 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -307,6 +307,7 @@ interface IAstariaRouter is IPausable, IBeacon { error InvalidCommitmentState(CommitmentState); error InvalidStrategy(uint16); error InvalidVault(address); + error InvalidUnderlying(address); error UnsupportedFile(); enum LienState { diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..f7b0ee14 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -61,6 +61,20 @@ contract RevertTesting is TestHelpers { MAX_LIENS } + function testCannotDeployUnderlyingWithNoCode() public { + vm.expectRevert( + abi.encodeWithSelector( + IAstariaRouter.InvalidUnderlying.selector, + address(3) + ) + ); + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo, + token: address(3) + }); + } + function testCannotRandomAccountIncrementNonce() public { address privateVault = _createPublicVault({ strategist: strategistOne, diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 9c170b6f..55bd5780 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -450,13 +450,21 @@ contract TestHelpers is Deploy, ConsiderationTester { function _createPrivateVault( address strategist, - address delegate + address delegate, + address token ) internal returns (address privateVault) { vm.startPrank(strategist); - privateVault = ASTARIA_ROUTER.newVault(delegate, address(WETH9)); + privateVault = ASTARIA_ROUTER.newVault(delegate, token); vm.stopPrank(); } + function _createPrivateVault( + address strategist, + address delegate + ) internal returns (address) { + return _createPrivateVault(strategist, delegate, address(WETH9)); + } + function _createPublicVault( address strategist, address delegate, From b0e655f5e9f0dfdde77685bf3d68edddb5f80ba4 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 4 Feb 2023 18:26:04 -0500 Subject: [PATCH 11/39] address https://github.com/code-423n4/2023-01-astaria-findings/issues/367 --- src/PublicVault.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PublicVault.sol b/src/PublicVault.sol index cdb620f5..c116dc55 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -100,10 +100,12 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { override(ERC4626Cloned) returns (uint256) { - if (ERC20(asset()).decimals() == uint8(18)) { - return 100 gwei; - } else { + if (ERC20(asset()).decimals() < 4) { return 10 ** (ERC20(asset()).decimals() - 1); + } else if (ERC20(asset()).decimals() < 8) { + return 10 ** (ERC20(asset()).decimals() - 2); + } else { + return 10 ** (ERC20(asset()).decimals() - 6); } } From 3e577cb1198724f01b68aa8316c5c9a64420653c Mon Sep 17 00:00:00 2001 From: = Date: Sat, 4 Feb 2023 18:17:25 -0500 Subject: [PATCH 12/39] partially fixed https://github.com/code-423n4/2023-01-astaria-findings/issues/489 --- src/Vault.sol | 2 +- src/test/AstariaTest.t.sol | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Vault.sol b/src/Vault.sol index 42733213..4a53f27e 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -64,7 +64,7 @@ contract Vault is VaultImplementation { function withdraw(uint256 amount) external { require(msg.sender == owner()); - ERC20(asset()).safeTransferFrom(address(this), msg.sender, amount); + ERC20(asset()).safeTransfer(msg.sender, amount); } function disableAllowList() external pure override(VaultImplementation) { diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index 444a1e68..1f02e127 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -160,6 +160,37 @@ contract AstariaTest is TestHelpers { assertEq(WETH9.balanceOf(address(this)), initialBalance + 10 ether); } + // From C4 #489 + function testPrivateVaultWithdraw() public { + uint256 amountToLend = 50 ether; + vm.deal(strategistOne, amountToLend); + + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + vm.startPrank(strategistOne); + + WETH9.deposit{value: amountToLend}(); + WETH9.approve(privateVault, amountToLend); + + // strategistOne deposits 50 ether WETH to privateVault + Vault(privateVault).deposit(amountToLend, strategistOne); + + // still reverting with APPROVE_FAILED + // ASTARIA_ROUTER.withdraw( + // IERC4626(privateVault), + // strategistOne, + // amountToLend, + // type(uint256).max + // ); + + Vault(privateVault).withdraw(amountToLend); + + vm.stopPrank(); + } + function testWithdrawProxy() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); From 3a051768a9825635fec80494347867fca0591904 Mon Sep 17 00:00:00 2001 From: GregTheDev Date: Thu, 9 Feb 2023 16:09:46 -0600 Subject: [PATCH 13/39] C5:565 - receiver flow reworked, prevents abuse on validating against holders, validates operator has code, https://github.com/code-423n4/2023-01-astaria-findings/issues/565, https://github.com/code-423n4/2023-01-astaria-findings/issues/134, https://github.com/code-423n4/2023-01-astaria-findings/issues/283 --- src/AstariaRouter.sol | 16 ++----- src/VaultImplementation.sol | 56 ++++++++++++------------- src/interfaces/IVaultImplementation.sol | 8 ++-- src/test/RevertTesting.t.sol | 9 +++- 4 files changed, 41 insertions(+), 48 deletions(-) diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index 22a73297..77d3c0d5 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -508,9 +508,8 @@ contract AstariaRouter is if (i != 0) { commitments[i].lienRequest.stack = stack; } - uint256 payout; - (lienIds[i], stack, payout) = _executeCommitment(s, commitments[i]); - totalBorrowed += payout; + (lienIds[i], stack) = _executeCommitment(s, commitments[i]); + totalBorrowed += stack[stack.length - 1].point.amount; unchecked { ++i; } @@ -753,10 +752,7 @@ contract AstariaRouter is function _executeCommitment( RouterStorage storage s, IAstariaRouter.Commitment memory c - ) - internal - returns (uint256, ILienToken.Stack[] memory stack, uint256 payout) - { + ) internal returns (uint256, ILienToken.Stack[] memory stack) { uint256 collateralId = c.tokenContract.computeId(c.tokenId); if (msg.sender != s.COLLATERAL_TOKEN.ownerOf(collateralId)) { @@ -766,11 +762,7 @@ contract AstariaRouter is revert InvalidVault(c.lienRequest.strategy.vault); } //router must be approved for the collateral to take a loan, - return - IVaultImplementation(c.lienRequest.strategy.vault).commitToLien( - c, - address(this) - ); + return IVaultImplementation(c.lienRequest.strategy.vault).commitToLien(c); } function _transferAndDepositAssetIfAble( diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 86a9e010..b5aae4b7 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -216,7 +216,7 @@ abstract contract VaultImplementation is } /** - * @dev Validates the terms for a requested loan. + * @dev Validates the incoming request for a lien * Who is requesting the borrow, is it a smart contract? or is it a user? * if a smart contract, then ensure that the contract is approved to borrow and is also receiving the funds. * if a user, then ensure that the user is approved to borrow and is also receiving the funds. @@ -224,24 +224,22 @@ abstract contract VaultImplementation is * lien details are decoded from the obligation data and validated the collateral * * @param params The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. - * @param receiver The address of the prospective borrower. */ - function _validateCommitment( - IAstariaRouter.Commitment calldata params, - address receiver - ) internal view { + function _validateRequest( + IAstariaRouter.Commitment calldata params + ) internal view returns (address) { uint256 collateralId = params.tokenContract.computeId(params.tokenId); ERC721 CT = ERC721(address(COLLATERAL_TOKEN())); address holder = CT.ownerOf(collateralId); address operator = CT.getApproved(collateralId); if ( msg.sender != holder && - receiver != holder && - receiver != operator && + msg.sender != operator && !CT.isApprovedForAll(holder, msg.sender) ) { revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } + VIData storage s = _loadVISlot(); address recovered = ecrecover( keccak256( @@ -263,6 +261,16 @@ abstract contract VaultImplementation is InvalidRequestReason.INVALID_SIGNATURE ); } + + if (holder != msg.sender) { + if (msg.sender.code.length > 0) { + return msg.sender; + } else { + revert InvalidRequest(InvalidRequestReason.OPERATOR_NO_CODE); + } + } else { + return holder; + } } function _afterCommitToLien( @@ -280,23 +288,18 @@ abstract contract VaultImplementation is * Origination consists of a few phases: pre-commitment validation, lien token issuance, strategist reward, and after commitment actions * Starts by depositing collateral and take optimized-out a lien against it. Next, verifies the merkle proof for a loan commitment. Vault owners are then rewarded fees for successful loan origination. * @param params Commitment data for the incoming lien request - * @param receiver The borrower receiving the loan. * @return lienId The id of the newly minted lien token. */ function commitToLien( - IAstariaRouter.Commitment calldata params, - address receiver + IAstariaRouter.Commitment calldata params ) external whenNotPaused - returns (uint256 lienId, ILienToken.Stack[] memory stack, uint256 payout) + returns (uint256 lienId, ILienToken.Stack[] memory stack) { _beforeCommitToLien(params); uint256 slopeAddition; - (lienId, stack, slopeAddition, payout) = _requestLienAndIssuePayout( - params, - receiver - ); + (lienId, stack, slopeAddition) = _requestLienAndIssuePayout(params); _afterCommitToLien( stack[stack.length - 1].point.end, lienId, @@ -328,7 +331,7 @@ abstract contract VaultImplementation is ); } - _validateCommitment(incomingTerms, recipient()); + _validateRequest(incomingTerms); ERC20(asset()).safeApprove(address(ROUTER().TRANSFER_PROXY()), buyout); @@ -373,24 +376,19 @@ abstract contract VaultImplementation is /** * @dev Generates a Lien for a valid loan commitment proof and sends the loan amount to the borrower. * @param c The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. - * @param receiver The borrower requesting the loan. */ function _requestLienAndIssuePayout( - IAstariaRouter.Commitment calldata c, - address receiver + IAstariaRouter.Commitment calldata c ) internal - returns ( - uint256 newLienId, - ILienToken.Stack[] memory stack, - uint256 slope, - uint256 payout - ) + returns (uint256 newLienId, ILienToken.Stack[] memory stack, uint256 slope) { - _validateCommitment(c, receiver); + address receiver = _validateRequest(c); (newLienId, stack, slope) = ROUTER().requestLienPosition(c, recipient()); - payout = _handleProtocolFee(c.lienRequest.amount); - ERC20(asset()).safeTransfer(receiver, payout); + ERC20(asset()).safeTransfer( + receiver, + _handleProtocolFee(c.lienRequest.amount) + ); } function _handleProtocolFee(uint256 amount) internal returns (uint256) { diff --git a/src/interfaces/IVaultImplementation.sol b/src/interfaces/IVaultImplementation.sol index 09ea327e..6398265a 100644 --- a/src/interfaces/IVaultImplementation.sol +++ b/src/interfaces/IVaultImplementation.sol @@ -21,6 +21,7 @@ import {IERC165} from "core/interfaces/IERC165.sol"; interface IVaultImplementation is IAstariaVaultBase, IERC165 { enum InvalidRequestReason { NO_AUTHORITY, + OPERATOR_NO_CODE, INVALID_SIGNATURE, INVALID_COMMITMENT, INVALID_AMOUNT, @@ -68,11 +69,8 @@ interface IVaultImplementation is IAstariaVaultBase, IERC165 { function incrementNonce() external; function commitToLien( - IAstariaRouter.Commitment calldata params, - address receiver - ) - external - returns (uint256 lienId, ILienToken.Stack[] memory stack, uint256 payout); + IAstariaRouter.Commitment calldata params + ) external returns (uint256 lienId, ILienToken.Stack[] memory stack); function buyoutLien( ILienToken.Stack[] calldata stack, diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..ec7c2940 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -129,8 +129,13 @@ contract RevertTesting is TestHelpers { COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); uint256 balanceOfBefore = ERC20(privateVault).balanceOf(address(this)); - vm.expectRevert(abi.encodePacked("InvalidRequest(1)")); - VaultImplementation(privateVault).commitToLien(terms, address(this)); + vm.expectRevert( + abi.encodeWithSelector( + IVaultImplementation.InvalidRequest.selector, + IVaultImplementation.InvalidRequestReason.NO_AUTHORITY + ) + ); + VaultImplementation(privateVault).commitToLien(terms); assertEq( balanceOfBefore, ERC20(privateVault).balanceOf(address(this)), From 30c04495fcca445ddd176fc4efec1548fbab98a4 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 14:49:47 -0400 Subject: [PATCH 14/39] C4: 25 - private vaults cannot be deposited into if shut down or protocol is paused --- src/Vault.sol | 2 +- src/test/RevertTesting.t.sol | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Vault.sol b/src/Vault.sol index 42733213..dfb91849 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -55,7 +55,7 @@ contract Vault is VaultImplementation { function deposit( uint256 amount, address receiver - ) public virtual returns (uint256) { + ) public virtual whenNotPaused returns (uint256) { VIData storage s = _loadVISlot(); require(s.allowList[msg.sender] && receiver == owner()); ERC20(asset()).safeTransferFrom(msg.sender, address(this), amount); diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..1e6b14af 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -94,6 +94,19 @@ contract RevertTesting is TestHelpers { ); } + function testFailDepositWhenProtocolPaused() public { + address privateVault = _createPrivateVault({ + delegate: strategistOne, + strategist: strategistTwo + }); + ASTARIA_ROUTER.__emergencyPause(); + + _lendToPrivateVault( + Lender({addr: strategistTwo, amountToLend: 50 ether}), + privateVault + ); + } + function testFailInvalidSignature() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); From e2fab018281db75e7bccd7940a6d431a0a6facc6 Mon Sep 17 00:00:00 2001 From: Joseph Delong Date: Thu, 9 Feb 2023 16:25:16 -0600 Subject: [PATCH 15/39] removed unchecked in processEpoch(), https://github.com/code-423n4/2023-01-astaria-findings/issues/362 --- src/PublicVault.sol | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/PublicVault.sol b/src/PublicVault.sol index cdb620f5..1f3256f6 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -315,24 +315,19 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { currentWithdrawProxy.setWithdrawRatio(s.liquidationWithdrawRatio); uint256 expected = currentWithdrawProxy.getExpected(); - unchecked { - if (totalAssets() > expected) { - s.withdrawReserve = (totalAssets() - expected) - .mulWadDown(s.liquidationWithdrawRatio) - .safeCastTo88(); - } else { - s.withdrawReserve = 0; - } + if (totalAssets() > expected) { + s.withdrawReserve = (totalAssets() - expected) + .mulWadDown(s.liquidationWithdrawRatio) + .safeCastTo88(); + } else { + s.withdrawReserve = 0; } - s.yIntercept = totalAssets().safeCastTo88(); - s.last = block.timestamp.safeCastTo40(); - _setYIntercept( s, - s.yIntercept - - totalAssets().mulDivDown(s.liquidationWithdrawRatio, 1e18) + totalAssets().mulDivDown(1e18 - s.liquidationWithdrawRatio, 1e18) ); + s.last = block.timestamp.safeCastTo40(); // burn the tokens of the LPs withdrawing _burn(address(this), proxySupply); } From 8f431eca34e17ed9b5bdf5d8eb35d5f998975ee8 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 20:56:23 -0400 Subject: [PATCH 16/39] C4:175 - fix redeem flow and underlying lib defintion --- lib/gpl | 2 +- src/test/TestHelpers.t.sol | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/gpl b/lib/gpl index 4b49fe99..608fc2c7 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit 4b49fe993d9b807fe68b3421ee7f2fe91267c9ef +Subproject commit 608fc2c7f41ecd7f3ec4a5d2ed4e2011b68e141f diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 9c170b6f..c36fd59e 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -1283,12 +1283,22 @@ contract TestHelpers is Deploy, ConsiderationTester { vm.startPrank(lender); ERC20(publicVault).safeApprove(address(ASTARIA_ROUTER), vaultTokenBalance); - ASTARIA_ROUTER.redeemFutureEpoch({ - vault: IPublicVault(publicVault), - shares: vaultTokenBalance, - receiver: lender, - epoch: epoch - }); + uint256 currentEpoch = PublicVault(publicVault).getCurrentEpoch(); + if (epoch == currentEpoch) { + ASTARIA_ROUTER.redeem({ + vault: IERC4626(address(publicVault)), + to: lender, + shares: vaultTokenBalance, + minAmountOut: 0 + }); + } else { + ASTARIA_ROUTER.redeemFutureEpoch({ + vault: IPublicVault(publicVault), + shares: vaultTokenBalance, + receiver: lender, + epoch: epoch + }); + } WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy( epoch From 0a3b3b27600065f6855be255db1704e6ed5d85fa Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 14:59:11 -0400 Subject: [PATCH 17/39] C4:379 refactor all uint88 values into uint256 --- src/AstariaRouter.sol | 4 ++-- src/LienToken.sol | 26 +++++++++++-------------- src/PublicVault.sol | 24 +++++++++++------------ src/VaultImplementation.sol | 4 ++-- src/WithdrawProxy.sol | 8 ++++---- src/interfaces/IAstariaRouter.sol | 2 +- src/interfaces/ILienToken.sol | 12 ++++++------ src/interfaces/IPublicVault.sol | 12 ++++++------ src/interfaces/IVaultImplementation.sol | 2 +- 9 files changed, 44 insertions(+), 50 deletions(-) diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index 22a73297..b07545b2 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -112,7 +112,7 @@ contract AstariaRouter is s.minInterestBPS = uint32((uint256(1e15) * 5) / (365 days)); s.minEpochLength = uint32(7 days); s.maxEpochLength = uint32(45 days); - s.maxInterestRate = ((uint256(1e16) * 200) / (365 days)).safeCastTo88(); + s.maxInterestRate = ((uint256(1e16) * 200) / (365 days)); //63419583966; // 200% apy / second s.buyoutFeeNumerator = uint32(100); s.buyoutFeeDenominator = uint32(1000); @@ -320,7 +320,7 @@ contract AstariaRouter is } else if (what == FileType.MaxEpochLength) { s.maxEpochLength = abi.decode(data, (uint256)).safeCastTo32(); } else if (what == FileType.MaxInterestRate) { - s.maxInterestRate = abi.decode(data, (uint256)).safeCastTo88(); + s.maxInterestRate = abi.decode(data, (uint256)); } else if (what == FileType.FeeTo) { address addr = abi.decode(data, (address)); if (addr == address(0)) revert InvalidFileData(); diff --git a/src/LienToken.sol b/src/LienToken.sol index 28559f60..c0d4e11a 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -304,7 +304,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { auctionStack.lienId = stack[i].point.lienId; auctionStack.end = stack[i].point.end; - uint88 owed = _getOwed(stack[i], block.timestamp); + uint256 owed = _getOwed(stack[i], block.timestamp); auctionStack.amountOwed = owed; s.lienMeta[auctionStack.lienId].atLiquidation = true; auctionData.stack[i] = auctionStack; @@ -332,12 +332,8 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { s.collateralStateHash[collateralId] = ACTIVE_AUCTION; auctionData.startTime = block.timestamp.safeCastTo48(); auctionData.endTime = (block.timestamp + auctionWindow).safeCastTo48(); - auctionData.startAmount = stack[0] - .lien - .details - .liquidationInitialAsk - .safeCastTo88(); - auctionData.endAmount = uint88(1000 wei); + auctionData.startAmount = stack[0].lien.details.liquidationInitialAsk; + auctionData.endAmount = uint256(1000 wei); s.COLLATERAL_TOKEN.getClearingHouse(collateralId).setAuctionData( auctionData ); @@ -441,7 +437,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { newLienId = uint256(keccak256(abi.encode(params.lien))); Point memory point = Point({ lienId: newLienId, - amount: params.amount.safeCastTo88(), + amount: params.amount, last: block.timestamp.safeCastTo40(), end: (block.timestamp + params.lien.details.duration).safeCastTo40() }); @@ -616,7 +612,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { //checks the lien exists address payee = _getPayee(s, lienId); uint256 remaining = 0; - if (owing > payment.safeCastTo88()) { + if (owing > payment) { remaining = owing - payment; } else { payment = owing; @@ -726,7 +722,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getOwed(Stack memory stack) external view returns (uint88) { + function getOwed(Stack memory stack) external view returns (uint256) { validateLien(stack.lien); return _getOwed(stack, block.timestamp); } @@ -734,7 +730,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { function getOwed( Stack memory stack, uint256 timestamp - ) external view returns (uint88) { + ) external view returns (uint256) { validateLien(stack.lien); return _getOwed(stack, timestamp); } @@ -747,8 +743,8 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { function _getOwed( Stack memory stack, uint256 timestamp - ) internal pure returns (uint88) { - return stack.point.amount + _getInterest(stack, timestamp).safeCastTo88(); + ) internal pure returns (uint256) { + return stack.point.amount + _getInterest(stack, timestamp); } /** @@ -807,11 +803,11 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } //bring the point up to block.timestamp, compute the owed - stack.point.amount = owed.safeCastTo88(); + stack.point.amount = owed; stack.point.last = block.timestamp.safeCastTo40(); if (stack.point.amount > amount) { - stack.point.amount -= amount.safeCastTo88(); + stack.point.amount -= amount; // // slope does not need to be updated if paying off the rest, since we neutralize slope in beforePayment() if (isPublicVault) { IPublicVault(lienOwner).afterPayment(calculateSlope(stack)); diff --git a/src/PublicVault.sol b/src/PublicVault.sol index cdb620f5..465c87bd 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -308,18 +308,16 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { if ((address(currentWithdrawProxy) != address(0))) { uint256 proxySupply = currentWithdrawProxy.totalSupply(); - s.liquidationWithdrawRatio = proxySupply - .mulDivDown(1e18, totalSupply()) - .safeCastTo88(); + s.liquidationWithdrawRatio = proxySupply.mulDivDown(1e18, totalSupply()); currentWithdrawProxy.setWithdrawRatio(s.liquidationWithdrawRatio); uint256 expected = currentWithdrawProxy.getExpected(); unchecked { if (totalAssets() > expected) { - s.withdrawReserve = (totalAssets() - expected) - .mulWadDown(s.liquidationWithdrawRatio) - .safeCastTo88(); + s.withdrawReserve = (totalAssets() - expected).mulWadDown( + s.liquidationWithdrawRatio + ); } else { s.withdrawReserve = 0; } @@ -375,7 +373,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { s.withdrawReserve = 0; } else { unchecked { - s.withdrawReserve -= withdrawBalance.safeCastTo88(); + s.withdrawReserve -= withdrawBalance; } } @@ -400,7 +398,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { s.epochData[s.currentEpoch - 1].withdrawProxy ); unchecked { - s.withdrawReserve -= drainBalance.safeCastTo88(); + s.withdrawReserve -= drainBalance; } WithdrawProxy(currentWithdrawProxy).increaseWithdrawReserveReceived( drainBalance @@ -460,7 +458,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { function _accrue(VaultData storage s) internal returns (uint256) { unchecked { - s.yIntercept = (_totalAssets(s)).safeCastTo88(); + s.yIntercept = (_totalAssets(s)); s.last = block.timestamp.safeCastTo40(); } emit YInterceptChanged(s.yIntercept); @@ -574,7 +572,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { VaultData storage s = _loadStorageSlot(); unchecked { - s.yIntercept += assets.safeCastTo88(); + s.yIntercept += assets; } VIData storage v = _loadVISlot(); if (v.depositCap != 0 && totalAssets() >= v.depositCap) { @@ -596,7 +594,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { if (VAULT_FEE() != uint256(0)) { uint256 x = (amount > interestOwing) ? interestOwing : amount; uint256 fee = x.mulDivDown(VAULT_FEE(), 10000); - uint88 feeInShares = convertToShares(fee).safeCastTo88(); + uint256 feeInShares = convertToShares(fee); s.strategistUnclaimedShares += feeInShares; emit StrategistFee(feeInShares); } @@ -614,7 +612,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { unchecked { uint48 newSlope = s.slope - params.lienSlope.safeCastTo48(); _setSlope(s, newSlope); - s.yIntercept += params.increaseYIntercept.safeCastTo88(); + s.yIntercept += params.increaseYIntercept; s.last = block.timestamp.safeCastTo40(); } @@ -685,7 +683,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { } function _setYIntercept(VaultData storage s, uint256 newYIntercept) internal { - s.yIntercept = newYIntercept.safeCastTo88(); + s.yIntercept = newYIntercept; emit YInterceptChanged(s.yIntercept); } diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 86a9e010..12905727 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -76,7 +76,7 @@ abstract contract VaultImplementation is */ function modifyDepositCap(uint256 newCap) external { require(msg.sender == owner()); //owner is "strategist" - _loadVISlot().depositCap = newCap.safeCastTo88(); + _loadVISlot().depositCap = newCap; } function _loadVISlot() internal pure returns (VIData storage s) { @@ -194,7 +194,7 @@ abstract contract VaultImplementation is if (params.delegate != address(0)) { s.delegate = params.delegate; } - s.depositCap = params.depositCap.safeCastTo88(); + s.depositCap = params.depositCap; if (params.allowListEnabled) { s.allowListEnabled = true; uint256 i; diff --git a/src/WithdrawProxy.sol b/src/WithdrawProxy.sol index 61b32aa9..b1ca3289 100644 --- a/src/WithdrawProxy.sol +++ b/src/WithdrawProxy.sol @@ -50,8 +50,8 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { uint256(keccak256("xyz.astaria.WithdrawProxy.storage.location")) - 1; struct WPStorage { - uint88 withdrawRatio; - uint88 expected; // The sum of the remaining debt (amountOwed) accrued against the NFT at the timestamp when it is liquidated. yIntercept (virtual assets) of a PublicVault are not modified on liquidation, only once an auction is completed. + uint256 withdrawRatio; + uint256 expected; // The sum of the remaining debt (amountOwed) accrued against the NFT at the timestamp when it is liquidated. yIntercept (virtual assets) of a PublicVault are not modified on liquidation, only once an auction is completed. uint40 finalAuctionEnd; // when this is deleted, we know the final auction is over uint256 withdrawReserveReceived; // amount received from PublicVault. The WETH balance of this contract - withdrawReserveReceived = amount received from liquidations. } @@ -292,7 +292,7 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { } function setWithdrawRatio(uint256 liquidationWithdrawRatio) public onlyVault { - _loadSlot().withdrawRatio = liquidationWithdrawRatio.safeCastTo88(); + _loadSlot().withdrawRatio = liquidationWithdrawRatio; } function handleNewLiquidation( @@ -302,7 +302,7 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { WPStorage storage s = _loadSlot(); unchecked { - s.expected += newLienExpectedValue.safeCastTo88(); + s.expected += newLienExpectedValue; uint40 auctionEnd = (block.timestamp + finalAuctionDelta).safeCastTo40(); if (auctionEnd > s.finalAuctionEnd) s.finalAuctionEnd = auctionEnd; } diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index c4ea03e4..3cb01c7c 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -70,7 +70,7 @@ interface IAstariaRouter is IPausable, IBeacon { ITransferProxy TRANSFER_PROXY; //20 address feeTo; //20 address BEACON_PROXY_IMPLEMENTATION; //20 - uint88 maxInterestRate; //6 + uint256 maxInterestRate; //6 uint32 minInterestBPS; // was uint64 //slot 3 + address guardian; //20 diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index f009e878..e3d98dab 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -67,7 +67,7 @@ interface ILienToken is IERC721 { } struct Point { - uint88 amount; //11 + uint256 amount; //11 uint40 last; //5 uint40 end; //5 uint256 lienId; //32 @@ -136,7 +136,7 @@ interface ILienToken is IERC721 { * @param stack The Lien stack * @return the amount owed in uint192 at the current block.timestamp */ - function getOwed(Stack calldata stack) external view returns (uint88); + function getOwed(Stack calldata stack) external view returns (uint256); /** * @notice Removes all liens for a given CollateralToken. @@ -147,7 +147,7 @@ interface ILienToken is IERC721 { function getOwed( Stack calldata stack, uint256 timestamp - ) external view returns (uint88); + ) external view returns (uint256); /** * @notice Public view function that computes the interest for a LienToken since its last payment. @@ -219,13 +219,13 @@ interface ILienToken is IERC721 { struct AuctionStack { uint256 lienId; - uint88 amountOwed; + uint256 amountOwed; uint40 end; } struct AuctionData { - uint88 startAmount; - uint88 endAmount; + uint256 startAmount; + uint256 endAmount; uint48 startTime; uint48 endTime; address liquidator; diff --git a/src/interfaces/IPublicVault.sol b/src/interfaces/IPublicVault.sol index b653eb3b..deb727d9 100644 --- a/src/interfaces/IPublicVault.sol +++ b/src/interfaces/IPublicVault.sol @@ -23,13 +23,13 @@ interface IPublicVault is IVaultImplementation { } struct VaultData { - uint88 yIntercept; + uint256 yIntercept; uint48 slope; uint40 last; uint64 currentEpoch; - uint88 withdrawReserve; - uint88 liquidationWithdrawRatio; - uint88 strategistUnclaimedShares; + uint256 withdrawReserve; + uint256 liquidationWithdrawRatio; + uint256 strategistUnclaimedShares; mapping(uint64 => EpochData) epochData; } @@ -165,9 +165,9 @@ interface IPublicVault is IVaultImplementation { DEPOSIT_CAP_EXCEEDED } - event StrategistFee(uint88 feeInShares); + event StrategistFee(uint256 feeInShares); event LiensOpenForEpochRemaining(uint64 epoch, uint256 liensOpenForEpoch); - event YInterceptChanged(uint88 newYintercept); + event YInterceptChanged(uint256 newYintercept); event WithdrawReserveTransferred(uint256 amount); event LienOpen(uint256 lienId, uint256 epoch); } diff --git a/src/interfaces/IVaultImplementation.sol b/src/interfaces/IVaultImplementation.sol index 09ea327e..2bdbb134 100644 --- a/src/interfaces/IVaultImplementation.sol +++ b/src/interfaces/IVaultImplementation.sol @@ -41,7 +41,7 @@ interface IVaultImplementation is IAstariaVaultBase, IERC165 { } struct VIData { - uint88 depositCap; + uint256 depositCap; address delegate; bool allowListEnabled; bool isShutdown; From f7278e85e12d893b00e0f0a895072d8222aa4839 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Thu, 9 Feb 2023 18:34:24 -0400 Subject: [PATCH 18/39] C4:564 - ClearingHouse updates, ensure token address for payment is stored in the clearing house state - refactor AuctionData definition into CH - update getters in LienToken to point to the underlying CH --- src/ClearingHouse.sol | 73 +++++++++----- src/LienToken.sol | 185 +++++++++++++++++++--------------- src/interfaces/ILienToken.sol | 99 +++++++++--------- src/test/RevertTesting.t.sol | 51 ++++++++++ src/test/TestHelpers.t.sol | 59 +++++++---- 5 files changed, 288 insertions(+), 179 deletions(-) diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index b6e1a792..44b93566 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -32,10 +32,29 @@ import {ERC721} from "solmate/tokens/ERC721.sol"; contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { using Bytes32AddressLib for bytes32; using SafeTransferLib for ERC20; + struct AuctionStack { + uint256 lienId; + uint88 amountOwed; + uint40 end; + } + + struct AuctionData { + uint88 startAmount; + uint88 endAmount; + uint48 startTime; + uint48 endTime; + address liquidator; + address token; + AuctionStack[] stack; + } struct ClearingHouseStorage { - ILienToken.AuctionData auctionStack; + AuctionData auctionStack; } + enum InvalidRequestReason { + NOT_ENOUGH_FUNDS_RECEIVED + } + error InvalidRequest(InvalidRequestReason); uint256 private constant CLEARING_HOUSE_STORAGE_SLOT = uint256(keccak256("xyz.astaria.ClearingHouse.storage.location")) - 1; @@ -63,9 +82,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { } } - function setAuctionData( - ILienToken.AuctionData calldata auctionData - ) external { + function setAuctionData(AuctionData calldata auctionData) external { IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); // get the router from the immutable arg //only execute from the conduit @@ -75,21 +92,27 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { s.auctionStack = auctionData; } + function getAuctionData() external view returns (AuctionData memory) { + return _getStorage().auctionStack; + } + function supportsInterface(bytes4 interfaceId) external view returns (bool) { return interfaceId == type(IERC1155).interfaceId; } - function balanceOf( - address account, - uint256 id - ) external view returns (uint256) { + function balanceOf(address account, uint256 id) + external + view + returns (uint256) + { return type(uint256).max; } - function balanceOfBatch( - address[] calldata accounts, - uint256[] calldata ids - ) external view returns (uint256[] memory output) { + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) + external + view + returns (uint256[] memory output) + { output = new uint256[](accounts.length); for (uint256 i; i < accounts.length; ) { output[i] = type(uint256).max; @@ -101,23 +124,19 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { function setApprovalForAll(address operator, bool approved) external {} - function isApprovedForAll( - address account, - address operator - ) external view returns (bool) { + function isApprovedForAll(address account, address operator) + external + view + returns (bool) + { return true; } - function _execute( - address tokenContract, // collateral token sending the fake nft - address to, // buyer - uint256 encodedMetaData, //retrieve token address from the encoded data - uint256 // space to encode whatever is needed, - ) internal { + function _execute() internal { IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); // get the router from the immutable arg ClearingHouseStorage storage s = _getStorage(); - address paymentToken = bytes32(encodedMetaData).fromLast20Bytes(); + address paymentToken = s.auctionStack.token; uint256 currentOfferPrice = _locateCurrentAmount({ startAmount: s.auctionStack.startAmount, @@ -128,12 +147,14 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { }); uint256 payment = ERC20(paymentToken).balanceOf(address(this)); - require(payment >= currentOfferPrice, "not enough funds received"); + if (currentOfferPrice > payment) { + revert InvalidRequest(InvalidRequestReason.NOT_ENOUGH_FUNDS_RECEIVED); + } uint256 collateralId = _getArgUint256(21); // pay liquidator fees here - ILienToken.AuctionStack[] storage stack = s.auctionStack.stack; + AuctionStack[] storage stack = s.auctionStack.stack; uint256 liquidatorPayment = ASTARIA_ROUTER.getLiquidatorFee(payment); @@ -171,7 +192,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { bytes calldata data //empty from seaport ) public { //data is empty and useless - _execute(from, to, identifier, amount); + _execute(); } function safeBatchTransferFrom( diff --git a/src/LienToken.sol b/src/LienToken.sol index 28559f60..dc85b4ad 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -36,6 +36,7 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {AuthInitializable} from "core/AuthInitializable.sol"; import {Initializable} from "./utils/Initializable.sol"; +import {ClearingHouse} from "core/ClearingHouse.sol"; /** * @title LienToken @@ -56,10 +57,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { _disableInitializers(); } - function initialize( - Authority _AUTHORITY, - ITransferProxy _TRANSFER_PROXY - ) public initializer { + function initialize(Authority _AUTHORITY, ITransferProxy _TRANSFER_PROXY) + public + initializer + { __initAuth(msg.sender, address(_AUTHORITY)); __initERC721("Astaria Lien Token", "ALT"); LienStorage storage s = _loadLienStorageSlot(); @@ -93,17 +94,18 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { emit FileUpdated(what, data); } - function supportsInterface( - bytes4 interfaceId - ) public view override(ERC721, IERC165) returns (bool) { + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC721, IERC165) + returns (bool) + { return interfaceId == type(ILienToken).interfaceId || super.supportsInterface(interfaceId); } - function buyoutLien( - ILienToken.LienActionBuyout calldata params - ) + function buyoutLien(ILienToken.LienActionBuyout calldata params) external validateStack(params.encumber.lien.collateralId, params.encumber.stack) returns (Stack[] memory, Stack memory newStack) @@ -251,10 +253,11 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack The Lien for the loan to calculate interest for. * @param timestamp The timestamp at which to compute interest for. */ - function _getInterest( - Stack memory stack, - uint256 timestamp - ) internal pure returns (uint256) { + function _getInterest(Stack memory stack, uint256 timestamp) + internal + pure + returns (uint256) + { uint256 delta_t = timestamp - stack.point.last; return (delta_t * stack.lien.details.rate).mulWadDown(stack.point.amount); @@ -294,13 +297,13 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { Stack[] calldata stack, address liquidator ) internal { - AuctionData memory auctionData; + ClearingHouse.AuctionData memory auctionData; auctionData.liquidator = liquidator; - auctionData.stack = new AuctionStack[](stack.length); - s.auctionData[collateralId].liquidator = liquidator; + auctionData.token = stack[0].lien.token; + auctionData.stack = new ClearingHouse.AuctionStack[](stack.length); uint256 i; for (; i < stack.length; ) { - AuctionStack memory auctionStack; + ClearingHouse.AuctionStack memory auctionStack; auctionStack.lienId = stack[i].point.lienId; auctionStack.end = stack[i].point.end; @@ -343,9 +346,12 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { ); } - function tokenURI( - uint256 tokenId - ) public view override(ERC721, IERC721) returns (string memory) { + function tokenURI(uint256 tokenId) + public + view + override(ERC721, IERC721) + returns (string memory) + { if (!_exists(tokenId)) { revert InvalidTokenId(tokenId); } @@ -381,13 +387,15 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _loadERC721Slot()._ownerOf[tokenId] != address(0); } - function createLien( - ILienToken.LienActionEncumber memory params - ) + function createLien(ILienToken.LienActionEncumber memory params) external requiresAuth validateStack(params.lien.collateralId, params.stack) - returns (uint256 lienId, Stack[] memory newStack, uint256 lienSlope) + returns ( + uint256 lienId, + Stack[] memory newStack, + uint256 lienSlope + ) { LienStorage storage s = _loadLienStorageSlot(); //0 - 4 are valid @@ -491,7 +499,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { address token, uint256 collateralId, uint256 payment, - AuctionStack[] memory auctionStack + ClearingHouse.AuctionStack[] memory auctionStack ) external { LienStorage storage s = _loadLienStorageSlot(); require( @@ -507,7 +515,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { address token, uint256 payment, address payer, - AuctionStack[] memory stack + ClearingHouse.AuctionStack[] memory stack ) internal returns (uint256 totalSpent) { uint256 i; for (; i < stack.length; ) { @@ -521,27 +529,35 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getAuctionData( - uint256 collateralId - ) external view returns (AuctionData memory) { - return _loadLienStorageSlot().auctionData[collateralId]; + function getAuctionData(uint256 collateralId) + public + view + returns (ClearingHouse.AuctionData memory) + { + return + ClearingHouse( + _loadLienStorageSlot().COLLATERAL_TOKEN.getClearingHouse(collateralId) + ).getAuctionData(); } - function getAuctionLiquidator( - uint256 collateralId - ) external view returns (address liquidator) { - liquidator = _loadLienStorageSlot().auctionData[collateralId].liquidator; + function getAuctionLiquidator(uint256 collateralId) + external + view + returns (address liquidator) + { + liquidator = getAuctionData(collateralId).liquidator; if (liquidator == address(0)) { revert InvalidState(InvalidStates.COLLATERAL_NOT_LIQUIDATED); } } - function getAmountOwingAtLiquidation( - ILienToken.Stack calldata stack - ) public view returns (uint256) { + function getAmountOwingAtLiquidation(ILienToken.Stack calldata stack) + public + view + returns (uint256) + { return - _loadLienStorageSlot() - .auctionData[stack.lien.collateralId] + getAuctionData(stack.lien.collateralId) .stack[stack.point.lienId] .amountOwed; } @@ -553,22 +569,27 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getCollateralState( - uint256 collateralId - ) external view returns (bytes32) { + function getCollateralState(uint256 collateralId) + external + view + returns (bytes32) + { return _loadLienStorageSlot().collateralStateHash[collateralId]; } - function getBuyout( - Stack calldata stack - ) public view returns (uint256 owed, uint256 buyout) { + function getBuyout(Stack calldata stack) + public + view + returns (uint256 owed, uint256 buyout) + { return _getBuyout(_loadLienStorageSlot(), stack); } - function _getBuyout( - LienStorage storage s, - Stack calldata stack - ) internal view returns (uint256 owed, uint256 buyout) { + function _getBuyout(LienStorage storage s, Stack calldata stack) + internal + view + returns (uint256 owed, uint256 buyout) + { owed = _getOwed(stack, block.timestamp); buyout = owed + @@ -605,7 +626,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { function _paymentAH( LienStorage storage s, address token, - AuctionStack[] memory stack, + ClearingHouse.AuctionStack[] memory stack, uint256 position, uint256 payment, address payer @@ -685,9 +706,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return stack.lien.details.rate.mulWadDown(stack.point.amount); } - function getMaxPotentialDebtForCollateral( - Stack[] memory stack - ) + function getMaxPotentialDebtForCollateral(Stack[] memory stack) public view validateStack(stack[0].lien.collateralId, stack) @@ -708,10 +727,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getMaxPotentialDebtForCollateral( - Stack[] memory stack, - uint256 end - ) + function getMaxPotentialDebtForCollateral(Stack[] memory stack, uint256 end) public view validateStack(stack[0].lien.collateralId, stack) @@ -731,10 +747,11 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _getOwed(stack, block.timestamp); } - function getOwed( - Stack memory stack, - uint256 timestamp - ) external view returns (uint88) { + function getOwed(Stack memory stack, uint256 timestamp) + external + view + returns (uint88) + { validateLien(stack.lien); return _getOwed(stack, timestamp); } @@ -744,10 +761,11 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack The specified Lien. * @return The amount owed to the Lien at the specified timestamp. */ - function _getOwed( - Stack memory stack, - uint256 timestamp - ) internal pure returns (uint88) { + function _getOwed(Stack memory stack, uint256 timestamp) + internal + pure + returns (uint88) + { return stack.point.amount + _getInterest(stack, timestamp).safeCastTo88(); } @@ -757,10 +775,11 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack the lien * @return The WETH still owed in interest to the Lien. */ - function _getRemainingInterest( - LienStorage storage s, - Stack memory stack - ) internal view returns (uint256) { + function _getRemainingInterest(LienStorage storage s, Stack memory stack) + internal + view + returns (uint256) + { uint256 delta_t = stack.point.end - block.timestamp; return (delta_t * stack.lien.details.rate).mulWadDown(stack.point.amount); } @@ -836,10 +855,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return (activeStack, amount); } - function _removeStackPosition( - Stack[] memory stack, - uint8 position - ) internal returns (Stack[] memory newStack) { + function _removeStackPosition(Stack[] memory stack, uint8 position) + internal + returns (Stack[] memory newStack) + { uint256 length = stack.length; require(position < length); newStack = new ILienToken.Stack[](length - 1); @@ -864,10 +883,11 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { ); } - function _isPublicVault( - LienStorage storage s, - address account - ) internal view returns (bool) { + function _isPublicVault(LienStorage storage s, address account) + internal + view + returns (bool) + { return s.ASTARIA_ROUTER.isValidVault(account) && IPublicVault(account).supportsInterface(type(IPublicVault).interfaceId); @@ -880,10 +900,11 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _getPayee(_loadLienStorageSlot(), lienId); } - function _getPayee( - LienStorage storage s, - uint256 lienId - ) internal view returns (address) { + function _getPayee(LienStorage storage s, uint256 lienId) + internal + view + returns (address) + { return s.lienMeta[lienId].payee != address(0) ? s.lienMeta[lienId].payee diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index f009e878..0a28fef0 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -18,6 +18,7 @@ import {IERC721} from "core/interfaces/IERC721.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; +import {ClearingHouse} from "core/ClearingHouse.sol"; interface ILienToken is IERC721 { enum FileType { @@ -40,7 +41,6 @@ interface ILienToken is IERC721 { IAstariaRouter ASTARIA_ROUTER; ICollateralToken COLLATERAL_TOKEN; mapping(uint256 => bytes32) collateralStateHash; - mapping(uint256 => AuctionData) auctionData; mapping(uint256 => LienMeta) lienMeta; } @@ -95,9 +95,10 @@ interface ILienToken is IERC721 { * @param lien The Lien. * @return lienId The lienId of the requested Lien, if valid (otherwise, reverts). */ - function validateLien( - Lien calldata lien - ) external view returns (uint256 lienId); + function validateLien(Lien calldata lien) + external + view + returns (uint256 lienId); function ASTARIA_ROUTER() external view returns (IAstariaRouter); @@ -108,9 +109,10 @@ interface ILienToken is IERC721 { * @param stack The Lien to compute the slope for. * @return slope The rate for the specified lien, in WETH per second. */ - function calculateSlope( - Stack calldata stack - ) external pure returns (uint256 slope); + function calculateSlope(Stack calldata stack) + external + pure + returns (uint256 slope); /** * @notice Stops accruing interest for all liens against a single CollateralToken. @@ -127,9 +129,10 @@ interface ILienToken is IERC721 { * @notice Computes and returns the buyout amount for a Lien. * @param stack the lien */ - function getBuyout( - Stack calldata stack - ) external view returns (uint256 owed, uint256 buyout); + function getBuyout(Stack calldata stack) + external + view + returns (uint256 owed, uint256 buyout); /** * @notice Removes all liens for a given CollateralToken. @@ -144,10 +147,10 @@ interface ILienToken is IERC721 { * @param timestamp the timestamp you want to inquire about * @return the amount owed in uint192 */ - function getOwed( - Stack calldata stack, - uint256 timestamp - ) external view returns (uint88); + function getOwed(Stack calldata stack, uint256 timestamp) + external + view + returns (uint88); /** * @notice Public view function that computes the interest for a LienToken since its last payment. @@ -159,33 +162,39 @@ interface ILienToken is IERC721 { * @notice Retrieves a lienCount for specific collateral * @param collateralId the Lien to compute a point for */ - function getCollateralState( - uint256 collateralId - ) external view returns (bytes32); + function getCollateralState(uint256 collateralId) + external + view + returns (bytes32); /** * @notice Retrieves a specific point by its lienId. * @param stack the Lien to compute a point for */ - function getAmountOwingAtLiquidation( - ILienToken.Stack calldata stack - ) external view returns (uint256); + function getAmountOwingAtLiquidation(ILienToken.Stack calldata stack) + external + view + returns (uint256); /** * @notice Creates a new lien against a CollateralToken. * @param params LienActionEncumber data containing CollateralToken information and lien parameters (rate, duration, and amount, rate, and debt caps). */ - function createLien( - LienActionEncumber memory params - ) external returns (uint256 lienId, Stack[] memory stack, uint256 slope); + function createLien(LienActionEncumber memory params) + external + returns ( + uint256 lienId, + Stack[] memory stack, + uint256 slope + ); /** * @notice Purchase a LienToken for its buyout price. * @param params The LienActionBuyout data specifying the lien position, receiver address, and underlying CollateralToken information of the lien. */ - function buyoutLien( - LienActionBuyout memory params - ) external returns (Stack[] memory, Stack memory); + function buyoutLien(LienActionBuyout memory params) + external + returns (Stack[] memory, Stack memory); /** * @notice Called by the ClearingHouse (through Seaport) to pay back debt with auction funds. @@ -196,7 +205,7 @@ interface ILienToken is IERC721 { address token, uint256 collateralId, uint256 payment, - AuctionStack[] memory auctionStack + ClearingHouse.AuctionStack[] memory auctionStack ) external; /** @@ -217,44 +226,32 @@ interface ILienToken is IERC721 { uint256 amount ) external returns (Stack[] memory newStack); - struct AuctionStack { - uint256 lienId; - uint88 amountOwed; - uint40 end; - } - - struct AuctionData { - uint88 startAmount; - uint88 endAmount; - uint48 startTime; - uint48 endTime; - address liquidator; - AuctionStack[] stack; - } - /** * @notice Retrieves the AuctionData for a CollateralToken (The liquidator address and the AuctionStack). * @param collateralId The ID of the CollateralToken. */ - function getAuctionData( - uint256 collateralId - ) external view returns (AuctionData memory); + function getAuctionData(uint256 collateralId) + external + view + returns (ClearingHouse.AuctionData memory); /** * @notice Retrieves the liquidator for a CollateralToken. * @param collateralId The ID of the CollateralToken. */ - function getAuctionLiquidator( - uint256 collateralId - ) external view returns (address liquidator); + function getAuctionLiquidator(uint256 collateralId) + external + view + returns (address liquidator); /** * Calculates the debt accrued by all liens against a CollateralToken, assuming no payments are made until the end timestamp in the stack. * @param stack The stack data for active liens against the CollateralToken. */ - function getMaxPotentialDebtForCollateral( - ILienToken.Stack[] memory stack - ) external view returns (uint256); + function getMaxPotentialDebtForCollateral(ILienToken.Stack[] memory stack) + external + view + returns (uint256); /** * Calculates the debt accrued by all liens against a CollateralToken, assuming no payments are made until the provided timestamp. diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..f0db10f8 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -61,6 +61,57 @@ contract RevertTesting is TestHelpers { MAX_LIENS } + function testCannotEndAuctionWithWrongToken() public { + address alice = address(1); + address bob = address(2); + TestNFT nft = new TestNFT(6); + uint256 tokenId = uint256(5); + address tokenContract = address(nft); + address publicVault = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 14 days + }); + + _lendToVault(Lender({addr: bob, amountToLend: 150 ether}), publicVault); + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: blueChipDetails, + amount: 100 ether, + isFirstLien: true + }); + + uint256 collateralId = tokenContract.computeId(tokenId); + vm.warp(block.timestamp + 11 days); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); + + erc20s[0].mint(address(this), 1000 ether); + ClearingHouse clearingHouse = ClearingHouse( + COLLATERAL_TOKEN.getClearingHouse(collateralId) + ); + erc20s[0].transfer(address(clearingHouse), 1000 ether); + vm.expectRevert( + abi.encodeWithSelector( + ClearingHouse.InvalidRequest.selector, + ClearingHouse.InvalidRequestReason.NOT_ENOUGH_FUNDS_RECEIVED + ) + ); + clearingHouse.safeTransferFrom( + address(this), + address(this), + uint256(uint160(address(erc20s[0]))), + 1000 ether, + "0x" + ); + } + function testCannotRandomAccountIncrementNonce() public { address privateVault = _createPublicVault({ strategist: strategistOne, diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 9c170b6f..b093e013 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -120,6 +120,17 @@ contract ConsiderationTester is BaseOrderTest { vm.label(address(consideration), "consideration"); vm.label(address(conduit), "conduit"); vm.label(address(this), "testContract"); + + _deployTestTokenContracts(); + erc20s = [token1, token2, token3]; + erc721s = [test721_1, test721_2, test721_3]; + erc1155s = [test1155_1, test1155_2, test1155_3]; + + // allocate funds and tokens to test addresses + allocateTokensAndApprovals(address(this), uint128(MAX_INT)); + allocateTokensAndApprovals(alice, uint128(MAX_INT)); + allocateTokensAndApprovals(bob, uint128(MAX_INT)); + allocateTokensAndApprovals(cal, uint128(MAX_INT)); } } @@ -323,9 +334,10 @@ contract TestHelpers is Deploy, ConsiderationTester { (rate * amount * duration).mulDivDown(1, 365 days).mulDivDown(1, 1e18); } - function setupLiquidation( - address borrower - ) public returns (address publicVault, ILienToken.Stack[] memory stack) { + function setupLiquidation(address borrower) + public + returns (address publicVault, ILienToken.Stack[] memory stack) + { TestNFT nft = new TestNFT(0); _mintNoDepositApproveRouterSpecific(borrower, address(nft), 99); address tokenContract = address(nft); @@ -411,10 +423,9 @@ contract TestHelpers is Deploy, ConsiderationTester { ); } - function _mintNoDepositApproveRouter( - address tokenContract, - uint256 tokenId - ) internal { + function _mintNoDepositApproveRouter(address tokenContract, uint256 tokenId) + internal + { TestNFT(tokenContract).mint(address(this), tokenId); TestNFT(tokenContract).approve(address(ASTARIA_ROUTER), tokenId); } @@ -448,10 +459,10 @@ contract TestHelpers is Deploy, ConsiderationTester { ); } - function _createPrivateVault( - address strategist, - address delegate - ) internal returns (address privateVault) { + function _createPrivateVault(address strategist, address delegate) + internal + returns (address privateVault) + { vm.startPrank(strategist); privateVault = ASTARIA_ROUTER.newVault(delegate, address(WETH9)); vm.stopPrank(); @@ -875,9 +886,14 @@ contract TestHelpers is Deploy, ConsiderationTester { uint256 amount; } - function _toVRS( - bytes memory signature - ) internal returns (uint8 v, bytes32 r, bytes32 s) { + function _toVRS(bytes memory signature) + internal + returns ( + uint8 v, + bytes32 r, + bytes32 s + ) + { emit log_bytes(signature); emit log_named_uint("signature length", signature.length); if (signature.length == 65) { @@ -894,9 +910,10 @@ contract TestHelpers is Deploy, ConsiderationTester { } } - function _generateTerms( - GenTerms memory params - ) internal returns (IAstariaRouter.Commitment memory terms) { + function _generateTerms(GenTerms memory params) + internal + returns (IAstariaRouter.Commitment memory terms) + { (uint8 v, bytes32 r, bytes32 s) = _toVRS(params.signature); return @@ -1243,9 +1260,11 @@ contract TestHelpers is Deploy, ConsiderationTester { return _mirrorOrderParameters; } - function _toOfferItems( - ConsiderationItem[] memory _considerationItems - ) internal pure returns (OfferItem[] memory) { + function _toOfferItems(ConsiderationItem[] memory _considerationItems) + internal + pure + returns (OfferItem[] memory) + { OfferItem[] memory _offerItems = new OfferItem[]( _considerationItems.length ); From 8e002474ad841a93e5581bbf4e38f1cb41ed228f Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Mon, 6 Feb 2023 18:17:51 -0400 Subject: [PATCH 19/39] C4:564 - remove tokenAddress encoding in the _generateValidOrderParameters method https://github.com/code-423n4/2023-01-astaria-findings/issues/564 AST-287 --- src/CollateralToken.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index 9d270b33..8763ffa2 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -441,7 +441,7 @@ contract CollateralToken is considerationItems[1] = ConsiderationItem( ItemType.ERC1155, s.clearingHouse[collateralId], - uint256(uint160(settlementToken)), + collateralId, prices[0], prices[1], payable(s.clearingHouse[collateralId]) From 557a9cc5d27090b63cddbb8d08b9c084276b11d6 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Mon, 6 Feb 2023 18:49:28 -0400 Subject: [PATCH 20/39] C4/287 - add a check to ensure that the price of an asset isn't set to 0 when trying to settle the auction(indicating none is running) https://github.com/code-423n4/2023-01-astaria-findings/issues/287, AST-299 --- src/ClearingHouse.sol | 31 +++++++++++----------- src/test/RevertTesting.t.sol | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index 44b93566..a7ab2c9a 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -52,7 +52,8 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { AuctionData auctionStack; } enum InvalidRequestReason { - NOT_ENOUGH_FUNDS_RECEIVED + NOT_ENOUGH_FUNDS_RECEIVED, + NO_AUCTION } error InvalidRequest(InvalidRequestReason); @@ -136,7 +137,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); // get the router from the immutable arg ClearingHouseStorage storage s = _getStorage(); - address paymentToken = s.auctionStack.token; + ERC20 paymentToken = ERC20(s.auctionStack.token); uint256 currentOfferPrice = _locateCurrentAmount({ startAmount: s.auctionStack.startAmount, @@ -145,8 +146,11 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { endTime: s.auctionStack.endTime, roundUp: true //we are a consideration we round up }); - uint256 payment = ERC20(paymentToken).balanceOf(address(this)); + if (currentOfferPrice == 0) { + revert InvalidRequest(InvalidRequestReason.NO_AUCTION); + } + uint256 payment = paymentToken.balanceOf(address(this)); if (currentOfferPrice > payment) { revert InvalidRequest(InvalidRequestReason.NOT_ENOUGH_FUNDS_RECEIVED); } @@ -158,27 +162,22 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { uint256 liquidatorPayment = ASTARIA_ROUTER.getLiquidatorFee(payment); - ERC20(paymentToken).safeTransfer( - s.auctionStack.liquidator, - liquidatorPayment - ); + payment -= liquidatorPayment; + paymentToken.safeTransfer(s.auctionStack.liquidator, liquidatorPayment); - ERC20(paymentToken).safeApprove( - address(ASTARIA_ROUTER.TRANSFER_PROXY()), - payment - liquidatorPayment - ); + paymentToken.safeApprove(address(ASTARIA_ROUTER.TRANSFER_PROXY()), payment); ASTARIA_ROUTER.LIEN_TOKEN().payDebtViaClearingHouse( - paymentToken, + address(paymentToken), collateralId, - payment - liquidatorPayment, + payment, s.auctionStack.stack ); - if (ERC20(paymentToken).balanceOf(address(this)) > 0) { - ERC20(paymentToken).safeTransfer( + if (payment > 0) { + paymentToken.safeTransfer( ASTARIA_ROUTER.COLLATERAL_TOKEN().ownerOf(collateralId), - ERC20(paymentToken).balanceOf(address(this)) + payment ); } ASTARIA_ROUTER.COLLATERAL_TOKEN().settleAuction(collateralId); diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index f0db10f8..4a4f8b6d 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -112,6 +112,56 @@ contract RevertTesting is TestHelpers { ); } + function testCannotSettleAuctionIfNoneRunning() public { + address alice = address(1); + address bob = address(2); + TestNFT nft = new TestNFT(6); + uint256 tokenId = uint256(5); + address tokenContract = address(nft); + address publicVault = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 14 days + }); + + _lendToVault(Lender({addr: bob, amountToLend: 150 ether}), publicVault); + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: blueChipDetails, + amount: 100 ether, + isFirstLien: true + }); + + uint256 collateralId = tokenContract.computeId(tokenId); + vm.warp(block.timestamp + 11 days); + // OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + // stack, + // uint8(0) + // ); + + ClearingHouse clearingHouse = ClearingHouse( + COLLATERAL_TOKEN.getClearingHouse(collateralId) + ); + deal(address(WETH9), address(clearingHouse), 1000 ether); + vm.expectRevert( + abi.encodeWithSelector( + ClearingHouse.InvalidRequest.selector, + ClearingHouse.InvalidRequestReason.NO_AUCTION + ) + ); + clearingHouse.safeTransferFrom( + address(this), + address(this), + uint256(uint160(address(erc20s[0]))), + 1000 ether, + "0x" + ); + } + function testCannotRandomAccountIncrementNonce() public { address privateVault = _createPublicVault({ strategist: strategistOne, From 936ebfed10fc6c6c66fa46e365c59c1879fe078c Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Mon, 6 Feb 2023 18:55:09 -0400 Subject: [PATCH 21/39] C4:582 fix, https://github.com/code-423n4/2023-01-astaria-findings/issues/582 in conjunction with 287 fixes both succintly, AST-306 --- src/CollateralToken.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index 8763ffa2..d4929ac1 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -510,10 +510,10 @@ contract CollateralToken is function settleAuction(uint256 collateralId) public { CollateralStorage storage s = _loadCollateralSlot(); if ( - s.collateralIdToAuction[collateralId] == bytes32(0) && + s.collateralIdToAuction[collateralId] == bytes32(0) || ERC721(s.idToUnderlying[collateralId].tokenContract).ownerOf( s.idToUnderlying[collateralId].tokenId - ) != + ) == s.clearingHouse[collateralId] ) { revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); From e6409e7dfe779289280133089cb93912e35372e3 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 10:54:11 -0400 Subject: [PATCH 22/39] handle https://github.com/code-423n4/2023-01-astaria-findings/issues/379 as apart of clearing house refactor/cleanup --- src/ClearingHouse.sol | 44 +++++++++++++++++++++-------------------- src/CollateralToken.sol | 10 +++++++++- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index a7ab2c9a..0cded2fe 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -49,7 +49,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { } struct ClearingHouseStorage { - AuctionData auctionStack; + AuctionData auctionData; } enum InvalidRequestReason { NOT_ENOUGH_FUNDS_RECEIVED, @@ -90,11 +90,11 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { require(msg.sender == address(ASTARIA_ROUTER.LIEN_TOKEN())); ClearingHouseStorage storage s = _getStorage(); - s.auctionStack = auctionData; + s.auctionData = auctionData; } function getAuctionData() external view returns (AuctionData memory) { - return _getStorage().auctionStack; + return _getStorage().auctionData; } function supportsInterface(bytes4 interfaceId) external view returns (bool) { @@ -134,16 +134,16 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { } function _execute() internal { - IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); // get the router from the immutable arg + IAstariaRouter ASTARIA_ROUTER = ROUTER(); // get the router from the immutable arg ClearingHouseStorage storage s = _getStorage(); - ERC20 paymentToken = ERC20(s.auctionStack.token); + ERC20 paymentToken = ERC20(s.auctionData.token); uint256 currentOfferPrice = _locateCurrentAmount({ - startAmount: s.auctionStack.startAmount, - endAmount: s.auctionStack.endAmount, - startTime: s.auctionStack.startTime, - endTime: s.auctionStack.endTime, + startAmount: s.auctionData.startAmount, + endAmount: s.auctionData.endAmount, + startTime: s.auctionData.startTime, + endTime: s.auctionData.endTime, roundUp: true //we are a consideration we round up }); @@ -155,29 +155,30 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { revert InvalidRequest(InvalidRequestReason.NOT_ENOUGH_FUNDS_RECEIVED); } - uint256 collateralId = _getArgUint256(21); + uint256 collateralId = COLLATERAL_ID(); // pay liquidator fees here - AuctionStack[] storage stack = s.auctionStack.stack; + AuctionStack[] storage stack = s.auctionData.stack; uint256 liquidatorPayment = ASTARIA_ROUTER.getLiquidatorFee(payment); payment -= liquidatorPayment; - paymentToken.safeTransfer(s.auctionStack.liquidator, liquidatorPayment); + paymentToken.safeTransfer(s.auctionData.liquidator, liquidatorPayment); - paymentToken.safeApprove(address(ASTARIA_ROUTER.TRANSFER_PROXY()), payment); + paymentToken.approve(address(ASTARIA_ROUTER.TRANSFER_PROXY()), payment); ASTARIA_ROUTER.LIEN_TOKEN().payDebtViaClearingHouse( address(paymentToken), collateralId, payment, - s.auctionStack.stack + s.auctionData.stack ); - if (payment > 0) { + uint remainingBalance = paymentToken.balanceOf(address(this)); + if (remainingBalance > 0) { paymentToken.safeTransfer( ASTARIA_ROUTER.COLLATERAL_TOKEN().ownerOf(collateralId), - payment + remainingBalance ); } ASTARIA_ROUTER.COLLATERAL_TOKEN().settleAuction(collateralId); @@ -212,7 +213,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { } function validateOrder(Order memory order) external { - IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); + IAstariaRouter ASTARIA_ROUTER = ROUTER(); require(msg.sender == address(ASTARIA_ROUTER.COLLATERAL_TOKEN())); Order[] memory listings = new Order[](1); listings[0] = order; @@ -229,21 +230,22 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { uint256 tokenId, address target ) external { - IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); + IAstariaRouter ASTARIA_ROUTER = ROUTER(); require(msg.sender == address(ASTARIA_ROUTER.COLLATERAL_TOKEN())); ERC721(tokenContract).safeTransferFrom(address(this), target, tokenId); } function settleLiquidatorNFTClaim() external { - IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); + IAstariaRouter ASTARIA_ROUTER = ROUTER(); require(msg.sender == address(ASTARIA_ROUTER.COLLATERAL_TOKEN())); ClearingHouseStorage storage s = _getStorage(); + uint collateralId = COLLATERAL_ID(); ASTARIA_ROUTER.LIEN_TOKEN().payDebtViaClearingHouse( address(0), - COLLATERAL_ID(), + collateralId, 0, - s.auctionStack.stack + s.auctionData.stack ); } } diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index d4929ac1..dcbb155f 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -140,6 +140,11 @@ contract CollateralToken is ClearingHouse CH = ClearingHouse(payable(s.clearingHouse[collateralId])); CH.settleLiquidatorNFTClaim(); _releaseToAddress(s, underlying, collateralId, liquidator); + _settleAuction(s, collateralId); + delete s.idToUnderlying[collateralId]; + delete s.idToUnderlying[collateralId].tokenId; + delete s.idToUnderlying[collateralId].tokenContract; + _burn(collateralId); } function _loadCollateralSlot() @@ -338,6 +343,8 @@ contract CollateralToken is address tokenContract = underlying.tokenContract; _burn(collateralId); delete s.idToUnderlying[collateralId]; + delete s.idToUnderlying[collateralId].tokenId; + delete s.idToUnderlying[collateralId].tokenContract; _releaseToAddress(s, underlying, collateralId, releaseTo); } @@ -509,6 +516,8 @@ contract CollateralToken is function settleAuction(uint256 collateralId) public { CollateralStorage storage s = _loadCollateralSlot(); + require(msg.sender == s.clearingHouse[collateralId]); + if ( s.collateralIdToAuction[collateralId] == bytes32(0) || ERC721(s.idToUnderlying[collateralId].tokenContract).ownerOf( @@ -518,7 +527,6 @@ contract CollateralToken is ) { revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); } - require(msg.sender == s.clearingHouse[collateralId]); _settleAuction(s, collateralId); delete s.idToUnderlying[collateralId]; _burn(collateralId); From c336c9c9efc98da292abc83b05931a9cba7a5e76 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 10 Feb 2023 11:13:35 -0500 Subject: [PATCH 23/39] added testCannotSelfLiquidateBeforeExpiration --- lib/gpl | 2 +- src/test/RevertTesting.t.sol | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/gpl b/lib/gpl index 4b49fe99..8a3ba7bd 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit 4b49fe993d9b807fe68b3421ee7f2fe91267c9ef +Subproject commit 8a3ba7bd7042d3b8fe45b1b3d4230da7cc915b01 diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index ee7bab72..4cd79398 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -579,4 +579,45 @@ contract RevertTesting is TestHelpers { ) }); } + + function testCannotSelfLiquidateBeforeExpiration() 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: 14 days + }); + + // lend 50 ether to the PublicVault as address(1) + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + // borrow 10 eth against the dummy NFT + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + vm.expectRevert( + abi.encodeWithSelector( + IAstariaRouter.InvalidLienState.selector, + IAstariaRouter.LienState.HEALTHY + ) + ); + ASTARIA_ROUTER.liquidate(stack, uint8(0)); + } } From 04c6ea77ac6afd8983444f2352408da184a48cf4 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Wed, 8 Feb 2023 10:57:50 -0400 Subject: [PATCH 24/39] C4:246 - deploy vaults using a create2 flow and now check that a vault hasn't been corrupted before deploying it, exploit detailed here, https://github.com/code-423n4/2023-01-astaria-findings/issues/246, another issue around front running vaults here, https://github.com/code-423n4/2023-01-astaria-findings/issues/571 --- .gitmodules | 4 ++++ foundry.toml | 2 ++ lib/create2-clones-with-immutable-args | 1 + remappings.txt | 2 ++ src/AstariaRouter.sol | 13 ++++++++----- 5 files changed, 17 insertions(+), 5 deletions(-) create mode 160000 lib/create2-clones-with-immutable-args diff --git a/.gitmodules b/.gitmodules index d2fe7df7..9e27f0fc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,7 @@ [submodule "lib/seaport"] path = lib/seaport url = https://github.com/projectopensea/seaport +[submodule "lib/create2-clones-with-immutable-args"] + path = lib/create2-clones-with-immutable-args + url = https://github.com/emo-eth/create2-clones-with-immutable-args + branch = v0.1.1 diff --git a/foundry.toml b/foundry.toml index d4a22e24..e014635f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,6 +10,8 @@ remappings = [ 'solmate/=lib/solmate/src/', 'gpl/=lib/gpl/src/', 'clones-with-immutable-args/=lib/clones-with-immutable-args/src/', + 'create2-clones-with-immutable-args/=lib/create2-clones-with-immutable-args/src/', + 'create2-helpers/=lib/create2-clones-with-immutable-args/lib/create2-helpers/src/', 'core/=./src/', 'seaport/=lib/seaport/contracts', ] diff --git a/lib/create2-clones-with-immutable-args b/lib/create2-clones-with-immutable-args new file mode 160000 index 00000000..2cc5e9f9 --- /dev/null +++ b/lib/create2-clones-with-immutable-args @@ -0,0 +1 @@ +Subproject commit 2cc5e9f978451b4964d570b8b6678aa172a95bfb diff --git a/remappings.txt b/remappings.txt index 0e03451e..228a25e4 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,6 @@ clones-with-immutable-args/=lib/clones-with-immutable-args/src/ +create2-clones-with-immutable-args/=lib/create2-clones-with-immutable-args/src/ +create2-helpers/=lib/create2-clones-with-immutable-args/lib/create2-helpers/src/ forge-std/=lib/forge-std/src/ eip4626/=lib/foundry_eip-4626/src/ gpl/=lib/gpl/src/ diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index cfa76f1a..bd18a84f 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -20,10 +20,9 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {ERC721} from "solmate/tokens/ERC721.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; - import { - ClonesWithImmutableArgs -} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; + Create2ClonesWithImmutableArgs +} from "create2-clones-with-immutable-args/Create2ClonesWithImmutableArgs.sol"; import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; @@ -721,7 +720,7 @@ contract AstariaRouter is } //immutable data - vaultAddr = ClonesWithImmutableArgs.clone( + vaultAddr = Create2ClonesWithImmutableArgs.clone( s.BEACON_PROXY_IMPLEMENTATION, abi.encodePacked( address(this), @@ -731,9 +730,13 @@ contract AstariaRouter is block.timestamp, epochLength, vaultFee - ) + ), + keccak256(abi.encode(msg.sender, blockhash(block.number - 1))) ); + if (s.LIEN_TOKEN.balanceOf(vaultAddr) > 0) { + revert InvalidVaultState(IAstariaRouter.VaultState.CORRUPTED); + } //mutable data IVaultImplementation(vaultAddr).init( IVaultImplementation.InitParams({ From 53aa3d480146b1e3bde317ea7118e6415b3907ba Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Wed, 8 Feb 2023 11:00:07 -0400 Subject: [PATCH 25/39] add missing enum --- src/interfaces/IAstariaRouter.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index e694fd21..7594033d 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -333,6 +333,7 @@ interface IAstariaRouter is IPausable, IBeacon { enum VaultState { UNINITIALIZED, + CORRUPTED, CLOSED, LIQUIDATED } From cec2600657acd1974de56247570c2a077cae39e9 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Wed, 8 Feb 2023 11:08:06 -0400 Subject: [PATCH 26/39] add test changes --- src/test/AstariaTest.t.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index 444a1e68..039a6e8a 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -219,23 +219,28 @@ contract AstariaTest is TestHelpers { delegate: strategistTwo, epochLength: 14 days }); + vm.roll(block.number + 1); address publicVault2 = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); + vm.roll(block.number + 1); + address publicVault3 = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); + vm.roll(block.number + 1); address publicVault4 = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); + vm.roll(block.number + 1); address publicVault5 = _createPublicVault({ strategist: strategistOne, From a004692739c39c8be431dd98f2c523675658873e Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Sat, 11 Feb 2023 10:31:07 -0400 Subject: [PATCH 27/39] - added tests - fixed and renamed invalidSignatureTest --- lib/gpl | 2 +- lib/seaport | 2 +- src/test/RevertTesting.t.sol | 93 ++++++++++++++++++++++++++++++++---- 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/lib/gpl b/lib/gpl index 8a3ba7bd..4b49fe99 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit 8a3ba7bd7042d3b8fe45b1b3d4230da7cc915b01 +Subproject commit 4b49fe993d9b807fe68b3421ee7f2fe91267c9ef diff --git a/lib/seaport b/lib/seaport index b05547e4..dfce06d0 160000 --- a/lib/seaport +++ b/lib/seaport @@ -1 +1 @@ -Subproject commit b05547e48183781ae3c7a1b4eda9eeac1a485a04 +Subproject commit dfce06d02413636f324f73352b54a4497d63c310 diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index 5423f8f6..e96aeefd 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -40,6 +40,9 @@ import {LienToken} from "../LienToken.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; +import { + Create2ClonesWithImmutableArgs +} from "create2-clones-with-immutable-args/Create2ClonesWithImmutableArgs.sol"; import {Strings2} from "./utils/Strings2.sol"; @@ -96,6 +99,82 @@ contract RevertTesting is TestHelpers { ); } + function testCannotCorruptVaults() public { + TestNFT nft = new TestNFT(3); + address tokenContract = address(nft); + uint256 tokenId = uint256(1); + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + _lendToPrivateVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + + IAstariaRouter.Commitment memory terms = _generateValidTerms({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + stack: new ILienToken.Stack[](0) + }); + + ERC721(tokenContract).safeTransferFrom( + address(this), + address(COLLATERAL_TOKEN), + tokenId, + "" + ); + + COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); + + uint256 balanceOfBefore = ERC20(WETH9).balanceOf(address(this)); + (uint256 lienId, , ) = VaultImplementation(privateVault).commitToLien( + terms, + address(this) + ); + + address underlying = address(WETH9); + uint256 epochLength = 10 days; + uint256 vaultFee = 0; + emit log_named_uint("block number", block.number); + address attackTarget = Create2ClonesWithImmutableArgs.deriveAddress( + address(ASTARIA_ROUTER), + ASTARIA_ROUTER.BEACON_PROXY_IMPLEMENTATION(), + abi.encodePacked( + address(ASTARIA_ROUTER), + uint8(IAstariaRouter.ImplementationType.PublicVault), + strategistTwo, + underlying, + block.timestamp, + epochLength, + vaultFee + ), + keccak256(abi.encode(strategistTwo, blockhash(block.number - 1))) + ); + + vm.startPrank(strategistOne); + LIEN_TOKEN.transferFrom(strategistOne, attackTarget, lienId); + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector( + IAstariaRouter.InvalidVaultState.selector, + IAstariaRouter.VaultState.CORRUPTED + ) + ); + address publicVault = _createPublicVault({ + strategist: strategistTwo, + delegate: strategistTwo, + epochLength: epochLength + }); + } + function testInvalidFileData() public { vm.expectRevert( abi.encodeWithSelector(IAstariaRouter.InvalidFileData.selector) @@ -108,7 +187,7 @@ contract RevertTesting is TestHelpers { ); } - function testFailInvalidSignature() public { + function testCannotCommitWithInvalidSignature() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); uint256 tokenId = uint256(1); @@ -117,7 +196,7 @@ contract RevertTesting is TestHelpers { delegate: strategistTwo }); - _lendToVault( + _lendToPrivateVault( Lender({addr: strategistOne, amountToLend: 50 ether}), privateVault ); @@ -142,19 +221,13 @@ contract RevertTesting is TestHelpers { COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); - uint256 balanceOfBefore = ERC20(privateVault).balanceOf(address(this)); vm.expectRevert( abi.encodeWithSelector( IVaultImplementation.InvalidRequest.selector, - IVaultImplementation.InvalidRequestReason.NO_AUTHORITY + IVaultImplementation.InvalidRequestReason.INVALID_SIGNATURE ) ); - VaultImplementation(privateVault).commitToLien(terms); - assertEq( - balanceOfBefore, - ERC20(privateVault).balanceOf(address(this)), - "balance changed" - ); + VaultImplementation(privateVault).commitToLien(terms, address(this)); } // Only strategists for PrivateVaults can supply capital From 498a94800abdaac4a6a664f274a51ff3b40add1e Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Sat, 11 Feb 2023 10:33:52 -0400 Subject: [PATCH 28/39] fixes after merging develop --- src/interfaces/IPublicVault.sol | 2 +- src/test/RevertTesting.t.sol | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/interfaces/IPublicVault.sol b/src/interfaces/IPublicVault.sol index b77fb6af..56612291 100644 --- a/src/interfaces/IPublicVault.sol +++ b/src/interfaces/IPublicVault.sol @@ -24,7 +24,7 @@ interface IPublicVault is IVaultImplementation { struct VaultData { uint256 yIntercept; - uint48 slope; + uint256 slope; uint40 last; uint64 currentEpoch; uint256 withdrawReserve; diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index e96aeefd..ca98993d 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -134,10 +134,7 @@ contract RevertTesting is TestHelpers { COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); uint256 balanceOfBefore = ERC20(WETH9).balanceOf(address(this)); - (uint256 lienId, , ) = VaultImplementation(privateVault).commitToLien( - terms, - address(this) - ); + (uint256 lienId, ) = VaultImplementation(privateVault).commitToLien(terms); address underlying = address(WETH9); uint256 epochLength = 10 days; @@ -227,7 +224,7 @@ contract RevertTesting is TestHelpers { IVaultImplementation.InvalidRequestReason.INVALID_SIGNATURE ) ); - VaultImplementation(privateVault).commitToLien(terms, address(this)); + VaultImplementation(privateVault).commitToLien(terms); } // Only strategists for PrivateVaults can supply capital From 08cbb922e1a9aa318c7b06e55cc93e2dc4459c26 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Tue, 7 Feb 2023 11:19:14 -0400 Subject: [PATCH 29/39] C4:409 - validate the strategy vault address matches the vault address processing the commitment, https://github.com/code-423n4/2023-01-astaria-findings/issues/409 --- src/interfaces/IVaultImplementation.sol | 7 +-- src/test/RevertTesting.t.sol | 62 ++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/interfaces/IVaultImplementation.sol b/src/interfaces/IVaultImplementation.sol index 1b4b0d0f..50ce2d0a 100644 --- a/src/interfaces/IVaultImplementation.sol +++ b/src/interfaces/IVaultImplementation.sol @@ -22,6 +22,7 @@ interface IVaultImplementation is IAstariaVaultBase, IERC165 { enum InvalidRequestReason { NO_AUTHORITY, OPERATOR_NO_CODE, + INVALID_VAULT, INVALID_SIGNATURE, INVALID_COMMITMENT, INVALID_AMOUNT, @@ -68,9 +69,9 @@ interface IVaultImplementation is IAstariaVaultBase, IERC165 { function incrementNonce() external; - function commitToLien( - IAstariaRouter.Commitment calldata params - ) external returns (uint256 lienId, ILienToken.Stack[] memory stack); + function commitToLien(IAstariaRouter.Commitment calldata params) + external + returns (uint256 lienId, ILienToken.Stack[] memory stack); function buyoutLien( ILienToken.Stack[] calldata stack, diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index 456f2d97..1560ff46 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -44,6 +44,7 @@ import { Create2ClonesWithImmutableArgs } from "create2-clones-with-immutable-args/Create2ClonesWithImmutableArgs.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; @@ -196,7 +197,66 @@ contract RevertTesting is TestHelpers { privateVault ); } - + + function testInvalidVaultRequest() public { + TestNFT nft = new TestNFT(2); + address tokenContract = address(nft); + uint256 initialBalance = WETH9.balanceOf(address(this)); + + // Create a private vault with WETH asset + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: address(0), + token: address(WETH9) + }); + + _lendToPrivateVault( + Lender({addr: strategistOne, amountToLend: 500 ether}), + privateVault + ); + + // Send the NFT to Collateral contract and receive Collateral token + ERC721(tokenContract).safeTransferFrom( + address(this), + address(COLLATERAL_TOKEN), + 1, + "" + ); + + // generate valid terms + uint256 amount = 50 ether; // amount to borrow + IAstariaRouter.Commitment memory c = _generateValidTerms({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: 1, + lienDetails: standardLienDetails, + amount: amount, + stack: new ILienToken.Stack[](0) + }); + + // Attack starts here + // The borrower an asset which has no value in the market + MockERC20 FakeToken = new MockERC20("USDC", "FakeAsset", 18); // this could be any ERC token created by the attacker + FakeToken.mint(address(this), 500 ether); + // The borrower creates a private vault with his/her asset + address privateVaultOfBorrower = _createPrivateVault({ + strategist: address(this), + delegate: address(0), + token: address(FakeToken) + }); + + c.lienRequest.strategy.vault = privateVaultOfBorrower; + vm.expectRevert( + abi.encodeWithSelector( + IVaultImplementation.InvalidRequest.selector, + IVaultImplementation.InvalidRequestReason.INVALID_VAULT + ) + ); + IVaultImplementation(privateVault).commitToLien(c); + } + function testCannotCommitWithInvalidSignature() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); From 1011dde41f3ac7d7240e62ad57df632ec4c6a1b6 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 9 Feb 2023 16:51:51 -0500 Subject: [PATCH 30/39] bring back original _createPrivateVault() --- lib/gpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gpl b/lib/gpl index 608fc2c7..4b49fe99 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit 608fc2c7f41ecd7f3ec4a5d2ed4e2011b68e141f +Subproject commit 4b49fe993d9b807fe68b3421ee7f2fe91267c9ef From b5a0448232881dd190375c026101fac88e12b88c Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Sat, 11 Feb 2023 18:09:36 -0400 Subject: [PATCH 31/39] re add dropped check after merge --- src/VaultImplementation.sol | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 0ad0d125..414affdd 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -225,9 +225,14 @@ abstract contract VaultImplementation is * * @param params The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. */ - function _validateRequest( - IAstariaRouter.Commitment calldata params - ) internal view returns (address) { + function _validateRequest(IAstariaRouter.Commitment calldata params) + internal + view + returns (address) + { + if (params.lienRequest.strategy.vault != address(this)) { + revert InvalidRequest(InvalidRequestReason.INVALID_VAULT); + } uint256 collateralId = params.tokenContract.computeId(params.tokenId); ERC721 CT = ERC721(address(COLLATERAL_TOKEN())); address holder = CT.ownerOf(collateralId); @@ -279,9 +284,10 @@ abstract contract VaultImplementation is uint256 slope ) internal virtual {} - function _beforeCommitToLien( - IAstariaRouter.Commitment calldata - ) internal virtual {} + function _beforeCommitToLien(IAstariaRouter.Commitment calldata) + internal + virtual + {} /** * @notice Pipeline for lifecycle of new loan origination. @@ -290,9 +296,7 @@ abstract contract VaultImplementation is * @param params Commitment data for the incoming lien request * @return lienId The id of the newly minted lien token. */ - function commitToLien( - IAstariaRouter.Commitment calldata params - ) + function commitToLien(IAstariaRouter.Commitment calldata params) external whenNotPaused returns (uint256 lienId, ILienToken.Stack[] memory stack) @@ -377,11 +381,13 @@ abstract contract VaultImplementation is * @dev Generates a Lien for a valid loan commitment proof and sends the loan amount to the borrower. * @param c The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. */ - function _requestLienAndIssuePayout( - IAstariaRouter.Commitment calldata c - ) + function _requestLienAndIssuePayout(IAstariaRouter.Commitment calldata c) internal - returns (uint256 newLienId, ILienToken.Stack[] memory stack, uint256 slope) + returns ( + uint256 newLienId, + ILienToken.Stack[] memory stack, + uint256 slope + ) { address receiver = _validateRequest(c); (newLienId, stack, slope) = ROUTER().requestLienPosition(c, recipient()); From 5ee0de4daa4354923441387d73841625efa48fd6 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Sun, 12 Feb 2023 12:12:48 -0400 Subject: [PATCH 32/39] fix test after validateRequest refacactor, update lib gpl --- lib/gpl | 2 +- src/AstariaRouter.sol | 55 +++++++++++++++++-------------- src/VaultImplementation.sol | 33 +++++++++++-------- src/interfaces/IAstariaRouter.sol | 29 ++++++++-------- 4 files changed, 67 insertions(+), 52 deletions(-) diff --git a/lib/gpl b/lib/gpl index 608fc2c7..4b49fe99 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit 608fc2c7f41ecd7f3ec4a5d2ed4e2011b68e141f +Subproject commit 4b49fe993d9b807fe68b3421ee7f2fe91267c9ef diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index bd18a84f..e11b11e6 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -403,10 +403,11 @@ contract AstariaRouter is return s.auctionWindow + (includeBuffer ? s.auctionWindowBuffer : 0); } - function _sliceUint( - bytes memory bs, - uint256 start - ) internal pure returns (uint256 x) { + function _sliceUint(bytes memory bs, uint256 start) + internal + pure + returns (uint256 x) + { uint256 length = bs.length; assembly { @@ -434,9 +435,6 @@ contract AstariaRouter is IAstariaRouter.Commitment calldata commitment, uint256 timeToSecondEpochEnd ) internal view returns (ILienToken.Lien memory lien) { - if (block.timestamp > commitment.lienRequest.strategy.deadline) { - revert InvalidCommitmentState(CommitmentState.EXPIRED); - } uint8 nlrType = uint8(_sliceUint(commitment.lienRequest.nlrDetails, 0)); address strategyValidator = s.strategyValidators[nlrType]; if (strategyValidator == address(0)) { @@ -485,9 +483,7 @@ contract AstariaRouter is }); } - function commitToLiens( - IAstariaRouter.Commitment[] memory commitments - ) + function commitToLiens(IAstariaRouter.Commitment[] memory commitments) public whenNotPaused returns (uint256[] memory lienIds, ILienToken.Stack[] memory stack) @@ -518,10 +514,11 @@ contract AstariaRouter is .safeTransfer(msg.sender, totalBorrowed); } - function newVault( - address delegate, - address underlying - ) external whenNotPaused returns (address) { + function newVault(address delegate, address underlying) + external + whenNotPaused + returns (address) + { address[] memory allowList = new address[](1); allowList[0] = msg.sender; RouterStorage storage s = _loadRouterSlot(); @@ -579,7 +576,11 @@ contract AstariaRouter is external whenNotPaused validVault(msg.sender) - returns (uint256, ILienToken.Stack[] memory, uint256) + returns ( + uint256, + ILienToken.Stack[] memory, + uint256 + ) { RouterStorage storage s = _loadRouterSlot(); @@ -602,17 +603,19 @@ contract AstariaRouter is ); } - function canLiquidate( - ILienToken.Stack memory stack - ) public view returns (bool) { + function canLiquidate(ILienToken.Stack memory stack) + public + view + returns (bool) + { RouterStorage storage s = _loadRouterSlot(); return (stack.point.end <= block.timestamp); } - function liquidate( - ILienToken.Stack[] memory stack, - uint8 position - ) public returns (OrderParameters memory listedOrder) { + function liquidate(ILienToken.Stack[] memory stack, uint8 position) + public + returns (OrderParameters memory listedOrder) + { if (!canLiquidate(stack[position])) { revert InvalidLienState(LienState.HEALTHY); } @@ -655,9 +658,11 @@ contract AstariaRouter is ); } - function getBuyoutFee( - uint256 remainingInterestIn - ) external view returns (uint256) { + function getBuyoutFee(uint256 remainingInterestIn) + external + view + returns (uint256) + { RouterStorage storage s = _loadRouterSlot(); return remainingInterestIn.mulDivDown( diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 0ad0d125..fad71e4a 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -225,9 +225,11 @@ abstract contract VaultImplementation is * * @param params The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. */ - function _validateRequest( - IAstariaRouter.Commitment calldata params - ) internal view returns (address) { + function _validateRequest(IAstariaRouter.Commitment calldata params) + internal + view + returns (address) + { uint256 collateralId = params.tokenContract.computeId(params.tokenId); ERC721 CT = ERC721(address(COLLATERAL_TOKEN())); address holder = CT.ownerOf(collateralId); @@ -240,6 +242,10 @@ abstract contract VaultImplementation is revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } + if (block.timestamp > params.lienRequest.strategy.deadline) { + revert InvalidRequest(InvalidRequestReason.EXPIRED); + } + VIData storage s = _loadVISlot(); address recovered = ecrecover( keccak256( @@ -279,9 +285,10 @@ abstract contract VaultImplementation is uint256 slope ) internal virtual {} - function _beforeCommitToLien( - IAstariaRouter.Commitment calldata - ) internal virtual {} + function _beforeCommitToLien(IAstariaRouter.Commitment calldata) + internal + virtual + {} /** * @notice Pipeline for lifecycle of new loan origination. @@ -290,9 +297,7 @@ abstract contract VaultImplementation is * @param params Commitment data for the incoming lien request * @return lienId The id of the newly minted lien token. */ - function commitToLien( - IAstariaRouter.Commitment calldata params - ) + function commitToLien(IAstariaRouter.Commitment calldata params) external whenNotPaused returns (uint256 lienId, ILienToken.Stack[] memory stack) @@ -377,11 +382,13 @@ abstract contract VaultImplementation is * @dev Generates a Lien for a valid loan commitment proof and sends the loan amount to the borrower. * @param c The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. */ - function _requestLienAndIssuePayout( - IAstariaRouter.Commitment calldata c - ) + function _requestLienAndIssuePayout(IAstariaRouter.Commitment calldata c) internal - returns (uint256 newLienId, ILienToken.Stack[] memory stack, uint256 slope) + returns ( + uint256 newLienId, + ILienToken.Stack[] memory stack, + uint256 slope + ) { address receiver = _validateRequest(c); (newLienId, stack, slope) = ROUTER().requestLienPosition(c, recipient()); diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index 7594033d..57a65ec0 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -162,10 +162,9 @@ interface IAstariaRouter is IPausable, IBeacon { * @param underlying The address of the underlying token. * @return The address of the new PrivateVault. */ - function newVault( - address delegate, - address underlying - ) external returns (address); + function newVault(address delegate, address underlying) + external + returns (address); /** * @notice Retrieves the address that collects protocol-level fees. @@ -177,9 +176,9 @@ interface IAstariaRouter is IPausable, IBeacon { * @param commitments The commitment proofs and requested loan data for each loan. * @return lienIds the lienIds for each loan. */ - function commitToLiens( - Commitment[] memory commitments - ) external returns (uint256[] memory, ILienToken.Stack[] memory); + function commitToLiens(Commitment[] memory commitments) + external + returns (uint256[] memory, ILienToken.Stack[] memory); /** * @notice Create a new lien against a CollateralToken. @@ -189,7 +188,13 @@ interface IAstariaRouter is IPausable, IBeacon { function requestLienPosition( IAstariaRouter.Commitment calldata params, address recipient - ) external returns (uint256, ILienToken.Stack[] memory, uint256); + ) + external + returns ( + uint256, + ILienToken.Stack[] memory, + uint256 + ); function LIEN_TOKEN() external view returns (ILienToken); @@ -226,10 +231,9 @@ interface IAstariaRouter is IPausable, IBeacon { * @param position The position of the defaulted lien. * @return reserve The amount owed on all liens for against the collateral being liquidated, including accrued interest. */ - function liquidate( - ILienToken.Stack[] calldata stack, - uint8 position - ) external returns (OrderParameters memory); + function liquidate(ILienToken.Stack[] calldata stack, uint8 position) + external + returns (OrderParameters memory); /** * @notice Returns whether a specified lien can be liquidated. @@ -326,7 +330,6 @@ interface IAstariaRouter is IPausable, IBeacon { INVALID, INVALID_RATE, INVALID_AMOUNT, - EXPIRED, COLLATERAL_AUCTION, COLLATERAL_NO_DEPOSIT } From bf826ceffaab6fa4204bade6d95bb0b6efdc108d Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Sun, 12 Feb 2023 12:18:26 -0400 Subject: [PATCH 33/39] update from uint88 to uint256 --- src/ClearingHouse.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index 0cded2fe..b60f88c9 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -34,13 +34,13 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { using SafeTransferLib for ERC20; struct AuctionStack { uint256 lienId; - uint88 amountOwed; + uint256 amountOwed; uint40 end; } struct AuctionData { - uint88 startAmount; - uint88 endAmount; + uint256 startAmount; + uint256 endAmount; uint48 startTime; uint48 endTime; address liquidator; @@ -174,7 +174,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { s.auctionData.stack ); - uint remainingBalance = paymentToken.balanceOf(address(this)); + uint256 remainingBalance = paymentToken.balanceOf(address(this)); if (remainingBalance > 0) { paymentToken.safeTransfer( ASTARIA_ROUTER.COLLATERAL_TOKEN().ownerOf(collateralId), @@ -240,7 +240,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { require(msg.sender == address(ASTARIA_ROUTER.COLLATERAL_TOKEN())); ClearingHouseStorage storage s = _getStorage(); - uint collateralId = COLLATERAL_ID(); + uint256 collateralId = COLLATERAL_ID(); ASTARIA_ROUTER.LIEN_TOKEN().payDebtViaClearingHouse( address(0), collateralId, From d1b4ae574f271910e58ae65b1e3355d83150479c Mon Sep 17 00:00:00 2001 From: Joseph Delong Date: Mon, 20 Feb 2023 12:32:22 -0600 Subject: [PATCH 34/39] formatting update --- src/AstariaRouter.sol | 52 ++++----- src/ClearingHouse.sol | 27 ++--- src/LienToken.sol | 142 +++++++++++------------- src/PublicVault.sol | 5 +- src/VaultImplementation.sol | 31 +++--- src/interfaces/IAstariaRouter.sol | 28 ++--- src/interfaces/ILienToken.sol | 72 +++++------- src/interfaces/IVaultImplementation.sol | 6 +- src/test/RevertTesting.t.sol | 2 +- src/test/TestHelpers.t.sol | 40 +++---- 10 files changed, 176 insertions(+), 229 deletions(-) diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index e11b11e6..72ef8b2b 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -403,11 +403,10 @@ contract AstariaRouter is return s.auctionWindow + (includeBuffer ? s.auctionWindowBuffer : 0); } - function _sliceUint(bytes memory bs, uint256 start) - internal - pure - returns (uint256 x) - { + function _sliceUint( + bytes memory bs, + uint256 start + ) internal pure returns (uint256 x) { uint256 length = bs.length; assembly { @@ -483,7 +482,9 @@ contract AstariaRouter is }); } - function commitToLiens(IAstariaRouter.Commitment[] memory commitments) + function commitToLiens( + IAstariaRouter.Commitment[] memory commitments + ) public whenNotPaused returns (uint256[] memory lienIds, ILienToken.Stack[] memory stack) @@ -514,11 +515,10 @@ contract AstariaRouter is .safeTransfer(msg.sender, totalBorrowed); } - function newVault(address delegate, address underlying) - external - whenNotPaused - returns (address) - { + function newVault( + address delegate, + address underlying + ) external whenNotPaused returns (address) { address[] memory allowList = new address[](1); allowList[0] = msg.sender; RouterStorage storage s = _loadRouterSlot(); @@ -576,11 +576,7 @@ contract AstariaRouter is external whenNotPaused validVault(msg.sender) - returns ( - uint256, - ILienToken.Stack[] memory, - uint256 - ) + returns (uint256, ILienToken.Stack[] memory, uint256) { RouterStorage storage s = _loadRouterSlot(); @@ -603,19 +599,17 @@ contract AstariaRouter is ); } - function canLiquidate(ILienToken.Stack memory stack) - public - view - returns (bool) - { + function canLiquidate( + ILienToken.Stack memory stack + ) public view returns (bool) { RouterStorage storage s = _loadRouterSlot(); return (stack.point.end <= block.timestamp); } - function liquidate(ILienToken.Stack[] memory stack, uint8 position) - public - returns (OrderParameters memory listedOrder) - { + function liquidate( + ILienToken.Stack[] memory stack, + uint8 position + ) public returns (OrderParameters memory listedOrder) { if (!canLiquidate(stack[position])) { revert InvalidLienState(LienState.HEALTHY); } @@ -658,11 +652,9 @@ contract AstariaRouter is ); } - function getBuyoutFee(uint256 remainingInterestIn) - external - view - returns (uint256) - { + function getBuyoutFee( + uint256 remainingInterestIn + ) external view returns (uint256) { RouterStorage storage s = _loadRouterSlot(); return remainingInterestIn.mulDivDown( diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index b60f88c9..907a1469 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -101,19 +101,17 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { return interfaceId == type(IERC1155).interfaceId; } - function balanceOf(address account, uint256 id) - external - view - returns (uint256) - { + function balanceOf( + address account, + uint256 id + ) external view returns (uint256) { return type(uint256).max; } - function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) - external - view - returns (uint256[] memory output) - { + function balanceOfBatch( + address[] calldata accounts, + uint256[] calldata ids + ) external view returns (uint256[] memory output) { output = new uint256[](accounts.length); for (uint256 i; i < accounts.length; ) { output[i] = type(uint256).max; @@ -125,11 +123,10 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { function setApprovalForAll(address operator, bool approved) external {} - function isApprovedForAll(address account, address operator) - external - view - returns (bool) - { + function isApprovedForAll( + address account, + address operator + ) external view returns (bool) { return true; } diff --git a/src/LienToken.sol b/src/LienToken.sol index fc498471..480d13b8 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -57,10 +57,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { _disableInitializers(); } - function initialize(Authority _AUTHORITY, ITransferProxy _TRANSFER_PROXY) - public - initializer - { + function initialize( + Authority _AUTHORITY, + ITransferProxy _TRANSFER_PROXY + ) public initializer { __initAuth(msg.sender, address(_AUTHORITY)); __initERC721("Astaria Lien Token", "ALT"); LienStorage storage s = _loadLienStorageSlot(); @@ -94,18 +94,17 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { emit FileUpdated(what, data); } - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC721, IERC165) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721, IERC165) returns (bool) { return interfaceId == type(ILienToken).interfaceId || super.supportsInterface(interfaceId); } - function buyoutLien(ILienToken.LienActionBuyout calldata params) + function buyoutLien( + ILienToken.LienActionBuyout calldata params + ) external validateStack(params.encumber.lien.collateralId, params.encumber.stack) returns (Stack[] memory, Stack memory newStack) @@ -253,11 +252,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack The Lien for the loan to calculate interest for. * @param timestamp The timestamp at which to compute interest for. */ - function _getInterest(Stack memory stack, uint256 timestamp) - internal - pure - returns (uint256) - { + function _getInterest( + Stack memory stack, + uint256 timestamp + ) internal pure returns (uint256) { uint256 delta_t = timestamp - stack.point.last; return (delta_t * stack.lien.details.rate).mulWadDown(stack.point.amount); @@ -342,12 +340,9 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { ); } - function tokenURI(uint256 tokenId) - public - view - override(ERC721, IERC721) - returns (string memory) - { + function tokenURI( + uint256 tokenId + ) public view override(ERC721, IERC721) returns (string memory) { if (!_exists(tokenId)) { revert InvalidTokenId(tokenId); } @@ -383,15 +378,13 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _loadERC721Slot()._ownerOf[tokenId] != address(0); } - function createLien(ILienToken.LienActionEncumber memory params) + function createLien( + ILienToken.LienActionEncumber memory params + ) external requiresAuth validateStack(params.lien.collateralId, params.stack) - returns ( - uint256 lienId, - Stack[] memory newStack, - uint256 lienSlope - ) + returns (uint256 lienId, Stack[] memory newStack, uint256 lienSlope) { LienStorage storage s = _loadLienStorageSlot(); //0 - 4 are valid @@ -525,33 +518,27 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getAuctionData(uint256 collateralId) - public - view - returns (ClearingHouse.AuctionData memory) - { + function getAuctionData( + uint256 collateralId + ) public view returns (ClearingHouse.AuctionData memory) { return ClearingHouse( _loadLienStorageSlot().COLLATERAL_TOKEN.getClearingHouse(collateralId) ).getAuctionData(); } - function getAuctionLiquidator(uint256 collateralId) - external - view - returns (address liquidator) - { + function getAuctionLiquidator( + uint256 collateralId + ) external view returns (address liquidator) { liquidator = getAuctionData(collateralId).liquidator; if (liquidator == address(0)) { revert InvalidState(InvalidStates.COLLATERAL_NOT_LIQUIDATED); } } - function getAmountOwingAtLiquidation(ILienToken.Stack calldata stack) - public - view - returns (uint256) - { + function getAmountOwingAtLiquidation( + ILienToken.Stack calldata stack + ) public view returns (uint256) { return getAuctionData(stack.lien.collateralId) .stack[stack.point.lienId] @@ -565,27 +552,22 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getCollateralState(uint256 collateralId) - external - view - returns (bytes32) - { + function getCollateralState( + uint256 collateralId + ) external view returns (bytes32) { return _loadLienStorageSlot().collateralStateHash[collateralId]; } - function getBuyout(Stack calldata stack) - public - view - returns (uint256 owed, uint256 buyout) - { + function getBuyout( + Stack calldata stack + ) public view returns (uint256 owed, uint256 buyout) { return _getBuyout(_loadLienStorageSlot(), stack); } - function _getBuyout(LienStorage storage s, Stack calldata stack) - internal - view - returns (uint256 owed, uint256 buyout) - { + function _getBuyout( + LienStorage storage s, + Stack calldata stack + ) internal view returns (uint256 owed, uint256 buyout) { owed = _getOwed(stack, block.timestamp); buyout = owed + @@ -702,7 +684,9 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return stack.lien.details.rate.mulWadDown(stack.point.amount); } - function getMaxPotentialDebtForCollateral(Stack[] memory stack) + function getMaxPotentialDebtForCollateral( + Stack[] memory stack + ) public view validateStack(stack[0].lien.collateralId, stack) @@ -723,7 +707,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } } - function getMaxPotentialDebtForCollateral(Stack[] memory stack, uint256 end) + function getMaxPotentialDebtForCollateral( + Stack[] memory stack, + uint256 end + ) public view validateStack(stack[0].lien.collateralId, stack) @@ -769,11 +756,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { * @param stack the lien * @return The WETH still owed in interest to the Lien. */ - function _getRemainingInterest(LienStorage storage s, Stack memory stack) - internal - view - returns (uint256) - { + function _getRemainingInterest( + LienStorage storage s, + Stack memory stack + ) internal view returns (uint256) { uint256 delta_t = stack.point.end - block.timestamp; return (delta_t * stack.lien.details.rate).mulWadDown(stack.point.amount); } @@ -849,10 +835,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return (activeStack, amount); } - function _removeStackPosition(Stack[] memory stack, uint8 position) - internal - returns (Stack[] memory newStack) - { + function _removeStackPosition( + Stack[] memory stack, + uint8 position + ) internal returns (Stack[] memory newStack) { uint256 length = stack.length; require(position < length); newStack = new ILienToken.Stack[](length - 1); @@ -877,11 +863,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { ); } - function _isPublicVault(LienStorage storage s, address account) - internal - view - returns (bool) - { + function _isPublicVault( + LienStorage storage s, + address account + ) internal view returns (bool) { return s.ASTARIA_ROUTER.isValidVault(account) && IPublicVault(account).supportsInterface(type(IPublicVault).interfaceId); @@ -894,11 +879,10 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _getPayee(_loadLienStorageSlot(), lienId); } - function _getPayee(LienStorage storage s, uint256 lienId) - internal - view - returns (address) - { + function _getPayee( + LienStorage storage s, + uint256 lienId + ) internal view returns (address) { return s.lienMeta[lienId].payee != address(0) ? s.lienMeta[lienId].payee diff --git a/src/PublicVault.sol b/src/PublicVault.sol index 24c38774..c3e99fa3 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -316,8 +316,9 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { uint256 expected = currentWithdrawProxy.getExpected(); if (totalAssets() > expected) { - s.withdrawReserve = (totalAssets() - expected) - .mulWadDown(s.liquidationWithdrawRatio); + s.withdrawReserve = (totalAssets() - expected).mulWadDown( + s.liquidationWithdrawRatio + ); } else { s.withdrawReserve = 0; } diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 0b9814e0..4d1d249d 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -225,15 +225,13 @@ abstract contract VaultImplementation is * * @param params The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. */ - function _validateRequest(IAstariaRouter.Commitment calldata params) - internal - view - returns (address) - { + function _validateRequest( + IAstariaRouter.Commitment calldata params + ) internal view returns (address) { if (params.lienRequest.strategy.vault != address(this)) { revert InvalidRequest(InvalidRequestReason.INVALID_VAULT); } - + uint256 collateralId = params.tokenContract.computeId(params.tokenId); ERC721 CT = ERC721(address(COLLATERAL_TOKEN())); address holder = CT.ownerOf(collateralId); @@ -289,10 +287,9 @@ abstract contract VaultImplementation is uint256 slope ) internal virtual {} - function _beforeCommitToLien(IAstariaRouter.Commitment calldata) - internal - virtual - {} + function _beforeCommitToLien( + IAstariaRouter.Commitment calldata + ) internal virtual {} /** * @notice Pipeline for lifecycle of new loan origination. @@ -301,7 +298,9 @@ abstract contract VaultImplementation is * @param params Commitment data for the incoming lien request * @return lienId The id of the newly minted lien token. */ - function commitToLien(IAstariaRouter.Commitment calldata params) + function commitToLien( + IAstariaRouter.Commitment calldata params + ) external whenNotPaused returns (uint256 lienId, ILienToken.Stack[] memory stack) @@ -386,13 +385,11 @@ abstract contract VaultImplementation is * @dev Generates a Lien for a valid loan commitment proof and sends the loan amount to the borrower. * @param c The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. */ - function _requestLienAndIssuePayout(IAstariaRouter.Commitment calldata c) + function _requestLienAndIssuePayout( + IAstariaRouter.Commitment calldata c + ) internal - returns ( - uint256 newLienId, - ILienToken.Stack[] memory stack, - uint256 slope - ) + returns (uint256 newLienId, ILienToken.Stack[] memory stack, uint256 slope) { address receiver = _validateRequest(c); (newLienId, stack, slope) = ROUTER().requestLienPosition(c, recipient()); diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index 57a65ec0..2c43884b 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -162,9 +162,10 @@ interface IAstariaRouter is IPausable, IBeacon { * @param underlying The address of the underlying token. * @return The address of the new PrivateVault. */ - function newVault(address delegate, address underlying) - external - returns (address); + function newVault( + address delegate, + address underlying + ) external returns (address); /** * @notice Retrieves the address that collects protocol-level fees. @@ -176,9 +177,9 @@ interface IAstariaRouter is IPausable, IBeacon { * @param commitments The commitment proofs and requested loan data for each loan. * @return lienIds the lienIds for each loan. */ - function commitToLiens(Commitment[] memory commitments) - external - returns (uint256[] memory, ILienToken.Stack[] memory); + function commitToLiens( + Commitment[] memory commitments + ) external returns (uint256[] memory, ILienToken.Stack[] memory); /** * @notice Create a new lien against a CollateralToken. @@ -188,13 +189,7 @@ interface IAstariaRouter is IPausable, IBeacon { function requestLienPosition( IAstariaRouter.Commitment calldata params, address recipient - ) - external - returns ( - uint256, - ILienToken.Stack[] memory, - uint256 - ); + ) external returns (uint256, ILienToken.Stack[] memory, uint256); function LIEN_TOKEN() external view returns (ILienToken); @@ -231,9 +226,10 @@ interface IAstariaRouter is IPausable, IBeacon { * @param position The position of the defaulted lien. * @return reserve The amount owed on all liens for against the collateral being liquidated, including accrued interest. */ - function liquidate(ILienToken.Stack[] calldata stack, uint8 position) - external - returns (OrderParameters memory); + function liquidate( + ILienToken.Stack[] calldata stack, + uint8 position + ) external returns (OrderParameters memory); /** * @notice Returns whether a specified lien can be liquidated. diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index c66e1fe3..2555c079 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -95,10 +95,9 @@ interface ILienToken is IERC721 { * @param lien The Lien. * @return lienId The lienId of the requested Lien, if valid (otherwise, reverts). */ - function validateLien(Lien calldata lien) - external - view - returns (uint256 lienId); + function validateLien( + Lien calldata lien + ) external view returns (uint256 lienId); function ASTARIA_ROUTER() external view returns (IAstariaRouter); @@ -109,10 +108,9 @@ interface ILienToken is IERC721 { * @param stack The Lien to compute the slope for. * @return slope The rate for the specified lien, in WETH per second. */ - function calculateSlope(Stack calldata stack) - external - pure - returns (uint256 slope); + function calculateSlope( + Stack calldata stack + ) external pure returns (uint256 slope); /** * @notice Stops accruing interest for all liens against a single CollateralToken. @@ -129,10 +127,9 @@ interface ILienToken is IERC721 { * @notice Computes and returns the buyout amount for a Lien. * @param stack the lien */ - function getBuyout(Stack calldata stack) - external - view - returns (uint256 owed, uint256 buyout); + function getBuyout( + Stack calldata stack + ) external view returns (uint256 owed, uint256 buyout); /** * @notice Removes all liens for a given CollateralToken. @@ -162,39 +159,33 @@ interface ILienToken is IERC721 { * @notice Retrieves a lienCount for specific collateral * @param collateralId the Lien to compute a point for */ - function getCollateralState(uint256 collateralId) - external - view - returns (bytes32); + function getCollateralState( + uint256 collateralId + ) external view returns (bytes32); /** * @notice Retrieves a specific point by its lienId. * @param stack the Lien to compute a point for */ - function getAmountOwingAtLiquidation(ILienToken.Stack calldata stack) - external - view - returns (uint256); + function getAmountOwingAtLiquidation( + ILienToken.Stack calldata stack + ) external view returns (uint256); /** * @notice Creates a new lien against a CollateralToken. * @param params LienActionEncumber data containing CollateralToken information and lien parameters (rate, duration, and amount, rate, and debt caps). */ - function createLien(LienActionEncumber memory params) - external - returns ( - uint256 lienId, - Stack[] memory stack, - uint256 slope - ); + function createLien( + LienActionEncumber memory params + ) external returns (uint256 lienId, Stack[] memory stack, uint256 slope); /** * @notice Purchase a LienToken for its buyout price. * @param params The LienActionBuyout data specifying the lien position, receiver address, and underlying CollateralToken information of the lien. */ - function buyoutLien(LienActionBuyout memory params) - external - returns (Stack[] memory, Stack memory); + function buyoutLien( + LienActionBuyout memory params + ) external returns (Stack[] memory, Stack memory); /** * @notice Called by the ClearingHouse (through Seaport) to pay back debt with auction funds. @@ -230,28 +221,25 @@ interface ILienToken is IERC721 { * @notice Retrieves the AuctionData for a CollateralToken (The liquidator address and the AuctionStack). * @param collateralId The ID of the CollateralToken. */ - function getAuctionData(uint256 collateralId) - external - view - returns (ClearingHouse.AuctionData memory); + function getAuctionData( + uint256 collateralId + ) external view returns (ClearingHouse.AuctionData memory); /** * @notice Retrieves the liquidator for a CollateralToken. * @param collateralId The ID of the CollateralToken. */ - function getAuctionLiquidator(uint256 collateralId) - external - view - returns (address liquidator); + function getAuctionLiquidator( + uint256 collateralId + ) external view returns (address liquidator); /** * Calculates the debt accrued by all liens against a CollateralToken, assuming no payments are made until the end timestamp in the stack. * @param stack The stack data for active liens against the CollateralToken. */ - function getMaxPotentialDebtForCollateral(ILienToken.Stack[] memory stack) - external - view - returns (uint256); + function getMaxPotentialDebtForCollateral( + ILienToken.Stack[] memory stack + ) external view returns (uint256); /** * Calculates the debt accrued by all liens against a CollateralToken, assuming no payments are made until the provided timestamp. diff --git a/src/interfaces/IVaultImplementation.sol b/src/interfaces/IVaultImplementation.sol index a6a67f3f..a73e3170 100644 --- a/src/interfaces/IVaultImplementation.sol +++ b/src/interfaces/IVaultImplementation.sol @@ -70,9 +70,9 @@ interface IVaultImplementation is IAstariaVaultBase, IERC165 { function incrementNonce() external; - function commitToLien(IAstariaRouter.Commitment calldata params) - external - returns (uint256 lienId, ILienToken.Stack[] memory stack); + function commitToLien( + IAstariaRouter.Commitment calldata params + ) external returns (uint256 lienId, ILienToken.Stack[] memory stack); function buyoutLien( ILienToken.Stack[] calldata stack, diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index 1b158a30..cedf25de 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -78,7 +78,7 @@ contract RevertTesting is TestHelpers { token: address(3) }); } - + function testCannotEndAuctionWithWrongToken() public { address alice = address(1); address bob = address(2); diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 1f993309..af4e3307 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -334,10 +334,9 @@ contract TestHelpers is Deploy, ConsiderationTester { (rate * amount * duration).mulDivDown(1, 365 days).mulDivDown(1, 1e18); } - function setupLiquidation(address borrower) - public - returns (address publicVault, ILienToken.Stack[] memory stack) - { + function setupLiquidation( + address borrower + ) public returns (address publicVault, ILienToken.Stack[] memory stack) { TestNFT nft = new TestNFT(0); _mintNoDepositApproveRouterSpecific(borrower, address(nft), 99); address tokenContract = address(nft); @@ -423,9 +422,10 @@ contract TestHelpers is Deploy, ConsiderationTester { ); } - function _mintNoDepositApproveRouter(address tokenContract, uint256 tokenId) - internal - { + function _mintNoDepositApproveRouter( + address tokenContract, + uint256 tokenId + ) internal { TestNFT(tokenContract).mint(address(this), tokenId); TestNFT(tokenContract).approve(address(ASTARIA_ROUTER), tokenId); } @@ -873,14 +873,9 @@ contract TestHelpers is Deploy, ConsiderationTester { uint256 amount; } - function _toVRS(bytes memory signature) - internal - returns ( - uint8 v, - bytes32 r, - bytes32 s - ) - { + function _toVRS( + bytes memory signature + ) internal returns (uint8 v, bytes32 r, bytes32 s) { emit log_bytes(signature); emit log_named_uint("signature length", signature.length); if (signature.length == 65) { @@ -897,10 +892,9 @@ contract TestHelpers is Deploy, ConsiderationTester { } } - function _generateTerms(GenTerms memory params) - internal - returns (IAstariaRouter.Commitment memory terms) - { + function _generateTerms( + GenTerms memory params + ) internal returns (IAstariaRouter.Commitment memory terms) { (uint8 v, bytes32 r, bytes32 s) = _toVRS(params.signature); return @@ -1247,11 +1241,9 @@ contract TestHelpers is Deploy, ConsiderationTester { return _mirrorOrderParameters; } - function _toOfferItems(ConsiderationItem[] memory _considerationItems) - internal - pure - returns (OfferItem[] memory) - { + function _toOfferItems( + ConsiderationItem[] memory _considerationItems + ) internal pure returns (OfferItem[] memory) { OfferItem[] memory _offerItems = new OfferItem[]( _considerationItems.length ); From 812b0af211b0dd206940a277b58ee70706262717 Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Mon, 20 Feb 2023 14:45:44 -0400 Subject: [PATCH 35/39] fix test workflow to use version 16 and setup-node@v3 --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ee87d82..69de5124 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,9 @@ jobs: submodules: recursive token: ${{ secrets.MY_REPO_PAT }} - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 + with: + node-version: 16 - run: yarn - uses: onbjerg/foundry-toolchain@v1 with: From 29d7682f91ebfd48c351bbdc77ee1cad9c392e2d Mon Sep 17 00:00:00 2001 From: Andrew Redden <=> Date: Mon, 20 Feb 2023 15:05:26 -0400 Subject: [PATCH 36/39] fix for https://github.com/code-423n4/2023-01-astaria-findings/issues/482, as well as a lost spearbit issue, https://github.com/spearbit-audits/review-astaria/issues/144, original by santiago --- src/WithdrawProxy.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/WithdrawProxy.sol b/src/WithdrawProxy.sol index b1ca3289..9653ab47 100644 --- a/src/WithdrawProxy.sol +++ b/src/WithdrawProxy.sol @@ -261,10 +261,7 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { if (s.withdrawRatio == uint256(0)) { ERC20(asset()).safeTransfer(VAULT(), balance); } else { - transferAmount = uint256(s.withdrawRatio).mulDivDown( - balance, - 10 ** ERC20(asset()).decimals() - ); + transferAmount = uint256(s.withdrawRatio).mulDivDown(balance, 1e18); unchecked { balance -= transferAmount; From dcdc891f152e819b668e8857d60ff27d5b07db74 Mon Sep 17 00:00:00 2001 From: Joseph Delong Date: Tue, 21 Feb 2023 18:26:46 -0600 Subject: [PATCH 37/39] Changes to buyoutLien (#30) * changes for buyOutLien * add accrual to refinance publicvault hooks * refinance fixes * mend refinance fixes * fix maxamount in publicvault refinance test * publicvault refinance assert update * gated privatevault buyouts to owner or delegate * changes for permissions * fix: add chargeable flag to LienActionBuyout * precision issues * fix test to account for additional second of interest caused by warping 1 second over the epoch boundry * removed payee switch for privatevaults and unnecessary loop on buyout * refactored into _appendToStack and _validateStackState methods --------- Co-authored-by: = Co-authored-by: Andrew Redden <=> --- src/AstariaRouter.sol | 50 --- src/LienToken.sol | 284 ++++++++---- src/PublicVault.sol | 35 +- src/VaultImplementation.sol | 77 ++-- src/interfaces/IAstariaRouter.sol | 27 -- src/interfaces/ILienToken.sol | 59 ++- src/interfaces/IPublicVault.sol | 17 +- src/test/AstariaTest.t.sol | 195 --------- src/test/RefinanceTesting.t.sol | 687 ++++++++++++++++++++++++++++++ src/test/TestHelpers.t.sol | 9 + 10 files changed, 1030 insertions(+), 410 deletions(-) create mode 100644 src/test/RefinanceTesting.t.sol diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index 72ef8b2b..ffae0d3f 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -108,14 +108,10 @@ contract AstariaRouter is s.liquidationFeeNumerator = uint32(130); s.liquidationFeeDenominator = uint32(1000); - s.minInterestBPS = uint32((uint256(1e15) * 5) / (365 days)); s.minEpochLength = uint32(7 days); s.maxEpochLength = uint32(45 days); s.maxInterestRate = ((uint256(1e16) * 200) / (365 days)); //63419583966; // 200% apy / second - s.buyoutFeeNumerator = uint32(100); - s.buyoutFeeDenominator = uint32(1000); - s.minDurationIncrease = uint32(5 days); s.guardian = msg.sender; } @@ -300,20 +296,6 @@ contract AstariaRouter is if (denominator < numerator) revert InvalidFileData(); s.protocolFeeNumerator = numerator.safeCastTo32(); s.protocolFeeDenominator = denominator.safeCastTo32(); - } else if (what == FileType.BuyoutFee) { - (uint256 numerator, uint256 denominator) = abi.decode( - data, - (uint256, uint256) - ); - if (denominator < numerator) revert InvalidFileData(); - s.buyoutFeeNumerator = numerator.safeCastTo32(); - s.buyoutFeeDenominator = denominator.safeCastTo32(); - } else if (what == FileType.MinInterestBPS) { - uint256 value = abi.decode(data, (uint256)); - s.minInterestBPS = value.safeCastTo32(); - } else if (what == FileType.MinDurationIncrease) { - uint256 value = abi.decode(data, (uint256)); - s.minDurationIncrease = value.safeCastTo32(); } else if (what == FileType.MinEpochLength) { s.minEpochLength = abi.decode(data, (uint256)).safeCastTo32(); } else if (what == FileType.MaxEpochLength) { @@ -652,42 +634,10 @@ contract AstariaRouter is ); } - function getBuyoutFee( - uint256 remainingInterestIn - ) external view returns (uint256) { - RouterStorage storage s = _loadRouterSlot(); - return - remainingInterestIn.mulDivDown( - s.buyoutFeeNumerator, - s.buyoutFeeDenominator - ); - } - function isValidVault(address vault) public view returns (bool) { return _loadRouterSlot().vaults[vault]; } - function isValidRefinance( - ILienToken.Lien calldata newLien, - uint8 position, - ILienToken.Stack[] calldata stack - ) public view returns (bool) { - RouterStorage storage s = _loadRouterSlot(); - uint256 maxNewRate = uint256(stack[position].lien.details.rate) - - s.minInterestBPS; - - if (newLien.collateralId != stack[0].lien.collateralId) { - revert InvalidRefinanceCollateral(newLien.collateralId); - } - return - (newLien.details.rate <= maxNewRate && - newLien.details.duration + block.timestamp >= - stack[position].point.end) || - (block.timestamp + newLien.details.duration - stack[position].point.end >= - s.minDurationIncrease && - newLien.details.rate <= stack[position].lien.details.rate); - } - /** * @dev Deploys a new Vault. * @param epochLength The length of each epoch for a new PublicVault. If 0, deploys a PrivateVault. diff --git a/src/LienToken.sol b/src/LienToken.sol index 480d13b8..e7c81388 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -29,7 +29,7 @@ import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; - +import {IVaultImplementation} from "core/interfaces/IVaultImplementation.sol"; import {IPublicVault} from "core/interfaces/IPublicVault.sol"; import {VaultImplementation} from "./VaultImplementation.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; @@ -38,11 +38,13 @@ import {AuthInitializable} from "core/AuthInitializable.sol"; import {Initializable} from "./utils/Initializable.sol"; import {ClearingHouse} from "core/ClearingHouse.sol"; +import {AmountDeriver} from "seaport/lib/AmountDeriver.sol"; + /** * @title LienToken * @notice This contract handles the creation, payments, buyouts, and liquidations of tokenized NFT-collateralized debt (liens). Vaults which originate loans against supported collateral are issued a LienToken representing the right to loan repayments and auctioned funds on liquidation. */ -contract LienToken is ERC721, ILienToken, AuthInitializable { +contract LienToken is ERC721, ILienToken, AuthInitializable, AmountDeriver { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; @@ -66,6 +68,12 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { LienStorage storage s = _loadLienStorageSlot(); s.TRANSFER_PROXY = _TRANSFER_PROXY; s.maxLiens = uint8(5); + s.buyoutFeeNumerator = uint32(100); + s.buyoutFeeDenominator = uint32(1000); + s.durationFeeCapNumerator = uint32(900); + s.durationFeeCapDenominator = uint32(1000); + s.minDurationIncrease = uint32(5 days); + s.minInterestBPS = uint32((uint256(1e15) * 5) / (365 days)); } function _loadLienStorageSlot() @@ -88,6 +96,28 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { s.COLLATERAL_TOKEN = ICollateralToken(abi.decode(data, (address))); } else if (what == FileType.AstariaRouter) { s.ASTARIA_ROUTER = IAstariaRouter(abi.decode(data, (address))); + } else if (what == FileType.BuyoutFee) { + (uint256 numerator, uint256 denominator) = abi.decode( + data, + (uint256, uint256) + ); + if (denominator < numerator) revert InvalidFileData(); + s.buyoutFeeNumerator = numerator.safeCastTo32(); + s.buyoutFeeDenominator = denominator.safeCastTo32(); + } else if (what == FileType.BuyoutFeeDurationCap) { + (uint256 numerator, uint256 denominator) = abi.decode( + data, + (uint256, uint256) + ); + if (denominator < numerator) revert InvalidFileData(); + s.durationFeeCapNumerator = numerator.safeCastTo32(); + s.durationFeeCapDenominator = denominator.safeCastTo32(); + } else if (what == FileType.MinInterestBPS) { + uint256 value = abi.decode(data, (uint256)); + s.minInterestBPS = value.safeCastTo32(); + } else if (what == FileType.MinDurationIncrease) { + uint256 value = abi.decode(data, (uint256)); + s.minDurationIncrease = value.safeCastTo32(); } else { revert UnsupportedFile(); } @@ -102,12 +132,57 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { super.supportsInterface(interfaceId); } + function isValidRefinance( + Lien calldata newLien, + uint8 position, + Stack[] calldata stack, + uint256 owed, + uint256 buyout, + bool chargeable + ) public view returns (bool) { + LienStorage storage s = _loadLienStorageSlot(); + uint256 maxNewRate = uint256(stack[position].lien.details.rate) - + s.minInterestBPS; + + if (newLien.collateralId != stack[0].lien.collateralId) { + revert InvalidRefinanceCollateral(newLien.collateralId); + } + + bool isPublicVault = _isPublicVault(s, msg.sender); + bool hasBuyoutFee = buyout > owed; + + // PublicVault refinances are only valid if they do not have a buyout fee. + // This happens when the borrower executes the buyout, or the lien duration is past the durationFeeCap. + if (hasBuyoutFee && !chargeable) { + revert RefinanceBlocked(); + } + + bool hasImprovedRate = (newLien.details.rate <= maxNewRate && + newLien.details.duration + block.timestamp >= stack[position].point.end); + + bool hasImprovedDuration = (block.timestamp + + newLien.details.duration - + stack[position].point.end >= + s.minDurationIncrease && + newLien.details.rate <= stack[position].lien.details.rate); + + bool hasNotDecreasedInitialAsk = newLien.details.liquidationInitialAsk >= + stack[position].lien.details.liquidationInitialAsk; + + return + (hasImprovedRate || hasImprovedDuration) && hasNotDecreasedInitialAsk; + } + function buyoutLien( ILienToken.LienActionBuyout calldata params ) external validateStack(params.encumber.lien.collateralId, params.encumber.stack) - returns (Stack[] memory, Stack memory newStack) + returns ( + Stack[] memory stacks, + Stack memory newStack, + ILienToken.BuyoutLienParams memory buyoutParams + ) { if (block.timestamp >= params.encumber.stack[params.position].point.end) { revert InvalidState(InvalidStates.EXPIRED_LIEN); @@ -122,14 +197,30 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { function _buyoutLien( LienStorage storage s, ILienToken.LienActionBuyout calldata params - ) internal returns (Stack[] memory newStack, Stack memory newLien) { + ) + internal + returns ( + Stack[] memory newStack, + Stack memory newLien, + ILienToken.BuyoutLienParams memory buyoutParams + ) + { //the borrower shouldn't incur more debt from the buyout than they already owe (, newLien) = _createLien(s, params.encumber); + + (uint256 owed, uint256 buyout) = _getBuyout( + s, + params.encumber.stack[params.position] + ); + if ( - !s.ASTARIA_ROUTER.isValidRefinance({ + !isValidRefinance({ newLien: params.encumber.lien, position: params.position, - stack: params.encumber.stack + stack: params.encumber.stack, + owed: owed, + buyout: buyout, + chargeable: params.chargeable }) ) { revert InvalidRefinance(); @@ -140,44 +231,29 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { ) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); } - (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); - } - uint256 potentialDebt = 0; - for (uint256 i = params.encumber.stack.length; i > 0; ) { - uint256 j = i - 1; - // should not be able to purchase lien if any lien in the stack is expired (and will be liquidated) - if (block.timestamp >= params.encumber.stack[j].point.end) { - revert InvalidState(InvalidStates.EXPIRED_LIEN); - } - - potentialDebt += _getOwed( - params.encumber.stack[j], - params.encumber.stack[j].point.end + if (params.encumber.lien.details.maxAmount < buyout) { + revert InvalidBuyoutDetails( + params.encumber.lien.details.maxAmount, + buyout ); - - if ( - potentialDebt > - params.encumber.stack[j].lien.details.liquidationInitialAsk - ) { - revert InvalidState(InvalidStates.INITIAL_ASK_EXCEEDED); - } - - unchecked { - --i; - } } address payee = _getPayee( s, params.encumber.stack[params.position].point.lienId ); + + if (_isPublicVault(s, payee)) { + IPublicVault(payee).handleLoseLienToBuyout( + ILienToken.BuyoutLienParams({ + lienSlope: calculateSlope(params.encumber.stack[params.position]), + lienEnd: params.encumber.stack[params.position].point.end + }), + buyout - owed + ); + } + s.TRANSFER_PROXY.tokenTransferFrom( params.encumber.stack[params.position].lien.token, msg.sender, @@ -185,17 +261,6 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { buyout ); - 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 - }) - ); - } - newStack = _replaceStackAtPositionWithNewLien( s, params.encumber.stack, @@ -203,31 +268,45 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { newLien, params.encumber.stack[params.position].point.lienId ); - uint256 maxPotentialDebt; - uint256 n = newStack.length; + + _validateStackState(newStack); + + buyoutParams = ILienToken.BuyoutLienParams({ + lienSlope: calculateSlope(newStack[params.position]), + lienEnd: newStack[params.position].point.end + }); + + s.collateralStateHash[params.encumber.lien.collateralId] = keccak256( + abi.encode(newStack) + ); + } + + function _validateStackState(Stack[] memory stack) internal { + uint256 potentialDebt = 0; uint256 i; - for (i; i < n; ) { - maxPotentialDebt += _getOwed(newStack[i], newStack[i].point.end); - //no need to check validity before the position we're buying - if (i == params.position) { - if (maxPotentialDebt > params.encumber.lien.details.maxPotentialDebt) { - revert InvalidState(InvalidStates.DEBT_LIMIT); - } + for (i; i < stack.length; ) { + if (block.timestamp >= stack[i].point.end) { + revert InvalidState(InvalidStates.EXPIRED_LIEN); } - if ( - i > params.position && - (maxPotentialDebt > newStack[i].lien.details.maxPotentialDebt) - ) { + if (potentialDebt > stack[i].lien.details.maxPotentialDebt) { revert InvalidState(InvalidStates.DEBT_LIMIT); } + potentialDebt += _getOwed(stack[i], stack[i].point.end); unchecked { ++i; } } - - s.collateralStateHash[params.encumber.lien.collateralId] = keccak256( - abi.encode(newStack) - ); + potentialDebt = 0; + i = stack.length; + for (i; i > 0; ) { + potentialDebt += _getOwed(stack[i - 1], stack[i - 1].point.end); + if (potentialDebt > stack[i - 1].lien.details.liquidationInitialAsk) { + revert InvalidState(InvalidStates.INITIAL_ASK_EXCEEDED); + } + unchecked { + --i; + } + } } function _replaceStackAtPositionWithNewLien( @@ -379,7 +458,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { } function createLien( - ILienToken.LienActionEncumber memory params + ILienToken.LienActionEncumber calldata params ) external requiresAuth @@ -392,6 +471,8 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { (lienId, newStackSlot) = _createLien(s, params); newStack = _appendStack(s, params.stack, newStackSlot); + _validateStackState(newStack); + s.collateralStateHash[params.lien.collateralId] = keccak256( abi.encode(newStack) ); @@ -413,7 +494,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { function _createLien( LienStorage storage s, - ILienToken.LienActionEncumber memory params + ILienToken.LienActionEncumber calldata params ) internal returns (uint256 newLienId, ILienToken.Stack memory newSlot) { if (s.collateralStateHash[params.lien.collateralId] == ACTIVE_AUCTION) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); @@ -448,7 +529,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { function _appendStack( LienStorage storage s, - Stack[] memory stack, + Stack[] calldata stack, Stack memory newSlot ) internal returns (Stack[] memory newStack) { if (stack.length >= s.maxLiens) { @@ -457,31 +538,14 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { 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); - } - - unchecked { - potentialDebt += _getOwed(newStack[j], newStack[j].point.end); - } - if (potentialDebt > newStack[j].lien.details.liquidationInitialAsk) { - revert InvalidState(InvalidStates.INITIAL_ASK_EXCEEDED); - } - + uint256 i; + for (i; i < stack.length; ) { + newStack[i] = stack[i]; unchecked { - --i; + ++i; } } - if ( - stack.length > 0 && potentialDebt > newSlot.lien.details.maxPotentialDebt - ) { - revert InvalidState(InvalidStates.DEBT_LIMIT); - } + return newStack; } function payDebtViaClearingHouse( @@ -558,6 +622,32 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { return _loadLienStorageSlot().collateralStateHash[collateralId]; } + function getBuyoutFee( + uint256 remainingInterestIn, + uint256 end, + uint256 duration + ) public view returns (uint256 fee) { + LienStorage storage s = _loadLienStorageSlot(); + + uint256 start = end - duration; + + // Buyout fees begin at (buyoutFee * remainingInterest) and decrease linearly until the durationFeeCap is reached. + fee = _locateCurrentAmount({ + startAmount: remainingInterestIn.mulDivDown( + s.buyoutFeeNumerator, + s.buyoutFeeDenominator + ), + endAmount: 0, + startTime: start, + endTime: start + + duration.mulDivDown( + s.durationFeeCapNumerator, + s.durationFeeCapDenominator + ), + roundUp: true + }); + } + function getBuyout( Stack calldata stack ) public view returns (uint256 owed, uint256 buyout) { @@ -569,9 +659,19 @@ contract LienToken is ERC721, ILienToken, AuthInitializable { Stack calldata stack ) internal view returns (uint256 owed, uint256 buyout) { owed = _getOwed(stack, block.timestamp); - buyout = - owed + - s.ASTARIA_ROUTER.getBuyoutFee(_getRemainingInterest(s, stack)); + buyout = owed; + + // Buyout fees are excluded if the borrower is executing the refinance or if the refinance is within the same Vault. + if ( + tx.origin != s.COLLATERAL_TOKEN.ownerOf(stack.lien.collateralId) && + msg.sender != stack.lien.vault + ) { + buyout += getBuyoutFee( + _getRemainingInterest(s, stack), + stack.point.end, + stack.lien.details.duration + ); + } } function makePayment( diff --git a/src/PublicVault.sol b/src/PublicVault.sol index c3e99fa3..4ecb01cc 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -597,19 +597,42 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { return ROUTER().LIEN_TOKEN(); } - function handleBuyoutLien( - BuyoutLienParams calldata params + function handleLoseLienToBuyout( + ILienToken.BuyoutLienParams calldata buyoutParams, + uint256 buyoutFeeIfAny ) public onlyLienToken { VaultData storage s = _loadStorageSlot(); + _accrue(s); unchecked { - uint256 newSlope = s.slope - params.lienSlope; + uint256 newSlope = s.slope - buyoutParams.lienSlope; _setSlope(s, newSlope); - s.yIntercept += params.increaseYIntercept; - s.last = block.timestamp.safeCastTo40(); + s.yIntercept += buyoutFeeIfAny; } - _decreaseEpochLienCount(s, getLienEpoch(params.lienEnd.safeCastTo64())); + _decreaseEpochLienCount( + s, + getLienEpoch(buyoutParams.lienEnd.safeCastTo64()) + ); + emit YInterceptChanged(s.yIntercept); + } + + function _handleReceiveBuyout( + ILienToken.BuyoutLienParams memory buyoutParams + ) internal virtual override { + VaultData storage s = _loadStorageSlot(); + + _accrue(s); + if (s.withdrawReserve > uint256(0)) { + transferWithdrawReserve(); + } + unchecked { + uint256 newSlope = s.slope + buyoutParams.lienSlope; + _setSlope(s, newSlope); + } + + _increaseOpenLiens(s, getLienEpoch(buyoutParams.lienEnd.safeCastTo64())); + emit YInterceptChanged(s.yIntercept); } diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 4d1d249d..d624676f 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -215,6 +215,10 @@ abstract contract VaultImplementation is emit AllowListUpdated(delegate_, true); } + function isDelegateOrOwner(address addr) external view returns (bool) { + return addr == owner() || addr == _loadVISlot().delegate; + } + /** * @dev Validates the incoming request for a lien * Who is requesting the borrow, is it a smart contract? or is it a user? @@ -248,6 +252,22 @@ abstract contract VaultImplementation is revert InvalidRequest(InvalidRequestReason.EXPIRED); } + _validateSignature(params); + + if (holder != msg.sender) { + if (msg.sender.code.length > 0) { + return msg.sender; + } else { + revert InvalidRequest(InvalidRequestReason.OPERATOR_NO_CODE); + } + } else { + return holder; + } + } + + function _validateSignature( + IAstariaRouter.Commitment calldata params + ) internal view { VIData storage s = _loadVISlot(); address recovered = ecrecover( keccak256( @@ -269,16 +289,6 @@ abstract contract VaultImplementation is InvalidRequestReason.INVALID_SIGNATURE ); } - - if (holder != msg.sender) { - if (msg.sender.code.length > 0) { - return msg.sender; - } else { - revert InvalidRequest(InvalidRequestReason.OPERATOR_NO_CODE); - } - } else { - return holder; - } } function _afterCommitToLien( @@ -327,7 +337,7 @@ abstract contract VaultImplementation is ) external whenNotPaused - returns (ILienToken.Stack[] memory, ILienToken.Stack memory) + returns (ILienToken.Stack[] memory stacks, ILienToken.Stack memory newStack) { LienToken lienToken = LienToken(address(ROUTER().LIEN_TOKEN())); @@ -339,27 +349,36 @@ abstract contract VaultImplementation is ); } - _validateRequest(incomingTerms); + _validateSignature(incomingTerms); ERC20(asset()).safeApprove(address(ROUTER().TRANSFER_PROXY()), buyout); - return - lienToken.buyoutLien( - ILienToken.LienActionBuyout({ - position: position, - encumber: ILienToken.LienActionEncumber({ - amount: owed, - receiver: recipient(), - lien: ROUTER().validateCommitment({ - commitment: incomingTerms, - timeToSecondEpochEnd: _timeToSecondEndIfPublic() - }), - stack: stack - }) + ILienToken.BuyoutLienParams memory buyoutParams; + + (stacks, newStack, buyoutParams) = lienToken.buyoutLien( + ILienToken.LienActionBuyout({ + chargeable: (!_isPublicVault() && + (msg.sender == owner() || msg.sender == _loadVISlot().delegate)), + position: position, + encumber: ILienToken.LienActionEncumber({ + amount: owed, + receiver: recipient(), + lien: ROUTER().validateCommitment({ + commitment: incomingTerms, + timeToSecondEpochEnd: _timeToSecondEndIfPublic() + }), + stack: stack }) - ); + }) + ); + + _handleReceiveBuyout(buyoutParams); } + function _handleReceiveBuyout( + ILienToken.BuyoutLienParams memory buyoutParams + ) internal virtual {} + function _timeToSecondEndIfPublic() internal view @@ -374,13 +393,17 @@ abstract contract VaultImplementation is * @return The address of the recipient. */ function recipient() public view returns (address) { - if (IMPL_TYPE() == uint8(IAstariaRouter.ImplementationType.PublicVault)) { + if (_isPublicVault()) { return address(this); } else { return owner(); } } + function _isPublicVault() internal view returns (bool) { + return IMPL_TYPE() == uint8(IAstariaRouter.ImplementationType.PublicVault); + } + /** * @dev Generates a Lien for a valid loan commitment proof and sends the loan amount to the borrower. * @param c The Commitment information containing the loan parameters and the merkle proof for the strategy supporting the requested loan. diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index 2c43884b..112381f9 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -31,13 +31,10 @@ interface IAstariaRouter is IPausable, IBeacon { LiquidationFee, ProtocolFee, StrategistFee, - MinInterestBPS, MinEpochLength, MaxEpochLength, MinInterestRate, MaxInterestRate, - BuyoutFee, - MinDurationIncrease, AuctionWindow, StrategyValidator, Implementation, @@ -71,13 +68,9 @@ interface IAstariaRouter is IPausable, IBeacon { address feeTo; //20 address BEACON_PROXY_IMPLEMENTATION; //20 uint256 maxInterestRate; //6 - uint32 minInterestBPS; // was uint64 //slot 3 + address guardian; //20 address newGuardian; //20 - uint32 buyoutFeeNumerator; - uint32 buyoutFeeDenominator; - uint32 minDurationIncrease; mapping(uint8 => address) strategyValidators; mapping(uint8 => address) implementations; //A strategist can have many deployed vaults @@ -210,11 +203,6 @@ interface IAstariaRouter is IPausable, IBeacon { */ function getProtocolFee(uint256) external view returns (uint256); - /** - * @notice Computes the fee Vaults earn when a Lien is bought out using the buyoutFee numerator and denominator. - */ - function getBuyoutFee(uint256) external view returns (uint256); - /** * @notice Computes the fee the users earn on liquidating an expired lien from the liquidationFee numerator and denominator. */ @@ -273,20 +261,6 @@ interface IAstariaRouter is IPausable, IBeacon { */ function getImpl(uint8 implType) external view returns (address impl); - /** - * @notice Returns whether a new lien offers more favorable terms over an old lien. - * A new lien must have a rate less than or equal to maxNewRate, - * or a duration lower by minDurationIncrease, provided the other parameter does not get any worse. - * @param newLien The new Lien for the proposed refinance. - * @param position The Lien position against the CollateralToken. - * @param stack The Stack of existing Liens against the CollateralToken. - */ - function isValidRefinance( - ILienToken.Lien calldata newLien, - uint8 position, - ILienToken.Stack[] calldata stack - ) external view returns (bool); - event Liquidation(uint256 collateralId, uint256 position); event NewVault( address strategist, @@ -299,7 +273,6 @@ interface IAstariaRouter is IPausable, IBeacon { error InvalidEpochLength(uint256); error InvalidRefinanceRate(uint256); error InvalidRefinanceDuration(uint256); - error InvalidRefinanceCollateral(uint256); error InvalidVaultState(VaultState); error InvalidSenderForCollateral(address, uint256); error InvalidLienState(LienState); diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index 2555c079..bd8569ab 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -24,7 +24,11 @@ interface ILienToken is IERC721 { enum FileType { NotSupported, CollateralToken, - AstariaRouter + AstariaRouter, + BuyoutFee, + BuyoutFeeDurationCap, + MinInterestBPS, + MinDurationIncrease } struct File { @@ -42,6 +46,12 @@ interface ILienToken is IERC721 { ICollateralToken COLLATERAL_TOKEN; mapping(uint256 => bytes32) collateralStateHash; mapping(uint256 => LienMeta) lienMeta; + uint32 buyoutFeeNumerator; + uint32 buyoutFeeDenominator; + uint32 durationFeeCapNumerator; + uint32 durationFeeCapDenominator; + uint32 minDurationIncrease; + uint32 minInterestBPS; } struct LienMeta { @@ -86,10 +96,16 @@ interface ILienToken is IERC721 { } struct LienActionBuyout { + bool chargeable; uint8 position; LienActionEncumber encumber; } + struct BuyoutLienParams { + uint256 lienSlope; + uint256 lienEnd; + } + /** * @notice Removes all liens for a given CollateralToken. * @param lien The Lien. @@ -123,6 +139,15 @@ interface ILienToken is IERC721 { address liquidator ) external; + /** + * @notice Computes the fee Vaults earn when a Lien is bought out using the buyoutFee numerator and denominator. + */ + function getBuyoutFee( + uint256 remainingInterestIn, + uint256 end, + uint256 duration + ) external view returns (uint256); + /** * @notice Computes and returns the buyout amount for a Lien. * @param stack the lien @@ -176,16 +201,39 @@ interface ILienToken is IERC721 { * @param params LienActionEncumber data containing CollateralToken information and lien parameters (rate, duration, and amount, rate, and debt caps). */ function createLien( - LienActionEncumber memory params + LienActionEncumber calldata params ) external returns (uint256 lienId, Stack[] memory stack, uint256 slope); + /** + * @notice Returns whether a new lien offers more favorable terms over an old lien. + * A new lien must have a rate less than or equal to maxNewRate, + * or a duration lower by minDurationIncrease, provided the other parameter does not get any worse. + * @param newLien The new Lien for the proposed refinance. + * @param position The Lien position against the CollateralToken. + * @param stack The Stack of existing Liens against the CollateralToken. + */ + function isValidRefinance( + Lien calldata newLien, + uint8 position, + Stack[] calldata stack, + uint256 owed, + uint256 buyout, + bool chargeable + ) external view returns (bool); + /** * @notice Purchase a LienToken for its buyout price. * @param params The LienActionBuyout data specifying the lien position, receiver address, and underlying CollateralToken information of the lien. */ function buyoutLien( - LienActionBuyout memory params - ) external returns (Stack[] memory, Stack memory); + LienActionBuyout calldata params + ) + external + returns ( + Stack[] memory stacks, + Stack memory newStack, + BuyoutLienParams memory buyoutParams + ); /** * @notice Called by the ClearingHouse (through Seaport) to pay back debt with auction funds. @@ -287,11 +335,14 @@ interface ILienToken is IERC721 { event BuyoutLien(address indexed buyer, uint256 lienId, uint256 buyout); event PayeeChanged(uint256 indexed lienId, address indexed payee); + error InvalidFileData(); error UnsupportedFile(); error InvalidTokenId(uint256 tokenId); error InvalidBuyoutDetails(uint256 lienMaxAmount, uint256 owed); error InvalidTerms(); error InvalidRefinance(); + error InvalidRefinanceCollateral(uint256); + error RefinanceBlocked(); error InvalidLoanState(); error InvalidSender(); enum InvalidStates { diff --git a/src/interfaces/IPublicVault.sol b/src/interfaces/IPublicVault.sol index 56612291..9322ed84 100644 --- a/src/interfaces/IPublicVault.sol +++ b/src/interfaces/IPublicVault.sol @@ -15,6 +15,7 @@ pragma solidity =0.8.17; import {IERC165} from "core/interfaces/IERC165.sol"; import {IVaultImplementation} from "core/interfaces/IVaultImplementation.sol"; +import {ILienToken} from "core/interfaces/ILienToken.sol"; interface IPublicVault is IVaultImplementation { struct EpochData { @@ -39,12 +40,6 @@ interface IPublicVault is IVaultImplementation { uint256 interestOwed; } - struct BuyoutLienParams { - uint256 lienSlope; - uint256 lienEnd; - uint256 increaseYIntercept; - } - struct AfterLiquidationParams { uint256 lienSlope; uint256 newAmount; @@ -134,10 +129,14 @@ interface IPublicVault is IVaultImplementation { function decreaseYIntercept(uint256 amount) external; /** - * Hook to update the PublicVault's slope, YIntercept, and last timestamp on a LienToken buyout. - * @param params The lien buyout parameters (lienSlope, lienEnd, and increaseYIntercept) + * Hook to update the PublicVault's slope, YIntercept, and last timestamp when a LienToken is bought out. Also decreases the active lien count for the lien's expiring epoch. + * @param buyoutParams The lien buyout parameters (lienSlope, lienEnd, and yInterceptChange) + * @param buyoutFeeIfAny The buyout fee if the target vault is a PrivateVault and the lien is being bought out before feeDurationCap has passed. */ - function handleBuyoutLien(BuyoutLienParams calldata params) external; + function handleLoseLienToBuyout( + ILienToken.BuyoutLienParams calldata buyoutParams, + uint256 buyoutFeeIfAny + ) external; /** * Hook to update the PublicVault owner of a LienToken when it is sent to liquidation. diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index 56d4f185..dcb7a9e3 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -401,201 +401,6 @@ contract AstariaTest is TestHelpers { assertEq(WETH9.balanceOf(address(1)), 50287680745810395000); } - function testBuyoutLien() 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: 14 days - }); - - // lend 50 ether to the PublicVault as address(1) - _lendToVault( - Lender({addr: address(1), amountToLend: 50 ether}), - publicVault - ); - - // borrow 10 eth against the dummy NFT - (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ - vault: publicVault, - strategist: strategistOne, - strategistPK: strategistOnePK, - tokenContract: tokenContract, - tokenId: tokenId, - lienDetails: standardLienDetails, - amount: 10 ether, - isFirstLien: true - }); - - 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, - delegate: strategistTwo - }); - - IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ - vault: privateVault, - strategist: strategistOne, - strategistPK: strategistOnePK, - tokenContract: tokenContract, - tokenId: tokenId, - lienDetails: refinanceLienDetails, - amount: 10 ether, - stack: stack - }); - - _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), - privateVault - ); - - VaultImplementation(privateVault).buyoutLien( - stack, - uint8(0), - refinanceTerms - ); - - 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 testBuyoutLienDifferentCollateral() public { - TestNFT nft = new TestNFT(2); - 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: 14 days - }); - - // lend 50 ether to the PublicVault as address(1) - _lendToVault( - Lender({addr: address(1), amountToLend: 50 ether}), - publicVault - ); - - // borrow 10 eth against the dummy NFT - (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ - vault: publicVault, - strategist: strategistOne, - strategistPK: strategistOnePK, - tokenContract: tokenContract, - tokenId: tokenId, - lienDetails: standardLienDetails, - amount: 10 ether, - isFirstLien: true - }); - // borrow 10 eth against the dummy NFT - (, ILienToken.Stack[] memory stack2) = _commitToLien({ - vault: publicVault, - strategist: strategistOne, - strategistPK: strategistOnePK, - tokenContract: tokenContract, - tokenId: uint256(1), - lienDetails: standardLienDetails, - amount: 10 ether, - isFirstLien: true - }); - - 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, - delegate: strategistTwo - }); - - IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ - vault: privateVault, - strategist: strategistOne, - strategistPK: strategistOnePK, - tokenContract: tokenContract, - tokenId: uint256(1), - lienDetails: refinanceLienDetails, - amount: 10 ether, - stack: stack - }); - - _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), - privateVault - ); - - vm.expectRevert( - abi.encodeWithSelector( - ILienToken.InvalidState.selector, - ILienToken.InvalidStates.INVALID_HASH - ) - ); - VaultImplementation(privateVault).buyoutLien( - stack, - uint8(0), - refinanceTerms - ); - } - function testTwoLoansDiffCollateralSameStack() public { TestNFT nft = new TestNFT(2); address tokenContract = address(nft); diff --git a/src/test/RefinanceTesting.t.sol b/src/test/RefinanceTesting.t.sol new file mode 100644 index 00000000..b065c959 --- /dev/null +++ b/src/test/RefinanceTesting.t.sol @@ -0,0 +1,687 @@ +// SPDX-License-Identifier: BUSL-1.1 + +/** + * █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ + * ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ + * ███████║███████╗ ██║ ███████║██████╔╝██║███████║ + * ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ + * ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ + * + * Astaria Labs, Inc + */ + +pragma solidity =0.8.17; + +import "forge-std/Test.sol"; + +import {Authority} from "solmate/auth/Auth.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; +import { + MultiRolesAuthority +} from "solmate/auth/authorities/MultiRolesAuthority.sol"; + +import { + IERC1155Receiver +} from "openzeppelin/token/ERC1155/IERC1155Receiver.sol"; +import {Strings} from "openzeppelin/utils/Strings.sol"; + +import {ERC721} from "gpl/ERC721.sol"; + +import {ICollateralToken} from "../interfaces/ICollateralToken.sol"; +import {ILienToken} from "../interfaces/ILienToken.sol"; +import {IPublicVault} from "../interfaces/IPublicVault.sol"; +import {CollateralToken, IFlashAction} from "../CollateralToken.sol"; +import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; +import {VaultImplementation} from "../VaultImplementation.sol"; +import {IVaultImplementation} from "../interfaces/IVaultImplementation.sol"; +import {LienToken} from "../LienToken.sol"; +import {PublicVault} from "../PublicVault.sol"; +import {TransferProxy} from "../TransferProxy.sol"; +import {WithdrawProxy} from "../WithdrawProxy.sol"; + +import {Strings2} from "./utils/Strings2.sol"; + +import "./TestHelpers.t.sol"; + +contract RefinanceTesting is TestHelpers { + using FixedPointMathLib for uint256; + using CollateralLookup for address; + + function testPrivateVaultBuysPublicVaultLien() 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: 14 days + }); + + // lend 50 ether to the PublicVault as address(1) + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + // borrow 10 eth against the dummy NFT + (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + vm.warp(block.timestamp + 7 days); + + uint256 accruedInterest = uint256(LIEN_TOKEN.getOwed(stack[0])); + uint256 tenthOfRemaining = (uint256( + LIEN_TOKEN.getOwed(stack[0], block.timestamp + 3 days) + ) - accruedInterest).mulDivDown(1, 10); + + uint256 buyoutFee = _locateCurrentAmount({ + startAmount: tenthOfRemaining, + endAmount: 0, + startTime: 1, + endTime: 1 + standardLienDetails.duration.mulDivDown(900, 1000), + roundUp: true + }); + + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: refinanceLienDetails, + amount: 10 ether, + stack: stack + }); + + _lendToPrivateVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + vm.startPrank(strategistTwo); + VaultImplementation(privateVault).buyoutLien( + stack, + uint8(0), + refinanceTerms + ); + vm.stopPrank(); + + assertEq( + WETH9.balanceOf(privateVault), + 40 ether - buyoutFee - (accruedInterest - stack[0].point.amount), + "Incorrect PrivateVault balance" + ); + assertEq( + WETH9.balanceOf(publicVault), + 50 ether + buyoutFee + ((accruedInterest - stack[0].point.amount)), + "Incorrect PublicVault balance" + ); + assertEq( + PublicVault(publicVault).getYIntercept(), + 50 ether + buyoutFee + ((accruedInterest - stack[0].point.amount)), + "Incorrect PublicVault YIntercept" + ); + assertEq( + PublicVault(publicVault).totalAssets(), + 50 ether + buyoutFee + (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 + buyoutFee + (accruedInterest - stack[0].point.amount), + "Incorrect withdrawer balance" + ); + } + + function testCannotBuyoutLienDifferentCollateral() public { + TestNFT nft = new TestNFT(2); + 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: 14 days + }); + + // lend 50 ether to the PublicVault as address(1) + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + // borrow 10 eth against the dummy NFT + (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + // borrow 10 eth against the dummy NFT + (, ILienToken.Stack[] memory stack2) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: uint256(1), + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + 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, + delegate: strategistTwo + }); + + IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: uint256(1), + lienDetails: refinanceLienDetails, + amount: 10 ether, + stack: stack + }); + + _lendToPrivateVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + + vm.expectRevert( + abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.INVALID_HASH + ) + ); + VaultImplementation(privateVault).buyoutLien( + stack, + uint8(0), + refinanceTerms + ); + } + + // Adapted from C4 #303 and #319 with new fee structure + function testBuyoutLienBothPublicVault() 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: 14 days + }); + + // lend 50 ether to the PublicVault as address(1) + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + // borrow 10 eth against the dummy NFT + (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + vm.warp(block.timestamp + 9 days); + + uint256 owed = uint256(LIEN_TOKEN.getOwed(stack[0])); + + address publicVault2 = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 14 days + }); + + ILienToken.Details memory sameRateRefinance = refinanceLienDetails; + sameRateRefinance.rate = getWadRateFromDecimal(150); + + IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ + vault: publicVault2, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: sameRateRefinance, + amount: 10 ether, + stack: stack + }); + + _lendToVault( + Lender({addr: address(2), amountToLend: 50 ether}), + publicVault2 + ); + + uint256 originalSlope = PublicVault(publicVault).getSlope(); + + VaultImplementation(publicVault2).buyoutLien( + stack, + uint8(0), + refinanceTerms + ); + + assertEq( + WETH9.balanceOf(publicVault2), + 50 ether - owed, + "Incorrect PublicVault2 balance" + ); + assertEq( + WETH9.balanceOf(publicVault), + 50 ether + (owed - stack[0].point.amount), + "Incorrect PublicVault balance" + ); + assertEq( + PublicVault(publicVault).getYIntercept(), + 50 ether + (owed - stack[0].point.amount), + "Incorrect PublicVault YIntercept" + ); + assertEq( + PublicVault(publicVault).totalAssets(), + 50 ether + (owed - stack[0].point.amount), + "Incorrect PublicVault totalAssets" + ); + 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 + (owed - stack[0].point.amount), + "Incorrect withdrawer balance" + ); + + _warpToEpochEnd(publicVault2); + PublicVault(publicVault2).processEpoch(); + + assertEq( + WETH9.balanceOf(publicVault2), + 50 ether - owed, + "Incorrect PublicVault2 balance" + ); + + assertEq( + PublicVault(publicVault2).totalAssets(), + 50 ether + + ((14 * 1 days + 1) * getWadRateFromDecimal(150).mulWadDown(owed)), + "Target PublicVault totalAssets incorrect" + ); + assertTrue( + PublicVault(publicVault2).getYIntercept() != 0, + "Incorrect PublicVault2 YIntercept" + ); + assertEq( + PublicVault(publicVault2).getYIntercept(), + 50 ether, + "Incorrect PublicVault2 YIntercept" + ); + } + + function getWadRateFromDecimal( + uint256 decimal + ) internal pure returns (uint256) { + return uint256(1e16).mulDivDown(decimal, 365 days); + } + + function testPublicVaultCannotBuyoutBefore90PercentDurationOver() 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: 14 days + }); + + // lend 50 ether to the PublicVault as address(1) + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + // borrow 10 eth against the dummy NFT + (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + vm.warp(block.timestamp + 7 days); + + address publicVault2 = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 14 days + }); + + ILienToken.Details memory sameRateRefinance = refinanceLienDetails; + sameRateRefinance.rate = (uint256(1e16) * 150) / (365 days); + + IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ + vault: publicVault2, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: sameRateRefinance, + amount: 10 ether, + stack: stack + }); + + _lendToVault( + Lender({addr: address(2), amountToLend: 50 ether}), + publicVault2 + ); + + vm.expectRevert( + abi.encodeWithSelector(ILienToken.RefinanceBlocked.selector) + ); + VaultImplementation(publicVault2).buyoutLien( + stack, + uint8(0), + refinanceTerms + ); + } + + function testSamePublicVaultRefinanceHasNoFee() 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: 14 days + }); + + // lend 50 ether to the PublicVault as address(1) + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + // borrow 10 eth against the dummy NFT + (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + assertEq( + LIEN_TOKEN.getPayee(stack[0].point.lienId), + publicVault, + "ASDFASDFDSAF" + ); + + IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: refinanceLienDetails, + amount: 10 ether, + stack: stack + }); + + VaultImplementation(publicVault).buyoutLien( + stack, + uint8(0), + refinanceTerms + ); + + assertEq( + WETH9.balanceOf(publicVault), + 40 ether, + "PublicVault was charged a fee" + ); + } + + function testSamePrivateVaultRefinanceHasNoFee() public { + TestNFT nft = new TestNFT(1); + address tokenContract = address(nft); + uint256 tokenId = uint256(0); + + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + _lendToPrivateVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + + (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + assertEq( + LIEN_TOKEN.getPayee(stack[0].point.lienId), + strategistOne, + "ASDFASDFDSAF" + ); + assertEq(LIEN_TOKEN.ownerOf(stack[0].point.lienId), strategistOne, "fuck"); + + IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: refinanceLienDetails, + amount: 10 ether, + stack: stack + }); + + VaultImplementation(privateVault).buyoutLien( + stack, + uint8(0), + refinanceTerms + ); + + assertEq( + WETH9.balanceOf(privateVault), + 30 ether, + "PrivateVault was charged a fee" + ); + + assertEq( + WETH9.balanceOf(strategistOne), + 10 ether, + "Strategist did not receive buyout amount" + ); + } + + function testFailCannotRefinanceAsNotVault() 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: 14 days + }); + + // lend 50 ether to the PublicVault as address(1) + _lendToVault( + Lender({addr: address(1), amountToLend: 50 ether}), + publicVault + ); + + // borrow 10 eth against the dummy NFT + (uint256[] memory liens, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + vm.warp(block.timestamp + 7 days); + + uint256 accruedInterest = uint256(LIEN_TOKEN.getOwed(stack[0])); + uint256 tenthOfRemaining = (uint256( + LIEN_TOKEN.getOwed(stack[0], block.timestamp + 3 days) + ) - accruedInterest).mulDivDown(1, 10); + + uint256 buyoutFee = _locateCurrentAmount({ + startAmount: tenthOfRemaining, + endAmount: 0, + startTime: 1, + endTime: 1 + standardLienDetails.duration.mulDivDown(900, 1000), + roundUp: true + }); + + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: refinanceLienDetails, + amount: 10 ether, + stack: stack + }); + + _lendToPrivateVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + + vm.startPrank(strategistTwo); + // vm.expectRevert( + // abi.encodeWithSelector( + // ILienToken.InvalidSender.selector + // ) + // ); + LIEN_TOKEN.buyoutLien( + ILienToken.LienActionBuyout({ + chargeable: true, + position: uint8(0), + encumber: ILienToken.LienActionEncumber({ + amount: accruedInterest, + receiver: address(1), + lien: ASTARIA_ROUTER.validateCommitment({ + commitment: refinanceTerms, + timeToSecondEpochEnd: 0 + }), + stack: stack + }) + }) + ); + vm.stopPrank(); + } +} diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index af4e3307..50d1d195 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -233,6 +233,15 @@ contract TestHelpers is Deploy, ConsiderationTester { liquidationInitialAsk: 500 ether }); + ILienToken.Details public refinanceLienDetails3DaysDurationIncrease = + ILienToken.Details({ + maxAmount: 50 ether, + rate: (uint256(1e16) * 150) / (365 days), + duration: 13 days, + maxPotentialDebt: 0 ether, + liquidationInitialAsk: 500 ether + }); + enum StrategyTypes { STANDARD, COLLECTION, From 9ba0a7748f5a6cd431760e3c5bc9b757415f7de4 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Wed, 22 Feb 2023 13:44:36 -0400 Subject: [PATCH 38/39] C4/token blacklist receiver (#24) * WIP, handle tokens with blacklist by handling their errors with a fallback receiver * fix testBuyoutLien * add pointer for libgpl, move testBuyoutLien to RefinanceTesting * remove testBuyoutLienDifferentCollateral as its a duplicate * remove testBuyoutLien its a duplicate --------- Co-authored-by: Andrew Redden <=> --- .gitmodules | 3 - lib/clones-with-immutable-args | 1 - lib/gpl | 2 +- src/AstariaRouter.sol | 2 +- src/AstariaVaultBase.sol | 2 +- src/BeaconProxy.sol | 2 +- src/ClearingHouse.sol | 2 +- src/CollateralToken.sol | 9 ++- src/LienToken.sol | 16 +++- src/PublicVault.sol | 9 ++- src/TransferProxy.sol | 53 +++++++++++++- src/WithdrawVaultBase.sol | 2 +- src/interfaces/ITransferProxy.sol | 7 ++ src/scripts/deployments/Deploy.sol | 6 +- src/test/AstariaTest.t.sol | 113 ++++++++++++++++++++++++++++- src/test/ForkedTesting.t.sol | 6 +- src/test/RefinanceTesting.t.sol | 29 +++++--- src/test/RevertTesting.t.sol | 26 +++++-- src/test/TestHelpers.t.sol | 35 ++++++--- 19 files changed, 273 insertions(+), 52 deletions(-) delete mode 160000 lib/clones-with-immutable-args diff --git a/.gitmodules b/.gitmodules index 9e27f0fc..310d2cb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,9 +5,6 @@ path = lib/gpl url = https://github.com/AstariaXYZ/astaria-gpl branch = main -[submodule "lib/clones-with-immutable-args"] - path = lib/clones-with-immutable-args - url = https://github.com/wighawag/clones-with-immutable-args [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std diff --git a/lib/clones-with-immutable-args b/lib/clones-with-immutable-args deleted file mode 160000 index 96f78557..00000000 --- a/lib/clones-with-immutable-args +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 96f785571b534764094e268f7e608c393e88f7b6 diff --git a/lib/gpl b/lib/gpl index 4b49fe99..40ad6a07 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit 4b49fe993d9b807fe68b3421ee7f2fe91267c9ef +Subproject commit 40ad6a0735cfd50219cef655394c2f868a2c21e4 diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index ffae0d3f..bf72e8a2 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -678,7 +678,7 @@ contract AstariaRouter is epochLength, vaultFee ), - keccak256(abi.encode(msg.sender, blockhash(block.number - 1))) + keccak256(abi.encodePacked(msg.sender, blockhash(block.number - 1))) ); if (s.LIEN_TOKEN.balanceOf(vaultAddr) > 0) { diff --git a/src/AstariaVaultBase.sol b/src/AstariaVaultBase.sol index 86a5d3d7..4325019d 100644 --- a/src/AstariaVaultBase.sol +++ b/src/AstariaVaultBase.sol @@ -14,7 +14,7 @@ pragma solidity =0.8.17; import {IAstariaVaultBase} from "core/interfaces/IAstariaVaultBase.sol"; -import {Clone} from "clones-with-immutable-args/Clone.sol"; +import {Clone} from "create2-clones-with-immutable-args/Clone.sol"; import {IERC4626} from "core/interfaces/IERC4626.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; diff --git a/src/BeaconProxy.sol b/src/BeaconProxy.sol index 2f5c3ed6..468f8a6c 100644 --- a/src/BeaconProxy.sol +++ b/src/BeaconProxy.sol @@ -14,7 +14,7 @@ pragma solidity =0.8.17; import {IBeacon} from "core/interfaces/IBeacon.sol"; -import {Clone} from "clones-with-immutable-args/Clone.sol"; +import {Clone} from "create2-clones-with-immutable-args/Clone.sol"; contract BeaconProxy is Clone { function _getBeacon() internal pure returns (IBeacon) { diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index 907a1469..7dbcd20e 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -17,7 +17,7 @@ import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {WETH} from "solmate/tokens/WETH.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {Clone} from "clones-with-immutable-args/Clone.sol"; +import {Clone} from "create2-clones-with-immutable-args/Clone.sol"; import {IERC1155} from "core/interfaces/IERC1155.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; import {Bytes32AddressLib} from "solmate/utils/Bytes32AddressLib.sol"; diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index a8af863f..2aeb142a 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -34,8 +34,8 @@ import {VaultImplementation} from "core/VaultImplementation.sol"; import {ZoneInterface} from "seaport/interfaces/ZoneInterface.sol"; import {Bytes32AddressLib} from "solmate/utils/Bytes32AddressLib.sol"; import { - ClonesWithImmutableArgs -} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; + Create2ClonesWithImmutableArgs +} from "create2-clones-with-immutable-args/Create2ClonesWithImmutableArgs.sol"; import { ConduitControllerInterface @@ -563,13 +563,14 @@ contract CollateralToken is require(ERC721(msg.sender).ownerOf(tokenId_) == address(this)); if (s.clearingHouse[collateralId] == address(0)) { - address clearingHouse = ClonesWithImmutableArgs.clone( + address clearingHouse = Create2ClonesWithImmutableArgs.clone( s.ASTARIA_ROUTER.BEACON_PROXY_IMPLEMENTATION(), abi.encodePacked( address(s.ASTARIA_ROUTER), uint8(IAstariaRouter.ImplementationType.ClearingHouse), collateralId - ) + ), + bytes32(collateralId) ); s.clearingHouse[collateralId] = clearingHouse; diff --git a/src/LienToken.sol b/src/LienToken.sol index e7c81388..6f5ecff1 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -254,7 +254,7 @@ contract LienToken is ERC721, ILienToken, AuthInitializable, AmountDeriver { ); } - s.TRANSFER_PROXY.tokenTransferFrom( + s.TRANSFER_PROXY.tokenTransferFromWithErrorReceiver( params.encumber.stack[params.position].lien.token, msg.sender, payee, @@ -721,7 +721,12 @@ contract LienToken is ERC721, ILienToken, AuthInitializable, AmountDeriver { payment = owing; } if (payment > 0) - s.TRANSFER_PROXY.tokenTransferFrom(token, payer, payee, payment); + s.TRANSFER_PROXY.tokenTransferFromWithErrorReceiver( + token, + payer, + payee, + payment + ); delete s.lienMeta[lienId]; //full delete delete stack[position]; @@ -929,7 +934,12 @@ contract LienToken is ERC721, ILienToken, AuthInitializable, AmountDeriver { activeStack = _removeStackPosition(activeStack, position); } - s.TRANSFER_PROXY.tokenTransferFrom(stack.lien.token, payer, payee, amount); + s.TRANSFER_PROXY.tokenTransferFromWithErrorReceiver( + stack.lien.token, + payer, + payee, + amount + ); emit Payment(lienId, amount); return (activeStack, amount); diff --git a/src/PublicVault.sol b/src/PublicVault.sol index 4ecb01cc..1aa7662e 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -26,8 +26,8 @@ import {IERC20} from "core/interfaces/IERC20.sol"; import {IERC20Metadata} from "core/interfaces/IERC20Metadata.sol"; import {ERC20Cloned} from "gpl/ERC20-Cloned.sol"; import { - ClonesWithImmutableArgs -} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; + Create2ClonesWithImmutableArgs +} from "create2-clones-with-immutable-args/Create2ClonesWithImmutableArgs.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; @@ -220,7 +220,7 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { uint64 epoch ) internal { if (s.epochData[epoch].withdrawProxy == address(0)) { - s.epochData[epoch].withdrawProxy = ClonesWithImmutableArgs.clone( + s.epochData[epoch].withdrawProxy = Create2ClonesWithImmutableArgs.clone( IAstariaRouter(ROUTER()).BEACON_PROXY_IMPLEMENTATION(), abi.encodePacked( address(ROUTER()), // router is the beacon @@ -228,7 +228,8 @@ contract PublicVault is VaultImplementation, IPublicVault, ERC4626Cloned { asset(), // token address(this), // vault epoch + 1 // claimable epoch - ) + ), + keccak256(abi.encodePacked(address(this), epoch)) ); } } diff --git a/src/TransferProxy.sol b/src/TransferProxy.sol index d998a28b..300d49ee 100644 --- a/src/TransferProxy.sol +++ b/src/TransferProxy.sol @@ -15,14 +15,52 @@ pragma solidity =0.8.17; import {Auth, Authority} from "solmate/auth/Auth.sol"; import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; +import { + Create2ClonesWithImmutableArgs +} from "create2-clones-with-immutable-args/Create2ClonesWithImmutableArgs.sol"; +import {Clone} from "create2-clones-with-immutable-args/Clone.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; +//error receivber is a simple contract that only transfers tokens to the owner +//it is used to test the transfer proxy +contract Receiver is Clone { + using SafeTransferLib for ERC20; + + function withdraw(ERC20 token, uint256 amount) public { + require(msg.sender == _getArgAddress(0), "only owner can withdraw"); + token.safeTransfer(msg.sender, token.balanceOf(address(this))); + } +} + contract TransferProxy is Auth, ITransferProxy { using SafeTransferLib for ERC20; + mapping(address => address) public errorReceivers; + address public immutable receiverImplementation; - constructor(Authority _AUTHORITY) Auth(msg.sender, _AUTHORITY) { - //only constructor we care about is Auth + constructor( + Authority _AUTHORITY, + address _receiverImplementation + ) Auth(msg.sender, _AUTHORITY) { + receiverImplementation = _receiverImplementation; + } + + function _transferToErrorReceiver( + address token, + address from, + address to, + uint256 amount + ) internal { + // Ensure that an initial owner has been supplied. + + if (errorReceivers[to] == address(0)) { + errorReceivers[to] = Create2ClonesWithImmutableArgs.clone( + receiverImplementation, + abi.encodePacked(to), + keccak256(abi.encodePacked(to)) + ); + } + ERC20(token).safeTransferFrom(from, errorReceivers[to], amount); } function tokenTransferFrom( @@ -33,4 +71,15 @@ contract TransferProxy is Auth, ITransferProxy { ) external requiresAuth { ERC20(token).safeTransferFrom(from, to, amount); } + + function tokenTransferFromWithErrorReceiver( + address token, + address from, + address to, + uint256 amount + ) external requiresAuth { + try ERC20(token).transferFrom(from, to, amount) {} catch { + _transferToErrorReceiver(token, from, to, amount); + } + } } diff --git a/src/WithdrawVaultBase.sol b/src/WithdrawVaultBase.sol index cce15ff7..f6b6d87c 100644 --- a/src/WithdrawVaultBase.sol +++ b/src/WithdrawVaultBase.sol @@ -16,7 +16,7 @@ import {IRouterBase} from "core/interfaces/IRouterBase.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {IWithdrawProxy} from "core/interfaces/IWithdrawProxy.sol"; import {IERC4626} from "core/interfaces/IERC4626.sol"; -import {Clone} from "clones-with-immutable-args/Clone.sol"; +import {Clone} from "create2-clones-with-immutable-args/Clone.sol"; abstract contract WithdrawVaultBase is Clone, IWithdrawProxy { function name() public view virtual returns (string memory); diff --git a/src/interfaces/ITransferProxy.sol b/src/interfaces/ITransferProxy.sol index 1f2345fa..a618b429 100644 --- a/src/interfaces/ITransferProxy.sol +++ b/src/interfaces/ITransferProxy.sol @@ -20,4 +20,11 @@ interface ITransferProxy { address to, uint256 amount ) external; + + function tokenTransferFromWithErrorReceiver( + address token, + address from, + address to, + uint256 amount + ) external; } diff --git a/src/scripts/deployments/Deploy.sol b/src/scripts/deployments/Deploy.sol index 0e2b19c9..3a674633 100644 --- a/src/scripts/deployments/Deploy.sol +++ b/src/scripts/deployments/Deploy.sol @@ -30,7 +30,7 @@ import {LienToken} from "core/LienToken.sol"; import {AstariaRouter} from "core/AstariaRouter.sol"; import {Vault} from "core/Vault.sol"; import {PublicVault} from "core/PublicVault.sol"; -import {TransferProxy} from "core/TransferProxy.sol"; +import {TransferProxy, Receiver} from "core/TransferProxy.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; @@ -123,7 +123,7 @@ contract Deploy is Script { ); } - TRANSFER_PROXY = new TransferProxy(MRA); + TRANSFER_PROXY = new TransferProxy(MRA, address(new Receiver())); if (testModeDisabled) { // vm.setEnv("TRANSFER_PROXY_ADDR", address(TRANSFER_PROXY)); vm.writeLine( @@ -401,7 +401,7 @@ contract Deploy is Script { MRA.setRoleCapability( uint8(UserRoles.LIEN_TOKEN), - TRANSFER_PROXY.tokenTransferFrom.selector, + TRANSFER_PROXY.tokenTransferFromWithErrorReceiver.selector, true ); diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index dcb7a9e3..381b9a1c 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -23,18 +23,62 @@ import { } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; -import {TransferProxy} from "../TransferProxy.sol"; +import {Receiver, TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; +import { + Create2ClonesWithImmutableArgs +} from "create2-clones-with-immutable-args/Create2ClonesWithImmutableArgs.sol"; + +contract MockERC20 is ERC20 { + mapping(address => bool) public blacklist; + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals + ) ERC20(_name, _symbol, _decimals) {} + + function mint(address to, uint256 value) public virtual { + _mint(to, value); + } + + function burn(address from, uint256 value) public virtual { + _burn(from, value); + } + + function setBlacklist(address addr, bool isBlacklisted) public { + blacklist[addr] = isBlacklisted; + } + + function transfer(address to, uint256 amount) public override returns (bool) { + require(!blacklist[msg.sender], "blacklisted"); + require(!blacklist[to], "blacklisted"); + return super.transfer(to, amount); + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public override returns (bool) { + require(!blacklist[from], "blacklisted"); + require(!blacklist[to], "blacklisted"); + require(!blacklist[msg.sender], "blacklisted"); + return super.transferFrom(from, to, amount); + } +} contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; @@ -142,7 +186,11 @@ contract AstariaTest is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + token: address(WETH9), + addr: strategistOne, + amountToLend: 50 ether + }), privateVault ); @@ -560,6 +608,67 @@ contract AstariaTest is TestHelpers { assert(LIEN_TOKEN.COLLATERAL_TOKEN() == ICollateralToken(address(0))); } + function testBasicPrivateVaultLoanBlacklistWrapper() public { + TestNFT nft = new TestNFT(2); + address tokenContract = address(nft); + uint256 tokenId = uint256(1); + + uint256 initialBalance = WETH9.balanceOf(address(this)); + MockERC20 token = new MockERC20("Test", "TST", 18); + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo, + token: address(token) + }); + vm.label(privateVault, "privateVault"); + token.mint(strategistOne, 50 ether); + token.mint(strategistOne, 50 ether); + _lendToPrivateVault( + PrivateLender({ + token: address(token), + addr: strategistOne, + amountToLend: 50 ether + }), + privateVault + ); + + ( + uint256[] memory lienIds, + ILienToken.Stack[] memory stack + ) = _commitToLien({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + assertEq(token.balanceOf(address(this)), initialBalance + 10 ether); + token.setBlacklist(strategistOne, true); + _repay(stack, 0, 10 ether, address(this)); + + address receiverCreated = Create2ClonesWithImmutableArgs.deriveAddress( + address(TRANSFER_PROXY), + TRANSFER_PROXY.receiverImplementation(), + abi.encodePacked(strategistOne), + keccak256(abi.encodePacked(strategistOne)) + ); + + assertEq(receiverCreated.code.length > 0, true, "receiver has no code"); + assertEq( + token.balanceOf(receiverCreated), + 10 ether, + "receiver has no tokens" + ); + token.setBlacklist(strategistOne, false); + vm.startPrank(strategistOne); + + Receiver(receiverCreated).withdraw(ERC20(address(token)), 10 ether); + } + function testEpochProcessionMultipleActors() public { address alice = address(1); address bob = address(2); diff --git a/src/test/ForkedTesting.t.sol b/src/test/ForkedTesting.t.sol index 54f33c60..175715a8 100644 --- a/src/test/ForkedTesting.t.sol +++ b/src/test/ForkedTesting.t.sol @@ -77,7 +77,11 @@ contract ForkedTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + token: address(WETH9), + addr: strategistOne, + amountToLend: 50 ether + }), privateVault ); address[] memory assets; diff --git a/src/test/RefinanceTesting.t.sol b/src/test/RefinanceTesting.t.sol index b065c959..f8bab9fd 100644 --- a/src/test/RefinanceTesting.t.sol +++ b/src/test/RefinanceTesting.t.sol @@ -113,7 +113,11 @@ contract RefinanceTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + addr: strategistOne, + token: address(WETH9), + amountToLend: 50 ether + }), privateVault ); vm.startPrank(strategistTwo); @@ -236,7 +240,11 @@ contract RefinanceTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + addr: strategistOne, + token: address(WETH9), + amountToLend: 50 ether + }), privateVault ); @@ -541,7 +549,11 @@ contract RefinanceTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + addr: strategistOne, + token: address(WETH9), + amountToLend: 50 ether + }), privateVault ); @@ -657,16 +669,15 @@ contract RefinanceTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + addr: strategistOne, + token: address(WETH9), + amountToLend: 50 ether + }), privateVault ); vm.startPrank(strategistTwo); - // vm.expectRevert( - // abi.encodeWithSelector( - // ILienToken.InvalidSender.selector - // ) - // ); LIEN_TOKEN.buyoutLien( ILienToken.LienActionBuyout({ chargeable: true, diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index cedf25de..0949dd4e 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -207,7 +207,11 @@ contract RevertTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + addr: strategistOne, + token: address(WETH9), + amountToLend: 50 ether + }), privateVault ); @@ -250,7 +254,7 @@ contract RevertTesting is TestHelpers { epochLength, vaultFee ), - keccak256(abi.encode(strategistTwo, blockhash(block.number - 1))) + keccak256(abi.encodePacked(strategistTwo, blockhash(block.number - 1))) ); vm.startPrank(strategistOne); @@ -290,7 +294,11 @@ contract RevertTesting is TestHelpers { ASTARIA_ROUTER.__emergencyPause(); _lendToPrivateVault( - Lender({addr: strategistTwo, amountToLend: 50 ether}), + PrivateLender({ + addr: strategistTwo, + token: address(WETH9), + amountToLend: 50 ether + }), privateVault ); } @@ -308,7 +316,11 @@ contract RevertTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 500 ether}), + PrivateLender({ + addr: strategistOne, + token: address(WETH9), + amountToLend: 500 ether + }), privateVault ); @@ -364,7 +376,11 @@ contract RevertTesting is TestHelpers { }); _lendToPrivateVault( - Lender({addr: strategistOne, amountToLend: 50 ether}), + PrivateLender({ + addr: strategistOne, + token: address(WETH9), + amountToLend: 50 ether + }), privateVault ); diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 50d1d195..139e5827 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -930,6 +930,11 @@ contract TestHelpers is Deploy, ConsiderationTester { address addr; uint256 amountToLend; } + struct PrivateLender { + address addr; + uint256 amountToLend; + address token; + } function _lendToVault(Lender memory lender, address vault) internal { vm.deal(lender.addr, lender.amountToLend); @@ -946,11 +951,19 @@ contract TestHelpers is Deploy, ConsiderationTester { vm.stopPrank(); } - function _lendToPrivateVault(Lender memory lender, address vault) internal { - vm.deal(lender.addr, lender.amountToLend); + function _lendToPrivateVault( + PrivateLender memory lender, + address vault + ) internal { vm.startPrank(lender.addr); - WETH9.deposit{value: lender.amountToLend}(); - WETH9.approve(vault, lender.amountToLend); + + if (lender.token == address(WETH9) || lender.token == address(0)) { + vm.deal(lender.addr, lender.amountToLend); + WETH9.deposit{value: lender.amountToLend}(); + WETH9.approve(vault, lender.amountToLend); + } else { + ERC20(lender.token).approve(vault, type(uint256).max); + } //min slippage on the deposit Vault(vault).deposit(lender.amountToLend, lender.addr); @@ -977,11 +990,15 @@ contract TestHelpers is Deploy, ConsiderationTester { uint256 amount, address payer ) internal returns (ILienToken.Stack[] memory newStack) { - vm.deal(payer, amount * 3); - vm.startPrank(payer); - WETH9.deposit{value: amount * 2}(); - WETH9.approve(address(TRANSFER_PROXY), amount * 2); - WETH9.approve(address(LIEN_TOKEN), amount * 2); + if (stack[0].lien.token == address(WETH9)) { + vm.deal(payer, amount * 3); + vm.startPrank(payer); + WETH9.deposit{value: amount * 2}(); + WETH9.approve(address(TRANSFER_PROXY), amount * 2); + WETH9.approve(address(LIEN_TOKEN), amount * 2); + } else { + ERC20(stack[0].lien.token).approve(address(TRANSFER_PROXY), amount); + } newStack = LIEN_TOKEN.makePayment( stack[position].lien.collateralId, From 59675deeaadad313755410221a6d5541dda0c6f0 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Wed, 22 Feb 2023 15:14:06 -0400 Subject: [PATCH 39/39] Refactor/state cleanup2 (#32) * qa fixes * clearingHouse address and auction hash storage locations brought into the Asset struct in collateral token * chore: run lint:fix * delete auction state after settlement * remove unneeded payable --------- Co-authored-by: Andrew Redden <=> --- src/AstariaRouter.sol | 15 ++--- src/ClearingHouse.sol | 16 ++++- src/CollateralToken.sol | 98 +++++++++++++++-------------- src/LienToken.sol | 16 +---- src/interfaces/IAstariaRouter.sol | 5 +- src/interfaces/ICollateralToken.sol | 4 +- src/interfaces/ILienToken.sol | 13 ++-- 7 files changed, 81 insertions(+), 86 deletions(-) diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index bf72e8a2..e681edcd 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -103,8 +103,7 @@ contract AstariaRouter is uint8(ImplementationType.ClearingHouse) ] = _CLEARING_HOUSE_IMPL; s.BEACON_PROXY_IMPLEMENTATION = _BEACON_PROXY_IMPL; - s.auctionWindow = uint32(2 days); - s.auctionWindowBuffer = uint32(1 days); + s.auctionWindow = uint32(3 days); s.liquidationFeeNumerator = uint32(130); s.liquidationFeeDenominator = uint32(1000); @@ -274,12 +273,8 @@ contract AstariaRouter is FileType what = incoming.what; bytes memory data = incoming.data; if (what == FileType.AuctionWindow) { - (uint256 window, uint256 windowBuffer) = abi.decode( - data, - (uint256, uint256) - ); + uint256 window = abi.decode(data, (uint256)); s.auctionWindow = window.safeCastTo32(); - s.auctionWindowBuffer = windowBuffer.safeCastTo32(); } else if (what == FileType.LiquidationFee) { (uint256 numerator, uint256 denominator) = abi.decode( data, @@ -380,9 +375,9 @@ contract AstariaRouter is } } - function getAuctionWindow(bool includeBuffer) public view returns (uint256) { + function getAuctionWindow() public view returns (uint256) { RouterStorage storage s = _loadRouterSlot(); - return s.auctionWindow + (includeBuffer ? s.auctionWindowBuffer : 0); + return s.auctionWindow; } function _sliceUint( @@ -597,7 +592,7 @@ contract AstariaRouter is } RouterStorage storage s = _loadRouterSlot(); - uint256 auctionWindowMax = s.auctionWindow + s.auctionWindowBuffer; + uint256 auctionWindowMax = s.auctionWindow; s.LIEN_TOKEN.stopLiens( stack[position].lien.collateralId, diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol index 7dbcd20e..c7117f90 100644 --- a/src/ClearingHouse.sol +++ b/src/ClearingHouse.sol @@ -53,7 +53,8 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { } enum InvalidRequestReason { NOT_ENOUGH_FUNDS_RECEIVED, - NO_AUCTION + NO_AUCTION, + INVALID_ORDER } error InvalidRequest(InvalidRequestReason); @@ -86,7 +87,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { function setAuctionData(AuctionData calldata auctionData) external { IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); // get the router from the immutable arg - //only execute from the conduit + //only execute from the lien token require(msg.sender == address(ASTARIA_ROUTER.LIEN_TOKEN())); ClearingHouseStorage storage s = _getStorage(); @@ -179,6 +180,7 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { ); } ASTARIA_ROUTER.COLLATERAL_TOKEN().settleAuction(collateralId); + _deleteLocalState(); } function safeTransferFrom( @@ -219,7 +221,9 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { ASTARIA_ROUTER.COLLATERAL_TOKEN().getConduit(), order.parameters.offer[0].identifierOrCriteria ); - ASTARIA_ROUTER.COLLATERAL_TOKEN().SEAPORT().validate(listings); + if (!ASTARIA_ROUTER.COLLATERAL_TOKEN().SEAPORT().validate(listings)) { + revert InvalidRequest(InvalidRequestReason.INVALID_ORDER); + } } function transferUnderlying( @@ -244,5 +248,11 @@ contract ClearingHouse is AmountDeriver, Clone, IERC1155, IERC721Receiver { 0, s.auctionData.stack ); + _deleteLocalState(); + } + + function _deleteLocalState() internal { + ClearingHouseStorage storage s = _getStorage(); + delete s.auctionData; } } diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index 2aeb142a..742101ca 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -112,16 +112,17 @@ contract CollateralToken is uint256 collateralId = params.offer[0].token.computeId( params.offer[0].identifierOrCriteria ); - address liquidator = s.LIEN_TOKEN.getAuctionLiquidator(collateralId); - if ( - s.collateralIdToAuction[collateralId] == bytes32(0) || - liquidator == address(0) - ) { + if (s.idToUnderlying[collateralId].auctionHash == bytes32(0)) { //revert no auction revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); } + address liquidator = ClearingHouse( + s.idToUnderlying[collateralId].clearingHouse + ).getAuctionData().liquidator; + if ( - s.collateralIdToAuction[collateralId] != keccak256(abi.encode(params)) + s.idToUnderlying[collateralId].auctionHash != + keccak256(abi.encode(params)) ) { //revert auction params dont match revert InvalidCollateralState( @@ -137,13 +138,13 @@ contract CollateralToken is Asset memory underlying = s.idToUnderlying[collateralId]; address tokenContract = underlying.tokenContract; uint256 tokenId = underlying.tokenId; - ClearingHouse CH = ClearingHouse(payable(s.clearingHouse[collateralId])); + ClearingHouse CH = ClearingHouse( + address(s.idToUnderlying[collateralId].clearingHouse) + ); CH.settleLiquidatorNFTClaim(); _releaseToAddress(s, underlying, collateralId, liquidator); _settleAuction(s, collateralId); - delete s.idToUnderlying[collateralId]; - delete s.idToUnderlying[collateralId].tokenId; - delete s.idToUnderlying[collateralId].tokenContract; + s.idToUnderlying[collateralId].deposited = false; _burn(collateralId); } @@ -258,7 +259,7 @@ contract CollateralToken is if (s.LIEN_TOKEN.getCollateralState(collateralId) != bytes32(0)) { revert InvalidCollateralState(InvalidCollateralStates.ACTIVE_LIENS); } - if (s.collateralIdToAuction[collateralId] != bytes32(0)) { + if (s.idToUnderlying[collateralId].auctionHash != bytes32(0)) { revert InvalidCollateralState(InvalidCollateralStates.AUCTION_ACTIVE); } _; @@ -298,16 +299,17 @@ contract CollateralToken is } // transfer the NFT to the destination optimistically - ClearingHouse(s.clearingHouse[collateralId]).transferUnderlying( - addr, - tokenId, - address(receiver) - ); + ClearingHouse(s.idToUnderlying[collateralId].clearingHouse) + .transferUnderlying(addr, tokenId, address(receiver)); //trigger the flash action on the receiver if ( receiver.onFlashAction( - IFlashAction.Underlying(s.clearingHouse[collateralId], addr, tokenId), + IFlashAction.Underlying( + s.idToUnderlying[collateralId].clearingHouse, + addr, + tokenId + ), data ) != keccak256("FlashAction.onFlashAction") ) { @@ -324,7 +326,8 @@ contract CollateralToken is // validate that the NFT returned after the call if ( - IERC721(addr).ownerOf(tokenId) != address(s.clearingHouse[collateralId]) + IERC721(addr).ownerOf(tokenId) != + address(s.idToUnderlying[collateralId].clearingHouse) ) { revert FlashActionNFTNotReturned(); } @@ -339,12 +342,10 @@ contract CollateralToken is if (msg.sender != ownerOf(collateralId)) { revert InvalidSender(); } - Asset memory underlying = s.idToUnderlying[collateralId]; + Asset storage underlying = s.idToUnderlying[collateralId]; address tokenContract = underlying.tokenContract; _burn(collateralId); - delete s.idToUnderlying[collateralId]; - delete s.idToUnderlying[collateralId].tokenId; - delete s.idToUnderlying[collateralId].tokenContract; + underlying.deposited = false; _releaseToAddress(s, underlying, collateralId, releaseTo); } @@ -358,11 +359,12 @@ contract CollateralToken is uint256 collateralId, address releaseTo ) internal { - ClearingHouse(s.clearingHouse[collateralId]).transferUnderlying( - underlyingAsset.tokenContract, - underlyingAsset.tokenId, - releaseTo - ); + ClearingHouse(s.idToUnderlying[collateralId].clearingHouse) + .transferUnderlying( + underlyingAsset.tokenContract, + underlyingAsset.tokenId, + releaseTo + ); emit ReleaseTo( underlyingAsset.tokenContract, underlyingAsset.tokenId, @@ -414,7 +416,9 @@ contract CollateralToken is uint256 collateralId ) external view returns (ClearingHouse) { return - ClearingHouse(payable(_loadCollateralSlot().clearingHouse[collateralId])); + ClearingHouse( + _loadCollateralSlot().idToUnderlying[collateralId].clearingHouse + ); } function _generateValidOrderParameters( @@ -443,19 +447,19 @@ contract CollateralToken is uint256(0), prices[0], prices[1], - payable(address(s.clearingHouse[collateralId])) + payable(address(s.idToUnderlying[collateralId].clearingHouse)) ); considerationItems[1] = ConsiderationItem( ItemType.ERC1155, - s.clearingHouse[collateralId], + s.idToUnderlying[collateralId].clearingHouse, collateralId, prices[0], prices[1], - payable(s.clearingHouse[collateralId]) + payable(s.idToUnderlying[collateralId].clearingHouse) ); orderParameters = OrderParameters({ - offerer: s.clearingHouse[collateralId], + offerer: s.idToUnderlying[collateralId].clearingHouse, zone: address(this), // 0x20 offer: offer, consideration: considerationItems, @@ -510,29 +514,31 @@ contract CollateralToken is revert InvalidZone(); } - ClearingHouse(s.clearingHouse[collateralId]).validateOrder(listingOrder); + ClearingHouse(s.idToUnderlying[collateralId].clearingHouse).validateOrder( + listingOrder + ); emit ListedOnSeaport(collateralId, listingOrder); - s.collateralIdToAuction[collateralId] = keccak256( + s.idToUnderlying[collateralId].auctionHash = keccak256( abi.encode(listingOrder.parameters) ); } function settleAuction(uint256 collateralId) public { CollateralStorage storage s = _loadCollateralSlot(); - require(msg.sender == s.clearingHouse[collateralId]); + require(msg.sender == s.idToUnderlying[collateralId].clearingHouse); if ( - s.collateralIdToAuction[collateralId] == bytes32(0) || + s.idToUnderlying[collateralId].auctionHash == bytes32(0) || ERC721(s.idToUnderlying[collateralId].tokenContract).ownerOf( s.idToUnderlying[collateralId].tokenId ) == - s.clearingHouse[collateralId] + s.idToUnderlying[collateralId].clearingHouse ) { revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); } _settleAuction(s, collateralId); - delete s.idToUnderlying[collateralId]; + s.idToUnderlying[collateralId].deposited = false; _burn(collateralId); } @@ -540,7 +546,7 @@ contract CollateralToken is CollateralStorage storage s, uint256 collateralId ) internal { - delete s.collateralIdToAuction[collateralId]; + delete s.idToUnderlying[collateralId].auctionHash; } /** @@ -558,11 +564,11 @@ contract CollateralToken is CollateralStorage storage s = _loadCollateralSlot(); uint256 collateralId = msg.sender.computeId(tokenId_); - Asset memory incomingAsset = s.idToUnderlying[collateralId]; + Asset storage incomingAsset = s.idToUnderlying[collateralId]; if (incomingAsset.tokenContract == address(0)) { require(ERC721(msg.sender).ownerOf(tokenId_) == address(this)); - if (s.clearingHouse[collateralId] == address(0)) { + if (incomingAsset.clearingHouse == address(0)) { address clearingHouse = Create2ClonesWithImmutableArgs.clone( s.ASTARIA_ROUTER.BEACON_PROXY_IMPLEMENTATION(), abi.encodePacked( @@ -573,11 +579,11 @@ contract CollateralToken is bytes32(collateralId) ); - s.clearingHouse[collateralId] = clearingHouse; + incomingAsset.clearingHouse = clearingHouse; } ERC721(msg.sender).safeTransferFrom( address(this), - s.clearingHouse[collateralId], + incomingAsset.clearingHouse, tokenId_ ); @@ -587,10 +593,8 @@ contract CollateralToken is _mint(from_, collateralId); - s.idToUnderlying[collateralId] = Asset({ - tokenContract: msg.sender, - tokenId: tokenId_ - }); + incomingAsset.tokenContract = msg.sender; + incomingAsset.tokenId = tokenId_; emit Deposit721(msg.sender, tokenId_, collateralId, from_); return IERC721Receiver.onERC721Received.selector; diff --git a/src/LienToken.sol b/src/LienToken.sol index 6f5ecff1..acc26c6c 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -481,14 +481,8 @@ contract LienToken is ERC721, ILienToken, AuthInitializable, AmountDeriver { emit AddLien( params.lien.collateralId, uint8(params.stack.length), - lienId, - newStackSlot - ); - emit LienStackUpdated( - params.lien.collateralId, uint8(params.stack.length), - StackAction.ADD, - uint8(newStack.length) + newStackSlot ); } @@ -965,12 +959,8 @@ contract LienToken is ERC721, ILienToken, AuthInitializable, AmountDeriver { ++i; } } - emit LienStackUpdated( - stack[position].lien.collateralId, - position, - StackAction.REMOVE, - uint8(newStack.length) - ); + + emit RemoveLien(stack[position].lien.collateralId, position); } function _isPublicVault( diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index 112381f9..57d74f3a 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -53,7 +53,6 @@ interface IAstariaRouter is IPausable, IBeacon { struct RouterStorage { //slot 1 uint32 auctionWindow; - uint32 auctionWindowBuffer; uint32 liquidationFeeNumerator; uint32 liquidationFeeDenominator; uint32 maxEpochLength; @@ -61,7 +60,6 @@ interface IAstariaRouter is IPausable, IBeacon { uint32 protocolFeeNumerator; uint32 protocolFeeDenominator; //slot 2 - ERC20 WETH; //20 ICollateralToken COLLATERAL_TOKEN; //20 ILienToken LIEN_TOKEN; //20 ITransferProxy TRANSFER_PROXY; //20 @@ -194,9 +192,8 @@ interface IAstariaRouter is IPausable, IBeacon { /** * @notice Returns the current auction duration. - * @param includeBuffer Adds the current auctionWindowBuffer if true. */ - function getAuctionWindow(bool includeBuffer) external view returns (uint256); + function getAuctionWindow() external view returns (uint256); /** * @notice Computes the fee the protocol earns on loan origination from the protocolFee numerator and denominator. diff --git a/src/interfaces/ICollateralToken.sol b/src/interfaces/ICollateralToken.sol index 03ef8d06..472af803 100644 --- a/src/interfaces/ICollateralToken.sol +++ b/src/interfaces/ICollateralToken.sol @@ -44,8 +44,11 @@ interface ICollateralToken is IERC721 { ); struct Asset { + bool deposited; + address clearingHouse; address tokenContract; uint256 tokenId; + bytes32 auctionHash; } struct CollateralStorage { @@ -62,7 +65,6 @@ interface ICollateralToken is IERC721 { mapping(uint256 => Asset) idToUnderlying; //mapping of a security token hook for an nft's token contract address mapping(address => address) securityHooks; - mapping(uint256 => address) clearingHouse; } struct ListUnderlyingForSaleParams { diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index bd8569ab..ec7b9d69 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -40,7 +40,6 @@ interface ILienToken is IERC721 { struct LienStorage { uint8 maxLiens; - address WETH; ITransferProxy TRANSFER_PROXY; IAstariaRouter ASTARIA_ROUTER; ICollateralToken COLLATERAL_TOKEN; @@ -315,21 +314,19 @@ interface ILienToken is IERC721 { event AddLien( uint256 indexed collateralId, uint8 position, - uint256 indexed lienId, + uint8 stackLength, Stack stack ); + + event RemoveLien(uint256 indexed collateralId, uint8 position); + enum StackAction { CLEAR, ADD, REMOVE, REPLACE } - event LienStackUpdated( - uint256 indexed collateralId, - uint8 position, - StackAction action, - uint8 stackLength - ); + event RemovedLiens(uint256 indexed collateralId); event Payment(uint256 indexed lienId, uint256 amount); event BuyoutLien(address indexed buyer, uint256 lienId, uint256 buyout);