From 9cfa4c3b1b2d25a3a2909caffd5ef3cd4c789363 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 22 Nov 2022 00:09:46 -0400 Subject: [PATCH] Dutch Auctions Via Seaport - Gas Golfing, Tests Galore, and many bugs squashed (#112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * seaport wip * removed AH * latest flow to account for insights from seaport team * fix for testCancelAuction to be correct * seaport wip * royalty engine added, with mock * add updates to loan proof gen, and seaport test dpeloyer * remove comments, disable mock for tests, needs to be fuzzed * remove dead code * more cleanup, mocks dynamic, use tokenID 99 for royalties enabled * cleanup, removing dead code, and gas golfing * golfing * natspec update * more natspec cleanup * mend more natspec cleanup * mend more natspec cleanup * mend more natspec cleanup * add liquidation fees calculator * updates for warping, cleanup in general * implemented collateraltoken errors * Fix install instructions and add markdown filetree (#116) * Fix install instructions and add markdown filetree * Cleaned up filetree * cleanup * tests passing using a combo of match and fufillment * tests passing - v3 changes * move match flow down so the buffer is closer to the actual bid * fix static values for the test * implement testNewLienExceeds2XEpoch() (#105) * implement testNewLienExceeds2XEpoch() * lien duration check * commitToLien now caps duration at 2 weeks * privatevault commitolienfix * add length cap on refinance * moved timeToSecondEpochEnd into method * minor changes * implement _timeToSecondEndIfPublic * remove isValidVault check in onlyVaults function Co-authored-by: = * replaced transferfrom with safertransferfrom (#111) * replaced transferfrom with safertransferfrom * update ierc721 Co-authored-by: Joseph Delong * implement testFinalAuctionEnd() (#115) * Refinance update (#110) * lien duration check * updates requested Co-authored-by: Joseph Delong * add sdk pointer for v3 fixes * Golfed CeilDiv🏌 * Hole 9 on LienToken * Finished up LienToken * Fixed syntax * clearing house is now a beacon * split opensea fees apart from seaport file * remove jsons for deploying and use compiled considerations * rather than looping the array copy it to memory then update the position with the new lien * removed log * Golfed ceilDiv and LienToken (#117) * Fix install instructions and add markdown filetree (#116) * Fix install instructions and add markdown filetree * Cleaned up filetree * implement testNewLienExceeds2XEpoch() (#105) * implement testNewLienExceeds2XEpoch() * lien duration check * commitToLien now caps duration at 2 weeks * privatevault commitolienfix * add length cap on refinance * moved timeToSecondEpochEnd into method * minor changes * implement _timeToSecondEndIfPublic * remove isValidVault check in onlyVaults function Co-authored-by: = * replaced transferfrom with safertransferfrom (#111) * replaced transferfrom with safertransferfrom * update ierc721 Co-authored-by: Joseph Delong * implement testFinalAuctionEnd() (#115) * Refinance update (#110) * lien duration check * updates requested Co-authored-by: Joseph Delong * Golfed CeilDiv🏌 * Hole 9 on LienToken * Finished up LienToken * Fixed syntax Co-authored-by: Joseph Delong Co-authored-by: = Co-authored-by: Andrew Redden <=> * remove reserve from the code, its unused, add validate stack guard on getMaxPotentialDebt * fixes and tests for adding liens against invalid stacks * fix invalid rename * getter to get opensea fee fees and payee data * fix typo * updates for uni v3 liquidiator to check the underlying token amounts if mins are set * cleanup and poitner update * liquidationInitialAsk Issue #99 (#121) * Added reverts for invalid liquidationInitialAsks * Added testing for liquidationInitialAsk revert cases * fixes and tests for adding liens against invalid stacks * fix invalid rename * getter to get opensea fee fees and payee data * fix typo * Fixed expectReverts for liquidationInitialAsk cases Co-authored-by: Andrew Redden <=> * Feat/exceeds initial ask (#120) * initial incorrect implementation * maybe broken liquidationinitialask test * fix maxpotentialdebt and added invalid hash enum * fix liquidationinitialask * implemented expectreverts in reverttesting * update testhelpers Co-authored-by: Andrew Redden <=> Co-authored-by: Andrew Redden * implement testLiquidationPaymentsOverbid and testLiquidationNftTransfer (#122) * cleanup dead code * cleanup * remove unused check * remove validator asset * cleanup and more fixes * remove unused interface * remove comments * remove dead test * update readme * removed comments from test helper * more nits Co-authored-by: Andrew Redden <=> Co-authored-by: = Co-authored-by: Joseph Delong Co-authored-by: GregTheDev <40359730+0xgregthedev@users.noreply.github.com> Co-authored-by: GregTheDev --- .gitignore | 5 +- .gitmodules | 3 + README.md | 33 -- foundry.toml | 3 +- lib/astaria-sdk | 2 +- lib/gpl | 2 +- lib/seaport | 1 + remappings.txt | 4 +- scripts/loanProofGenerator.ts | 3 + scripts/typechain.sh | 4 +- src/AstariaRouter.sol | 198 +++---- src/AstariaVaultBase.sol | 5 - src/ClearingHouse.sol | 35 ++ src/CollateralToken.sol | 419 +++++++++++++- src/LienToken.sol | 460 +++++++++------- src/PublicVault.sol | 64 +-- src/Vault.sol | 5 +- src/VaultImplementation.sol | 42 +- src/WithdrawProxy.sol | 17 - src/actions/UNIV3/ClaimFees.sol | 12 +- src/interfaces/IAstariaRouter.sol | 114 ++-- src/interfaces/IAstariaVaultBase.sol | 3 - src/interfaces/ICollateralToken.sol | 110 +++- src/interfaces/IERC1155.sol | 141 +++++ src/interfaces/IERC1155Receiver.sol | 58 ++ src/interfaces/ILienToken.sol | 111 ++-- src/interfaces/IPublicVault.sol | 21 + src/interfaces/IRoyaltyEngine.sol | 47 ++ src/interfaces/IWithdrawProxy.sol | 31 ++ src/libraries/CollateralLookup.sol | 6 +- src/scripts/deployments/AstariaStack.sol | 1 - src/scripts/deployments/Deploy.sol | 141 +---- src/strategies/CollectionValidator.sol | 2 +- src/strategies/UNI_V3Validator.sol | 102 ++-- src/strategies/UniqueValidator.sol | 18 +- src/test/AstariaTest.t.sol | 469 +++++++++++++--- src/test/ForkedTesting.t.sol | 2 - src/test/IntegrationTest.t.sol | 59 +- src/test/RevertTesting.t.sol | 235 +++++++- src/test/TestHelpers.t.sol | 664 +++++++++++++++++++---- src/test/WithdrawTesting.t.sol | 98 ++-- src/test/utils/RoyaltyEngineMock.sol | 65 +++ src/utils/Math.sol | 7 +- 43 files changed, 2780 insertions(+), 1042 deletions(-) create mode 160000 lib/seaport create mode 100644 src/ClearingHouse.sol create mode 100644 src/interfaces/IERC1155.sol create mode 100644 src/interfaces/IERC1155Receiver.sol create mode 100644 src/interfaces/IRoyaltyEngine.sol create mode 100644 src/test/utils/RoyaltyEngineMock.sol diff --git a/.gitignore b/.gitignore index eaa76982..9aeba578 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ out lcov.info -scripts/loanProofGenerator.js -.idea \ No newline at end of file +.idea + +scripts/loanProofGenerator.js \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 48e19ebd..cd3a6163 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,3 +20,6 @@ [submodule "lib/astaria-sdk"] path = lib/astaria-sdk url = git@github.com:AstariaXYZ/astaria-sdk.git +[submodule "lib/seaport"] + path = lib/seaport + url = https://github.com/projectopensea/seaport diff --git a/README.md b/README.md index 60876ad2..239074a6 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,3 @@ -# Astaria contest details - -- 50,000 USDC main award pot -- Join [Sherlock Discord](https://discord.gg/MABEWyASkp) -- Submit findings using the issue page in your private contest repo (label issues as med or high) -- [Read for more details](https://docs.sherlock.xyz/audits/watsons) -- Starts October 20, 2022 15:00 UTC -- Ends November 03, 2022 15:00 UTC - -# Resources - -TBD - -# Audit scope - - -[astaria-gpl](https://github.com/AstariaXYZ/astaria-gpl) - -[astaria-core](https://github.com/sherlock-audit/2022-10-astaria) - -All contracts in these repos are in scope unless specified below - -Not in scope - -``` - -libraries/Base64.sol -libraries/CollateralLookup.sol -scripts/deployments/strategies/* -utils -test -``` - # Astaria Docs ```ml src diff --git a/foundry.toml b/foundry.toml index c4fc4c46..4af5a3b4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,6 +9,7 @@ remappings = [ 'solmate/=lib/solmate/src/', 'gpl/=lib/gpl/src/', 'clones-with-immutable-args/=lib/clones-with-immutable-args/src/', - "core/=./src/" + 'core/=./src/', + 'seaport/=lib/seaport/contracts', ] # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/lib/astaria-sdk b/lib/astaria-sdk index d0f41a50..1fe31eef 160000 --- a/lib/astaria-sdk +++ b/lib/astaria-sdk @@ -1 +1 @@ -Subproject commit d0f41a50d59989d1bc40962cbe9e6bb87d9d62e7 +Subproject commit 1fe31eef03c0af9de32819e1ef01f450200082f3 diff --git a/lib/gpl b/lib/gpl index ce7d52c4..afdd1dfb 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit ce7d52c491121a6e81ca4a4f77b0d8dba609cf64 +Subproject commit afdd1dfbef99e6690aeb6ead37116d73a84c2244 diff --git a/lib/seaport b/lib/seaport new file mode 160000 index 00000000..dfce06d0 --- /dev/null +++ b/lib/seaport @@ -0,0 +1 @@ +Subproject commit dfce06d02413636f324f73352b54a4497d63c310 diff --git a/remappings.txt b/remappings.txt index 18446551..0e03451e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,10 +1,8 @@ -auction-house/=lib/auction-house/src/ -auction/=lib/auction-house/src/ clones-with-immutable-args/=lib/clones-with-immutable-args/src/ -contracts/=lib/seaport/contracts/ forge-std/=lib/forge-std/src/ eip4626/=lib/foundry_eip-4626/src/ gpl/=lib/gpl/src/ solmate/=lib/solmate/src/ +seaport/=lib/seaport/contracts/ core/=./src/ diff --git a/scripts/loanProofGenerator.ts b/scripts/loanProofGenerator.ts index 06233b09..4079347a 100644 --- a/scripts/loanProofGenerator.ts +++ b/scripts/loanProofGenerator.ts @@ -15,6 +15,7 @@ if (detailsType === 0) { "uint256", "uint256", "uint256", + "uint256", ]; } else if (detailsType === 1) { mapping = [ @@ -25,6 +26,7 @@ if (detailsType === 0) { "uint256", "uint256", "uint256", + "uint256", ]; } else if (detailsType === 2) { mapping = [ @@ -43,6 +45,7 @@ if (detailsType === 0) { "uint256", "uint256", "uint256", + "uint256", ]; } // Create tree diff --git a/scripts/typechain.sh b/scripts/typechain.sh index 5b88c567..76727c2e 100644 --- a/scripts/typechain.sh +++ b/scripts/typechain.sh @@ -2,9 +2,9 @@ # This script is used to generate typechain types for the contracts in the myArray+=(item) -# define array and add all contracts from out.sol that are in typechainabi +# define array and add all contracts from optimized-out.sol that are in typechainabi -accepted_file_names=("AuctionHouse.sol" "CollateralToken.sol" "LienToken.sol" "MultiRolesAuthority.sol" "PublicVault.sol" "Vault.sol" "WithdrawProxy.sol" "AstariaRouter.sol" "VaultImplementation.sol") +accepted_file_names=(CollateralToken.sol" "LienToken.sol" "MultiRolesAuthority.sol" "PublicVault.sol" "Vault.sol" "WithdrawProxy.sol" "AstariaRouter.sol" "VaultImplementation.sol") forge build # loop through the array and generate types for each contract diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index 792cf5cf..296ae521 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -24,7 +24,6 @@ import { import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; @@ -40,6 +39,7 @@ import {ERC4626Router} from "gpl/ERC4626Router.sol"; import {ERC4626RouterBase} from "gpl/ERC4626RouterBase.sol"; import {IERC4626} from "core/interfaces/IERC4626.sol"; import {IPublicVault} from "core/interfaces/IPublicVault.sol"; +import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; /** * @title AstariaRouter @@ -51,8 +51,8 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { using CollateralLookup for address; using FixedPointMathLib for uint256; - bytes32 constant ROUTER_SLOT = - keccak256("xyz.astaria.AstariaRouter.storage.location"); + uint256 constant ROUTER_SLOT = + 0xb5d37468eefb1c75507259f9212a7d55dca0c7d08d9ef7be1cda5c5103eaa88e; /** * @dev Setup transfer authority and set up addresses for deployed CollateralToken, LienToken, TransferProxy contracts, as well as PublicVault and SoloVault implementations to clone. @@ -73,7 +73,8 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { address _VAULT_IMPL, address _SOLO_IMPL, address _WITHDRAW_IMPL, - address _BEACON_PROXY_IMPL + address _BEACON_PROXY_IMPL, + address _CLEARING_HOUSE_IMPL ) Auth(address(msg.sender), _AUTHORITY) { RouterStorage storage s = _loadRouterSlot(); @@ -84,6 +85,9 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { s.implementations[uint8(ImplementationType.PrivateVault)] = _SOLO_IMPL; s.implementations[uint8(ImplementationType.PublicVault)] = _VAULT_IMPL; s.implementations[uint8(ImplementationType.WithdrawProxy)] = _WITHDRAW_IMPL; + s.implementations[ + uint8(ImplementationType.ClearingHouse) + ] = _CLEARING_HOUSE_IMPL; s.BEACON_PROXY_IMPLEMENTATION = _BEACON_PROXY_IMPL; s.auctionWindow = uint32(2 days); s.auctionWindowBuffer = uint32(1 days); @@ -129,9 +133,8 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { } function _loadRouterSlot() internal pure returns (RouterStorage storage rs) { - bytes32 slot = ROUTER_SLOT; assembly { - rs.slot := slot + rs.slot := ROUTER_SLOT } } @@ -150,11 +153,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { return s.LIEN_TOKEN; } - function AUCTION_HOUSE() public view returns (IAuctionHouse) { - RouterStorage storage s = _loadRouterSlot(); - return s.AUCTION_HOUSE; - } - function TRANSFER_PROXY() public view returns (ITransferProxy) { RouterStorage storage s = _loadRouterSlot(); return s.TRANSFER_PROXY; @@ -189,20 +187,12 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { _unpause(); } - /** - * @notice Sets universal protocol parameters or changes the addresses for deployed contracts. - * @param files structs to file - */ function fileBatch(File[] calldata files) external requiresAuth { for (uint256 i = 0; i < files.length; i++) { file(files[i]); } } - /** - * @notice Sets universal protocol parameters or changes the addresses for deployed contracts. - * @param incoming incoming files - */ function file(File calldata incoming) public requiresAuth { RouterStorage storage s = _loadRouterSlot(); FileType what = incoming.what; @@ -275,9 +265,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { s.guardian = _guardian; } - /* @notice specially guarded file - * @param file incoming data to file - */ function fileGuardian(File[] calldata file) external { RouterStorage storage s = _loadRouterSlot(); require(address(msg.sender) == address(s.guardian)); @@ -288,9 +275,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { if (what == FileType.Implementation) { (uint8 implType, address addr) = abi.decode(data, (uint8, address)); s.implementations[implType] = addr; - } else if (what == FileType.AuctionHouse) { - address addr = abi.decode(data, (address)); - s.AUCTION_HOUSE = IAuctionHouse(addr); } else if (what == FileType.CollateralToken) { address addr = abi.decode(data, (address)); s.COLLATERAL_TOKEN = ICollateralToken(addr); @@ -344,7 +328,7 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { function validateCommitment( IAstariaRouter.Commitment calldata commitment, uint256 timeToSecondEpochEnd - ) external view returns (ILienToken.Lien memory lien) { + ) public view returns (ILienToken.Lien memory lien) { return _validateCommitment(_loadRouterSlot(), commitment, timeToSecondEpochEnd); } @@ -357,8 +341,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { if (block.timestamp > commitment.lienRequest.strategy.deadline) { revert InvalidCommitmentState(CommitmentState.EXPIRED); } - - uint256 strategyLength = 5; uint8 nlrType = uint8(_sliceUint(commitment.lienRequest.nlrDetails, 0)); if (s.strategyValidators[nlrType] == address(0)) { revert InvalidStrategy(nlrType); @@ -407,25 +389,28 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { //todo fix this //return from _executeCommitment is a stack array, this needs to be a multi dimension stack to support updates to many tokens at once function commitToLiens(IAstariaRouter.Commitment[] memory commitments) - external + public whenNotPaused returns (uint256[] memory lienIds, ILienToken.Stack[] memory stack) { RouterStorage storage s = _loadRouterSlot(); - uint256 totalBorrowed = 0; + uint256 totalBorrowed; lienIds = new uint256[](commitments.length); _transferAndDepositAssetIfAble( s, commitments[0].tokenContract, commitments[0].tokenId ); - for (uint256 i = 0; i < commitments.length; ++i) { + for (uint256 i; i < commitments.length; ) { if (i != 0) { commitments[i].lienRequest.stack = stack; } (lienIds[i], stack) = _executeCommitment(s, commitments[i]); totalBorrowed += commitments[i].lienRequest.amount; + unchecked { + ++i; + } } s.WETH.safeApprove(address(s.TRANSFER_PROXY), totalBorrowed); @@ -441,9 +426,18 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { address[] memory allowList = new address[](2); allowList[0] = address(msg.sender); allowList[1] = delegate; + RouterStorage storage s = _loadRouterSlot(); return - _newVault(uint256(0), delegate, uint256(0), true, allowList, uint256(0)); + _newVault( + s, + uint256(0), + delegate, + uint256(0), + true, + allowList, + uint256(0) + ); } function newPublicVault( @@ -453,9 +447,21 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { bool allowListEnabled, address[] calldata allowList, uint256 depositCap - ) external whenNotPaused returns (address) { + ) public whenNotPaused returns (address) { + RouterStorage storage s = _loadRouterSlot(); + if (s.minEpochLength > epochLength) { + revert IPublicVault.InvalidState( + IPublicVault.InvalidStates.EPOCH_TOO_LOW + ); + } + if (s.maxEpochLength < epochLength) { + revert IPublicVault.InvalidState( + IPublicVault.InvalidStates.EPOCH_TOO_HIGH + ); + } return _newVault( + s, epochLength, delegate, vaultFee, @@ -500,10 +506,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { ); } - /** - * @notice Returns whether a specific lien can be liquidated. - * @return A boolean value indicating whether the specified lien can be liquidated. - */ function canLiquidate(ILienToken.Stack memory stack) public view @@ -514,82 +516,41 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { msg.sender == s.COLLATERAL_TOKEN.ownerOf(stack.lien.collateralId)); } - function liquidate( - uint256 collateralId, - uint8 position, - ILienToken.Stack[] memory stack - ) external returns (uint256 reserve) { + function liquidate(ILienToken.Stack[] memory stack, uint8 position) + public + returns (OrderParameters memory listedOrder) + { if (!canLiquidate(stack[position])) { revert InvalidLienState(LienState.HEALTHY); } RouterStorage storage s = _loadRouterSlot(); uint256 auctionWindowMax = s.auctionWindow + s.auctionWindowBuffer; - ILienToken.AuctionStack[] - memory stackAtLiquidation = new ILienToken.AuctionStack[](stack.length); - (reserve, stackAtLiquidation) = s.LIEN_TOKEN.stopLiens( - collateralId, - auctionWindowMax, - stack - ); - - reserve += reserve.mulDivDown( - s.liquidationFeeNumerator, - s.liquidationFeeDenominator - ); - s.AUCTION_HOUSE.createAuction( - collateralId, - s.auctionWindow, + s.LIEN_TOKEN.stopLiens( + stack[position].lien.collateralId, auctionWindowMax, - msg.sender, - s.liquidationFeeNumerator, - s.liquidationFeeDenominator, - reserve, - stackAtLiquidation + stack, + msg.sender + ); + emit Liquidation(stack[position].lien.collateralId, position); + listedOrder = s.COLLATERAL_TOKEN.auctionVault( + ICollateralToken.AuctionVaultParams({ + settlementToken: address(s.WETH), + collateralId: stack[position].lien.collateralId, + maxDuration: uint256(s.auctionWindow + s.auctionWindowBuffer), + startingPrice: stack[0].lien.details.liquidationInitialAsk, + endingPrice: 1_000 wei + }) ); - - uint256[] memory fees = new uint256[](2); - fees[0] = s.liquidationFeeNumerator; - fees[1] = s.liquidationFeeDenominator; - - emit Liquidation(collateralId, position, reserve, fees); - } - - function cancelAuction(uint256 collateralId) external { - RouterStorage storage s = _loadRouterSlot(); - - require(msg.sender == s.COLLATERAL_TOKEN.ownerOf(collateralId)); - - s.AUCTION_HOUSE.cancelAuction(collateralId, msg.sender); - s.COLLATERAL_TOKEN.releaseToAddress(collateralId, msg.sender); - } - - function endAuction(uint256 collateralId) external { - RouterStorage storage s = _loadRouterSlot(); - - if (!s.AUCTION_HOUSE.auctionExists(collateralId)) { - revert InvalidCollateralState(CollateralStates.NO_AUCTION); - } - - address winner = s.AUCTION_HOUSE.endAuction(collateralId); - s.COLLATERAL_TOKEN.releaseToAddress(collateralId, winner); } - /** - * @notice Retrieves the fee PublicVault strategists earn on loan origination. - * @return The numerator and denominator used to compute the percentage fee strategists earn by receiving minted vault shares. - */ function getStrategistFee(uint256 amountIn) external view returns (uint256) { RouterStorage storage s = _loadRouterSlot(); return amountIn.mulDivDown(s.strategistFeeNumerator, s.strategistFeeDenominator); } - /** - * @notice Retrieves the fee the protocol earns on loan origination. - * @return The numerator and denominator used to compute the percentage fee taken by the protocol - */ function getProtocolFee(uint256 amountIn) external view returns (uint256) { RouterStorage storage s = _loadRouterSlot(); @@ -597,10 +558,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { amountIn.mulDivDown(s.protocolFeeNumerator, s.protocolFeeDenominator); } - /** - * @notice Retrieves the fee the liquidator earns for processing auctions - * @return The numerator and denominator used to compute the percentage fee taken by the liquidator - */ function getLiquidatorFee(uint256 amountIn) external view returns (uint256) { RouterStorage storage s = _loadRouterSlot(); @@ -611,11 +568,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { ); } - /** - * @notice Retrieves the fee the protocol earns on loan origination. - * @return The numerator and denominator used to compute the percentage fee taken by the protocol - */ - function getBuyoutFee(uint256 remainingInterestIn) external view @@ -638,37 +590,36 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { return _loadRouterSlot().vaults[vault] != address(0); } - /** - * @notice Determines whether a potential refinance meets the minimum requirements for replacing a lien. - * @param newLien The new Lien to replace the existing one. - * @param newLien The new Lien to replace the existing one. - * @return A boolean representing whether the potential refinance is valid. - */ function isValidRefinance( ILienToken.Lien calldata newLien, uint8 position, ILienToken.Stack[] calldata stack - ) external view returns (bool) { + ) 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) || + 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); } - //INTERNAL FUNCS - /** - * @dev Deploys a new PublicVault. - * @param epochLength The length of each epoch for the new PublicVault. - * @return vaultAddr The address for the new PublicVault. + * @dev Deploys a new Vault. + * @param epochLength The length of each epoch for a new PublicVault. If 0, deploys a PrivateVault. + * @param delegate The address of the Vault delegate. + * @param allowListEnabled Whether or not the Vault has an LP whitelist. + * @return vaultAddr The address for the new Vault. */ function _newVault( + RouterStorage storage s, uint256 epochLength, address delegate, uint256 vaultFee, @@ -678,11 +629,7 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { ) internal returns (address vaultAddr) { uint8 vaultType; - RouterStorage storage s = _loadRouterSlot(); if (epochLength > uint256(0)) { - if (s.minEpochLength > epochLength || epochLength > s.maxEpochLength) { - revert InvalidEpochLength(epochLength); - } vaultType = uint8(ImplementationType.PublicVault); } else { vaultType = uint8(ImplementationType.PrivateVault); @@ -719,11 +666,6 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { return vaultAddr; } - /** - * @dev validates msg sender is owner - * @param c The commitment Data - * @return the amount borrowed - */ function _executeCommitment( RouterStorage storage s, IAstariaRouter.Commitment memory c @@ -748,7 +690,7 @@ contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { ) internal { ERC721 token = ERC721(tokenContract); if (token.ownerOf(tokenId) == address(msg.sender)) { - ERC721(tokenContract).safeTransferFrom( + token.safeTransferFrom( address(msg.sender), address(s.COLLATERAL_TOKEN), tokenId, diff --git a/src/AstariaVaultBase.sol b/src/AstariaVaultBase.sol index 207287fe..3bf8357d 100644 --- a/src/AstariaVaultBase.sol +++ b/src/AstariaVaultBase.sol @@ -4,7 +4,6 @@ import {Clone} from "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"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IRouterBase} from "core/interfaces/IRouterBase.sol"; abstract contract AstariaVaultBase is Clone, IAstariaVaultBase { @@ -40,10 +39,6 @@ abstract contract AstariaVaultBase is Clone, IAstariaVaultBase { return _getArgUint256(125); } - function AUCTION_HOUSE() public view returns (IAuctionHouse) { - return ROUTER().AUCTION_HOUSE(); - } - function COLLATERAL_TOKEN() public view returns (ICollateralToken) { return ROUTER().COLLATERAL_TOKEN(); } diff --git a/src/ClearingHouse.sol b/src/ClearingHouse.sol new file mode 100644 index 00000000..de0a2681 --- /dev/null +++ b/src/ClearingHouse.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED + +/** + * __ ___ __ + * /\ /__' | /\ |__) | /\ + * /~~\ .__/ | /~~\ | \ | /~~\ + * + * Copyright (c) Astaria Labs, Inc + */ +pragma solidity ^0.8.17; + +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"; + +contract ClearingHouse is Clone { + using SafeTransferLib for ERC20; + + fallback() external payable { + IAstariaRouter ASTARIA_ROUTER = IAstariaRouter(_getArgAddress(0)); + require(msg.sender == address(ASTARIA_ROUTER.COLLATERAL_TOKEN().SEAPORT())); + WETH(payable(address(ASTARIA_ROUTER.WETH()))).deposit{value: msg.value}(); + uint256 payment = ASTARIA_ROUTER.WETH().balanceOf(address(this)); + ASTARIA_ROUTER.WETH().safeApprove( + address(ASTARIA_ROUTER.TRANSFER_PROXY()), + payment + ); + ASTARIA_ROUTER.LIEN_TOKEN().payDebtViaClearingHouse( + _getArgUint256(21), + payment + ); + } +} diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index 6eed2253..9322c357 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -12,7 +12,6 @@ pragma solidity ^0.8.17; pragma experimental ABIEncoderV2; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {IERC165} from "core/interfaces/IERC165.sol"; @@ -22,7 +21,6 @@ import {IFlashAction} from "core/interfaces/IFlashAction.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; import {ISecurityHook} from "core/interfaces/ISecurityHook.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; - import {Auth, Authority} from "solmate/auth/Auth.sol"; import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; @@ -30,18 +28,53 @@ import {ERC721} from "gpl/ERC721.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {VaultImplementation} from "core/VaultImplementation.sol"; - -contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { +import {ZoneInterface} from "seaport/interfaces/ZoneInterface.sol"; +import {Bytes32AddressLib} from "solmate/utils/Bytes32AddressLib.sol"; +import { + ClonesWithImmutableArgs +} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; + +import { + ConduitControllerInterface +} from "seaport/interfaces/ConduitControllerInterface.sol"; +import { + ConsiderationInterface +} from "seaport/interfaces/ConsiderationInterface.sol"; +import { + AdvancedOrder, + CriteriaResolver, + OfferItem, + ConsiderationItem, + ItemType, + OrderParameters, + OrderComponents, + OrderType, + Order +} from "seaport/lib/ConsiderationStructs.sol"; + +import {Consideration} from "seaport/lib/Consideration.sol"; +import {SeaportInterface} from "seaport/interfaces/SeaportInterface.sol"; +import {IRoyaltyEngine} from "core/interfaces/IRoyaltyEngine.sol"; + +contract CollateralToken is + Auth, + ERC721, + IERC721Receiver, + ICollateralToken, + ZoneInterface +{ using SafeTransferLib for ERC20; using CollateralLookup for address; - - bytes32 constant COLLATERAL_TOKEN_SLOT = - keccak256("xyz.astaria.collateral.token.storage.location"); + using FixedPointMathLib for uint256; + uint256 constant COLLATERAL_TOKEN_SLOT = + 0xd6569137fd08fe949c286f37fb9bd830d1299d041edc1069ea252b26b4db9551; constructor( Authority AUTHORITY_, ITransferProxy TRANSFER_PROXY_, - ILienToken LIEN_TOKEN_ + ILienToken LIEN_TOKEN_, + ConsiderationInterface SEAPORT_, + IRoyaltyEngine ROYALTY_REGISTRY_ ) Auth(msg.sender, Authority(AUTHORITY_)) ERC721("Astaria Collateral Token", "ACT") @@ -49,6 +82,47 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { CollateralStorage storage s = _loadCollateralSlot(); s.TRANSFER_PROXY = TRANSFER_PROXY_; s.LIEN_TOKEN = LIEN_TOKEN_; + s.SEAPORT = SEAPORT_; + s.OS_FEE_PAYEE = address(0x8De9C5A032463C561423387a9648c5C7BCC5BC90); + s.osFeeNumerator = uint16(250); + s.osFeeDenominator = uint16(10000); + s.ROYALTY_ENGINE = ROYALTY_REGISTRY_; + (, , address conduitController) = s.SEAPORT.information(); + bytes32 CONDUIT_KEY = Bytes32AddressLib.fillLast12Bytes(address(this)); + s.CONDUIT_KEY = CONDUIT_KEY; + s.CONDUIT_CONTROLLER = ConduitControllerInterface(conduitController); + + s.CONDUIT = s.CONDUIT_CONTROLLER.createConduit(CONDUIT_KEY, address(this)); + s.CONDUIT_CONTROLLER.updateChannel( + address(s.CONDUIT), + address(SEAPORT_), + true + ); + } + + function SEAPORT() public view returns (ConsiderationInterface) { + return _loadCollateralSlot().SEAPORT; + } + + function liquidatorNFTClaim(OrderParameters memory params) external { + CollateralStorage storage s = _loadCollateralSlot(); + + uint256 collateralId = params.offer[0].token.computeId( + params.offer[0].identifierOrCriteria + ); + address liquidator = s.LIEN_TOKEN.getAuctionData(collateralId).liquidator; + if (!s.collateralIdToAuction[collateralId] || liquidator == address(0)) { + //revert no auction + revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); + } + + if (block.timestamp < params.endTime) { + //auction hasn't ended yet + revert InvalidCollateralState(InvalidCollateralStates.AUCTION_ACTIVE); + } + + _settleAuction(s, collateralId); + _releaseToAddress(s, collateralId, liquidator); } function _loadCollateralSlot() @@ -56,12 +130,39 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { pure returns (CollateralStorage storage s) { - bytes32 position = COLLATERAL_TOKEN_SLOT; assembly { - s.slot := position + s.slot := COLLATERAL_TOKEN_SLOT } } + function isValidOrder( + bytes32 orderHash, + address caller, + address offerer, + bytes32 zoneHash + ) external view returns (bytes4 validOrderMagicValue) { + CollateralStorage storage s = _loadCollateralSlot(); + return + s.collateralIdToAuction[uint256(zoneHash)] + ? ZoneInterface.isValidOrder.selector + : bytes4(0xffffffff); + } + + // Called by Consideration whenever any extraData is provided by the caller. + function isValidOrderIncludingExtraData( + bytes32 orderHash, + address caller, + AdvancedOrder calldata order, + bytes32[] calldata priorOrderHashes, + CriteriaResolver[] calldata criteriaResolvers + ) external view returns (bytes4 validOrderMagicValue) { + CollateralStorage storage s = _loadCollateralSlot(); + return + s.collateralIdToAuction[uint256(order.parameters.zoneHash)] + ? ZoneInterface.isValidOrder.selector + : bytes4(0xffffffff); + } + function supportsInterface(bytes4 interfaceId) public view @@ -101,6 +202,37 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { } else if (what == FileType.FlashEnabled) { (address target, bool enabled) = abi.decode(data, (address, bool)); s.flashEnabled[target] = enabled; + } else if (what == FileType.OpenSeaFees) { + (address target, uint16 numerator, uint16 denominator) = abi.decode( + data, + (address, uint16, uint16) + ); + s.osFeeNumerator = numerator; + s.osFeeDenominator = denominator; + s.OS_FEE_PAYEE = target; + } else if (what == FileType.Seaport) { + s.SEAPORT = ConsiderationInterface(abi.decode(data, (address))); + (, , address conduitController) = s.SEAPORT.information(); + if (s.CONDUIT_KEY == bytes32(0)) { + s.CONDUIT_KEY = Bytes32AddressLib.fillLast12Bytes(address(this)); + } + s.CONDUIT_CONTROLLER = ConduitControllerInterface(conduitController); + (address conduit, bool exists) = s.CONDUIT_CONTROLLER.getConduit( + s.CONDUIT_KEY + ); + if (!exists) { + s.CONDUIT = s.CONDUIT_CONTROLLER.createConduit( + s.CONDUIT_KEY, + address(this) + ); + } else { + s.CONDUIT = conduit; + } + s.CONDUIT_CONTROLLER.updateChannel( + address(s.CONDUIT), + address(s.SEAPORT), + true + ); } else { revert UnsupportedFile(); } @@ -113,8 +245,8 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { if (s.LIEN_TOKEN.getCollateralState(collateralId) != bytes32(0)) { revert InvalidCollateralState(InvalidCollateralStates.ACTIVE_LIENS); } - if (s.ASTARIA_ROUTER.AUCTION_HOUSE().auctionExists(collateralId)) { - revert InvalidCollateralState(InvalidCollateralStates.AUCTION); + if (s.collateralIdToAuction[collateralId]) { + revert InvalidCollateralState(InvalidCollateralStates.AUCTION_ACTIVE); } _; } @@ -136,10 +268,16 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { CollateralStorage storage s = _loadCollateralSlot(); (addr, tokenId) = getUnderlying(collateralId); - require( - s.flashEnabled[addr] && - !s.ASTARIA_ROUTER.AUCTION_HOUSE().auctionExists(collateralId) - ); + if (!s.flashEnabled[addr]) { + revert InvalidCollateralState(InvalidCollateralStates.AUCTION_ACTIVE); + } + + if ( + s.LIEN_TOKEN.getCollateralState(collateralId) == bytes32("ACTIVE_AUCTION") + ) { + revert InvalidCollateralState(InvalidCollateralStates.AUCTION_ACTIVE); + } + IERC721 nft = IERC721(addr); bytes memory preTransferState; @@ -154,8 +292,8 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { // transfer the NFT to the destination optimistically nft.safeTransferFrom(address(this), address(receiver), tokenId); - // invoke the call passed by the msg.sender + //trigger the flash action on the receiver if ( receiver.onFlashAction(IFlashAction.Underlying(addr, tokenId), data) != keccak256("FlashAction.onFlashAction") @@ -183,10 +321,7 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { releaseCheck(collateralId) { CollateralStorage storage s = _loadCollateralSlot(); - if ( - msg.sender != address(s.ASTARIA_ROUTER) && - msg.sender != ownerOf(collateralId) - ) { + if (msg.sender != ownerOf(collateralId)) { revert InvalidSender(); } _releaseToAddress(s, collateralId, releaseTo); @@ -214,6 +349,16 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { emit ReleaseTo(underlyingAsset, assetId, releaseTo); } + function getConduitKey() public view returns (bytes32) { + CollateralStorage storage s = _loadCollateralSlot(); + return s.CONDUIT_KEY; + } + + function getConduit() public view returns (address) { + CollateralStorage storage s = _loadCollateralSlot(); + return s.CONDUIT; + } + /** * @notice Retrieve the address and tokenId of the underlying NFT of a CollateralToken. * @param collateralId The ID of the CollateralToken wrapping the NFT. @@ -250,6 +395,224 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { return _loadCollateralSlot().securityHooks[target]; } + function getClearingHouse(uint256 collateralId) + external + view + returns (address) + { + return (_loadCollateralSlot().clearingHouse[collateralId]); + } + + function listForSaleOnSeaport(ListUnderlyingForSaleParams calldata params) + external + onlyOwner(params.stack[0].lien.collateralId) + { + //check that the incoming listed price is above the max total debt the asset can occur by the time the listing expires + CollateralStorage storage s = _loadCollateralSlot(); + + //check the collateral isn't at auction + + if (s.collateralIdToAuction[params.stack[0].lien.collateralId]) { + revert InvalidCollateralState(InvalidCollateralStates.AUCTION_ACTIVE); + } + //fetch the current total debt of the asset + uint256 maxPossibleDebtAtMaxDuration = s + .LIEN_TOKEN + .getMaxPotentialDebtForCollateral( + params.stack, + block.timestamp + params.maxDuration + ); + + if (maxPossibleDebtAtMaxDuration > params.listPrice) { + revert ListPriceTooLow(); + } + + OrderParameters memory orderParameters = _generateValidOrderParameters( + s, + params.stack[0].lien.collateralId, + params.listPrice, + params.listPrice, + params.maxDuration + ); + + _listUnderlyingOnSeaport( + s, + params.stack[0].lien.collateralId, + Order(orderParameters, new bytes(0)) + ); + } + + function _generateValidOrderParameters( + CollateralStorage storage s, + uint256 collateralId, + uint256 startingPrice, + uint256 endingPrice, + uint256 maxDuration + ) internal returns (OrderParameters memory orderParameters) { + OfferItem[] memory offer = new OfferItem[](1); + + Asset memory underlying = s.idToUnderlying[collateralId]; + + offer[0] = OfferItem( + ItemType.ERC721, + underlying.tokenContract, + underlying.tokenId, + 1, + 1 + ); + + address payable[] memory recipients; + uint256[] memory royaltyStartingAmounts; + uint256[] memory royaltyEndingAmounts; + + try + s.ROYALTY_ENGINE.getRoyaltyView( + underlying.tokenContract, + underlying.tokenId, + startingPrice + ) + returns ( + address payable[] memory foundRecipients, + uint256[] memory foundAmounts + ) { + if (foundRecipients.length > 0) { + recipients = foundRecipients; + royaltyStartingAmounts = foundAmounts; + (, royaltyEndingAmounts) = s.ROYALTY_ENGINE.getRoyaltyView( + underlying.tokenContract, + underlying.tokenId, + endingPrice + ); + } + } catch { + //do nothing + } + ConsiderationItem[] memory considerationItems = new ConsiderationItem[]( + recipients.length > 0 ? 3 : 2 + ); + considerationItems[0] = ConsiderationItem( + ItemType.NATIVE, + address(0), + uint256(0), + startingPrice, + endingPrice, + payable(address(s.clearingHouse[collateralId])) + ); + considerationItems[1] = ConsiderationItem( + ItemType.NATIVE, + address(0), + uint256(0), + startingPrice.mulDivDown(uint256(s.osFeeNumerator), s.osFeeDenominator), + endingPrice.mulDivDown(uint256(s.osFeeNumerator), s.osFeeDenominator), + payable(s.OS_FEE_PAYEE) + ); + + if (recipients.length > 0) { + considerationItems[2] = ConsiderationItem( + ItemType.NATIVE, + address(0), + uint256(0), + royaltyStartingAmounts[0], + royaltyEndingAmounts[0], + payable(recipients[0]) // royalties + ); + } + //put in royalty considerationItems + + orderParameters = OrderParameters({ + offerer: address(this), + zone: address(this), // 0x20 + offer: offer, + consideration: considerationItems, + orderType: OrderType.FULL_OPEN, + startTime: uint256(block.timestamp), + endTime: uint256(block.timestamp + maxDuration), + zoneHash: bytes32(collateralId), + salt: uint256(blockhash(block.number)), + conduitKey: s.CONDUIT_KEY, // 0x120 + totalOriginalConsiderationItems: considerationItems.length + }); + } + + function auctionVault(AuctionVaultParams calldata params) + external + requiresAuth + returns (OrderParameters memory orderParameters) + { + CollateralStorage storage s = _loadCollateralSlot(); + + orderParameters = _generateValidOrderParameters( + s, + params.collateralId, + params.startingPrice, + params.endingPrice, + params.maxDuration + ); + + _listUnderlyingOnSeaport( + s, + params.collateralId, + Order(orderParameters, new bytes(0)) + ); + } + + function getOpenSeaData() + external + view + returns ( + address, + uint16, + uint16 + ) + { + CollateralStorage storage s = _loadCollateralSlot(); + return (s.OS_FEE_PAYEE, s.osFeeNumerator, s.osFeeDenominator); + } + + function _listUnderlyingOnSeaport( + CollateralStorage storage s, + uint256 collateralId, + Order memory listingOrder + ) internal { + //get total Debt and ensure its being sold for more than that + + if (listingOrder.parameters.conduitKey != s.CONDUIT_KEY) { + revert InvalidConduitKey(); + } + if (listingOrder.parameters.zone != address(this)) { + revert InvalidZone(); + } + + IERC721(listingOrder.parameters.offer[0].token).approve( + s.CONDUIT, + listingOrder.parameters.offer[0].identifierOrCriteria + ); + Order[] memory listings = new Order[](1); + listings[0] = listingOrder; + s.SEAPORT.validate(listings); + emit ListedOnSeaport(collateralId, listingOrder); + + s.collateralIdToAuction[uint256(listingOrder.parameters.zoneHash)] = true; + } + + event ListedOnSeaport(uint256 collateralId, Order listingOrder); + + function settleAuction(uint256 collateralId) public requiresAuth { + CollateralStorage storage s = _loadCollateralSlot(); + if (!s.collateralIdToAuction[collateralId]) { + revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); + } + _settleAuction(s, collateralId); + delete s.idToUnderlying[collateralId]; + _burn(collateralId); + } + + function _settleAuction(CollateralStorage storage s, uint256 collateralId) + internal + { + delete s.collateralIdToAuction[collateralId]; + } + /** * @dev Mints a new CollateralToken wrapping an NFT. * @param operator_ the approved sender that called safeTransferFrom @@ -263,9 +626,21 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { uint256 tokenId_, bytes calldata data_ ) external override whenNotPaused returns (bytes4) { + CollateralStorage storage s = _loadCollateralSlot(); uint256 collateralId = msg.sender.computeId(tokenId_); - CollateralStorage storage s = _loadCollateralSlot(); + if (s.clearingHouse[collateralId] == address(0)) { + address clearingHouse = ClonesWithImmutableArgs.clone( + s.ASTARIA_ROUTER.BEACON_PROXY_IMPLEMENTATION(), + abi.encodePacked( + address(s.ASTARIA_ROUTER), + uint8(IAstariaRouter.ImplementationType.ClearingHouse), + collateralId + ) + ); + + s.clearingHouse[collateralId] = clearingHouse; + } Asset memory incomingAsset = s.idToUnderlying[collateralId]; if (incomingAsset.tokenContract == address(0)) { require(ERC721(msg.sender).ownerOf(tokenId_) == address(this)); diff --git a/src/LienToken.sol b/src/LienToken.sol index c2d2adef..dd27afbe 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -16,7 +16,6 @@ import {Auth, Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {ERC721} from "gpl/ERC721.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IERC721} from "core/interfaces/IERC721.sol"; import {IERC165} from "core/interfaces/IERC165.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; @@ -31,9 +30,8 @@ import {ILienToken} from "core/interfaces/ILienToken.sol"; import {IPublicVault} from "core/interfaces/IPublicVault.sol"; import {VaultImplementation} from "./VaultImplementation.sol"; -import "./interfaces/ICollateralToken.sol"; -import "./interfaces/IAstariaRouter.sol"; -import "./interfaces/IPublicVault.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; /** * @title LienToken @@ -43,8 +41,11 @@ contract LienToken is ERC721, ILienToken, Auth { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; + using SafeTransferLib for ERC20; - bytes32 constant LIEN_SLOT = keccak256("xyz.astaria.lien.storage.location"); + //keccak256("xyz.astaria.lien.storage.location"); + bytes32 constant LIEN_SLOT = + 0x784074eabfd770c66f3ce9775f7de467d76c7082c00549d7c34362d167cafc4b; /** * @dev Setup transfer authority and initialize the buyoutNumerator and buyoutDenominator for the lien buyout premium. @@ -68,9 +69,8 @@ contract LienToken is ERC721, ILienToken, Auth { pure returns (LienStorage storage s) { - bytes32 slot = LIEN_SLOT; assembly { - s.slot := slot + s.slot := LIEN_SLOT } } @@ -78,15 +78,10 @@ contract LienToken is ERC721, ILienToken, Auth { FileType what = incoming.what; bytes memory data = incoming.data; LienStorage storage s = _loadLienStorageSlot(); - if (what == FileType.AuctionHouse) { - address addr = abi.decode(data, (address)); - s.AUCTION_HOUSE = IAuctionHouse(addr); - } else if (what == FileType.CollateralToken) { - address addr = abi.decode(data, (address)); - s.COLLATERAL_TOKEN = ICollateralToken(addr); + if (what == FileType.CollateralToken) { + s.COLLATERAL_TOKEN = ICollateralToken(abi.decode(data, (address))); } else if (what == FileType.AstariaRouter) { - address addr = abi.decode(data, (address)); - s.ASTARIA_ROUTER = IAstariaRouter(addr); + s.ASTARIA_ROUTER = IAstariaRouter(abi.decode(data, (address))); } else { revert UnsupportedFile(); } @@ -106,10 +101,7 @@ contract LienToken is ERC721, ILienToken, Auth { function buyoutLien(ILienToken.LienActionBuyout calldata params) external - validateStack( - params.encumber.stack[0].lien.collateralId, - params.encumber.stack - ) + validateStack(params.encumber.collateralId, params.encumber.stack) returns (Stack[] memory, Stack memory newStack) { if (msg.sender != params.encumber.receiver) { @@ -118,9 +110,7 @@ contract LienToken is ERC721, ILienToken, Auth { ); } - LienStorage storage s = _loadLienStorageSlot(); - - return _buyoutLien(s, params); + return _buyoutLien(_loadLienStorageSlot(), params); } function _buyoutLien( @@ -150,6 +140,13 @@ contract LienToken is ERC721, ILienToken, Auth { params.encumber.stack[params.position] ); + if ( + _getMaxPotentialDebtForCollateral(params.encumber.stack) > + params.encumber.lien.details.maxPotentialDebt + ) { + revert InvalidState(InvalidStates.DEBT_LIMIT); + } + if (params.encumber.lien.details.maxAmount < owed) { revert InvalidBuyoutDetails(params.encumber.lien.details.maxAmount, owed); } @@ -197,20 +194,13 @@ contract LienToken is ERC721, ILienToken, Auth { uint256 oldLienId, uint256 newLienId ) internal returns (ILienToken.Stack[] memory newStack) { - newStack = new ILienToken.Stack[](stack.length); - for (uint256 i = 0; i < stack.length; i++) { - if (i == position) { - newStack[i] = newLien; - _burn(oldLienId); - delete s.lienMeta[oldLienId]; - } else { - newStack[i] = stack[i]; - } - } + newStack = stack; + newStack[position] = newLien; + _burn(oldLienId); + delete s.lienMeta[oldLienId]; } function getInterest(Stack calldata stack) public view returns (uint256) { - LienStorage storage s = _loadLienStorageSlot(); return _getInterest(stack, block.timestamp); } @@ -235,8 +225,8 @@ contract LienToken is ERC721, ILienToken, Auth { modifier validateStack(uint256 collateralId, Stack[] memory stack) { LienStorage storage s = _loadLienStorageSlot(); bytes32 stateHash = s.collateralStateHash[collateralId]; - if (stateHash != bytes32(0)) { - require(keccak256(abi.encode(stack)) == stateHash, "invalid hash"); + if (stateHash != bytes32(0) && keccak256(abi.encode(stack)) != stateHash) { + revert InvalidState(InvalidStates.INVALID_HASH); } _; } @@ -244,41 +234,40 @@ contract LienToken is ERC721, ILienToken, Auth { function stopLiens( uint256 collateralId, uint256 auctionWindow, - Stack[] calldata stack - ) - external - validateStack(collateralId, stack) - requiresAuth - returns (uint256 reserve, AuctionStack[] memory lienIds) - { - (reserve, lienIds) = _stopLiens( - _loadLienStorageSlot(), - collateralId, - auctionWindow, - stack - ); + Stack[] calldata stack, + address liquidator + ) external validateStack(collateralId, stack) requiresAuth { + return + _stopLiens( + _loadLienStorageSlot(), + collateralId, + auctionWindow, + stack, + liquidator + ); } function _stopLiens( LienStorage storage s, uint256 collateralId, uint256 auctionWindow, - Stack[] calldata stack - ) internal returns (uint256 reserve, AuctionStack[] memory lienIds) { - reserve = 0; - lienIds = new AuctionStack[](stack.length); - - for (uint256 i = 0; i < stack.length; ++i) { - lienIds[i].lienId = stack[i].point.lienId; - lienIds[i].end = stack[i].point.end; + Stack[] calldata stack, + address liquidator + ) internal { + s.auctionData[collateralId].liquidator = liquidator; + for (uint256 i = 0; i < stack.length; ) { + AuctionStack memory auctionStack; + auctionStack.lienId = stack[i].point.lienId; + auctionStack.end = stack[i].point.end; uint88 owed; unchecked { owed = _getOwed(stack[i], block.timestamp); - reserve += owed; - s.lienMeta[stack[i].point.lienId].amountAtLiquidation = owed; + auctionStack.amountOwed = owed; + s.lienMeta[auctionStack.lienId].atLiquidation = true; } - address payee = _getPayee(s, lienIds[i].lienId); + s.auctionData[collateralId].stack.push(auctionStack); + address payee = _getPayee(s, auctionStack.lienId); if (_isPublicVault(s, payee)) { // update the public vault state and get the liquidation accountant back if any address withdrawProxyIfNearBoundary = IPublicVault(payee) @@ -292,9 +281,12 @@ contract LienToken is ERC721, ILienToken, Auth { ); if (withdrawProxyIfNearBoundary != address(0)) { - _setPayee(s, lienIds[i].lienId, withdrawProxyIfNearBoundary); + _setPayee(s, auctionStack.lienId, withdrawProxyIfNearBoundary); } } + unchecked { + ++i; + } } s.collateralStateHash[collateralId] = bytes32("ACTIVE_AUCTION"); } @@ -314,16 +306,12 @@ contract LienToken is ERC721, ILienToken, Auth { uint256 id ) public override(ERC721, IERC721) { LienStorage storage s = _loadLienStorageSlot(); - if (s.lienMeta[id].amountAtLiquidation > 0) { + if (s.lienMeta[id].atLiquidation) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); } super.transferFrom(from, to, id); } - function AUCTION_HOUSE() public view returns (IAuctionHouse) { - return _loadLienStorageSlot().AUCTION_HOUSE; - } - function ASTARIA_ROUTER() public view returns (IAstariaRouter) { return _loadLienStorageSlot().ASTARIA_ROUTER; } @@ -382,14 +370,26 @@ contract LienToken is ERC721, ILienToken, Auth { ) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); } - if (params.stack.length >= s.maxLiens) { revert InvalidState(InvalidStates.MAX_LIENS); } - uint256 maxPotentialDebt = getMaxPotentialDebtForCollateral(params.stack); + if ( + params.lien.details.liquidationInitialAsk < params.amount || + params.lien.details.liquidationInitialAsk == 0 + ) { + revert InvalidState(InvalidStates.INVALID_LIQUIDATION_INITIAL_ASK); + } - if (maxPotentialDebt > params.lien.details.maxPotentialDebt) { - revert InvalidState(InvalidStates.DEBT_LIMIT); + if (params.stack.length > 0) { + if (params.lien.collateralId != params.stack[0].lien.collateralId) { + revert InvalidState(InvalidStates.COLLATERAL_MISMATCH); + } + } + + if (params.stack.length > 0) { + if (params.lien.collateralId != params.stack[0].lien.collateralId) { + revert InvalidState(InvalidStates.COLLATERAL_MISMATCH); + } } unchecked { @@ -410,44 +410,89 @@ contract LienToken is ERC721, ILienToken, Auth { LienStorage storage s, Stack[] memory stack, Stack memory newSlot - ) internal view returns (Stack[] memory newStack) { + ) internal returns (Stack[] memory newStack) { newStack = new Stack[](stack.length + 1); - for (uint256 i = 0; i < stack.length; ++i) { - if (block.timestamp > stack[i].point.end) { + newStack[stack.length] = newSlot; + + uint256 potentialDebt = _getOwed(newSlot, newSlot.point.end); + for (uint256 i = stack.length; i > 0; ) { + uint256 j = i - 1; + newStack[j] = stack[j]; + if (block.timestamp > newStack[j].point.end) { revert InvalidState(InvalidStates.EXPIRED_LIEN); } - newStack[i] = stack[i]; + unchecked { + potentialDebt += _getOwed(newStack[j], newStack[j].point.end); + } + if (potentialDebt > newStack[j].lien.details.liquidationInitialAsk) { + revert InvalidState(InvalidStates.INITIAL_ASK_EXCEEDED); + } + + unchecked { + --i; + } + } + if ( + stack.length > 0 && potentialDebt > newSlot.lien.details.maxPotentialDebt + ) { + revert InvalidState(InvalidStates.DEBT_LIMIT); } - newStack[stack.length] = newSlot; } - function removeLiens( - uint256 collateralId, - AuctionStack[] memory remainingLiens - ) external requiresAuth { + function payDebtViaClearingHouse(uint256 collateralId, uint256 payment) + external + { LienStorage storage s = _loadLienStorageSlot(); - for (uint256 i = 0; i < remainingLiens.length; i++) { - address owner = ownerOf(remainingLiens[i].lienId); - address payee = _getPayee(s, remainingLiens[i].lienId); - if (_isPublicVault(s, owner) && payee == owner) { - IPublicVault(owner).decreaseYIntercept( - s.lienMeta[remainingLiens[i].lienId].amountAtLiquidation - ); - } + require(msg.sender == s.COLLATERAL_TOKEN.getClearingHouse(collateralId)); - delete s.lienMeta[remainingLiens[i].lienId]; - _burn(remainingLiens[i].lienId); //burn the underlying lien associated - } + uint256 spent = _payDebt(s, collateralId, payment, msg.sender); delete s.collateralStateHash[collateralId]; - emit RemovedLiens(collateralId); + + if (spent < payment) { + s.TRANSFER_PROXY.tokenTransferFrom( + s.WETH, + msg.sender, + s.COLLATERAL_TOKEN.ownerOf(collateralId), + payment - spent + ); + } + s.COLLATERAL_TOKEN.settleAuction(collateralId); } - function getAmountOwingAtLiquidation(uint256 lienId) + function _payDebt( + LienStorage storage s, + uint256 collateralId, + uint256 payment, + address payer + ) internal returns (uint256 totalSpent) { + AuctionStack[] storage stack = s.auctionData[collateralId].stack; + + uint256 liquidatorPayment = s.ASTARIA_ROUTER.getLiquidatorFee(payment); + + s.TRANSFER_PROXY.tokenTransferFrom( + s.WETH, + payer, + s.auctionData[collateralId].liquidator, + liquidatorPayment + ); + payment -= liquidatorPayment; + totalSpent += liquidatorPayment; + for (uint256 i = 0; i < stack.length; i++) { + uint256 spent; + unchecked { + spent = _paymentAH(s, collateralId, stack, i, payment, payer); + totalSpent += spent; + payment -= spent; + } + } + } + + function getAuctionData(uint256 collateralId) external view - returns (uint256) + returns (AuctionData memory) { - return _loadLienStorageSlot().lienMeta[lienId].amountAtLiquidation; + return _loadLienStorageSlot().auctionData[collateralId]; } function getAmountOwingAtLiquidation(ILienToken.Stack calldata stack) @@ -457,13 +502,14 @@ contract LienToken is ERC721, ILienToken, Auth { { return _loadLienStorageSlot() - .lienMeta[uint256(keccak256(abi.encode(stack.lien)))] - .amountAtLiquidation; + .auctionData[stack.lien.collateralId] + .stack[stack.point.lienId] + .amountOwed; } function validateLien(Lien memory lien) public view returns (uint256 lienId) { lienId = uint256(keccak256(abi.encode(lien))); - if (!_exists(lienId)) { + if (!_exists(uint256(keccak256(abi.encode(lien))))) { revert InvalidState(InvalidStates.INVALID_LIEN_ID); } } @@ -481,9 +527,7 @@ contract LienToken is ERC721, ILienToken, Auth { view returns (uint256, uint256) { - LienStorage storage s = _loadLienStorageSlot(); - - return _getBuyout(s, stack); + return _getBuyout(_loadLienStorageSlot(), stack); } function _getBuyout(LienStorage storage s, Stack calldata stack) @@ -491,115 +535,80 @@ contract LienToken is ERC721, ILienToken, Auth { view returns (uint256, uint256) { - uint256 remainingInterest = _getRemainingInterest(s, stack); uint256 owed = _getOwed(stack, block.timestamp); uint256 buyoutTotal = owed + - s.ASTARIA_ROUTER.getBuyoutFee(remainingInterest); - + s.ASTARIA_ROUTER.getBuyoutFee(_getRemainingInterest(s, stack)); return (owed, buyoutTotal); } - function makePayment(Stack[] calldata stack, uint256 amount) + function makePayment( + uint256 collateralId, + Stack[] calldata stack, + uint256 amount + ) public - validateStack(stack[0].lien.collateralId, stack) + validateStack(collateralId, stack) returns (Stack[] memory newStack) { - LienStorage storage s = _loadLienStorageSlot(); - (newStack, ) = _makePayment(s, stack, amount); + (newStack, ) = _makePayment(_loadLienStorageSlot(), stack, amount); } function makePayment( + uint256 collateralId, Stack[] calldata stack, uint8 position, uint256 amount ) external - validateStack(stack[0].lien.collateralId, stack) + validateStack(collateralId, stack) returns (Stack[] memory newStack) { - (newStack, ) = _payment( - _loadLienStorageSlot(), - stack, - position, - amount, - address(msg.sender) - ); - } - - function makePaymentAuctionHouse( - AuctionStack[] memory stack, - uint256 collateralId, - uint256 payment, - address payer - ) - external - requiresAuth - returns (AuctionStack[] memory outStack, uint256 spent) - { - spent = 0; - outStack = stack; LienStorage storage s = _loadLienStorageSlot(); - uint256 loops = stack.length; - for (uint256 i = 0; i < loops; i++) { - uint256 paymentMade; - (outStack, paymentMade) = _paymentAH( - s, - outStack, - collateralId, - payment, - payer - ); - unchecked { - spent += paymentMade; - } - } - if (outStack.length == 0) { - delete s.collateralStateHash[collateralId]; - } + (newStack, ) = _payment(s, stack, position, amount, address(msg.sender)); + _updateCollateralStateHash(s, collateralId, newStack); } function _paymentAH( LienStorage storage s, - AuctionStack[] memory stack, uint256 collateralId, + AuctionStack[] memory stack, + uint256 position, uint256 payment, address payer - ) internal returns (AuctionStack[] memory, uint256) { - uint256 lienId = stack[0].lienId; - uint256 end = stack[0].end; + ) internal returns (uint256) { + uint256 lienId = stack[position].lienId; + uint256 end = stack[position].end; + uint256 owing = stack[position].amountOwed; //checks the lien exists + address owner = ownerOf(lienId); address payee = _getPayee(s, lienId); - //owing at liquidation - if (s.lienMeta[lienId].amountAtLiquidation > payment.safeCastTo88()) { - s.lienMeta[lienId].amountAtLiquidation -= payment.safeCastTo88(); + if (owing > payment.safeCastTo88()) { + stack[position].amountOwed -= payment.safeCastTo88(); } else { - payment = s.lienMeta[lienId].amountAtLiquidation; - delete s.lienMeta[lienId]; //full delete - _burn(lienId); - AuctionStack[] memory newStack = new AuctionStack[](stack.length - 1); + payment = owing; + } + s.TRANSFER_PROXY.tokenTransferFrom(s.WETH, payer, payee, payment); - if (_isPublicVault(s, payee)) { - IPublicVault(payee).decreaseEpochLienCount( - IPublicVault(payee).getLienEpoch(uint64(end)) - ); - } + delete s.lienMeta[lienId]; //full delete + delete stack[position]; + _burn(lienId); - for (uint256 i = 1; i < stack.length; i++) { - newStack[i - 1] = stack[i]; + if (_isPublicVault(s, payee)) { + if (owner == payee) { + IPublicVault(payee).updateAfterLiquidationPayment( + IPublicVault.LiquidationPaymentParams({lienEnd: end}) + ); + } else { + IPublicVault(payee).decreaseEpochLienCount(stack[position].end); } - stack = newStack; } - - s.TRANSFER_PROXY.tokenTransferFrom(s.WETH, payer, payee, payment); - emit Payment(lienId, payment); - return (stack, payment); + return payment; } /** - - * @notice Have a specified payer make a payment for the debt against a CollateralToken. + * @dev Have a specified payer make a payment for the debt against a CollateralToken. * @param stack the stack for the payment * @param totalCapitalAvailable The amount to pay against the debts */ @@ -608,16 +617,32 @@ contract LienToken is ERC721, ILienToken, Auth { Stack[] calldata stack, uint256 totalCapitalAvailable ) internal returns (Stack[] memory newStack, uint256 spent) { - uint256 amount = totalCapitalAvailable; - for (uint256 i = 0; i < stack.length; ++i) { + uint256 n = stack.length; + for (uint256 i; i < n; ) { (newStack, spent) = _payment( s, stack, uint8(i), - amount, + totalCapitalAvailable, address(msg.sender) ); - amount -= spent; + totalCapitalAvailable -= spent; + unchecked { + ++i; + } + } + _updateCollateralStateHash(s, stack[0].lien.collateralId, newStack); + } + + function _updateCollateralStateHash( + LienStorage storage s, + uint256 collateralId, + Stack[] memory stack + ) internal { + if (stack.length == 0) { + delete s.collateralStateHash[collateralId]; + } else { + s.collateralStateHash[collateralId] = keccak256(abi.encode(stack)); } } @@ -630,22 +655,43 @@ contract LienToken is ERC721, ILienToken, Auth { ); } - /** - * @notice Computes the total amount owed on all liens against a CollateralToken. - * @return maxPotentialDebt the total possible debt for the collateral - */ function getMaxPotentialDebtForCollateral(Stack[] memory stack) + public + view + validateStack(stack[0].lien.collateralId, stack) + returns (uint256 maxPotentialDebt) + { + return _getMaxPotentialDebtForCollateral(stack); + } + + function _getMaxPotentialDebtForCollateral(Stack[] memory stack) public pure returns (uint256 maxPotentialDebt) + { + uint256 n = stack.length; + for (uint256 i; i < n; ) { + maxPotentialDebt += _getOwed(stack[i], stack[i].point.end); + unchecked { + ++i; + } + } + } + + function getMaxPotentialDebtForCollateral(Stack[] memory stack, uint256 end) + public + view + validateStack(stack[0].lien.collateralId, stack) + returns (uint256 maxPotentialDebt) { maxPotentialDebt = 0; for (uint256 i = 0; i < stack.length; ++i) { - maxPotentialDebt += _getOwed(stack[i], stack[i].point.end); + maxPotentialDebt += _getOwed(stack[i], end); } } function getOwed(Stack memory stack) external view returns (uint192) { + validateLien(stack.lien); return _getOwed(stack, block.timestamp); } @@ -654,7 +700,7 @@ contract LienToken is ERC721, ILienToken, Auth { view returns (uint192) { - uint256 lienId = validateLien(stack.lien); + validateLien(stack.lien); return _getOwed(stack, timestamp); } @@ -682,10 +728,7 @@ contract LienToken is ERC721, ILienToken, Auth { view returns (uint256) { - uint256 end = stack.point.end; - - uint256 delta_t = end - block.timestamp; - + uint256 delta_t = stack.point.end - block.timestamp; return delta_t.mulDivDown(stack.lien.details.rate, 1).mulWadDown( stack.point.amount @@ -707,16 +750,16 @@ contract LienToken is ERC721, ILienToken, Auth { ) internal returns (Stack[] memory, uint256) { Stack memory stack = activeStack[position]; uint256 lienId = stack.point.lienId; - uint256 liquidatedAmount = s.lienMeta[lienId].amountAtLiquidation; - if (liquidatedAmount > 0) { + + if (s.lienMeta[lienId].atLiquidation) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); } + uint64 end = stack.point.end; // Blocking off payments for a lien that has exceeded the lien.end to prevent repayment unless the msg.sender() is the AuctionHouse - if (block.timestamp > activeStack[position].point.end) { + if (block.timestamp > end) { revert InvalidLoanState(); } - uint256 owed = _getOwed(activeStack[position], block.timestamp); - + uint256 owed = _getOwed(stack, block.timestamp); address lienOwner = ownerOf(lienId); bool isPublicVault = _isPublicVault(s, lienOwner); @@ -749,20 +792,13 @@ contract LienToken is ERC721, ILienToken, Auth { // since the openLiens count is only positive when there are liens that haven't been paid off // that should be liquidated, this lien should not be counted anymore IPublicVault(lienOwner).decreaseEpochLienCount( - IPublicVault(lienOwner).getLienEpoch(stack.point.end) + IPublicVault(lienOwner).getLienEpoch(end) ); } delete s.lienMeta[lienId]; //full delete of point data for the lien _burn(lienId); activeStack = _removeStackPosition(activeStack, position); } - if (activeStack.length == 0) { - delete s.collateralStateHash[stack.lien.collateralId]; - } else { - s.collateralStateHash[stack.lien.collateralId] = keccak256( - abi.encode(activeStack) - ); - } s.TRANSFER_PROXY.tokenTransferFrom(s.WETH, payer, payee, amount); @@ -774,16 +810,27 @@ contract LienToken is ERC721, ILienToken, Auth { internal returns (Stack[] memory newStack) { - require(position < stack.length); - uint256 collateralId = stack[position].lien.collateralId; - - newStack = new ILienToken.Stack[](stack.length - 1); - for (uint256 i = 0; i < stack.length; i++) { - if (i == position) continue; + uint256 length = stack.length; + require(position < length); + newStack = new ILienToken.Stack[](length - 1); + uint256 i; + for (i; i < position; ) { newStack[i] = stack[i]; + unchecked { + ++i; + } + } + unchecked { + ++i; + } + for (i; i < length; ) { + unchecked { + newStack[i] = stack[i + 1]; + ++i; + } } emit LienStackUpdated( - collateralId, + stack[position].lien.collateralId, position, StackAction.REMOVE, uint8(newStack.length) @@ -801,11 +848,10 @@ contract LienToken is ERC721, ILienToken, Auth { } function getPayee(uint256 lienId) public view returns (address) { - LienStorage storage s = _loadLienStorageSlot(); if (!_exists(lienId)) { revert InvalidState(InvalidStates.INVALID_LIEN_ID); } - return _getPayee(s, lienId); + return _getPayee(_loadLienStorageSlot(), lienId); } function _getPayee(LienStorage storage s, uint256 lienId) @@ -825,7 +871,7 @@ contract LienToken is ERC721, ILienToken, Auth { require( msg.sender == ownerOf(lienId) || msg.sender == address(s.ASTARIA_ROUTER) ); - if (s.AUCTION_HOUSE.auctionExists(lien.collateralId)) { + if (s.lienMeta[lienId].atLiquidation) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); } _setPayee(s, lienId, newPayee); diff --git a/src/PublicVault.sol b/src/PublicVault.sol index 38e247df..56cd93bf 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -55,8 +55,8 @@ contract PublicVault is using SafeTransferLib for ERC20; using SafeCastLib for uint256; - bytes32 constant PUBLIC_VAULT_SLOT = - keccak256("xyz.astaria.PublicVault.storage.location"); + uint256 constant PUBLIC_VAULT_SLOT = + 0xc8b9e850684c861cb4124c86f9eebbd425d1f899eefe14aef183cd9cd8e16ef0; function asset() public @@ -152,39 +152,27 @@ contract PublicVault is } function getWithdrawProxy(uint64 epoch) public view returns (WithdrawProxy) { - VaultData storage s = _loadStorageSlot(); - - return WithdrawProxy(s.epochData[epoch].withdrawProxy); + return WithdrawProxy(_loadStorageSlot().epochData[epoch].withdrawProxy); } function getCurrentEpoch() public view returns (uint64) { - VaultData storage s = _loadStorageSlot(); - - return s.currentEpoch; + return _loadStorageSlot().currentEpoch; } function getSlope() public view returns (uint256) { - VaultData storage s = _loadStorageSlot(); - - return uint256(s.slope); + return uint256(_loadStorageSlot().slope); } function getWithdrawReserve() public view returns (uint256) { - VaultData storage s = _loadStorageSlot(); - - return uint256(s.withdrawReserve); + return uint256(_loadStorageSlot().withdrawReserve); } function getLiquidationWithdrawRatio() public view returns (uint256) { - VaultData storage s = _loadStorageSlot(); - - return s.liquidationWithdrawRatio; + return _loadStorageSlot().liquidationWithdrawRatio; } function getYIntercept() public view returns (uint256) { - VaultData storage s = _loadStorageSlot(); - - return s.yIntercept; + return _loadStorageSlot().yIntercept; } function _deployWithdrawProxyIfNotDeployed(VaultData storage s, uint64 epoch) @@ -271,14 +259,6 @@ contract PublicVault is s.epochData[s.currentEpoch].withdrawProxy ); - if (address(currentWithdrawProxy) != address(0)) { - if (currentWithdrawProxy.getFinalAuctionEnd() > block.timestamp) { - revert InvalidState( - InvalidStates.LIQUIDATION_ACCOUNTANT_FINAL_AUCTION_OPEN - ); - } - } - // split funds from previous WithdrawProxy with PublicVault if hasn't been already if (s.currentEpoch != 0) { WithdrawProxy previousWithdrawProxy = WithdrawProxy( @@ -418,9 +398,8 @@ contract PublicVault is } function _loadStorageSlot() internal pure returns (VaultData storage s) { - bytes32 slot = PUBLIC_VAULT_SLOT; assembly { - s.slot := slot + s.slot := PUBLIC_VAULT_SLOT } } @@ -496,7 +475,8 @@ contract PublicVault is _loadStorageSlot().strategistUnclaimedShares; } - function claim() external onlyOwner { + function claim() external { + require(msg.sender == owner()); //owner is "strategist" VaultData storage s = _loadStorageSlot(); uint256 unclaimed = s.strategistUnclaimedShares; s.strategistUnclaimedShares = 0; @@ -517,8 +497,7 @@ contract PublicVault is require( msg.sender == address(ROUTER()) || msg.sender == address(LIEN_TOKEN()) ); - VaultData storage s = _loadStorageSlot(); - _decreaseEpochLienCount(s, epoch); + _decreaseEpochLienCount(_loadStorageSlot(), epoch); } function _decreaseEpochLienCount(VaultData storage s, uint64 epoch) internal { @@ -544,10 +523,9 @@ contract PublicVault is } function afterPayment(uint256 computedSlope) public { - VaultData storage s = _loadStorageSlot(); require(msg.sender == address(LIEN_TOKEN())); unchecked { - s.slope += computedSlope.safeCastTo48(); + _loadStorageSlot().slope += computedSlope.safeCastTo48(); } } @@ -603,11 +581,20 @@ contract PublicVault is s.last = block.timestamp.safeCastTo40(); } - uint64 lienEpoch = getLienEpoch(params.lienEnd.safeCastTo64()); - _decreaseEpochLienCount(s, lienEpoch); + _decreaseEpochLienCount(s, getLienEpoch(params.lienEnd.safeCastTo64())); emit YInterceptChanged(s.yIntercept); } + function updateAfterLiquidationPayment( + LiquidationPaymentParams calldata params + ) external { + require(msg.sender == address(LIEN_TOKEN())); + _decreaseEpochLienCount( + _loadStorageSlot(), + getLienEpoch(params.lienEnd.safeCastTo64()) + ); + } + /** * @notice * @param maxAuctionWindow The max possible auction duration. @@ -668,8 +655,7 @@ contract PublicVault is } function timeToEpochEnd() public view returns (uint256) { - VaultData storage s = _loadStorageSlot(); - return timeToEpochEnd(s.currentEpoch); + return timeToEpochEnd(_loadStorageSlot().currentEpoch); } function timeToEpochEnd(uint256 epoch) public view returns (uint256) { diff --git a/src/Vault.sol b/src/Vault.sol index 7d2853bc..ae264d01 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -55,10 +55,11 @@ contract Vault is AstariaVaultBase, VaultImplementation { string(abi.encodePacked("AST-V", owner(), "-", ERC20(asset()).symbol())); } - function supportsInterface(bytes4 interfaceId) + function supportsInterface(bytes4) public pure virtual + override(IERC165) returns (bool) { return false; @@ -93,7 +94,7 @@ contract Vault is AstariaVaultBase, VaultImplementation { revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } - function modifyAllowList(address depositor, bool enabled) + function modifyAllowList(address, bool) external pure override(VaultImplementation) diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index 3847ec52..67e2caa7 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -15,8 +15,6 @@ import {ERC721, ERC721TokenReceiver} from "solmate/tokens/ERC721.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; - import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; @@ -45,8 +43,8 @@ abstract contract VaultImplementation is function symbol() public view virtual override returns (string memory); - bytes32 constant VI_SLOT = - keccak256("xyz.astaria.VaultImplementation.storage.location"); + uint256 constant VI_SLOT = + 0x8db05f23e24c991e45d8dd3599daf8e419ee5ab93565cf65b18905286a24ec14; function getStrategistNonce() external view returns (uint32) { return _loadVISlot().strategistNonce; @@ -65,14 +63,14 @@ abstract contract VaultImplementation is * @notice modify the deposit cap for the vault * @param newCap The deposit cap. */ - function modifyDepositCap(uint256 newCap) public onlyOwner { + function modifyDepositCap(uint256 newCap) public { + require(msg.sender == owner()); //owner is "strategist" _loadVISlot().depositCap = newCap.safeCastTo88(); } function _loadVISlot() internal pure returns (VIData storage vi) { - bytes32 slot = VI_SLOT; assembly { - vi.slot := slot + vi.slot := VI_SLOT } } @@ -81,25 +79,24 @@ abstract contract VaultImplementation is * @param depositor the depositor to modify * @param enabled the status of the depositor */ - function modifyAllowList(address depositor, bool enabled) - external - virtual - onlyOwner - { + function modifyAllowList(address depositor, bool enabled) external virtual { + require(msg.sender == owner()); //owner is "strategist" _loadVISlot().allowList[depositor] = enabled; } /** - * @notice disable the allowlist for the vault + * @notice disable the allowList for the vault */ - function disableAllowList() external virtual onlyOwner { + function disableAllowList() external virtual { + require(msg.sender == owner()); //owner is "strategist" _loadVISlot().allowListEnabled = false; } /** - * @notice enable the allowl ist for the vault + * @notice enable the allowList for the vault */ - function enableAllowList() external virtual onlyOwner { + function enableAllowList() external virtual { + require(msg.sender == owner()); //owner is "strategist" _loadVISlot().allowListEnabled = true; } @@ -130,7 +127,8 @@ abstract contract VaultImplementation is return _loadVISlot().isShutdown; } - function shutdown() external onlyOwner { + function shutdown() external { + require(msg.sender == owner()); //owner is "strategist" _loadVISlot().isShutdown = true; emit VaultShutdown(); } @@ -194,12 +192,8 @@ abstract contract VaultImplementation is } } - modifier onlyOwner() { + function setDelegate(address delegate_) external { require(msg.sender == owner()); //owner is "strategist" - _; - } - - function setDelegate(address delegate_) external onlyOwner { VIData storage s = _loadVISlot(); s.allowList[s.delegate] = false; s.allowList[delegate_] = true; @@ -275,7 +269,7 @@ abstract contract VaultImplementation is /** * @notice Pipeline for lifecycle of new loan origination. * Origination consists of a few phases: pre-commitment validation, lien token issuance, strategist reward, and after commitment actions - * Starts by depositing collateral and take out a lien against it. Next, verifies the merkle proof for a loan commitment. Vault owners are then rewarded fees for successful loan origination. + * 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. @@ -303,7 +297,7 @@ abstract contract VaultImplementation is } /** - * @notice Buy out a lien to replace it with new terms. + * @notice Buy optimized-out a lien to replace it with new terms. * @param collateralId The ID of the underlying CollateralToken. * @param position The position of the specified lien. * @param incomingTerms The loan terms of the new lien. diff --git a/src/WithdrawProxy.sol b/src/WithdrawProxy.sol index 6ab749ad..57fef64b 100644 --- a/src/WithdrawProxy.sol +++ b/src/WithdrawProxy.sol @@ -214,9 +214,6 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { s.withdrawReserveReceived += amount; } - /** - * @notice Return any excess funds to the PublicVault. - */ function claim() public { WPStorage storage s = _loadSlot(); @@ -267,11 +264,6 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { emit Claimed(address(this), transferAmount, VAULT(), balance); } - /** - * @notice Called by PublicVault if previous epoch's withdrawReserve hasn't been met. - * @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) public returns (uint256) @@ -285,10 +277,6 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { return amount; } - /** - * @notice Called at epoch boundary, computes the ratio between the funds of withdrawing liquidity providers and the balance of the underlying PublicVault so that claim() proportionally pays out to all parties. - * @param liquidationWithdrawRatio The ratio of withdrawing to remaining LPs for the current epoch boundary. - */ function setWithdrawRatio(uint256 liquidationWithdrawRatio) public { require(msg.sender == VAULT()); unchecked { @@ -296,11 +284,6 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { } } - /** - * @notice Adds an auction scheduled to end in a new epoch to this WithdrawProxy, to ensure that withdrawing LPs get a proportional share of auction returns. - * @param newLienExpectedValue The expected auction value for the lien being auctioned. - * @param finalAuctionDelta The timestamp by which the auction being added is guaranteed to end. As new auctions are added to the WithdrawProxy, this value will strictly increase as all auctions have the same maximum duration. - */ function handleNewLiquidation( uint256 newLienExpectedValue, uint256 finalAuctionDelta diff --git a/src/actions/UNIV3/ClaimFees.sol b/src/actions/UNIV3/ClaimFees.sol index 473b986b..0b4e9a53 100644 --- a/src/actions/UNIV3/ClaimFees.sol +++ b/src/actions/UNIV3/ClaimFees.sol @@ -3,14 +3,24 @@ pragma solidity ^0.8.17; import {IFlashAction} from "core/interfaces/IFlashAction.sol"; import {IV3PositionManager} from "core/interfaces/IV3PositionManager.sol"; import {ERC721} from "gpl/ERC721.sol"; +import {IERC721Receiver} from "core/interfaces/IERC721Receiver.sol"; -contract ClaimFees is IFlashAction { +contract ClaimFees is IFlashAction, IERC721Receiver { address public immutable positionManager; constructor(address positionManager_) { positionManager = positionManager_; } + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external override returns (bytes4) { + return this.onERC721Received.selector; + } + function onFlashAction( IFlashAction.Underlying calldata asset, bytes calldata data diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index 260f1563..99bdf72a 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -16,11 +16,11 @@ import {IERC4626} from "core/interfaces/IERC4626.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IPausable} from "core/utils/Pausable.sol"; import {IBeacon} from "core/interfaces/IBeacon.sol"; import {IERC4626RouterBase} from "gpl/interfaces/IERC4626RouterBase.sol"; +import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; interface IAstariaRouter is IPausable, IBeacon { enum FileType { @@ -68,7 +68,6 @@ interface IAstariaRouter is IPausable, IBeacon { ICollateralToken COLLATERAL_TOKEN; //20 ILienToken LIEN_TOKEN; //20 ITransferProxy TRANSFER_PROXY; //20 - IAuctionHouse AUCTION_HOUSE; //20 address feeTo; //20 address BEACON_PROXY_IMPLEMENTATION; //20 uint88 maxInterestRate; //6 @@ -89,7 +88,8 @@ interface IAstariaRouter is IPausable, IBeacon { enum ImplementationType { PrivateVault, PublicVault, - WithdrawProxy + WithdrawProxy, + ClearingHouse } enum LienRequestType { @@ -128,13 +128,14 @@ interface IAstariaRouter is IPausable, IBeacon { } /** - * @notice Validates the incoming commitment + * @notice Validates the incoming loan commitment. * @param commitment The commitment proofs and requested loan data for each loan. - * @return lien the new Lien data + * @return lien the new Lien data. */ - function validateCommitment(IAstariaRouter.Commitment calldata commitment, uint256 timeToSecondEpochEnd) - external - returns (ILienToken.Lien memory lien); + function validateCommitment( + IAstariaRouter.Commitment calldata commitment, + uint256 timeToSecondEpochEnd + ) external returns (ILienToken.Lien memory lien); /** * @notice Deploys a new PublicVault. @@ -161,6 +162,9 @@ interface IAstariaRouter is IPausable, IBeacon { */ function newVault(address delegate) external returns (address); + /** + * @notice Retrieves the address that collects protocol-level fees. + */ function feeTo() external returns (address); /** @@ -188,9 +192,9 @@ interface IAstariaRouter is IPausable, IBeacon { uint256 ); - function LIEN_TOKEN() external view returns (ILienToken); + function WETH() external view returns (ERC20); - function AUCTION_HOUSE() external view returns (IAuctionHouse); + function LIEN_TOKEN() external view returns (ILienToken); function TRANSFER_PROXY() external view returns (ITransferProxy); @@ -200,58 +204,99 @@ interface IAstariaRouter is IPausable, IBeacon { function maxInterestRate() external view returns (uint256); + /** + * @notice Returns the current auction duration. + * @param includeBuffer Adds the current auctionWindowBuffer if true. + */ function getAuctionWindow(bool includeBuffer) external view returns (uint256); + /** + * @notice Computes the fee PublicVault strategists earn on loan origination from the strategistFee numerator and denominator. + */ function getStrategistFee(uint256) external view returns (uint256); + /** + * @notice Computes the fee the protocol earns on loan origination from the protocolFee numerator and denominator. + */ 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. + */ function getLiquidatorFee(uint256) external view returns (uint256); /** * @notice Liquidate a CollateralToken that has defaulted on one of its liens. - * @param collateralId The ID of the CollateralToken. + * @param stack the stack being liquidated * @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( - uint256 collateralId, - uint8 position, - ILienToken.Stack[] calldata stack - ) external returns (uint256 reserve); + function liquidate(ILienToken.Stack[] calldata stack, uint8 position) + external + returns (OrderParameters memory); + /** + * @notice Returns whether a specified lien can be liquidated. + */ function canLiquidate(ILienToken.Stack calldata) external view returns (bool); - function isValidVault(address) external view returns (bool); + /** + * @notice Returns whether a given address is that of a Vault. + * @param vault The Vault address. + * @return A boolean representing whether the address exists as a Vault. + */ + function isValidVault(address vault) external view returns (bool); + /** + * @notice Sets universal protocol parameters or changes the addresses for deployed contracts. + * @param files Structs to file. + */ + function fileBatch(File[] calldata files) external; + + /** + * @notice Sets universal protocol parameters or changes the addresses for deployed contracts. + * @param incoming The incoming File. + */ function file(File calldata incoming) external; - function isValidRefinance( - ILienToken.Lien calldata newLien, - uint8 position, - ILienToken.Stack[] calldata stack - ) external view returns (bool); + /** + * @notice Updates the guardian address. + * @param _guardian The new guardian. + */ + function setNewGuardian(address _guardian) external; /** - * @notice Cancels the auction for a CollateralToken and returns the NFT to the borrower. - * @param tokenId The ID of the CollateralToken to cancel the auction for. + * @notice Specially guarded file(). + * @param file The incoming data to file. */ - function cancelAuction(uint256 tokenId) external; + function fileGuardian(File[] calldata file) external; /** - * @notice Ends the auction for a CollateralToken. - * @param tokenId The ID of the CollateralToken to stop the auction for. + * @notice Returns the address for the current implementation of a contract from the ImplementationType enum. + * @return impl The address of the clone implementation. */ - function endAuction(uint256 tokenId) external; + function getImpl(uint8 implType) external view returns (address impl); - event Liquidation( - uint256 collateralId, - uint256 position, - uint256 reserve, - uint256[] fee - ); + /** + * @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, address delegate, @@ -262,6 +307,7 @@ 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/IAstariaVaultBase.sol b/src/interfaces/IAstariaVaultBase.sol index a254deba..c95fe030 100644 --- a/src/interfaces/IAstariaVaultBase.sol +++ b/src/interfaces/IAstariaVaultBase.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.16; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IRouterBase} from "core/interfaces/IRouterBase.sol"; interface IAstariaVaultBase is IRouterBase { @@ -10,8 +9,6 @@ interface IAstariaVaultBase is IRouterBase { function COLLATERAL_TOKEN() external view returns (ICollateralToken); - function AUCTION_HOUSE() external view returns (IAuctionHouse); - function START() external view returns (uint256); function EPOCH_LENGTH() external view returns (uint256); diff --git a/src/interfaces/ICollateralToken.sol b/src/interfaces/ICollateralToken.sol index 76941d20..ed041a7a 100644 --- a/src/interfaces/ICollateralToken.sol +++ b/src/interfaces/ICollateralToken.sol @@ -10,35 +10,65 @@ pragma solidity ^0.8.15; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IERC721} from "core/interfaces/IERC721.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IFlashAction} from "core/interfaces/IFlashAction.sol"; +import { + ConsiderationInterface +} from "seaport/interfaces/ConsiderationInterface.sol"; +import { + ConduitControllerInterface +} from "seaport/interfaces/ConduitControllerInterface.sol"; +import {IERC1155} from "core/interfaces/IERC1155.sol"; +import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; +import {ClearingHouse} from "core/ClearingHouse.sol"; +import {IRoyaltyEngine} from "core/interfaces/IRoyaltyEngine.sol"; interface ICollateralToken is IERC721 { struct Asset { address tokenContract; uint256 tokenId; } + struct CollateralStorage { ITransferProxy TRANSFER_PROXY; ILienToken LIEN_TOKEN; IAstariaRouter ASTARIA_ROUTER; + ConsiderationInterface SEAPORT; + IRoyaltyEngine ROYALTY_ENGINE; + ConduitControllerInterface CONDUIT_CONTROLLER; + address CLEARING_HOUSE_IMPLEMENTATION; + address CONDUIT; + address OS_FEE_PAYEE; + uint16 osFeeNumerator; + uint16 osFeeDenominator; + bytes32 CONDUIT_KEY; + mapping(uint256 => bool) collateralIdToAuction; + mapping(bytes32 => bool) orderSigned; mapping(address => bool) flashEnabled; //mapping of the collateralToken ID and its underlying asset 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 { + ILienToken.Stack[] stack; + uint256 listPrice; + uint56 maxDuration; } + enum FileType { NotSupported, AstariaRouter, AuctionHouse, SecurityHook, - FlashEnabled + FlashEnabled, + Seaport, + OpenSeaFees } struct File { @@ -48,6 +78,18 @@ interface ICollateralToken is IERC721 { event FileUpdated(FileType what, bytes data); + /** + * @notice Sets universal protocol parameters or changes the addresses for deployed contracts. + * @param files Structs to file. + */ + function fileBatch(File[] calldata files) external; + + /** + * @notice Sets universal protocol parameters or changes the addresses for deployed contracts. + * @param incoming The incoming File. + */ + function file(File calldata incoming) external; + /** * @notice Executes a FlashAction using locked collateral. A valid FlashAction performs a specified action with the collateral within a single transaction and must end with the collateral being returned to the Vault it was locked in. * @param receiver The FlashAction to execute. @@ -62,14 +104,50 @@ interface ICollateralToken is IERC721 { function securityHooks(address) external view returns (address); + function getClearingHouse(uint256) external view returns (address); + + struct AuctionVaultParams { + address settlementToken; + uint256 collateralId; + uint256 maxDuration; + uint256 startingPrice; + uint256 endingPrice; + } + + /** + * @notice Send a CollateralToken to a Seaport auction on liquidation. + * @param params The auction data. + */ + function auctionVault(AuctionVaultParams calldata params) + external + returns (OrderParameters memory); + + /** + * @notice Clears the auction for a CollateralToken. + * @param collateralId The ID of the CollateralToken. + */ + function settleAuction(uint256 collateralId) external; + + function SEAPORT() external view returns (ConsiderationInterface); + + function getOpenSeaData() + external + view + returns ( + address, + uint16, + uint16 + ); + /** * @notice Retrieve the address and tokenId of the underlying NFT of a CollateralToken. * @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. @@ -78,6 +156,19 @@ interface ICollateralToken is IERC721 { */ function releaseToAddress(uint256 collateralId, address releaseTo) external; + /** + * @notice Permissionless hook which returns the underlying NFT for a CollateralToken to the liquidator after an auction. + * @param params The Seaport data from the liquidation. + */ + function liquidatorNFTClaim(OrderParameters memory params) external; + + /** + * @notice Lists a liquidated CollateralToken as a Seaport auction. + * @param params The liquidation information (Lien data, listing price, and maximum auction duration). + */ + function listForSaleOnSeaport(ListUnderlyingForSaleParams calldata params) + external; + event Deposit721( address indexed tokenContract, uint256 indexed tokenId, @@ -95,10 +186,15 @@ interface ICollateralToken is IERC721 { error InvalidSender(); error InvalidCollateralState(InvalidCollateralStates); error ProtocolPaused(); + error ListPriceTooLow(); + error InvalidConduitKey(); + error InvalidZone(); enum InvalidCollateralStates { + NO_AUTHORITY, NO_AUCTION, - AUCTION, + FLASH_DISABLED, + AUCTION_ACTIVE, ACTIVE_LIENS } diff --git a/src/interfaces/IERC1155.sol b/src/interfaces/IERC1155.sol new file mode 100644 index 00000000..4e26d96b --- /dev/null +++ b/src/interfaces/IERC1155.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC1155/IERC1155.sol) + +pragma solidity ^0.8.0; + +import {IERC165} from "core/interfaces/IERC165.sol"; + +/** + * @dev Required interface of an ERC1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155 is IERC165 { + /** + * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); + + /** + * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all + * transfers. + */ + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /** + * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to + * `approved`. + */ + event ApprovalForAll( + address indexed account, + address indexed operator, + bool approved + ); + + /** + * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. + * + * If an {URI} event was emitted for `id`, the standard + * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value + * returned by {IERC1155MetadataURI-uri}. + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the amount of tokens of token type `id` owned by `account`. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) + external + view + returns (uint256); + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + 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`, + * + * Emits an {ApprovalForAll} event. + * + * Requirements: + * + * - `operator` cannot be the caller. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address account, address operator) + external + view + returns (bool); + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} diff --git a/src/interfaces/IERC1155Receiver.sol b/src/interfaces/IERC1155Receiver.sol new file mode 100644 index 00000000..82e0a8a4 --- /dev/null +++ b/src/interfaces/IERC1155Receiver.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) + +pragma solidity ^0.8.0; + +import {IERC165} from "core/interfaces/IERC165.sol"; + +/** + * @dev _Available since v3.1._ + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev Handles the receipt of a single ERC1155 token type. This function is + * called at the end of a `safeTransferFrom` after the balance has been updated. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param operator The address which initiated the transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param id The ID of the token being transferred + * @param value The amount of tokens being transferred + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev Handles the receipt of a multiple ERC1155 token types. This function + * is called at the end of a `safeBatchTransferFrom` after the balances have + * been updated. + * + * NOTE: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param operator The address which initiated the batch transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param ids An array containing ids of each token being transferred (order and length must match values array) + * @param values An array containing amounts of each token being transferred (order and length must match ids array) + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index 0af24764..6d92be72 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -14,7 +14,6 @@ import {IERC721} from "core/interfaces/IERC721.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; interface ILienToken is IERC721 { @@ -36,16 +35,16 @@ interface ILienToken is IERC721 { uint8 maxLiens; address WETH; ITransferProxy TRANSFER_PROXY; - IAuctionHouse AUCTION_HOUSE; IAstariaRouter ASTARIA_ROUTER; ICollateralToken COLLATERAL_TOKEN; mapping(uint256 => bytes32) collateralStateHash; + mapping(uint256 => AuctionData) auctionData; mapping(uint256 => LienMeta) lienMeta; } struct LienMeta { address payee; - uint88 amountAtLiquidation; + bool atLiquidation; } struct Details { @@ -53,6 +52,7 @@ interface ILienToken is IERC721 { uint256 rate; //rate per second uint256 duration; uint256 maxPotentialDebt; + uint256 liquidationInitialAsk; } struct Lien { @@ -92,16 +92,14 @@ interface ILienToken is IERC721 { /** * @notice Removes all liens for a given CollateralToken. - * @param lien The Lien - * @return lienId the lienId if valid otherwise reverts + * @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 AUCTION_HOUSE() external view returns (IAuctionHouse); - function ASTARIA_ROUTER() external view returns (IAstariaRouter); function COLLATERAL_TOKEN() external view returns (ICollateralToken); @@ -122,8 +120,9 @@ interface ILienToken is IERC721 { function stopLiens( uint256 collateralId, uint256 auctionWindow, - ILienToken.Stack[] memory stack - ) external returns (uint256 reserve, AuctionStack[] memory); + Stack[] calldata stack, + address liquidator + ) external; /** * @notice Computes and returns the buyout amount for a Lien. @@ -135,16 +134,6 @@ interface ILienToken is IERC721 { view returns (uint256, uint256); - /** - * @notice Removes all liens for a given CollateralToken. - * @param collateralId The ID for the underlying CollateralToken. - * @param remainingLiens The IDs for the unpaid liens - */ - function removeLiens( - uint256 collateralId, - AuctionStack[] memory remainingLiens - ) external; - /** * @notice Removes all liens for a given CollateralToken. * @param stack The Lien stack @@ -187,15 +176,6 @@ interface ILienToken is IERC721 { view returns (uint256); - /** - * @notice Retrieves a specific point by its lienId. - * @param lienId the ID to get the point for - */ - function getAmountOwingAtLiquidation(uint256 lienId) - 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). @@ -216,40 +196,71 @@ interface ILienToken is IERC721 { external returns (Stack[] memory, Stack memory); + /** + * @notice Called by the ClearingHouse (through Seaport) to pay back debt with auction funds. + * @param collateralId The CollateralId of the liquidated NFT. + * @param payment The payment amount. + */ + function payDebtViaClearingHouse(uint256 collateralId, uint256 payment) + external; + /** * @notice Make a payment for the debt against a CollateralToken. * @param stack the stack to pay against * @param amount The amount to pay against the debt. */ - function makePayment(Stack[] memory stack, uint256 amount) - external - returns (Stack[] memory newStack); + function makePayment( + uint256 collateralId, + Stack[] memory stack, + uint256 amount + ) external returns (Stack[] memory newStack); + + function makePayment( + uint256 collateralId, + Stack[] calldata stack, + uint8 position, + uint256 amount + ) external returns (Stack[] memory newStack); struct AuctionStack { uint256 lienId; + uint88 amountOwed; uint40 end; } + struct AuctionData { + address liquidator; + AuctionStack[] stack; + } + /** - * @notice Make a payment for the debt against a CollateralToken for a specific lien. - * @param stack the stack to repay - * @param collateralId the Lien to make a payment towards - * @param paymentAmount The amount to pay against the debt. - * @param payer the account paying - * @return the amount of the payment that was applied to the lien + * @notice Retrieves the AuctionData for a CollateralToken (The liquidator address and the AuctionStack). + * @param collateralId The ID of the CollateralToken. */ - function makePaymentAuctionHouse( - AuctionStack[] memory stack, - uint256 collateralId, - uint256 paymentAmount, - address payer - ) external returns (ILienToken.AuctionStack[] memory, uint256); + function getAuctionData(uint256 collateralId) + external + view + returns (AuctionData memory); - function getMaxPotentialDebtForCollateral(ILienToken.Stack[] memory) + /** + * 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); + /** + * Calculates the debt accrued by all liens against a CollateralToken, assuming no payments are made until the provided timestamp. + * @param stack The stack data for active liens against the CollateralToken. + * @param end The timestamp to accrue potential debt until. + */ + function getMaxPotentialDebtForCollateral( + ILienToken.Stack[] memory stack, + uint256 end + ) external view returns (uint256); + /** * @notice Retrieve the payee (address that receives payments and auction funds) for a specified Lien. * @param lienId The ID of the Lien. @@ -259,14 +270,14 @@ interface ILienToken is IERC721 { /** * @notice Change the payee for a specified Lien. - * @param lien the lienevent + * @param lien the Lien to change the payee for. * @param newPayee The new Lien payee. */ function setPayee(Lien calldata lien, address newPayee) external; /** * @notice Sets addresses for the AuctionHouse, CollateralToken, and AstariaRouter contracts to use. - * @param file The incoming file to handle + * @param file The incoming file to handle. */ function file(File calldata file) external; @@ -299,13 +310,19 @@ interface ILienToken is IERC721 { error InvalidRefinance(); error InvalidLoanState(); enum InvalidStates { + NO_AUTHORITY, + COLLATERAL_MISMATCH, + NOT_ENOUGH_FUNDS, INVALID_LIEN_ID, COLLATERAL_AUCTION, COLLATERAL_NOT_DEPOSITED, LIEN_NO_DEBT, EXPIRED_LIEN, DEBT_LIMIT, - MAX_LIENS + MAX_LIENS, + INVALID_HASH, + INVALID_LIQUIDATION_INITIAL_ASK, + INITIAL_ASK_EXCEEDED } error InvalidState(InvalidStates); diff --git a/src/interfaces/IPublicVault.sol b/src/interfaces/IPublicVault.sol index 6b3d02cf..13219519 100644 --- a/src/interfaces/IPublicVault.sol +++ b/src/interfaces/IPublicVault.sol @@ -48,6 +48,14 @@ interface IPublicVault is IVaultImplementation { uint40 lienEnd; } + struct LiquidationPaymentParams { + uint256 lienEnd; + } + + function updateAfterLiquidationPayment( + LiquidationPaymentParams calldata params + ) external; + /** * @notice Signal a withdrawal of funds (redeeming for underlying asset) in an arbitrary future epoch. * @param shares The number of VaultToken shares to redeem. @@ -110,10 +118,23 @@ interface IPublicVault is IVaultImplementation { */ function processEpoch() external; + /** + * @notice Decrease the PublicVault YIntercept. + * @param amount The amount to decrement by. + */ 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) + */ function handleBuyoutLien(BuyoutLienParams calldata params) external; + /** + * Hook to update the PublicVault owner of a LienToken when it is sent to liquidation. + * @param auctionWindow The auction duration. + * @param params Liquidation data (lienSlope amount to deduct from the PublicVault slope, newAmount, and lienEnd timestamp) + */ function updateVaultAfterLiquidation( uint256 auctionWindow, AfterLiquidationParams calldata params diff --git a/src/interfaces/IRoyaltyEngine.sol b/src/interfaces/IRoyaltyEngine.sol new file mode 100644 index 00000000..f61c5085 --- /dev/null +++ b/src/interfaces/IRoyaltyEngine.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +/// @author: manifold.xyz + +import {IERC165} from "core/interfaces/IERC165.sol"; + +/** + * @dev Lookup engine interface + */ +interface IRoyaltyEngine is IERC165 { + /** + * Get the royalty for a given token (address, id) and value amount. Does not cache the bps/amounts. Caches the spec for a given token address + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyalty( + address tokenAddress, + uint256 tokenId, + uint256 value + ) + external + returns (address payable[] memory recipients, uint256[] memory amounts); + + /** + * View only version of getRoyalty + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyaltyView( + address tokenAddress, + uint256 tokenId, + uint256 value + ) + external + view + returns (address payable[] memory recipients, uint256[] memory amounts); +} diff --git a/src/interfaces/IWithdrawProxy.sol b/src/interfaces/IWithdrawProxy.sol index c65dc48c..c2d3a2c5 100644 --- a/src/interfaces/IWithdrawProxy.sol +++ b/src/interfaces/IWithdrawProxy.sol @@ -11,25 +11,56 @@ interface IWithdrawProxy is IRouterBase, IERC165, IERC4626 { function CLAIMABLE_EPOCH() external pure returns (uint64); + /** + * @notice Called at epoch boundary, computes the ratio between the funds of withdrawing liquidity providers and the balance of the underlying PublicVault so that claim() proportionally pays optimized-out to all parties. + * @param liquidationWithdrawRatio The ratio of withdrawing to remaining LPs for the current epoch boundary. + */ function setWithdrawRatio(uint256 liquidationWithdrawRatio) external; + /** + * @notice Adds an auction scheduled to end in a new epoch to this WithdrawProxy, to ensure that withdrawing LPs get a proportional share of auction returns. + * @param newLienExpectedValue The expected auction value for the lien being auctioned. + * @param finalAuctionDelta The timestamp by which the auction being added is guaranteed to end. As new auctions are added to the WithdrawProxy, this value will strictly increase as all auctions have the same maximum duration. + */ function handleNewLiquidation( uint256 newLienExpectedValue, uint256 finalAuctionDelta ) external; + /** + * @notice Called by PublicVault if previous epoch's withdrawReserve hasn't been met. + * @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); + /** + * @notice Return any excess funds to the PublicVault, according to the withdrawRatio between withdrawing and remaining LPs. + */ function claim() external; + /** + * @notice Called when PublicVault sends a payment to the WithdrawProxy + * to track how much of its WETH balance is from withdrawReserve payments instead of auction repayments + * @param amount The amount paid by the PublicVault, deducted from its withdrawReserve. + */ function increaseWithdrawReserveReceived(uint256 amount) external; + /** + * @notice Returns the expected value of auctions tracked by this WithdrawProxy (total debt owed against liquidated collateral). + */ function getExpected() external view returns (uint256); + /** + * @notice Returns the ratio between the balance of LPs exiting the PublicVault and those remaining. + */ function getWithdrawRatio() external view returns (uint256); + /** + * Returns the end timestamp of the last auction tracked by this WithdrawProxy. After this timestamp has passed, claim() can be called. + */ function getFinalAuctionEnd() external view returns (uint256); error NotSupported(); diff --git a/src/libraries/CollateralLookup.sol b/src/libraries/CollateralLookup.sol index e8a45a3c..92a9c4d5 100644 --- a/src/libraries/CollateralLookup.sol +++ b/src/libraries/CollateralLookup.sol @@ -15,13 +15,9 @@ import {IERC721} from "core/interfaces/IERC721.sol"; library CollateralLookup { function computeId(address token, uint256 tokenId) internal - view + pure returns (uint256) { - require( - IERC721(token).ownerOf(tokenId) != address(0), - "must be a valid token id" - ); return uint256(keccak256(abi.encodePacked(token, tokenId))); } } diff --git a/src/scripts/deployments/AstariaStack.sol b/src/scripts/deployments/AstariaStack.sol index 05d8fd9b..47cf066f 100644 --- a/src/scripts/deployments/AstariaStack.sol +++ b/src/scripts/deployments/AstariaStack.sol @@ -15,5 +15,4 @@ contract AstariaStack is Script { address WITHDRAW_PROXY_ADDR = vm.envAddress("WITHDRAW_PROXY_ADDR"); address LIQUIDATION_ACCOUNTANT_ADDR = vm.envAddress("LIQUIDATION_ACCOUNTANT_ADDR"); - address AUCTION_HOUSE_ADDR = vm.envAddress("AUCTION_HOUSE_ADDR"); } diff --git a/src/scripts/deployments/Deploy.sol b/src/scripts/deployments/Deploy.sol index 141e6296..c75d763c 100644 --- a/src/scripts/deployments/Deploy.sol +++ b/src/scripts/deployments/Deploy.sol @@ -16,9 +16,8 @@ import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {WEth} from "eip4626/WEth.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; import {ERC721} from "gpl/ERC721.sol"; -import {AuctionHouse} from "gpl/AuctionHouse.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; import {IERC20} from "core/interfaces/IERC20.sol"; @@ -37,6 +36,11 @@ import {ILienToken} from "core/interfaces/ILienToken.sol"; import {WithdrawProxy} from "core/WithdrawProxy.sol"; import {BeaconProxy} from "core/BeaconProxy.sol"; +import {ClearingHouse} from "core/ClearingHouse.sol"; +import {IRoyaltyEngine} from "core/interfaces/IRoyaltyEngine.sol"; +import { + ConsiderationInterface +} from "seaport/interfaces/ConsiderationInterface.sol"; interface IWETH9 is IERC20 { function deposit() external payable; @@ -49,7 +53,6 @@ contract Deploy is Script { ADMIN, ASTARIA_ROUTER, WRAPPER, - AUCTION_HOUSE, TRANSFER_PROXY, LIEN_TOKEN } @@ -65,7 +68,6 @@ contract Deploy is Script { PublicVault VAULT_IMPLEMENTATION; WithdrawProxy WITHDRAW_PROXY; AstariaRouter ASTARIA_ROUTER; - AuctionHouse AUCTION_HOUSE; function run() external { vm.startBroadcast(msg.sender); @@ -81,9 +83,7 @@ contract Deploy is Script { weth = vm.envAddress("WETH9_ADDR"); } catch {} if (weth == address(0)) { - WETH9 = IWETH9( - address(new WEth("Wrapped Ether Test", "WETH", uint8(18))) - ); + WETH9 = IWETH9(address(new WETH())); vm.writeLine( string(".env"), string(abi.encodePacked("WETH9_ADDR=", vm.toString(address(WETH9)))) @@ -120,10 +120,19 @@ contract Deploy is Script { ) ); + address SEAPORT = address(1); + + ClearingHouse CLEARING_HOUSE_IMPL = new ClearingHouse(); + address royaltyRegistry = address( + 0x0385603ab55642cb4Dd5De3aE9e306809991804f + ); + IRoyaltyEngine ROYALTY_REGISTRY = IRoyaltyEngine(address(royaltyRegistry)); COLLATERAL_TOKEN = new CollateralToken( MRA, TRANSFER_PROXY, - ILienToken(address(LIEN_TOKEN)) + ILienToken(address(LIEN_TOKEN)), + ConsiderationInterface(SEAPORT), + ROYALTY_REGISTRY ); emit Deployed(address(COLLATERAL_TOKEN)); @@ -179,7 +188,8 @@ contract Deploy is Script { address(VAULT_IMPLEMENTATION), address(SOLO_IMPLEMENTATION), address(WITHDRAW_PROXY), - address(BEACON_PROXY) + address(BEACON_PROXY), + address(CLEARING_HOUSE_IMPL) ); emit Deployed(address(ASTARIA_ROUTER)); vm.writeLine( @@ -188,49 +198,6 @@ contract Deploy is Script { abi.encodePacked("ROUTER_ADDR=", vm.toString(address(ASTARIA_ROUTER))) ) ); - // bytes32[] calldata what = new bytes32[](2); - // bytes[] calldata data = new bytes[](2); - // what[0] = bytes32("WITHDRAW_IMPLEMENTATION"); - // what[1] = bytes32("LIQUIDATION_IMPLEMENTATION"); - // data[0] = abi.encode(address(WITHDRAW_PROXY)); - // data[1] = abi.encode(address(LIQUIDATION_IMPLEMENTATION)); - - // AstariaRouter.File[] memory files = new AstariaRouter.File[](2); - // files[0] = AstariaRouter.File( - // bytes32("WITHDRAW_IMPLEMENTATION"), - // abi.encode(address(WITHDRAW_PROXY)) - // ); - // files[1] = AstariaRouter.File( - // bytes32("LIQUIDATION_IMPLEMENTATION"), - // abi.encode(address(LIQUIDATION_IMPLEMENTATION)) - // ); - // ASTARIA_ROUTER.fileBatch(files); - - AUCTION_HOUSE = new AuctionHouse( - address(WETH9), - MRA, - ICollateralToken(address(COLLATERAL_TOKEN)), - ILienToken(address(LIEN_TOKEN)), - TRANSFER_PROXY, - ASTARIA_ROUTER - ); - vm.writeLine( - string(".env"), - string( - abi.encodePacked( - "AUCTION_HOUSE_ADDR=", - vm.toString(address(AUCTION_HOUSE)) - ) - ) - ); - - IAstariaRouter.File[] memory files = new IAstariaRouter.File[](1); - - files[0] = IAstariaRouter.File( - IAstariaRouter.FileType.AuctionHouse, - abi.encode(address(AUCTION_HOUSE)) - ); - ASTARIA_ROUTER.fileGuardian(files); ICollateralToken.File[] memory ctfiles = new ICollateralToken.File[](1); @@ -239,81 +206,13 @@ contract Deploy is Script { data: abi.encode(address(ASTARIA_ROUTER)) }); COLLATERAL_TOKEN.fileBatch(ctfiles); - emit Deployed(address(AUCTION_HOUSE)); - _setupRolesAndCapabilities(); _setOwner(); vm.stopBroadcast(); } function _setupRolesAndCapabilities() internal { - MRA.setRoleCapability( - uint8(UserRoles.ASTARIA_ROUTER), - AuctionHouse.createAuction.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.ASTARIA_ROUTER), - AuctionHouse.endAuction.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.ASTARIA_ROUTER), - LienToken.createLien.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.WRAPPER), - AuctionHouse.cancelAuction.selector, - true - ); - // MRA.setRoleCapability( - // uint8(UserRoles.ASTARIA_ROUTER), - // CollateralToken.auctionVault.selector, - // true - // ); - MRA.setRoleCapability( - uint8(UserRoles.ASTARIA_ROUTER), - TRANSFER_PROXY.tokenTransferFrom.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.AUCTION_HOUSE), - LienToken.removeLiens.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.AUCTION_HOUSE), - LienToken.stopLiens.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.AUCTION_HOUSE), - TRANSFER_PROXY.tokenTransferFrom.selector, - true - ); - MRA.setUserRole( - address(ASTARIA_ROUTER), - uint8(UserRoles.ASTARIA_ROUTER), - true - ); - MRA.setUserRole(address(COLLATERAL_TOKEN), uint8(UserRoles.WRAPPER), true); - MRA.setUserRole( - address(AUCTION_HOUSE), - uint8(UserRoles.AUCTION_HOUSE), - true - ); - MRA.setRoleCapability( - uint8(UserRoles.AUCTION_HOUSE), - bytes4(keccak256(bytes("makePayment(uint256,uint256,uint8,address)"))), - true - ); - MRA.setRoleCapability( - uint8(UserRoles.LIEN_TOKEN), - TRANSFER_PROXY.tokenTransferFrom.selector, - true - ); - MRA.setUserRole(address(LIEN_TOKEN), uint8(UserRoles.LIEN_TOKEN), true); + //TODO refactor deploy flow to use single set of contracts to deploy in test and prod } function _setOwner() internal { diff --git a/src/strategies/CollectionValidator.sol b/src/strategies/CollectionValidator.sol index ea548aef..99dede09 100644 --- a/src/strategies/CollectionValidator.sol +++ b/src/strategies/CollectionValidator.sol @@ -68,7 +68,7 @@ contract CollectionValidator is ICollectionValidator { } require(cd.token == collateralTokenContract, "invalid token contract"); - leaf = keccak256(assembleLeaf(cd)); + leaf = keccak256(params.nlrDetails); ld = cd.lien; } } diff --git a/src/strategies/UNI_V3Validator.sol b/src/strategies/UNI_V3Validator.sol index c486cb74..5715d71c 100644 --- a/src/strategies/UNI_V3Validator.sol +++ b/src/strategies/UNI_V3Validator.sol @@ -17,6 +17,10 @@ import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; import {IStrategyValidator} from "core/interfaces/IStrategyValidator.sol"; import {IV3PositionManager} from "core/interfaces/IV3PositionManager.sol"; +import {IUniswapV3Factory} from "gpl/interfaces/IUniswapV3Factory.sol"; +import {IUniswapV3PoolState} from "gpl/interfaces/IUniswapV3PoolState.sol"; +import {TickMath} from "gpl/utils/TickMath.sol"; +import {LiquidityAmounts} from "gpl/utils/LiquidityAmounts.sol"; interface IUNI_V3Validator is IStrategyValidator { struct Details { @@ -38,20 +42,35 @@ interface IUNI_V3Validator is IStrategyValidator { contract UNI_V3Validator is IUNI_V3Validator { using CollateralLookup for address; + error InvalidFee(); + error InvalidType(); + error InvalidBorrower(); + error InvalidCollateral(); + error InvalidPair(); + error InvalidAmounts(); + error InvalidRange(); + error InvalidLiquidity(); + uint8 public constant VERSION_TYPE = uint8(3); - IV3PositionManager V3_NFT_POSITION_MGR = + IV3PositionManager public V3_NFT_POSITION_MGR = IV3PositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); + 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)); } @@ -69,17 +88,16 @@ contract UNI_V3Validator is IUNI_V3Validator { IUNI_V3Validator.Details memory details = getLeafDetails(params.nlrDetails); if (details.version != VERSION_TYPE) { - revert("invalid type"); + revert InvalidType(); } - if (details.borrower != address(0)) { - require( - borrower == details.borrower, - "invalid borrower requesting commitment" - ); + if (details.borrower != address(0) && borrower != details.borrower) { + revert InvalidBorrower(); } //ensure its also the correct token - require(details.lp == collateralTokenContract, "invalid token contract"); + if (details.lp != collateralTokenContract) { + revert InvalidCollateral(); + } ( , @@ -92,30 +110,46 @@ contract UNI_V3Validator is IUNI_V3Validator { uint128 liquidity, , , - uint128 tokensOwed0, - uint128 tokensOwed1 + , + ) = V3_NFT_POSITION_MGR.positions(collateralTokenId); - if (details.fee != uint24(0)) { - require(fee == details.fee, "fee mismatch"); + if (details.fee != uint24(0) && fee != details.fee) { + revert InvalidFee(); + } + + if (details.token0 != token0 || details.token1 != token1) { + revert InvalidPair(); + } + + //get pool from factory + + //get pool state + //get slot 0 + (uint160 poolSQ96, , , , , , ) = IUniswapV3PoolState( + V3_FACTORY.getPool(token0, token1, fee) + ).slot0(); + + (uint256 amount0, uint256 amount1) = LiquidityAmounts + .getAmountsForLiquidity( + poolSQ96, + TickMath.getSqrtRatioAtTick(tickLower), + TickMath.getSqrtRatioAtTick(tickUpper), + liquidity + ); + + if (details.amount0Min > amount0 || details.amount1Min > amount1) { + revert InvalidAmounts(); + } + if (details.tickUpper != tickUpper || details.tickLower != tickLower) { + revert InvalidRange(); + } + + if (details.minLiquidity > liquidity) { + revert InvalidLiquidity(); } - require( - details.token0 == token0 && details.token1 == token1, - "invalid pair" - ); - require( - details.amount0Min <= tokensOwed0 && details.amount1Min <= tokensOwed1, - "invalid fees available" - ); - require( - details.tickUpper == tickUpper && details.tickLower == tickLower, - "invalid range" - ); - - require(details.minLiquidity <= liquidity, "insufficient liquidity"); - - leaf = keccak256(assembleLeaf(details)); + leaf = keccak256(params.nlrDetails); ld = details.lien; } } diff --git a/src/strategies/UniqueValidator.sol b/src/strategies/UniqueValidator.sol index a77c8dff..8be7d041 100644 --- a/src/strategies/UniqueValidator.sol +++ b/src/strategies/UniqueValidator.sol @@ -29,15 +29,19 @@ 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); } @@ -67,7 +71,7 @@ contract UniqueValidator is IUniqueValidator { require(cd.token == collateralTokenContract, "invalid token contract"); require(cd.tokenId == collateralTokenId, "invalid token id"); - leaf = keccak256(assembleLeaf(cd)); + leaf = keccak256(params.nlrDetails); ld = cd.lien; } } diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index 8c05d03f..29184148 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -19,9 +19,7 @@ import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; -import {AuctionHouse} from "gpl/AuctionHouse.sol"; import {ERC721} from "gpl/ERC721.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; @@ -33,6 +31,7 @@ import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; +import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; @@ -198,6 +197,7 @@ contract AstariaTest is TestHelpers { function testLiquidationAtBoundary() public { TestNFT nft = new TestNFT(3); + vm.label(address(nft), "nft"); address tokenContract = address(nft); uint256 tokenId = uint256(1); address publicVault = _createPublicVault({ @@ -326,10 +326,12 @@ contract AstariaTest is TestHelpers { assertEq(vaultTokenBalance, IERC20(withdrawProxy).balanceOf(address(1))); skip(14 days); // end of loan - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); - - _bid(address(2), collateralId, 33 ether); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); + _bid(Bidder(bidder, bidderPK), listedOrder, 50 ether); skip(WithdrawProxy(withdrawProxy).getFinalAuctionEnd()); // epoch boundary PublicVault(publicVault).processEpoch(); @@ -461,6 +463,140 @@ contract AstariaTest is TestHelpers { ); } + 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 + }); + + _lendToVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + + vm.expectRevert( + abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.COLLATERAL_MISMATCH + ) + ); + VaultImplementation(privateVault).buyoutLien( + tokenContract.computeId(tokenId), + uint8(0), + refinanceTerms, + stack + ); + } + + function testTwoLoansDiffCollateralSameStack() 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 + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + isFirstLien: true + }); + + _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: uint256(1), + lienDetails: rogueBuyoutLien, + amount: 10 ether, + isFirstLien: false, + stack: stack, + revertMessage: abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.COLLATERAL_MISMATCH + ) + }); + } + function testReleaseToAddress() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); @@ -507,6 +643,49 @@ contract AstariaTest is TestHelpers { assertEq(ERC721(tokenContract).ownerOf(tokenId), address(this)); } + function testMakeTwoPayments() 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 + }); + + ILienToken.Stack[] memory newStack = _repay( + stack, + 0, + 5 ether, + address(this) + ); + + skip(1 days); + _repay(newStack, 0, 6 ether, address(this)); + } + function testCollateralTokenFileSetup() public { bytes memory astariaRouterAddr = abi.encode(address(0)); @@ -521,12 +700,6 @@ contract AstariaTest is TestHelpers { } function testLienTokenFileSetup() public { - bytes memory auctionHouseAddr = abi.encode(address(0)); - LIEN_TOKEN.file( - ILienToken.File(ILienToken.FileType.AuctionHouse, auctionHouseAddr) - ); - assert(LIEN_TOKEN.AUCTION_HOUSE() == IAuctionHouse(address(0))); - bytes memory collateralIdAddr = abi.encode(address(0)); LIEN_TOKEN.file( ILienToken.File(ILienToken.FileType.CollateralToken, collateralIdAddr) @@ -593,42 +766,6 @@ contract AstariaTest is TestHelpers { }); } - function testCancelAuction() 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); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); - _bid(address(2), collateralId, 10 ether); - _cancelAuction(collateralId, address(this)); - assertEq( - address(this), - ERC721(tokenContract).ownerOf(tokenId), - "liquidator did not receive NFT" - ); - } - function testAuctionEnd() public { address alice = address(1); address bob = address(2); @@ -655,11 +792,13 @@ contract AstariaTest is TestHelpers { uint256 collateralId = tokenContract.computeId(tokenId); vm.warp(block.timestamp + 11 days); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); - _bid(address(2), collateralId, 10 ether); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); + _bid(Bidder(bidder, bidderPK), listedOrder, 10 ether); skip(4 days); - ASTARIA_ROUTER.endAuction(collateralId); - assertEq(nft.ownerOf(tokenId), address(2), "the owner is not the bidder"); + assertEq(nft.ownerOf(tokenId), bidder, "the owner is not the bidder"); } function testAuctionEndNoBids() public { @@ -688,9 +827,12 @@ contract AstariaTest is TestHelpers { uint256 collateralId = tokenContract.computeId(tokenId); vm.warp(block.timestamp + 11 days); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); skip(4 days); - ASTARIA_ROUTER.endAuction(collateralId); + COLLATERAL_TOKEN.liquidatorNFTClaim(listedOrder); PublicVault(publicVault).processEpoch(); assertEq( nft.ownerOf(tokenId), @@ -699,16 +841,6 @@ contract AstariaTest is TestHelpers { ); } - function _cancelAuction(uint256 auctionId, address sender) internal { - (, , , uint256 reserve, ) = AUCTION_HOUSE.getAuctionData(auctionId); - vm.deal(sender, reserve); - vm.startPrank(sender); - WETH9.deposit{value: reserve}(); - WETH9.approve(address(TRANSFER_PROXY), reserve); - ASTARIA_ROUTER.cancelAuction(auctionId); - vm.stopPrank(); - } - uint8 FUZZ_SIZE = uint8(10); struct FuzzInputs { @@ -751,7 +883,7 @@ contract AstariaTest is TestHelpers { } _; } - + function testFinalAuctionEnd() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); @@ -773,7 +905,7 @@ contract AstariaTest is TestHelpers { uint256 collateralId = tokenContract.computeId(tokenId); - (, ILienToken.Stack[] memory stack) = _commitToLien({ + (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, @@ -785,12 +917,20 @@ contract AstariaTest is TestHelpers { }); vm.warp(block.timestamp + 14 days); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); + ASTARIA_ROUTER.liquidate(stack, uint8(0)); - address withdrawProxy = address(PublicVault(publicVault).getWithdrawProxy(0)); - assertTrue(withdrawProxy != address(0), "WithdrawProxy not deployed inside 3 days window from epoch end"); - uint256 actual = WithdrawProxy(withdrawProxy).getFinalAuctionEnd(); - assertEq(actual, block.timestamp + 3 days, "Auction time is not being set correctly"); + address withdrawProxy = address( + PublicVault(publicVault).getWithdrawProxy(0) + ); + assertTrue( + withdrawProxy != address(0), + "WithdrawProxy not deployed inside 3 days window from epoch end" + ); + assertEq( + WithdrawProxy(withdrawProxy).getFinalAuctionEnd(), + block.timestamp + 3 days, + "Auction time is not being set correctly" + ); } function testNewLienExceeds2XEpoch() public { @@ -813,16 +953,187 @@ contract AstariaTest is TestHelpers { lienDetails.duration = 30 days; (, ILienToken.Stack[] memory stack) = _commitToLien({ - vault: publicVault, - strategist: strategistOne, - strategistPK: strategistOnePK, - tokenContract: tokenContract, - tokenId: tokenId, - lienDetails: lienDetails, - amount: 10 ether, - isFirstLien: true + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: lienDetails, + amount: 10 ether, + isFirstLien: true + }); + + assertEq( + stack[0].lien.details.duration, + 4 weeks, + "Incorrect lien duration" + ); + } + + function testLiquidationNftTransfer() public { + address borrower = address(69); + address liquidator = address(7); + TestNFT nft = new TestNFT(0); + _mintNoDepositApproveRouterSpecific(borrower, address(nft), 99); + address tokenContract = address(nft); + uint256 tokenId = uint256(99); + + // 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 + ); + + _signalWithdraw(address(1), publicVault); + + ILienToken.Details memory lien = standardLienDetails; + lien.duration = 14 days; + + // borrow 10 eth against the dummy NFT + vm.startPrank(borrower); + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: lien, + amount: 50 ether, + isFirstLien: true + }); + vm.stopPrank(); + + vm.warp(block.timestamp + lien.duration); + + vm.startPrank(liquidator); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); + vm.stopPrank(); + uint256 bid = 100 ether; + _bid(Bidder(bidder, bidderPK), listedOrder, bid); + + // assert the bidder received the NFT + assertEq( + nft.ownerOf(tokenId), + bidder, + "Bidder did not receive NFT" + ); + } + + function testLiquidationPaymentsOverbid () public { + address borrower = address(69); + address liquidator = address(7); + (address publicVault, ILienToken.Stack[] memory stack) = setupLiquidation(borrower); + + vm.startPrank(liquidator); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); + vm.stopPrank(); + + PublicVault(publicVault).processEpoch(); + uint256 bid = 1000 ether; + uint256 amountOwedToLender = getAmountOwedToLender(15e17, 50e18, 14 days); + + uint256 actualPrice = 500 ether; + Fees memory fees = getFeesForLiquidation( + 500 ether, + 25e15, + 25e15, + 13e16, + amountOwedToLender + ); + + (address opensea, , ) = COLLATERAL_TOKEN.getOpenSeaData(); + + Fees memory balances = Fees({ + opensea: opensea.balance, + royalties: tx.origin.balance, + liquidator: WETH9.balanceOf(liquidator), + lender: amountOwedToLender, + borrower: WETH9.balanceOf(borrower) }); - assertEq(stack[0].lien.details.duration, 4 weeks, "Incorrect lien duration"); + uint256 bidderBalance = bidder.balance; + _bid(Bidder(bidder, bidderPK), listedOrder, bid); + + EVMGarbage memory garbage = EVMGarbage({ + fees: fees, + balances: balances, + borrower: borrower, + liquidator: liquidator, + actualPrice: actualPrice, + bidderBalance: bidderBalance, + opensea: opensea, + bid: bid, + publicVault: publicVault, + amountOwedToLender: amountOwedToLender + }); + assertBecauseEVMIsGarbage(garbage); + } + + struct EVMGarbage { + Fees fees; + Fees balances; + address borrower; + address liquidator; + uint256 actualPrice; + uint256 bidderBalance; + address opensea; + uint256 bid; + address publicVault; + uint256 amountOwedToLender; + } + + function assertBecauseEVMIsGarbage(EVMGarbage memory garbage) internal { + // assert the bidder balance is reduced + assertEq( + bidder.balance, + garbage.bidderBalance + (garbage.bid * 3) - garbage.actualPrice - garbage.fees.opensea - garbage.fees.royalties, + "Bidder balance not reduced" + ); + // assert opensea eth balance + assertEq( + garbage.opensea.balance - garbage.balances.opensea, + garbage.fees.opensea, + "Opensea balance not increased" + ); + + // assert royalty eth balance + assertEq( + tx.origin.balance - garbage.balances.royalties, + garbage.fees.royalties, + "Royalty balance not increased" + ); + + // assert withdrawProxy weth balance + WithdrawProxy withdrawProxy = PublicVault(garbage.publicVault).getWithdrawProxy(0); + assertEq( + WETH9.balanceOf(address(withdrawProxy)), + 52876712328728000000, + "WithdrawProxy balance not correct" + ); + // assert the liquidator weth balance + assertEq( + WETH9.balanceOf(garbage.liquidator), + garbage.fees.liquidator, + "Liquidator balance not correct" + ); + // assert the borrower weth balance + assertEq( + WETH9.balanceOf(garbage.borrower) - garbage.balances.borrower, + 382123287671272000000, + "Borrower balance not correct" + ); } } diff --git a/src/test/ForkedTesting.t.sol b/src/test/ForkedTesting.t.sol index b935ae83..82299111 100644 --- a/src/test/ForkedTesting.t.sol +++ b/src/test/ForkedTesting.t.sol @@ -24,9 +24,7 @@ import { } from "openzeppelin/token/ERC1155/IERC1155Receiver.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; -import {AuctionHouse} from "gpl/AuctionHouse.sol"; import {ERC721} from "gpl/ERC721.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IV3PositionManager} from "core/interfaces/IV3PositionManager.sol"; import {ICollateralToken} from "../interfaces/ICollateralToken.sol"; diff --git a/src/test/IntegrationTest.t.sol b/src/test/IntegrationTest.t.sol index da4d03ea..73435662 100644 --- a/src/test/IntegrationTest.t.sol +++ b/src/test/IntegrationTest.t.sol @@ -19,9 +19,7 @@ import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; -import {AuctionHouse} from "gpl/AuctionHouse.sol"; import {ERC721} from "gpl/ERC721.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; @@ -52,7 +50,8 @@ contract IntegrationTest is TestHelpers { maxAmount: 50 ether, rate: uint256(1e16).mulDivDown(150, 1).mulDivDown(1, 365 days), duration: 14 days, - maxPotentialDebt: 0 ether + maxPotentialDebt: 0 ether, + liquidationInitialAsk: 500 ether }); // deploy a new PublicVault @@ -152,7 +151,8 @@ contract IntegrationTest is TestHelpers { maxAmount: 50 ether, rate: uint256(1e16).mulDivDown(150, 1).mulDivDown(1, 365 days), duration: (dayCount * 1 days), - maxPotentialDebt: i * 20 ether + maxPotentialDebt: i * 20 ether, + liquidationInitialAsk: 500 ether }); } @@ -177,13 +177,16 @@ contract IntegrationTest is TestHelpers { "first lien payee before liq", LIEN_TOKEN.getPayee(stack[0].point.lienId) ); - ASTARIA_ROUTER.liquidate(collateralId, uint8(3), stack); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(3) + ); emit log_named_address("vault", address(publicVaults[0])); emit log_named_address( "first lien payee", LIEN_TOKEN.getPayee(stack[0].point.lienId) ); - _bid(address(2), collateralId, 200 ether); + _bid(Bidder(bidder, bidderPK), listedOrder, 200 ether); address[5] memory withdrawProxies; for (uint256 i = 0; i < lienSize; i++) { @@ -240,48 +243,4 @@ contract IntegrationTest is TestHelpers { "vault 4 invalid" ); } - - function testBorrowerReservePriceCancellationTest() 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 - ); - - (, 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 + 14 days); - - uint256 collateralId = tokenContract.computeId(tokenId); - - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); - - _bid(address(3), collateralId, 5 ether); - - vm.warp(block.timestamp + 4 days); - ASTARIA_ROUTER.endAuction(collateralId); - assertEq( - ERC721(tokenContract).ownerOf(tokenId), - address(3), - "Bidder address does not own NFT" - ); - } } diff --git a/src/test/RevertTesting.t.sol b/src/test/RevertTesting.t.sol index 62f57c47..1ed08a74 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -24,9 +24,7 @@ import { } from "openzeppelin/token/ERC1155/IERC1155Receiver.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; -import {AuctionHouse} from "gpl/AuctionHouse.sol"; import {ERC721} from "gpl/ERC721.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {ICollateralToken} from "../interfaces/ICollateralToken.sol"; import {ILienToken} from "../interfaces/ILienToken.sol"; @@ -48,14 +46,31 @@ contract RevertTesting is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; - function testFailRandomAccountIncrementNonce() public { + enum InvalidStates { + NO_AUTHORITY, + NOT_ENOUGH_FUNDS, + INVALID_LIEN_ID, + COLLATERAL_AUCTION, + COLLATERAL_NOT_DEPOSITED, + LIEN_NO_DEBT, + EXPIRED_LIEN, + DEBT_LIMIT, + MAX_LIENS + } + + function testCannotRandomAccountIncrementNonce() public { address privateVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 10 days }); - vm.expectRevert(abi.encodePacked("InvalidRequest(0)")); + vm.expectRevert( + abi.encodeWithSelector( + IVaultImplementation.InvalidRequest.selector, + IVaultImplementation.InvalidRequestReason.NO_AUTHORITY + ) + ); VaultImplementation(privateVault).incrementNonce(); assertEq( VaultImplementation(privateVault).getStrategistNonce(), @@ -127,7 +142,7 @@ contract RevertTesting is TestHelpers { ); } - function testFailBorrowMoreThanMaxAmount() public { + function testCannotBorrowMoreThanMaxAmount() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -150,8 +165,9 @@ contract RevertTesting is TestHelpers { ILienToken.Details memory details = standardLienDetails; details.maxAmount = 10 ether; + ILienToken.Stack[] memory stack; // borrow 10 eth against the dummy NFT - (, ILienToken.Stack[] memory stack) = _commitToLien({ + (, stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, @@ -159,12 +175,17 @@ contract RevertTesting is TestHelpers { tokenId: tokenId, lienDetails: details, amount: 11 ether, - isFirstLien: true + isFirstLien: true, + stack: stack, + revertMessage: abi.encodeWithSelector( + IAstariaRouter.InvalidCommitmentState.selector, + IAstariaRouter.CommitmentState.INVALID_AMOUNT + ) }); } // PublicVaults should not be able to progress to the next epoch unless all liens that are able to be liquidated have been liquidated - function testFailProcessEpochWithUnliquidatedLien() public { + function testCannotProcessEpochWithUnliquidatedLien() public { TestNFT nft = new TestNFT(3); address tokenContract = address(nft); uint256 tokenId = uint256(1); @@ -197,10 +218,17 @@ contract RevertTesting is TestHelpers { }); vm.warp(block.timestamp + 15 days); + + vm.expectRevert( + abi.encodeWithSelector( + IPublicVault.InvalidState.selector, + IPublicVault.InvalidStates.LIENS_OPEN_FOR_EPOCH_NOT_ZERO + ) + ); PublicVault(publicVault).processEpoch(); } - function testFailBorrowMoreThanMaxPotentialDebt() public { + function testCannotBorrowMoreThanMaxPotentialDebt() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -216,20 +244,23 @@ contract RevertTesting is TestHelpers { // lend 50 ether to the PublicVault as address(1) _lendToVault( - Lender({addr: address(1), amountToLend: 50 ether}), + Lender({addr: address(1), amountToLend: 100 ether}), publicVault ); + ILienToken.Stack[] memory stack; + // borrow 10 eth against the dummy NFT - _commitToLien({ + (, stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, - amount: 10 ether, - isFirstLien: true + amount: 50 ether, + isFirstLien: true, + stack: stack }); _commitToLien({ @@ -238,13 +269,18 @@ contract RevertTesting is TestHelpers { strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, - lienDetails: standardLienDetails, + lienDetails: standardLienDetails2, amount: 10 ether, - isFirstLien: false + isFirstLien: false, + stack: stack, + revertMessage: abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.DEBT_LIMIT + ) }); } - function testFailMinMaxPublicVaultEpochLength() public { + function testCannotExceedMinMaxPublicVaultEpochLength() public { vm.expectRevert( abi.encodeWithSelector( IPublicVault.InvalidState.selector, @@ -305,7 +341,7 @@ contract RevertTesting is TestHelpers { }); } - function testFailLienRateZero() public { + function testCannotLienRateZero() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -328,8 +364,9 @@ contract RevertTesting is TestHelpers { ILienToken.Details memory zeroRate = standardLienDetails; zeroRate.rate = 0; + ILienToken.Stack[] memory stack; // borrow 10 eth against the dummy NFT - (, ILienToken.Stack[] memory stack) = _commitToLien({ + (, stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, @@ -337,14 +374,100 @@ contract RevertTesting is TestHelpers { tokenId: tokenId, lienDetails: zeroRate, amount: 10 ether, - isFirstLien: true + isFirstLien: true, + stack: stack, + revertMessage: abi.encodeWithSelector( + IAstariaRouter.InvalidCommitmentState.selector, + IAstariaRouter.CommitmentState.INVALID_RATE + ) + }); + } + function testCannotLiquidationInitialAskExceedsAmountBorrowed() public { + 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 + ); + + ILienToken.Details memory standardLien = standardLienDetails; + standardLien.liquidationInitialAsk = 5 ether; + standardLien.maxAmount = 10 ether; + + // borrow amount over liquidation initial ask + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLien, + amount: 7.5 ether, + isFirstLien: true, + stack: new ILienToken.Stack[](0), + revertMessage: abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.INVALID_LIQUIDATION_INITIAL_ASK + ) + }); + } + function testCannotLiquidationInitialAsk0() 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 + ); + + ILienToken.Details memory zeroInitAsk = standardLienDetails; + zeroInitAsk.liquidationInitialAsk = 0; + + // borrow 10 eth against the dummy NFT + (, ILienToken.Stack[] memory stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: zeroInitAsk, + amount: 10 ether, + isFirstLien: true, + stack: new ILienToken.Stack[](0), + revertMessage: abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.INVALID_LIQUIDATION_INITIAL_ASK + ) }); } function testFailPayLienAfterLiquidate() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); - uint256 tokenId = uint256(1); + uint256 tokenId = uint256(0); address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, @@ -356,9 +479,7 @@ contract RevertTesting is TestHelpers { publicVault ); - // uint256[][] memory liens = new uint256[][](1); - ILienToken.Stack[][] memory stack = new ILienToken.Stack[][](1); - (, stack[0]) = _commitToLien({ + (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, @@ -373,8 +494,72 @@ contract RevertTesting is TestHelpers { vm.warp(block.timestamp + 14 days); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack[0]); + ASTARIA_ROUTER.liquidate(stack, uint8(0)); + + _repay(stack, 0, 10 ether, address(this)); + } + + function testCannotCommitToLienPotentialDebtExceedsLiquidationInitialAsk() + public + { + TestNFT nft = new TestNFT(1); + address tokenContract = address(nft); + uint256 tokenId = uint256(0); - _repay(stack[0], 0, 10 ether, address(this)); + uint256 initialBalance = WETH9.balanceOf(address(this)); + + // create a PublicVault with a 14-day epoch + address publicVault = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 30 days + }); + + _lendToVault( + Lender({addr: address(1), amountToLend: 500 ether}), + publicVault + ); + + ILienToken.Details memory details1 = standardLienDetails; + details1.duration = 14 days; + details1.liquidationInitialAsk = 100 ether; + details1.maxPotentialDebt = 1000 ether; + + ILienToken.Details memory details2 = standardLienDetails; + details2.duration = 25 days; + details2.liquidationInitialAsk = 100 ether; + details2.maxPotentialDebt = 1000 ether; + + IAstariaRouter.Commitment[] + memory commitments = new IAstariaRouter.Commitment[](2); + ILienToken.Stack[] memory stack; + + (, stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: details1, + amount: 50 ether, + isFirstLien: true, + stack: stack + }); + + _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: details2, + amount: 50 ether, + isFirstLien: false, + stack: stack, + revertMessage: abi.encodeWithSelector( + ILienToken.InvalidState.selector, + ILienToken.InvalidStates.INITIAL_ASK_EXCEEDED + ) + }); } } diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 6f11a3a8..b029e2b1 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -21,7 +21,6 @@ import { } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {AuctionHouse} from "gpl/AuctionHouse.sol"; import {ERC721} from "gpl/ERC721.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; @@ -30,9 +29,11 @@ import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; import {IERC20} from "core/interfaces/IERC20.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; import {IStrategyValidator} from "core/interfaces/IStrategyValidator.sol"; - +import {IRoyaltyEngine} from "core/interfaces/IRoyaltyEngine.sol"; import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; - +import { + ConduitControllerInterface +} from "seaport/interfaces/ConduitControllerInterface.sol"; import { ICollectionValidator, CollectionValidator @@ -58,8 +59,29 @@ import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import {BeaconProxy} from "../BeaconProxy.sol"; -import {IERC721Receiver} from "core/interfaces/IERC721Receiver.sol"; +import {Bytes32AddressLib} from "solmate/utils/Bytes32AddressLib.sol"; + import {IERC4626} from "core/interfaces/IERC4626.sol"; +import { + ConsiderationInterface +} from "seaport/interfaces/ConsiderationInterface.sol"; +import { + OrderParameters, + OrderComponents, + Order, + CriteriaResolver, + AdvancedOrder, + OfferItem, + ConsiderationItem, + OrderType, + Fulfillment, + FulfillmentComponent +} from "seaport/lib/ConsiderationStructs.sol"; +import {ClearingHouse} from "core/ClearingHouse.sol"; +import {RoyaltyEngineMock} from "./utils/RoyaltyEngineMock.sol"; +import {ConduitController} from "seaport/conduit/ConduitController.sol"; +import {Conduit} from "seaport/conduit/Conduit.sol"; +import {Consideration} from "seaport/lib/Consideration.sol"; string constant weth9Artifact = "src/test/WETH9.json"; interface IWETH9 is IERC20 { @@ -75,13 +97,41 @@ contract TestNFT is MockERC721 { } } } +import {BaseOrderTest} from "lib/seaport/test/foundry/utils/BaseOrderTest.sol"; + +contract ConsiderationTester is BaseOrderTest { + function _deployAndConfigureConsideration() public { + conduitController = new ConduitController(); + consideration = new Consideration(address(conduitController)); + + //create conduit, update channel + conduit = Conduit( + conduitController.createConduit(conduitKeyOne, address(this)) + ); + conduitController.updateChannel( + address(conduit), + address(consideration), + true + ); + } + + function setUp() public virtual override(BaseOrderTest) { + conduitKeyOne = bytes32(uint256(uint160(address(this))) << 96); + _deployAndConfigureConsideration(); + + vm.label(address(conduitController), "conduitController"); + vm.label(address(consideration), "consideration"); + vm.label(address(conduit), "conduit"); + vm.label(address(this), "testContract"); + } +} -contract TestHelpers is Test, IERC721Receiver { +contract TestHelpers is ConsiderationTester { using CollateralLookup for address; using Strings2 for bytes; using SafeCastLib for uint256; using SafeTransferLib for ERC20; - + using FixedPointMathLib for uint256; uint256 strategistOnePK = uint256(0x1339); uint256 strategistTwoPK = uint256(0x1344); // strategistTwo is delegate for PublicVault created by strategistOne uint256 strategistRoguePK = uint256(0x1559); // strategist who doesn't have a vault @@ -90,8 +140,11 @@ contract TestHelpers is Test, IERC721Receiver { address strategistRogue = vm.addr(strategistRoguePK); address borrower = vm.addr(0x1341); + uint256 bidderPK = uint256(2566); + uint256 bidderTwoPK = uint256(2567); + address bidder = vm.addr(bidderPK); address bidderOne = vm.addr(0x1342); - address bidderTwo = vm.addr(0x1343); + address bidderTwo = vm.addr(bidderTwoPK); string private checkpointLabel; uint256 private checkpointGasLeft = 1; // Start the slot warm. @@ -101,14 +154,32 @@ contract TestHelpers is Test, IERC721Receiver { maxAmount: 150 ether, rate: (uint256(1e16) * 150) / (365 days), duration: 10 days, - maxPotentialDebt: 0 ether + maxPotentialDebt: 0 ether, + liquidationInitialAsk: 500 ether + }); + ILienToken.Details public rogueBuyoutLien = + ILienToken.Details({ + maxAmount: 50 ether, + rate: (uint256(1e16) * 150) / (365 days), + duration: 10 days, + maxPotentialDebt: 50 ether, + liquidationInitialAsk: 500 ether }); ILienToken.Details public standardLienDetails = ILienToken.Details({ maxAmount: 50 ether, rate: (uint256(1e16) * 150) / (365 days), duration: 10 days, - maxPotentialDebt: 0 ether + maxPotentialDebt: 0 ether, + liquidationInitialAsk: 500 ether + }); + ILienToken.Details public standardLienDetails2 = + ILienToken.Details({ + maxAmount: 50 ether, + rate: (uint256(1e16) * 150) / (365 days), + duration: 11 days, + maxPotentialDebt: 0 ether, + liquidationInitialAsk: 500 ether }); ILienToken.Details public refinanceLienDetails = @@ -116,14 +187,16 @@ contract TestHelpers is Test, IERC721Receiver { maxAmount: 50 ether, rate: (uint256(1e16) * 150) / (365 days), duration: 25 days, - maxPotentialDebt: 53 ether + maxPotentialDebt: 53 ether, + liquidationInitialAsk: 500 ether }); ILienToken.Details public refinanceLienDetails2 = ILienToken.Details({ maxAmount: 50 ether, rate: (uint256(1e16) * 150) / (365 days), duration: 25 days, - maxPotentialDebt: 52 ether + maxPotentialDebt: 52 ether, + liquidationInitialAsk: 500 ether }); ILienToken.Details public refinanceLienDetails3 = @@ -131,7 +204,8 @@ contract TestHelpers is Test, IERC721Receiver { maxAmount: 50 ether, rate: (uint256(1e16) * 150) / (365 days), duration: 25 days, - maxPotentialDebt: 51 ether + maxPotentialDebt: 51 ether, + liquidationInitialAsk: 500 ether }); ILienToken.Details public refinanceLienDetails4 = @@ -139,7 +213,8 @@ contract TestHelpers is Test, IERC721Receiver { maxAmount: 50 ether, rate: (uint256(1e16) * 150) / (365 days), duration: 25 days, - maxPotentialDebt: 55 ether + maxPotentialDebt: 55 ether, + liquidationInitialAsk: 500 ether }); enum UserRoles { @@ -148,7 +223,9 @@ contract TestHelpers is Test, IERC721Receiver { WRAPPER, AUCTION_HOUSE, TRANSFER_PROXY, - LIEN_TOKEN + LIEN_TOKEN, + SEAPORT, + AUCTION_VALIDATOR } enum StrategyTypes { @@ -157,6 +234,14 @@ contract TestHelpers is Test, IERC721Receiver { UNIV3_LIQUIDITY } + struct Fees { + uint256 opensea; + uint256 royalties; + uint256 liquidator; + uint256 lender; + uint256 borrower; + } + event NewTermCommitment(bytes32 vault, uint256 collateralId, uint256 amount); event Repayment(bytes32 vault, uint256 collateralId, uint256 amount); event Liquidation(bytes32 vault, uint256 collateralId); @@ -167,6 +252,7 @@ contract TestHelpers is Test, IERC721Receiver { uint256 expiration ); event RedeemVault(bytes32 vault, uint256 amount, address indexed redeemer); + mapping(uint256 => OrderParameters) seaportOrders; CollateralToken COLLATERAL_TOKEN; LienToken LIEN_TOKEN; @@ -177,20 +263,39 @@ contract TestHelpers is Test, IERC721Receiver { TransferProxy TRANSFER_PROXY; IWETH9 WETH9; MultiRolesAuthority MRA; - AuctionHouse AUCTION_HOUSE; + ConsiderationInterface SEAPORT; - function setUp() public virtual { - WETH9 = IWETH9(deployCode(weth9Artifact)); + address bidderConduit; + bytes32 bidderConduitKey; + function setUp() public virtual override { + super.setUp(); + WETH9 = IWETH9(deployCode(weth9Artifact)); + vm.label(address(WETH9), "WETH9"); MRA = new MultiRolesAuthority(address(this), Authority(address(0))); - + vm.label(address(MRA), "MRA"); TRANSFER_PROXY = new TransferProxy(MRA); + vm.label(address(TRANSFER_PROXY), "TRANSFER_PROXY"); + LIEN_TOKEN = new LienToken(MRA, TRANSFER_PROXY, address(WETH9)); + vm.label(address(LIEN_TOKEN), "LIEN_TOKEN"); + + SEAPORT = ConsiderationInterface(address(consideration)); + + RoyaltyEngineMock royaltyEngine = new RoyaltyEngineMock(); + IRoyaltyEngine ROYALTY_REGISTRY = IRoyaltyEngine(address(royaltyEngine)); + + ClearingHouse CLEARING_HOUSE_IMPL = new ClearingHouse(); COLLATERAL_TOKEN = new CollateralToken( MRA, TRANSFER_PROXY, - ILienToken(address(LIEN_TOKEN)) + ILienToken(address(LIEN_TOKEN)), + SEAPORT, + ROYALTY_REGISTRY ); + vm.label(address(COLLATERAL_TOKEN), "COLLATERAL_TOKEN"); + + vm.label(COLLATERAL_TOKEN.getConduit(), "collateral conduit"); PUBLIC_VAULT = new PublicVault(); SOLO_VAULT = new Vault(); @@ -206,17 +311,12 @@ contract TestHelpers is Test, IERC721Receiver { address(PUBLIC_VAULT), address(SOLO_VAULT), address(WITHDRAW_PROXY), - address(BEACON_PROXY) + address(BEACON_PROXY), + address(CLEARING_HOUSE_IMPL) ); - AUCTION_HOUSE = new AuctionHouse( - address(WETH9), - MRA, - ICollateralToken(address(COLLATERAL_TOKEN)), - ILienToken(address(LIEN_TOKEN)), - TRANSFER_PROXY, - ASTARIA_ROUTER - ); + vm.label(address(ASTARIA_ROUTER), "ASTARIA_ROUTER"); + V3SecurityHook V3_SECURITY_HOOK = new V3SecurityHook( address(0xC36442b4a4522E871399CD717aBDD847Ab11FE88) ); @@ -259,20 +359,7 @@ contract TestHelpers is Test, IERC721Receiver { ); ASTARIA_ROUTER.fileBatch(files); - files = new IAstariaRouter.File[](1); - - files[0] = IAstariaRouter.File( - IAstariaRouter.FileType.AuctionHouse, - abi.encode(address(AUCTION_HOUSE)) - ); - ASTARIA_ROUTER.fileGuardian(files); - LIEN_TOKEN.file( - ILienToken.File( - ILienToken.FileType.AuctionHouse, - abi.encode(address(AUCTION_HOUSE)) - ) - ); LIEN_TOKEN.file( ILienToken.File( ILienToken.FileType.CollateralToken, @@ -290,79 +377,146 @@ contract TestHelpers is Test, IERC721Receiver { } function _setupRolesAndCapabilities() internal { - MRA.setRoleCapability( - uint8(UserRoles.ASTARIA_ROUTER), - AuctionHouse.createAuction.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.ASTARIA_ROUTER), - AuctionHouse.endAuction.selector, - true - ); - MRA.setRoleCapability( - uint8(UserRoles.ASTARIA_ROUTER), - AuctionHouse.cancelAuction.selector, - true - ); + // ROUTER CAPABILITIES MRA.setRoleCapability( uint8(UserRoles.ASTARIA_ROUTER), LienToken.createLien.selector, true ); - MRA.setRoleCapability( uint8(UserRoles.ASTARIA_ROUTER), TRANSFER_PROXY.tokenTransferFrom.selector, true ); + MRA.setRoleCapability( - uint8(UserRoles.AUCTION_HOUSE), - LienToken.removeLiens.selector, + uint8(UserRoles.ASTARIA_ROUTER), + CollateralToken.auctionVault.selector, true ); + + // LIEN TOKEN CAPABILITIES MRA.setRoleCapability( uint8(UserRoles.ASTARIA_ROUTER), LienToken.stopLiens.selector, true ); + MRA.setRoleCapability( - uint8(UserRoles.AUCTION_HOUSE), - TRANSFER_PROXY.tokenTransferFrom.selector, + uint8(UserRoles.LIEN_TOKEN), + CollateralToken.settleAuction.selector, true ); + MRA.setRoleCapability( - uint8(UserRoles.AUCTION_HOUSE), - ILienToken.makePaymentAuctionHouse.selector, //bytes4(keccak256(bytes("makePayment(uint256,uint256,uint8,address)"))), + uint8(UserRoles.LIEN_TOKEN), + TRANSFER_PROXY.tokenTransferFrom.selector, true ); + + // SEAPORT CAPABILITIES + MRA.setUserRole( address(ASTARIA_ROUTER), uint8(UserRoles.ASTARIA_ROUTER), true ); MRA.setUserRole(address(COLLATERAL_TOKEN), uint8(UserRoles.WRAPPER), true); - MRA.setUserRole( - address(AUCTION_HOUSE), - uint8(UserRoles.AUCTION_HOUSE), - true - ); + MRA.setUserRole(address(SEAPORT), uint8(UserRoles.SEAPORT), true); + MRA.setUserRole(address(LIEN_TOKEN), uint8(UserRoles.LIEN_TOKEN), true); + } - MRA.setRoleCapability( - uint8(UserRoles.LIEN_TOKEN), - TRANSFER_PROXY.tokenTransferFrom.selector, - true + function getAmountOwedToLender( + uint256 rate, + uint256 amount, + uint256 duration + ) public pure returns (uint256) { + return + amount + + (rate * amount * duration).mulDivDown(1, 365 days).mulDivDown(1, 1e18); + } + + 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); + uint256 tokenId = uint256(99); + + // create a PublicVault with a 14-day epoch + 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 ); - MRA.setUserRole(address(LIEN_TOKEN), uint8(UserRoles.LIEN_TOKEN), true); + + _signalWithdraw(address(1), publicVault); + + ILienToken.Details memory lien = standardLienDetails; + lien.duration = 14 days; + + // borrow 10 eth against the dummy NFT + vm.startPrank(borrower); + (, stack) = _commitToLien({ + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: lien, + amount: 50 ether, + isFirstLien: true + }); + vm.stopPrank(); + + vm.warp(block.timestamp + lien.duration); + } + + function getFeesForLiquidation( + uint256 bid, + uint256 openseaPercentage, + uint256 royaltyPercentage, + uint256 liquidatorPercentage, + uint256 lenderAmountOwed + ) public returns (Fees memory fees) { + uint256 remainder = bid; + fees = Fees({ + opensea: bid.mulDivDown(openseaPercentage, 1e18), + royalties: bid.mulDivDown(royaltyPercentage, 1e18), + liquidator: bid.mulDivDown(liquidatorPercentage, 1e18), + lender: 0, + borrower: 0 + }); + remainder -= fees.liquidator; + if (remainder <= lenderAmountOwed) { + fees.lender = remainder; + } else { + fees.lender = lenderAmountOwed; + } + remainder -= fees.lender; + fees.borrower = remainder; } - function onERC721Received( - address operator_, - address from_, - uint256 tokenId_, - bytes calldata data_ - ) external pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; + event FeesCalculated(Fees fees); + + function testFeesExample() public { + uint256 amountOwedToLender = getAmountOwedToLender(15e17, 10e18, 14 days); + Fees memory fees = getFeesForLiquidation( + 20e18, + 25e15, + 10e16, + 13e16, + amountOwedToLender + ); + emit FeesCalculated(fees); } // wrap NFT in a CollateralToken @@ -393,6 +547,17 @@ contract TestHelpers is Test, IERC721Receiver { TestNFT(tokenContract).approve(address(ASTARIA_ROUTER), tokenId); } + function _mintNoDepositApproveRouterSpecific( + address mintTo, + address tokenContract, + uint256 tokenId + ) internal { + TestNFT(tokenContract).mint(mintTo, tokenId); + vm.startPrank(mintTo); + TestNFT(tokenContract).approve(address(ASTARIA_ROUTER), tokenId); + vm.stopPrank(); + } + function _mintAndDeposit(address tokenContract, uint256 tokenId) internal { _mintAndDeposit(tokenContract, tokenId, address(this)); } @@ -495,15 +660,22 @@ contract TestHelpers is Test, IERC721Receiver { lienDetails: lienDetails, amount: amount, isFirstLien: isFirstLien, - stack: new ILienToken.Stack[](0) + stack: new ILienToken.Stack[](0), + revertMessage: new bytes(0) }); } - function _executeCommitments(IAstariaRouter.Commitment[] memory commitments) + function _executeCommitments( + IAstariaRouter.Commitment[] memory commitments, + bytes memory revertMessage + ) internal returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) { COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); + if (revertMessage.length > 0) { + vm.expectRevert(revertMessage); + } return ASTARIA_ROUTER.commitToLiens(commitments); } @@ -549,7 +721,11 @@ contract TestHelpers is Test, IERC721Receiver { IAstariaRouter.Commitment[] memory commitments = new IAstariaRouter.Commitment[](1); commitments[0] = terms; - return _executeCommitments({commitments: commitments}); + return + _executeCommitments({ + commitments: commitments, + revertMessage: new bytes(0) + }); } function _commitToLien( @@ -565,6 +741,36 @@ contract TestHelpers is Test, IERC721Receiver { ) internal returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) + { + return + _commitToLien({ + vault: vault, + strategist: strategist, + strategistPK: strategistPK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: lienDetails, + amount: amount, + isFirstLien: isFirstLien, + stack: stack, + revertMessage: new bytes(0) + }); + } + + function _commitToLien( + address vault, // address of deployed Vault + address strategist, + uint256 strategistPK, + address tokenContract, // original NFT address + uint256 tokenId, // original NFT id + ILienToken.Details memory lienDetails, // loan information + uint256 amount, // requested amount + bool isFirstLien, + ILienToken.Stack[] memory stack, + bytes memory revertMessage + ) + internal + returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) { IAstariaRouter.Commitment memory terms = _generateValidTerms({ vault: vault, @@ -583,7 +789,11 @@ contract TestHelpers is Test, IERC721Receiver { IAstariaRouter.Commitment[] memory commitments = new IAstariaRouter.Commitment[](1); commitments[0] = terms; - return _executeCommitments({commitments: commitments}); + return + _executeCommitments({ + commitments: commitments, + revertMessage: revertMessage + }); } function _generateEncodedStrategyData( @@ -806,13 +1016,19 @@ contract TestHelpers is Test, IERC721Receiver { uint8 position, uint256 amount, address payer - ) internal { + ) 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); - LIEN_TOKEN.makePayment(stack, position, amount * 2); + + newStack = LIEN_TOKEN.makePayment( + stack[position].lien.collateralId, + stack, + position, + amount + ); vm.stopPrank(); } @@ -827,24 +1043,280 @@ contract TestHelpers is Test, IERC721Receiver { WETH9.deposit{value: amount}(); WETH9.approve(address(TRANSFER_PROXY), amount); WETH9.approve(address(LIEN_TOKEN), amount); - newStack = LIEN_TOKEN.makePayment(stack, position, amount); + newStack = LIEN_TOKEN.makePayment( + stack[0].lien.collateralId, + stack, + position, + amount + ); vm.stopPrank(); } + struct Bidder { + address bidder; + uint256 bidderPK; + } + + struct Conduit { + bytes32 conduitKey; + address conduit; + } + + mapping(address => Conduit) bidderConduits; + function _bid( - address bidder, - uint256 tokenId, - uint256 amount + Bidder memory incomingBidder, + OrderParameters memory params, + uint256 bidAmount ) internal { - vm.deal(bidder, amount * 2); // TODO check amount multiplier, was 1.5 in old testhelpers - vm.startPrank(bidder); - WETH9.deposit{value: amount}(); - WETH9.approve(address(TRANSFER_PROXY), amount); - emit log_named_uint("bidder balance", WETH9.balanceOf(bidder)); - AUCTION_HOUSE.createBid(tokenId, amount); + vm.deal(incomingBidder.bidder, bidAmount * 3); // TODO check amount multiplier, was 1.5 in old testhelpers + vm.startPrank(incomingBidder.bidder); + + if (bidderConduits[incomingBidder.bidder].conduitKey == bytes32(0)) { + (, , address conduitController) = SEAPORT.information(); + bidderConduits[incomingBidder.bidder].conduitKey = Bytes32AddressLib + .fillLast12Bytes(address(incomingBidder.bidder)); + + bidderConduits[incomingBidder.bidder] + .conduit = ConduitControllerInterface(conduitController).createConduit( + bidderConduits[incomingBidder.bidder].conduitKey, + address(incomingBidder.bidder) + ); + + ConduitControllerInterface(conduitController).updateChannel( + address(bidderConduits[incomingBidder.bidder].conduit), + address(SEAPORT), + true + ); + vm.label( + address(bidderConduits[incomingBidder.bidder].conduit), + "bidder conduit" + ); + } + + OrderParameters memory mirror = _createMirrorOrderParameters( + params, + payable(incomingBidder.bidder), + params.zone, + bidderConduits[incomingBidder.bidder].conduitKey + ); + mirror.offer[0].startAmount = bidAmount + 1 ether; + mirror.offer[0].endAmount = bidAmount + 1 ether; + mirror.offer[1].startAmount = (bidAmount + 1 ether + 200 wei).mulDivDown( + 25, + 1000 + ); + mirror.offer[1].endAmount = (bidAmount + 1 ether + 200 wei).mulDivDown( + 25, + 1000 + ); + + Order[] memory orders = new Order[](2); + orders[0] = Order(params, new bytes(0)); + + OrderComponents memory matchOrderComponents = getOrderComponents( + mirror, + consideration.getCounter(incomingBidder.bidder) + ); + + emit log_order(mirror); + + bytes memory mirrorSignature = signOrder( + SEAPORT, + incomingBidder.bidderPK, + consideration.getOrderHash(matchOrderComponents) + ); + orders[1] = Order(mirror, mirrorSignature); + + //order 0 - 1 offer 3 consideration + + // order 1 - 3 offer 1 consideration + + //offers fulfillments + // 0,0 1,0 + // 1,0 0,0 + // 1,1 0,1 + // 1,2 0,2 + + // offer 0,0 + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + + //for each fulfillment we need to match them up + firstFulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + firstFulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(firstFulfillment); // 0,0 + + // offer 1,0 + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + secondFulfillment.offerComponents = fulfillmentComponents; + + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + secondFulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(secondFulfillment); // 1,0 + + // offer 1,1 + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 1); + fulfillmentComponents.push(fulfillmentComponent); + thirdFulfillment.offerComponents = fulfillmentComponents; + + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 1); + fulfillmentComponents.push(fulfillmentComponent); + + //for each fulfillment we need to match them up + thirdFulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(thirdFulfillment); // 1,1 + + //offer 1,2 + delete fulfillmentComponents; + + //royalty stuff, setup :TODO: + fulfillmentComponent = FulfillmentComponent(1, 2); + fulfillmentComponents.push(fulfillmentComponent); + fourthFulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 2); + fulfillmentComponents.push(fulfillmentComponent); + fourthFulfillment.considerationComponents = fulfillmentComponents; + + if (params.consideration.length == uint8(3)) { + fulfillments.push(fourthFulfillment); // 1,2 + } + + delete fulfillmentComponents; + + uint256 currentPrice = _locateCurrentAmount( + params.consideration[0].startAmount, + params.consideration[0].endAmount, + params.startTime, + params.endTime, + false + ); + if (bidAmount < currentPrice) { + uint256 warp = _computeWarp( + currentPrice, + bidAmount, + params.startTime, + params.endTime + ); + emit log_named_uint("start", params.consideration[0].startAmount); + emit log_named_uint("amount", bidAmount); + emit log_named_uint("warping", warp); + skip(warp + 1000); //TODO: figure this slope thing out + uint256 currentAmount = _locateCurrentAmount( + orders[0].parameters.consideration[0].startAmount, + orders[0].parameters.consideration[0].endAmount, + orders[0].parameters.startTime, + orders[0].parameters.endTime, + false + ); + emit log_named_uint("currentAmount asset", currentAmount); + uint256 currentAmountFee = _locateCurrentAmount( + orders[0].parameters.consideration[1].startAmount, + orders[0].parameters.consideration[1].endAmount, + orders[0].parameters.startTime, + orders[0].parameters.endTime, + false + ); + emit log_named_uint("currentAmount fee", currentAmountFee); + emit log_fills(fulfillments); + emit log_named_uint("length", fulfillments.length); + consideration.matchOrders{value: bidAmount + 5 ether}( + orders, + fulfillments + ); + } else { + consideration.fulfillOrder{value: bidAmount * 2}( + orders[0], + bidderConduits[incomingBidder.bidder].conduitKey + ); + } + delete fulfillments; vm.stopPrank(); } + event log_fills(Fulfillment[] fulfillments); + + function _computeWarp( + uint256 currentPrice, + uint256 bidAmount, + uint256 startTime, + uint256 endTime + ) internal returns (uint256) { + emit log_named_uint("currentPrice", currentPrice); + emit log_named_uint("bidAmount", bidAmount); + emit log_named_uint("startTime", startTime); + emit log_named_uint("endTime", endTime); + uint256 m = ((currentPrice - 1000 wei - 25 wei - 80 wei) / + (endTime - startTime)); + uint256 x = ((currentPrice - bidAmount) / m); + emit log_named_uint("m", m); + emit log_named_uint("x", x); + return x; + } + + function _createMirrorOrderParameters( + OrderParameters memory orderParameters, + address payable offerer, + address zone, + bytes32 conduitKey + ) public pure returns (OrderParameters memory) { + OfferItem[] memory _offerItems = _toOfferItems( + orderParameters.consideration + ); + ConsiderationItem[] memory _considerationItems = toConsiderationItems( + orderParameters.offer, + offerer + ); + + OrderParameters memory _mirrorOrderParameters = OrderParameters( + offerer, + zone, + _offerItems, + _considerationItems, + orderParameters.orderType, + orderParameters.startTime, + orderParameters.endTime, + orderParameters.zoneHash, + orderParameters.salt, + conduitKey, + _considerationItems.length + ); + return _mirrorOrderParameters; + } + + function _toOfferItems(ConsiderationItem[] memory _considerationItems) + internal + pure + returns (OfferItem[] memory) + { + OfferItem[] memory _offerItems = new OfferItem[]( + _considerationItems.length + ); + for (uint256 i = 0; i < _offerItems.length; i++) { + _offerItems[i] = OfferItem( + _considerationItems[i].itemType, + _considerationItems[i].token, + _considerationItems[i].identifierOrCriteria, + _considerationItems[i].startAmount + 1, + _considerationItems[i].endAmount + 1 + ); + } + return _offerItems; + } + + event log_order(OrderParameters); + // Redeem VaultTokens for WithdrawTokens redeemable by the end of the next epoch. function _signalWithdraw(address lender, address publicVault) internal { _signalWithdrawAtFutureEpoch( diff --git a/src/test/WithdrawTesting.t.sol b/src/test/WithdrawTesting.t.sol index 39d2de5d..7436c99a 100644 --- a/src/test/WithdrawTesting.t.sol +++ b/src/test/WithdrawTesting.t.sol @@ -19,9 +19,7 @@ import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; -import {AuctionHouse} from "gpl/AuctionHouse.sol"; import {ERC721} from "gpl/ERC721.sol"; -import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {IPublicVault} from "core/interfaces/IPublicVault.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; @@ -34,6 +32,7 @@ import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; +import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract WithdrawTest is TestHelpers { using FixedPointMathLib for uint256; @@ -80,12 +79,13 @@ contract WithdrawTest is TestHelpers { vm.warp(block.timestamp + lien.duration); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); vm.warp(block.timestamp + 2 days); // end of auction - AUCTION_HOUSE.endAuction(0); - _warpToEpochEnd(publicVault); PublicVault(publicVault).processEpoch(); PublicVault(publicVault).transferWithdrawReserve(); @@ -158,11 +158,18 @@ contract WithdrawTest is TestHelpers { skip(14 days); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack1); - ASTARIA_ROUTER.liquidate(collateralId2, uint8(0), stack2); // TODO test this + OrderParameters memory listedOrder1 = ASTARIA_ROUTER.liquidate( + stack1, + uint8(0) + ); + OrderParameters memory listedOrder2 = ASTARIA_ROUTER.liquidate( + stack2, + uint8(0) + ); + + //TODO: figure out how to do multiple bids here properly - _bid(address(3), collateralId, 5 ether); - _bid(address(3), collateralId2, 20 ether); + _bid(Bidder(bidder, bidderPK), listedOrder2, 20 ether); vm.warp(withdrawProxy.getFinalAuctionEnd()); emit log_named_uint("finalAuctionEnd", block.timestamp); PublicVault(publicVault).processEpoch(); @@ -257,8 +264,6 @@ contract WithdrawTest is TestHelpers { isFirstLien: true }); - uint256 lienId1 = liens[0][0]; - ILienToken.Details memory lien2 = standardLienDetails; lien2.duration = 27 days; // payee will be sent to WithdrawProxy at liquidation (liens[1], stacks[1]) = _commitToLien({ @@ -271,7 +276,6 @@ contract WithdrawTest is TestHelpers { amount: 10 ether, isFirstLien: true }); - uint256 lienId2 = liens[1][0]; _warpToEpochEnd(publicVault); @@ -283,38 +287,40 @@ contract WithdrawTest is TestHelpers { ); PublicVault(publicVault).processEpoch(); - uint256 collateralId1 = tokenContract.computeId(tokenId1); - - ASTARIA_ROUTER.liquidate(collateralId1, uint8(0), stacks[0]); + OrderParameters memory listedOrder1 = ASTARIA_ROUTER.liquidate( + stacks[0], + uint8(0) + ); WithdrawProxy withdrawProxy1 = PublicVault(publicVault).getWithdrawProxy(0); assertEq( - LIEN_TOKEN.getPayee(lienId1), + LIEN_TOKEN.getPayee(liens[0][0]), address(withdrawProxy1), "First lien not pointing to first WithdrawProxy" ); - _bid(address(3), collateralId1, 20 ether); + _bid(Bidder(bidder, bidderPK), listedOrder1, 200 ether); vm.warp(withdrawProxy1.getFinalAuctionEnd()); PublicVault(publicVault).processEpoch(); // epoch 0 processing vm.warp(block.timestamp + 14 days); - uint256 collateralId2 = tokenContract.computeId(tokenId2); - - ASTARIA_ROUTER.liquidate(collateralId2, uint8(0), stacks[1]); + OrderParameters memory listedOrder2 = ASTARIA_ROUTER.liquidate( + stacks[1], + uint8(0) + ); WithdrawProxy withdrawProxy2 = PublicVault(publicVault).getWithdrawProxy(1); assertEq( - LIEN_TOKEN.getPayee(lienId2), + LIEN_TOKEN.getPayee(liens[1][0]), address(withdrawProxy2), "Second lien not pointing to second WithdrawProxy" ); - _bid(address(3), collateralId2, 20 ether); + _bid(Bidder(bidderTwo, bidderTwoPK), listedOrder2, 200 ether); PublicVault(publicVault).transferWithdrawReserve(); @@ -387,22 +393,27 @@ contract WithdrawTest is TestHelpers { epochLength: 14 days }); + vm.label(publicVault, "publicVault"); + _lendToVault( Lender({addr: address(1), amountToLend: 25 ether}), publicVault ); + vm.label(address(1), "lender 1"); _signalWithdrawAtFutureEpoch(address(1), publicVault, 0); _lendToVault( Lender({addr: address(2), amountToLend: 25 ether}), publicVault ); + vm.label(address(2), "lender 2"); _signalWithdrawAtFutureEpoch(address(2), publicVault, 0); _lendToVault( Lender({addr: address(3), amountToLend: 50 ether}), publicVault ); + vm.label(address(3), "lender 3"); _signalWithdrawAtFutureEpoch(address(3), publicVault, 1); ILienToken.Details memory lien1 = standardLienDetails; @@ -444,8 +455,11 @@ contract WithdrawTest is TestHelpers { _warpToEpochEnd(publicVault); uint256 collateralId = tokenContract.computeId(tokenId); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); - _bid(address(4), collateralId, 150 ether); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); + _bid(Bidder(bidder, bidderPK), listedOrder, 150 ether); assertEq( PublicVault(publicVault).getSlope(), @@ -477,7 +491,7 @@ contract WithdrawTest is TestHelpers { PublicVault(publicVault).getWithdrawReserve(), 0, "withdrawReserve should be 0 after transfer" - ); // TODO check + ); assertEq( PublicVault(publicVault).getYIntercept(), @@ -590,9 +604,12 @@ contract WithdrawTest is TestHelpers { _warpToEpochEnd(publicVault); uint256 collateralId1 = tokenContract.computeId(tokenId1); - ASTARIA_ROUTER.liquidate(collateralId1, 0, stacks[0]); + OrderParameters memory listedOrder1 = ASTARIA_ROUTER.liquidate( + stacks[0], + uint8(0) + ); - _bid(address(3), collateralId1, 20 ether); + _bid(Bidder(bidder, bidderPK), listedOrder1, 10000 ether); WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); @@ -605,8 +622,11 @@ contract WithdrawTest is TestHelpers { withdrawProxy.claim(); uint256 collateralId2 = tokenContract.computeId(tokenId2); - ASTARIA_ROUTER.liquidate(collateralId2, 0, stacks[1]); - _bid(address(3), collateralId2, 20 ether); + OrderParameters memory listedOrder2 = ASTARIA_ROUTER.liquidate( + stacks[1], + uint8(0) + ); + _bid(Bidder(bidder, bidderPK), listedOrder2, 10000 ether); vm.expectRevert( abi.encodeWithSelector( @@ -839,36 +859,34 @@ contract WithdrawTest is TestHelpers { "Incorrect lien interest" ); - ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); - _bid(address(3), collateralId, 5 ether); + OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( + stack, + uint8(0) + ); + + _bid(Bidder(bidder, bidderPK), listedOrder, 6.96 ether); WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); vm.warp(withdrawProxy.getFinalAuctionEnd()); PublicVault(publicVault).processEpoch(); vm.warp(block.timestamp + 4 days); - AUCTION_HOUSE.endAuction(collateralId); - assertEq( - address(this), - COLLATERAL_TOKEN.ownerOf(collateralId), - "liquidator did not receive NFT" - ); withdrawProxy.claim(); assertEq( WETH9.balanceOf(publicVault), - 44350000000000000000, + 44378530092592593454, "Incorrect PublicVault balance" ); assertEq( PublicVault(publicVault).getYIntercept(), - 44350000000000000000, + 44378530092592593454, "Incorrect PublicVault YIntercept" ); assertEq( PublicVault(publicVault).totalAssets(), - 44350000000000000000, + 44378530092592593454, "Incorrect PublicVault totalAssets()" ); assertEq( diff --git a/src/test/utils/RoyaltyEngineMock.sol b/src/test/utils/RoyaltyEngineMock.sol new file mode 100644 index 00000000..9cdd4d4e --- /dev/null +++ b/src/test/utils/RoyaltyEngineMock.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IRoyaltyEngine} from "core/interfaces/IRoyaltyEngine.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {IERC165} from "core/interfaces/IERC165.sol"; + +contract RoyaltyEngineMock is IRoyaltyEngine { + using FixedPointMathLib for uint256; + + function supportsInterface(bytes4 interfaceId) + external + pure + override + returns (bool) + { + return + interfaceId == type(IRoyaltyEngine).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + constructor() {} + + function getRoyalty( + address tokenAddress, + uint256 tokenId, + uint256 value + ) + external + returns (address payable[] memory recipients, uint256[] memory amounts) + { + if (tokenId == uint256(99)) { + recipients = new address payable[](1); + amounts = new uint256[](1); + recipients[0] = payable(address(tx.origin)); + amounts[0] = value.mulDivDown(250, 10000); + } + } + + /** + * View only version of getRoyalty + * + * @param tokenAddress - The address of the token + * @param tokenId - The id of the token + * @param value - The value you wish to get the royalty of + * + * returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get + */ + function getRoyaltyView( + address tokenAddress, + uint256 tokenId, + uint256 value + ) + external + view + returns (address payable[] memory recipients, uint256[] memory amounts) + { + if (tokenId == uint256(99)) { + recipients = new address payable[](1); + amounts = new uint256[](1); + recipients[0] = payable(address(tx.origin)); + amounts[0] = value.mulDivDown(250, 10000); + } + } +} diff --git a/src/utils/Math.sol b/src/utils/Math.sol index 63ac94e1..7cecbba9 100644 --- a/src/utils/Math.sol +++ b/src/utils/Math.sol @@ -36,8 +36,11 @@ library Math { * This differs from standard division with `/` in that it rounds up instead * of rounding down. */ - function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256 ret) { // (a + b - 1) / b can overflow on addition, so we distribute. - return a / b + (a % b == 0 ? 0 : 1); + assembly { + if iszero(b) { revert(0, 0) } + ret := add(div(a, b), gt(mod(a, b), 0x0)) + } } }