diff --git a/.gas-snapshot b/.gas-snapshot deleted file mode 100644 index a3572bc7..00000000 --- a/.gas-snapshot +++ /dev/null @@ -1,25 +0,0 @@ -AstariaTest:testBasicPrivateVaultLoan() (gas: 1864818) -AstariaTest:testBasicPublicVaultLoan() (gas: 1908358) -AstariaTest:testBuyoutLien() (gas: 2453145) -AstariaTest:testCancelAuction() (gas: 2040295) -AstariaTest:testCollateralTokenFileSetup() (gas: 37100) -AstariaTest:testEpochProcessionMultipleActors() (gas: 2845744) -AstariaTest:testJustLiquidationAccountant() (gas: 5889854) -AstariaTest:testLienTokenFileSetup() (gas: 56240) -AstariaTest:testReleaseToAddress() (gas: 1866398) -AstariaTest:testWithdrawProxy() (gas: 1596452) -IntegrationTest:testMultipleVaultsWithLiensOnTheSameCollateral() (gas: 3151511) -IntegrationTest:testPublicVaultSlopeIncDecIntegration() (gas: 2060697) -RevertTesting:testFailBorrowMoreThanMaxAmount() (gas: 1703970) -RevertTesting:testFailBorrowMoreThanMaxPotentialDebt() (gas: 2186363) -RevertTesting:testFailLienDurationZero() (gas: 1816287) -RevertTesting:testFailLienRateZero() (gas: 1703688) -RevertTesting:testFailPayLienAfterLiquidate() (gas: 1558829) -RevertTesting:testFailProcessEpochWithUnliquidatedLien() (gas: 2037464) -RevertTesting:testFailSoloLendNotAppraiser() (gas: 1300894) -WithdrawTest:testBlockingLiquidationsProcessEpoch() (gas: 3113566) -WithdrawTest:testFutureLiquidationWithBlockingWithdrawReserve() (gas: 3013998) -WithdrawTest:testLiquidationAccountant5050Split() (gas: 3463384) -WithdrawTest:testLiquidationAccountantEpochOrdering() (gas: 3360801) -WithdrawTest:testWithdrawLiquidatedNoBids() (gas: 2301945) -WithdrawTest:testZeroizedVaultNewLP() (gas: 3086403) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26b53919..0ee87d82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,11 +18,8 @@ jobs: token: ${{ secrets.MY_REPO_PAT }} - uses: actions/setup-node@v2 - - run: yarn && npx tsc ./scripts/loanProofGenerator.ts + - run: yarn - uses: onbjerg/foundry-toolchain@v1 with: version: nightly - - run: forge test --ffi --no-match-contract ForkedTest - - - name: Run snapshot - run: NO_COLOR=1 forge snapshot --ffi >> $GITHUB_STEP_SUMMARY + - run: forge test --ffi --no-match-contract ForkedTest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1d2044ac..fdc245aa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage.json typechain .DS_Store +dist #Hardhat files cache artifacts diff --git a/.sherlock/contest.json b/.sherlock/contest.json deleted file mode 100644 index 6def8a6a..00000000 --- a/.sherlock/contest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Astaria", - "starts_at": "2022-10-20 15:00", - "ends_at": "2022-11-03 16:00", - "prize_pool": "50000", - "short_description": "Astaria’s mission is to build a highly functional on-chain lending protocol, with instant highly liquid NFT lending.", - "sponsor": "Astaria", - "github_team": "astaria", - "logo_url": "https://pbs.twimg.com/profile_images/1539278106971848706/k11NuoI5_400x400.jpg", - "private": false, - "lead_senior_auditor_handle": "0xRajeev" -} diff --git a/dummy-scripts/retry.py b/dummy-scripts/retry.py deleted file mode 100644 index 62fccbc2..00000000 --- a/dummy-scripts/retry.py +++ /dev/null @@ -1,8 +0,0 @@ -import os - - -while True: - output = os.popen("forge test --ffi --fork-url https://eth-mainnet.alchemyapi.io/v2/crm65ztnFlqvr08HoTDXF_Z_8wF0Pt-j").read() - if "429" not in output: - print(output) - break \ No newline at end of file diff --git a/lib/astaria-sdk b/lib/astaria-sdk index 228ea240..d0f41a50 160000 --- a/lib/astaria-sdk +++ b/lib/astaria-sdk @@ -1 +1 @@ -Subproject commit 228ea240ffef7ac7dd5823b345d0483f856b6f4c +Subproject commit d0f41a50d59989d1bc40962cbe9e6bb87d9d62e7 diff --git a/lib/gpl b/lib/gpl index 6cdc23c6..ce7d52c4 160000 --- a/lib/gpl +++ b/lib/gpl @@ -1 +1 @@ -Subproject commit 6cdc23c6fead3f6c0a70c382e66ce191abfcd23d +Subproject commit ce7d52c491121a6e81ca4a4f77b0d8dba609cf64 diff --git a/package.json b/package.json index bfaf7680..dee3b93b 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,6 @@ "private": true, "main": "index.js", "dependencies": { - "@openzeppelin/contracts": "^4.6.0", - "@rari-capital/solmate": "^6.2.0", "global": "^4.4.0", "hardhat": "^2.9.3", "hardhat-preprocessor": "^0.1.4", @@ -15,7 +13,8 @@ "keccak256": "^1.0.6", "merkletreejs": "^0.2.31", "solidity-metrics": "^0.0.5", - "ts-generator": "^0.1.1" + "ts-generator": "^0.1.1", + "ts-node": "^10.9.1" }, "engines": { "node": "^16.0.0" @@ -24,7 +23,7 @@ "lint": "eslint src/**/*.sol", "lint:fix": "prettier --write src/**/*.sol", "write-headers": "node scripts/writeHeaders.js", - "postinstall": "cd lib/astaria-sdk && yarn && yarn build" + "postinstall": "bash scripts/postinstall.sh" }, "devDependencies": { "@dethcrypto/eth-sdk": "^0.3.3", @@ -34,12 +33,9 @@ "@nomiclabs/hardhat-waffle": "^2.0.0", "@typechain/ethers-v5": "^7.0.1", "@typechain/hardhat": "^2.3.0", - "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", "@types/node": "^12.0.0", "@typescript-eslint/eslint-plugin": "^4.29.1", "@typescript-eslint/parser": "^4.29.1", - "chai": "^4.2.0", "dotenv": "^10.0.0", "eslint": "^7.29.0", "eslint-config-prettier": "^8.3.0", diff --git a/scripts/loanProofGenerator.ts b/scripts/loanProofGenerator.ts index 8903ba93..06233b09 100644 --- a/scripts/loanProofGenerator.ts +++ b/scripts/loanProofGenerator.ts @@ -30,19 +30,21 @@ if (detailsType === 0) { mapping = [ "uint8", "address", - "address[]", + "address", + "address", + "address", "uint24", "int24", "int24", "uint128", - "address", + "uint256", + "uint256", "uint256", "uint256", "uint256", "uint256", ]; } -// console.error(leaves); // Create tree const termData: string[] = defaultAbiCoder @@ -57,12 +59,11 @@ const termData: string[] = defaultAbiCoder // @ts-ignore leaves.push(termData); -// -const csvOuput: string = leaves.reduce((acc, cur) => { +const output: string = leaves.reduce((acc, cur) => { return acc + cur.join(",") + "\n"; }, ""); -const merkleTree = new StrategyTree(csvOuput); +const merkleTree = new StrategyTree(output); const rootHash: string = merkleTree.getHexRoot(); const proof = merkleTree.getHexProof(merkleTree.getLeaf(0)); diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh new file mode 100644 index 00000000..abb36e16 --- /dev/null +++ b/scripts/postinstall.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +NPX="npx" + +cd lib/astaria-sdk && yarn && yarn build && cd ../.. + +if [[ -z $CI ]] ; then + NPX= +fi +SCRIPT="${NPX-:""} tsc" + +${SCRIPT} \ No newline at end of file diff --git a/scripts/typechain.sh b/scripts/typechain.sh new file mode 100644 index 00000000..5b88c567 --- /dev/null +++ b/scripts/typechain.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env zsh + +# 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 + +accepted_file_names=("AuctionHouse.sol" "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 +rm -rf typechainabi && mkdir -p typechainabi +for i in ./out/*; +do + file=$(basename "${i}") + if [[ ${accepted_file_names[(ie)$file]} -le ${#accepted_file_names} ]]; then + cp -r "$i"/*.json "typechainabi/" + fi +done +typechain --target=ethers-v5 typechainabi/**/**.json --out-dir=typechain --show-stack-traces diff --git a/src/AstariaRouter.sol b/src/AstariaRouter.sol index 53fd7f83..ec136435 100644 --- a/src/AstariaRouter.sol +++ b/src/AstariaRouter.sol @@ -24,34 +24,35 @@ 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"; +import {IVaultImplementation} from "core/interfaces/IVaultImplementation.sol"; import {IStrategyValidator} from "core/interfaces/IStrategyValidator.sol"; -import {IPublicVault} from "core/interfaces/IPublicVault.sol"; - -import {IVault} from "core/interfaces/IVault.sol"; -import {PublicVault} from "core/PublicVault.sol"; -import {VaultImplementation} from "core/VaultImplementation.sol"; +import {IVaultImplementation} from "core/interfaces/IVaultImplementation.sol"; import {MerkleProofLib} from "core/utils/MerkleProofLib.sol"; import {Pausable} from "core/utils/Pausable.sol"; -import "./interfaces/ILienToken.sol"; -import "./interfaces/IAstariaRouter.sol"; +import {IERC4626} from "core/interfaces/IERC4626.sol"; +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"; /** * @title AstariaRouter * @notice This contract manages the deployment of Vaults and universal Astaria actions. */ -contract AstariaRouter is Auth, Pausable, IAstariaRouter { +contract AstariaRouter is Auth, ERC4626Router, Pausable, IAstariaRouter { using SafeTransferLib for ERC20; using SafeCastLib for uint256; using CollateralLookup for address; using FixedPointMathLib for uint256; bytes32 constant ROUTER_SLOT = - keccak256("xyz.astaria.router.storage.location"); + keccak256("xyz.astaria.AstariaRouter.storage.location"); /** * @dev Setup transfer authority and set up addresses for deployed CollateralToken, LienToken, TransferProxy contracts, as well as PublicVault and SoloVault implementations to clone. @@ -85,6 +86,7 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { s.implementations[uint8(ImplementationType.WithdrawProxy)] = _WITHDRAW_IMPL; s.BEACON_PROXY_IMPLEMENTATION = _BEACON_PROXY_IMPL; s.auctionWindow = uint32(2 days); + s.auctionWindowBuffer = uint32(1 days); s.liquidationFeeNumerator = uint32(130); s.liquidationFeeDenominator = uint32(1000); @@ -101,6 +103,31 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { s.guardian = address(msg.sender); } + function redeemFutureEpoch( + IPublicVault vault, + uint256 shares, + address receiver, + uint64 epoch + ) public virtual returns (uint256 assets) { + pullToken(address(vault), shares, address(this)); + ERC20(address(vault)).safeApprove(address(vault), shares); + vault.redeemFutureEpoch(shares, receiver, msg.sender, epoch); + } + + function pullToken( + address token, + uint256 amount, + address recipient + ) public payable override { + RouterStorage storage s = _loadRouterSlot(); + s.TRANSFER_PROXY.tokenTransferFrom( + address(token), + msg.sender, + recipient, + amount + ); + } + function _loadRouterSlot() internal pure returns (RouterStorage storage rs) { bytes32 slot = ROUTER_SLOT; assembly { @@ -108,11 +135,6 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { } } - function strategistNonce(address strategist) public view returns (uint256) { - RouterStorage storage s = _loadRouterSlot(); - return s.strategistNonce[strategist]; - } - function feeTo() public view returns (address) { RouterStorage storage s = _loadRouterSlot(); return s.feeTo; @@ -167,17 +189,6 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { _unpause(); } - function incrementNonce() external { - _loadRouterSlot().strategistNonce[msg.sender]++; - } - - struct File { - bytes32 what; - bytes data; - } - - event FileUpdated(bytes32 indexed what, bytes data); - /** * @notice Sets universal protocol parameters or changes the addresses for deployed contracts. * @param files structs to file @@ -194,62 +205,68 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { */ function file(File calldata incoming) public requiresAuth { RouterStorage storage s = _loadRouterSlot(); - bytes32 what = incoming.what; + FileType what = incoming.what; bytes memory data = incoming.data; - if (what == "setAuctionWindow") { - uint256 value = abi.decode(data, (uint256)); - s.auctionWindow = value.safeCastTo32(); - } else if (what == "setLiquidationFee") { + if (what == FileType.AuctionWindow) { + (uint256 window, uint256 windowBuffer) = abi.decode( + data, + (uint256, uint256) + ); + s.auctionWindow = window.safeCastTo32(); + s.auctionWindowBuffer = windowBuffer.safeCastTo32(); + } else if (what == FileType.LiquidationFee) { (uint256 numerator, uint256 denominator) = abi.decode( data, (uint256, uint256) ); s.liquidationFeeNumerator = numerator.safeCastTo32(); s.liquidationFeeDenominator = denominator.safeCastTo32(); - } else if (what == "setStrategistFee") { + } else if (what == FileType.StrategistFee) { (uint256 numerator, uint256 denominator) = abi.decode( data, (uint256, uint256) ); s.strategistFeeNumerator = numerator.safeCastTo32(); s.strategistFeeDenominator = denominator.safeCastTo32(); - } else if (what == "setProtocolFee") { + } else if (what == FileType.ProtocolFee) { (uint256 numerator, uint256 denominator) = abi.decode( data, (uint256, uint256) ); s.protocolFeeNumerator = numerator.safeCastTo32(); s.protocolFeeDenominator = denominator.safeCastTo32(); - } else if (what == "setBuyoutFee") { + } else if (what == FileType.BuyoutFee) { (uint256 numerator, uint256 denominator) = abi.decode( data, (uint256, uint256) ); s.buyoutFeeNumerator = numerator.safeCastTo32(); s.buyoutFeeDenominator = denominator.safeCastTo32(); - } else if (what == "MIN_INTEREST_BPS") { + } else if (what == FileType.MinInterestBPS) { uint256 value = abi.decode(data, (uint256)); s.minInterestBPS = value.safeCastTo32(); - } else if (what == "MIN_DURATION_INCREASE") { + } else if (what == FileType.MinDurationIncrease) { uint256 value = abi.decode(data, (uint256)); s.minDurationIncrease = value.safeCastTo32(); - } else if (what == "MIN_EPOCH_LENGTH") { + } else if (what == FileType.MinEpochLength) { s.minEpochLength = abi.decode(data, (uint256)).safeCastTo32(); - } else if (what == "MAX_EPOCH_LENGTH") { + } else if (what == FileType.MaxEpochLength) { s.maxEpochLength = abi.decode(data, (uint256)).safeCastTo32(); - } else if (what == "MAX_INTEREST_RATE") { + } else if (what == FileType.MaxInterestRate) { + s.maxInterestRate = abi.decode(data, (uint256)).safeCastTo48(); + } else if (what == FileType.MinInterestRate) { s.maxInterestRate = abi.decode(data, (uint256)).safeCastTo48(); - } else if (what == "feeTo") { + } else if (what == FileType.FeeTo) { address addr = abi.decode(data, (address)); s.feeTo = addr; - } else if (what == "setBuyoutInterestWindow") { + } else if (what == FileType.BuyoutInterestWindow) { uint256 value = abi.decode(data, (uint256)); s.buyoutInterestWindow = value.safeCastTo32(); - } else if (what == "setStrategyValidator") { + } else if (what == FileType.StrategyValidator) { (uint8 TYPE, address addr) = abi.decode(data, (uint8, address)); s.strategyValidators[TYPE] = addr; } else { - revert("unsupported/file"); + revert UnsupportedFile(); } emit FileUpdated(what, data); @@ -268,25 +285,25 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { RouterStorage storage s = _loadRouterSlot(); require(address(msg.sender) == address(s.guardian)); //only the guardian can call this for (uint256 i = 0; i < file.length; i++) { - bytes32 what = file[i].what; + FileType what = file[i].what; bytes memory data = file[i].data; - if (what == "setImplementation") { + if (what == FileType.Implementation) { (uint8 implType, address addr) = abi.decode(data, (uint8, address)); s.implementations[implType] = addr; - } else if (what == "setAuctionHouse") { + } else if (what == FileType.AuctionHouse) { address addr = abi.decode(data, (address)); s.AUCTION_HOUSE = IAuctionHouse(addr); - } else if (what == "setCollateralToken") { + } else if (what == FileType.CollateralToken) { address addr = abi.decode(data, (address)); s.COLLATERAL_TOKEN = ICollateralToken(addr); - } else if (what == "setLienToken") { + } else if (what == FileType.LienToken) { address addr = abi.decode(data, (address)); s.LIEN_TOKEN = ILienToken(addr); - } else if (what == "setTransferProxy") { + } else if (what == FileType.TransferProxy) { address addr = abi.decode(data, (address)); s.TRANSFER_PROXY = ITransferProxy(addr); } else { - revert("unsupported/guardian-file"); + revert UnsupportedFile(); } } } @@ -308,12 +325,27 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { } } - function getAuctionWindow() public view returns (uint256) { - return _loadRouterSlot().auctionWindow; + function getAuctionWindow(bool includeBuffer) public view returns (uint256) { + RouterStorage storage s = _loadRouterSlot(); + return s.auctionWindow + (includeBuffer ? s.auctionWindowBuffer : 0); + } + + function _sliceUint(bytes memory bs, uint256 start) + internal + pure + returns (uint256) + { + require(bs.length >= start + 32); + uint256 x; + assembly { + x := mload(add(bs, add(0x20, start))) + } + return x; } function validateCommitment(IAstariaRouter.Commitment calldata commitment) external + view returns (ILienToken.Lien memory lien) { return _validateCommitment(_loadRouterSlot(), commitment); @@ -322,17 +354,18 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { function _validateCommitment( RouterStorage storage s, IAstariaRouter.Commitment calldata commitment - ) internal returns (ILienToken.Lien memory lien) { + ) internal view returns (ILienToken.Lien memory lien) { if (block.timestamp > commitment.lienRequest.strategy.deadline) { revert InvalidCommitmentState(CommitmentState.EXPIRED); } - if (s.strategyValidators[commitment.lienRequest.nlrType] == address(0)) { - revert InvalidStrategy(commitment.lienRequest.nlrType); + uint256 strategyLength = 5; + uint8 nlrType = uint8(_sliceUint(commitment.lienRequest.nlrDetails, 0)); + if (s.strategyValidators[nlrType] == address(0)) { + revert InvalidStrategy(nlrType); } - (bytes32 leaf, ILienToken.Details memory details) = IStrategyValidator( - s.strategyValidators[commitment.lienRequest.nlrType] + s.strategyValidators[nlrType] ).validateAndParse( commitment.lienRequest, s.COLLATERAL_TOKEN.ownerOf( @@ -392,6 +425,7 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { totalBorrowed += commitments[i].lienRequest.amount; } s.WETH.safeApprove(address(s.TRANSFER_PROXY), totalBorrowed); + s.TRANSFER_PROXY.tokenTransferFrom( address(s.WETH), address(this), @@ -442,21 +476,12 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { ) { RouterStorage storage s = _loadRouterSlot(); - ILienToken.Lien memory newLien = _validateCommitment(s, params); - uint256 collateralId = params.tokenContract.computeId(params.tokenId); - if (s.AUCTION_HOUSE.auctionExists(collateralId)) { - revert InvalidCommitmentState(CommitmentState.COLLATERAL_AUCTION); - } - (address tokenContract, ) = s.COLLATERAL_TOKEN.getUnderlying(collateralId); - if (tokenContract == address(0)) { - revert InvalidCommitmentState(CommitmentState.COLLATERAL_NO_DEPOSIT); - } return s.LIEN_TOKEN.createLien( ILienToken.LienActionEncumber({ - collateralId: collateralId, - lien: newLien, + collateralId: params.tokenContract.computeId(params.tokenId), + lien: _validateCommitment(s, params), amount: params.lienRequest.amount, stack: params.lienRequest.stack, receiver: receiver @@ -464,23 +489,6 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { ); } - function lendToVault(IVault vault, uint256 amount) external whenNotPaused { - RouterStorage storage s = _loadRouterSlot(); - s.TRANSFER_PROXY.tokenTransferFrom( - address(s.WETH), - address(msg.sender), - address(this), - amount - ); - - if (s.vaults[address(vault)] == address(0)) { - revert InvalidVaultState(VaultState.UNINITIALIZED); - } - - s.WETH.safeApprove(address(vault), amount); - vault.deposit(amount, address(msg.sender)); - } - /** * @notice Returns whether a specific lien can be liquidated. * @return A boolean value indicating whether the specified lien can be liquidated. @@ -505,22 +513,36 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { } RouterStorage storage s = _loadRouterSlot(); - uint256[] memory stackAtLiquidation = new uint256[](stack.length); + uint256 auctionWindowMax = s.auctionWindow + s.auctionWindowBuffer; + ILienToken.AuctionStack[] + memory stackAtLiquidation = new ILienToken.AuctionStack[](stack.length); (reserve, stackAtLiquidation) = s.LIEN_TOKEN.stopLiens( collateralId, - s.auctionWindow, + auctionWindowMax, stack ); + reserve += reserve.mulDivDown( + s.liquidationFeeNumerator, + s.liquidationFeeDenominator + ); + s.AUCTION_HOUSE.createAuction( collateralId, s.auctionWindow, + auctionWindowMax, msg.sender, + s.liquidationFeeNumerator, + s.liquidationFeeDenominator, reserve, stackAtLiquidation ); - emit Liquidation(collateralId, position, reserve); + 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 { @@ -628,15 +650,12 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { uint256 minNewRate = uint256(stack[position].lien.details.rate) - s.minInterestBPS; - if ( - (newLien.details.rate < minNewRate) || - (block.timestamp + newLien.details.duration - stack[position].point.end < - s.minDurationIncrease) - ) { - return false; - } - - return true; + return + !((newLien.details.rate < minNewRate) || + (block.timestamp + + newLien.details.duration - + stack[position].point.end < + s.minDurationIncrease)); } //INTERNAL FUNCS @@ -661,7 +680,6 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { if (s.minEpochLength > epochLength || epochLength > s.maxEpochLength) { revert InvalidEpochLength(epochLength); } - vaultType = uint8(ImplementationType.PublicVault); } else { vaultType = uint8(ImplementationType.PrivateVault); @@ -682,8 +700,8 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { ); //mutable data - VaultImplementation(vaultAddr).init( - VaultImplementation.InitParams({ + IVaultImplementation(vaultAddr).init( + IVaultImplementation.InitParams({ delegate: delegate, allowListEnabled: allowListEnabled, allowList: allowList, @@ -693,7 +711,7 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { s.vaults[vaultAddr] = msg.sender; - emit NewVault(msg.sender, vaultAddr); + emit NewVault(msg.sender, delegate, vaultAddr, vaultType); return vaultAddr; } @@ -714,7 +732,7 @@ contract AstariaRouter is Auth, Pausable, IAstariaRouter { } //router must be approved for the collateral to take a loan, return - VaultImplementation(c.lienRequest.strategy.vault).commitToLien( + IVaultImplementation(c.lienRequest.strategy.vault).commitToLien( c, address(this) ); diff --git a/src/AstariaVaultBase.sol b/src/AstariaVaultBase.sol index e6a3f6cc..207287fe 100644 --- a/src/AstariaVaultBase.sol +++ b/src/AstariaVaultBase.sol @@ -1,13 +1,13 @@ pragma solidity ^0.8.17; import {IAstariaVaultBase} from "core/interfaces/IAstariaVaultBase.sol"; -import {ERC4626Base} from "core/ERC4626Base.sol"; -import {IERC4626Base} from "core/interfaces/IERC4626Base.sol"; +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 ERC4626Base, IAstariaVaultBase { +abstract contract AstariaVaultBase is Clone, IAstariaVaultBase { function name() public view virtual returns (string memory); function symbol() public view virtual returns (string memory); @@ -24,13 +24,7 @@ abstract contract AstariaVaultBase is ERC4626Base, IAstariaVaultBase { return _getArgAddress(21); //ends at 44 } - function underlying() - public - pure - virtual - override(IERC4626Base, ERC4626Base) - returns (address) - { + function asset() public pure virtual returns (address) { return _getArgAddress(41); //ends at 64 } diff --git a/src/CollateralToken.sol b/src/CollateralToken.sol index 48712732..d50f6e89 100644 --- a/src/CollateralToken.sol +++ b/src/CollateralToken.sol @@ -90,19 +90,19 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { function file(File calldata incoming) public requiresAuth { CollateralStorage storage s = _loadCollateralSlot(); - bytes32 what = incoming.what; + FileType what = incoming.what; bytes memory data = incoming.data; - if (what == "setAstariaRouter") { + if (what == FileType.AstariaRouter) { address addr = abi.decode(data, (address)); s.ASTARIA_ROUTER = IAstariaRouter(addr); - } else if (what == "setSecurityHook") { + } else if (what == FileType.SecurityHook) { (address target, address hook) = abi.decode(data, (address, address)); s.securityHooks[target] = hook; - } else if (what == "setFlashEnabled") { + } else if (what == FileType.FlashEnabled) { (address target, bool enabled) = abi.decode(data, (address, bool)); s.flashEnabled[target] = enabled; } else { - revert("unsupported/file"); + revert UnsupportedFile(); } emit FileUpdated(what, data); } @@ -136,7 +136,10 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { CollateralStorage storage s = _loadCollateralSlot(); (addr, tokenId) = getUnderlying(collateralId); - require(s.flashEnabled[addr]); + require( + s.flashEnabled[addr] && + !s.ASTARIA_ROUTER.AUCTION_HOUSE().auctionExists(collateralId) + ); IERC721 nft = IERC721(addr); bytes memory preTransferState; @@ -202,7 +205,12 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { (address underlyingAsset, uint256 assetId) = getUnderlying(collateralId); delete s.idToUnderlying[collateralId]; _burn(collateralId); - IERC721(underlyingAsset).transferFrom(address(this), releaseTo, assetId); + IERC721(underlyingAsset).safeTransferFrom( + address(this), + releaseTo, + assetId, + "" + ); emit ReleaseTo(underlyingAsset, assetId, releaseTo); } @@ -258,9 +266,10 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { uint256 collateralId = msg.sender.computeId(tokenId_); CollateralStorage storage s = _loadCollateralSlot(); - Asset memory underlying = s.idToUnderlying[collateralId]; - (address underlyingAsset, ) = getUnderlying(collateralId); - if (underlyingAsset == address(0)) { + Asset memory incomingAsset = s.idToUnderlying[collateralId]; + if (incomingAsset.tokenContract == address(0)) { + require(ERC721(msg.sender).ownerOf(tokenId_) == address(this)); + if (msg.sender == address(this) || msg.sender == address(s.LIEN_TOKEN)) { revert InvalidCollateral(); } @@ -279,8 +288,10 @@ contract CollateralToken is Auth, ERC721, IERC721Receiver, ICollateralToken { }); emit Deposit721(msg.sender, tokenId_, collateralId, depositFor); + return IERC721Receiver.onERC721Received.selector; + } else { + revert(); } - return IERC721Receiver.onERC721Received.selector; } modifier whenNotPaused() { diff --git a/src/ERC4626Base.sol b/src/ERC4626Base.sol deleted file mode 100644 index a7e305fa..00000000 --- a/src/ERC4626Base.sol +++ /dev/null @@ -1,7 +0,0 @@ -pragma solidity ^0.8.17; -import {Clone} from "clones-with-immutable-args/Clone.sol"; -import {IERC4626Base} from "./interfaces/IERC4626Base.sol"; - -abstract contract ERC4626Base is Clone, IERC4626Base { - function underlying() public view virtual returns (address); -} diff --git a/src/LienToken.sol b/src/LienToken.sol index 74c02e78..8d41f257 100644 --- a/src/LienToken.sol +++ b/src/LienToken.sol @@ -22,7 +22,7 @@ import {IERC165} from "core/interfaces/IERC165.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; -import {Base64} from "./libraries/Base64.sol"; +import {Base64} from "core/libraries/Base64.sol"; import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; @@ -60,7 +60,7 @@ contract LienToken is ERC721, ILienToken, Auth { LienStorage storage s = _loadLienStorageSlot(); s.TRANSFER_PROXY = _TRANSFER_PROXY; s.WETH = _WETH; - s.maxLiens = uint256(5); + s.maxLiens = uint8(5); } function _loadLienStorageSlot() @@ -74,21 +74,23 @@ contract LienToken is ERC721, ILienToken, Auth { } } - function file(bytes32 what, bytes calldata data) external requiresAuth { + function file(File calldata incoming) external requiresAuth { + FileType what = incoming.what; + bytes memory data = incoming.data; LienStorage storage s = _loadLienStorageSlot(); - if (what == "setAuctionHouse") { + if (what == FileType.AuctionHouse) { address addr = abi.decode(data, (address)); s.AUCTION_HOUSE = IAuctionHouse(addr); - } else if (what == "setCollateralToken") { + } else if (what == FileType.CollateralToken) { address addr = abi.decode(data, (address)); s.COLLATERAL_TOKEN = ICollateralToken(addr); - } else if (what == "setAstariaRouter") { + } else if (what == FileType.AstariaRouter) { address addr = abi.decode(data, (address)); s.ASTARIA_ROUTER = IAstariaRouter(addr); } else { revert UnsupportedFile(); } - emit File(what, data); + emit FileUpdated(what, data); } function supportsInterface(bytes4 interfaceId) @@ -138,9 +140,8 @@ contract LienToken is ERC721, ILienToken, Auth { } if ( - s - .lienMeta[params.encumber.stack[params.position].point.lienId] - .amountAtLiquidation > 0 + s.collateralStateHash[params.encumber.lien.collateralId] == + bytes32("ACTIVE_AUCTION") ) { revert InvalidState(InvalidStates.COLLATERAL_AUCTION); } @@ -192,11 +193,16 @@ contract LienToken is ERC721, ILienToken, Auth { if (i == position) { newStack[i] = newLien; _burn(oldLienId); + delete s.lienMeta[oldLienId]; + if (s.ASTARIA_ROUTER.isValidVault(stack[i].lien.vault)) { + IPublicVault(stack[i].lien.vault).decreaseEpochLienCount( + stack[i].point.end + ); + } } else { newStack[i] = stack[i]; } } - emit LienStackUpdated(stack[0].lien.collateralId, newStack); } function getInterest(Stack calldata stack) public view returns (uint256) { @@ -231,15 +237,6 @@ contract LienToken is ERC721, ILienToken, Auth { _; } - modifier validateAuctionStack(uint256 collateralId, uint256[] memory stack) { - LienStorage storage s = _loadLienStorageSlot(); - bytes32 stateHash = s.collateralStateHash[collateralId]; - if (stateHash != bytes32(0)) { - require(keccak256(abi.encode(stack)) == stateHash, "invalid hash"); - } - _; - } - function stopLiens( uint256 collateralId, uint256 auctionWindow, @@ -248,7 +245,7 @@ contract LienToken is ERC721, ILienToken, Auth { external validateStack(collateralId, stack) requiresAuth - returns (uint256 reserve, uint256[] memory lienIds) + returns (uint256 reserve, AuctionStack[] memory lienIds) { (reserve, lienIds) = _stopLiens( _loadLienStorageSlot(), @@ -263,23 +260,24 @@ contract LienToken is ERC721, ILienToken, Auth { uint256 collateralId, uint256 auctionWindow, Stack[] calldata stack - ) internal returns (uint256 reserve, uint256[] memory lienIds) { + ) internal returns (uint256 reserve, AuctionStack[] memory lienIds) { reserve = 0; - lienIds = new uint256[](stack.length); + lienIds = new AuctionStack[](stack.length); for (uint256 i = 0; i < stack.length; ++i) { - lienIds[i] = stack[i].point.lienId; + lienIds[i].lienId = stack[i].point.lienId; + lienIds[i].end = stack[i].point.end; + uint88 owed; unchecked { owed = _getOwed(stack[i], block.timestamp); reserve += owed; s.lienMeta[stack[i].point.lienId].amountAtLiquidation = owed; } - if ( - s.ASTARIA_ROUTER.isValidVault(_getPayee(s, lienIds[i])) - ) { + address payee = _getPayee(s, lienIds[i].lienId); + if (_isPublicVault(s, payee)) { // update the public vault state and get the liquidation accountant back if any - address withdrawProxyIfNearBoundary = IPublicVault(_getPayee(s, lienIds[i])) + address withdrawProxyIfNearBoundary = IPublicVault(payee) .updateVaultAfterLiquidation( auctionWindow, IPublicVault.AfterLiquidationParams({ @@ -290,11 +288,11 @@ contract LienToken is ERC721, ILienToken, Auth { ); if (withdrawProxyIfNearBoundary != address(0)) { - _setPayee(s, lienIds[i], withdrawProxyIfNearBoundary); + _setPayee(s, lienIds[i].lienId, withdrawProxyIfNearBoundary); } } } - s.collateralStateHash[collateralId] = keccak256(abi.encode(lienIds)); + s.collateralStateHash[collateralId] = bytes32("ACTIVE_AUCTION"); } function tokenURI(uint256 tokenId) @@ -306,6 +304,18 @@ contract LienToken is ERC721, ILienToken, Auth { return ""; } + function transferFrom( + address from, + address to, + uint256 id + ) public override(ERC721, IERC721) { + LienStorage storage s = _loadLienStorageSlot(); + if (s.lienMeta[id].amountAtLiquidation > 0) { + revert InvalidState(InvalidStates.COLLATERAL_AUCTION); + } + super.transferFrom(from, to, id); + } + function AUCTION_HOUSE() public view returns (IAuctionHouse) { return _loadLienStorageSlot().AUCTION_HOUSE; } @@ -334,7 +344,6 @@ contract LienToken is ERC721, ILienToken, Auth { { LienStorage storage s = _loadLienStorageSlot(); //0 - 4 are valid - Stack memory newStackSlot; (lienId, newStackSlot) = _createLien(s, params); @@ -343,15 +352,33 @@ contract LienToken is ERC721, ILienToken, Auth { abi.encode(newStack) ); - lienSlope = calculateSlope(newStackSlot); - emit AddLien(params.collateralId, lienId, newStackSlot.point.position); - emit LienStackUpdated(params.collateralId, newStack); + unchecked { + lienSlope = calculateSlope(newStackSlot); + } + emit AddLien( + params.collateralId, + newStackSlot.point.position, + lienId, + newStackSlot + ); + emit LienStackUpdated( + params.collateralId, + newStackSlot.point.position, + StackAction.ADD, + uint8(newStack.length) + ); } function _createLien( LienStorage storage s, ILienToken.LienActionEncumber memory params ) internal returns (uint256 newLienId, ILienToken.Stack memory newSlot) { + if ( + s.collateralStateHash[params.collateralId] == bytes32("ACTIVE_AUCTION") + ) { + revert InvalidState(InvalidStates.COLLATERAL_AUCTION); + } + if (params.stack.length >= s.maxLiens) { revert InvalidState(InvalidStates.MAX_LIENS); } @@ -379,31 +406,33 @@ contract LienToken is ERC721, ILienToken, Auth { LienStorage storage s, Stack[] memory stack, Stack memory newSlot - ) internal pure returns (Stack[] memory newStack) { + ) internal view 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) { + revert InvalidState(InvalidStates.EXPIRED_LIEN); + } newStack[i] = stack[i]; } newStack[stack.length] = newSlot; } - function removeLiens(uint256 collateralId, uint256[] memory remainingLiens) - external - requiresAuth - { + function removeLiens( + uint256 collateralId, + AuctionStack[] memory remainingLiens + ) external requiresAuth { LienStorage storage s = _loadLienStorageSlot(); for (uint256 i = 0; i < remainingLiens.length; i++) { - address payee = _getPayee(s, remainingLiens[i]); - if ( - s.ASTARIA_ROUTER.isValidVault(payee) - ) { - IPublicVault(payee).decreaseYIntercept( - s.lienMeta[remainingLiens[i]].amountAtLiquidation + 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 ); } - delete s.lienMeta[remainingLiens[i]]; - _burn(remainingLiens[i]); //burn the underlying lien associated + delete s.lienMeta[remainingLiens[i].lienId]; + _burn(remainingLiens[i].lienId); //burn the underlying lien associated } delete s.collateralStateHash[collateralId]; emit RemovedLiens(collateralId); @@ -485,15 +514,14 @@ contract LienToken is ERC721, ILienToken, Auth { } function makePaymentAuctionHouse( - uint256[] memory stack, + AuctionStack[] memory stack, uint256 collateralId, uint256 payment, address payer ) external - validateAuctionStack(collateralId, stack) requiresAuth - returns (uint256[] memory outStack, uint256 spent) + returns (AuctionStack[] memory outStack, uint256 spent) { spent = 0; outStack = stack; @@ -512,32 +540,37 @@ contract LienToken is ERC721, ILienToken, Auth { spent += paymentMade; } } - if (outStack.length != 0) { - s.collateralStateHash[collateralId] = keccak256(abi.encode(outStack)); - } else { + if (outStack.length == 0) { delete s.collateralStateHash[collateralId]; } } function _paymentAH( LienStorage storage s, - uint256[] memory stack, + AuctionStack[] memory stack, uint256 collateralId, uint256 payment, address payer - ) internal returns (uint256[] memory, uint256) { - uint256 lienId = stack[0]; + ) internal returns (AuctionStack[] memory, uint256) { + uint256 lienId = stack[0].lienId; + uint256 end = stack[0].end; //checks the lien exists address payee = _getPayee(s, lienId); //owing at liquidation - if (s.lienMeta[lienId].amountAtLiquidation > payment) { + if (s.lienMeta[lienId].amountAtLiquidation > payment.safeCastTo88()) { s.lienMeta[lienId].amountAtLiquidation -= payment.safeCastTo88(); } else { payment = s.lienMeta[lienId].amountAtLiquidation; delete s.lienMeta[lienId]; //full delete _burn(lienId); - uint256[] memory newStack = new uint256[](stack.length - 1); + AuctionStack[] memory newStack = new AuctionStack[](stack.length - 1); + + if (_isPublicVault(s, payee)) { + IPublicVault(payee).decreaseEpochLienCount( + IPublicVault(payee).getLienEpoch(uint64(end)) + ); + } for (uint256 i = 1; i < stack.length; i++) { newStack[i - 1] = stack[i]; @@ -679,7 +712,7 @@ contract LienToken is ERC721, ILienToken, Auth { uint256 owed = _getOwed(activeStack[position], block.timestamp); address lienOwner = ownerOf(lienId); - bool isPublicVault = _isPublicVault(lienOwner); + bool isPublicVault = _isPublicVault(s, lienOwner); address payee = _getPayee(s, lienId); @@ -743,12 +776,21 @@ contract LienToken is ERC721, ILienToken, Auth { if (i == position) continue; newStack[i] = stack[i]; } - emit RemovedLien(collateralId, position); - emit LienStackUpdated(collateralId, newStack); + emit LienStackUpdated( + collateralId, + position, + StackAction.REMOVE, + uint8(newStack.length) + ); } - function _isPublicVault(address account) internal view returns (bool) { + function _isPublicVault(LienStorage storage s, address account) + internal + view + returns (bool) + { return + s.ASTARIA_ROUTER.isValidVault(account) && IPublicVault(account).supportsInterface(type(IPublicVault).interfaceId); } diff --git a/src/PublicVault.sol b/src/PublicVault.sol index 105aaf7c..af0cd9f8 100644 --- a/src/PublicVault.sol +++ b/src/PublicVault.sol @@ -20,23 +20,24 @@ import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IERC165} from "core/interfaces/IERC165.sol"; import {ERC4626Cloned} from "gpl/ERC4626-Cloned.sol"; import {ITokenBase} from "core/interfaces/ITokenBase.sol"; -import {ERC4626Base} from "core/ERC4626Base.sol"; +import {IERC4626} from "core/interfaces/IERC4626.sol"; +import {IERC20} from "core/interfaces/IERC20.sol"; +import {IERC20Metadata} from "core/interfaces/IERC20Metadata.sol"; +import {ERC20Cloned} from "gpl/ERC20-Cloned.sol"; import { ClonesWithImmutableArgs } from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; -import {IAstariaRouter} from "./interfaces/IAstariaRouter.sol"; -import {ILienToken} from "./interfaces/ILienToken.sol"; -import {IVault} from "core/interfaces/IVault.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; +import {ILienToken} from "core/interfaces/ILienToken.sol"; -import {LienToken} from "./LienToken.sol"; -import {VaultImplementation} from "./VaultImplementation.sol"; -import {WithdrawProxy} from "./WithdrawProxy.sol"; +import {LienToken} from "core/LienToken.sol"; +import {VaultImplementation} from "core/VaultImplementation.sol"; +import {WithdrawProxy} from "core/WithdrawProxy.sol"; -import {Math} from "./utils/Math.sol"; -import {IPublicVault} from "./interfaces/IPublicVault.sol"; -import {Vault} from "./Vault.sol"; +import {Math} from "core/utils/Math.sol"; +import {IPublicVault} from "core/interfaces/IPublicVault.sol"; import {AstariaVaultBase} from "core/AstariaVaultBase.sol"; /* @@ -44,22 +45,57 @@ import {AstariaVaultBase} from "core/AstariaVaultBase.sol"; * @author androolloyd * @notice */ -contract PublicVault is Vault, IPublicVault, ERC4626Cloned { +contract PublicVault is + AstariaVaultBase, + VaultImplementation, + IPublicVault, + ERC4626Cloned +{ using FixedPointMathLib for uint256; using SafeTransferLib for ERC20; using SafeCastLib for uint256; bytes32 constant PUBLIC_VAULT_SLOT = - keccak256("xyz.astaria.core.PublicVault.storage.location"); + keccak256("xyz.astaria.PublicVault.storage.location"); - function underlying() + function asset() public pure virtual - override(ERC4626Base, AstariaVaultBase) + override(AstariaVaultBase, ERC4626Cloned) returns (address) { - return super.underlying(); + return super.asset(); + } + + function decimals() + public + pure + virtual + override(IERC20Metadata) + returns (uint8) + { + return 18; + } + + function name() + public + view + virtual + override(IERC20Metadata, AstariaVaultBase, VaultImplementation) + returns (string memory) + { + return string(abi.encodePacked("AST-Vault-", ERC20(asset()).symbol())); + } + + function symbol() + public + view + virtual + override(IERC20Metadata, AstariaVaultBase, VaultImplementation) + returns (string memory) + { + return string(abi.encodePacked("AST-V-", ERC20(asset()).symbol())); } /** @@ -73,7 +109,7 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { uint256 shares, address receiver, address owner - ) public virtual override returns (uint256 assets) { + ) public virtual override(ERC4626Cloned) returns (uint256 assets) { VaultData storage s = _loadStorageSlot(); assets = redeemFutureEpoch(shares, receiver, owner, s.currentEpoch); } @@ -82,7 +118,7 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { uint256 assets, address receiver, address owner - ) public virtual override returns (uint256 shares) { + ) public virtual override(ERC4626Cloned) returns (uint256 shares) { shares = previewWithdraw(assets); VaultData storage s = _loadStorageSlot(); @@ -95,8 +131,7 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { address owner, uint64 epoch ) public virtual returns (uint256 assets) { - // check to ensure that the requested epoch is not the current epoch or in the past - require(msg.sender == owner); + // check to ensure that the requested epoch is not in the past VaultData storage s = _loadStorageSlot(); if (epoch < s.currentEpoch) { @@ -105,7 +140,7 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { // check for rounding error since we round down in previewRedeem. - ERC20(address(this)).safeTransferFrom(owner, address(this), shares); + ERC20(address(this)).safeTransferFrom(msg.sender, address(this), shares); // Deploy WithdrawProxy if no WithdrawProxy exists for the specified epoch _deployWithdrawProxyIfNotDeployed(s, epoch); @@ -113,13 +148,13 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { emit Withdraw(msg.sender, receiver, owner, assets, shares); // WithdrawProxy shares are minted 1:1 with PublicVault shares - WithdrawProxy(s.epochData[epoch].withdrawProxy).mint(receiver, shares); // was withdrawProxies[withdrawEpoch] + WithdrawProxy(s.epochData[epoch].withdrawProxy).mint(shares, receiver); } - function getWithdrawProxy(uint64 epoch) public view returns (address) { + function getWithdrawProxy(uint64 epoch) public view returns (WithdrawProxy) { VaultData storage s = _loadStorageSlot(); - return s.epochData[epoch].withdrawProxy; + return WithdrawProxy(s.epochData[epoch].withdrawProxy); } function getCurrentEpoch() public view returns (uint64) { @@ -162,7 +197,7 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { address(ROUTER()), // router is the beacon uint8(IAstariaRouter.ImplementationType.WithdrawProxy), address(this), // owner - underlying(), // token + asset(), // token address(this), // vault epoch + 1 // claimable epoch ) @@ -170,6 +205,25 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { } } + function mint(uint256 shares, address receiver) + public + override(ERC4626Cloned) + whenNotPaused + returns (uint256) + { + VIData storage s = _loadVISlot(); + if (s.allowListEnabled) { + require(s.allowList[receiver]); + } + + uint256 assets = totalAssets(); + if (s.depositCap != 0 && assets >= s.depositCap) { + revert InvalidState(InvalidStates.DEPOSIT_CAP_EXCEEDED); + } + + return super.mint(shares, receiver); + } + /** * @notice Deposit funds into the PublicVault. * @param amount The amount of funds to deposit. @@ -177,7 +231,7 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { */ function deposit(uint256 amount, address receiver) public - override(IVault, Vault, ERC4626Cloned) + override(ERC4626Cloned) whenNotPaused returns (uint256) { @@ -213,13 +267,12 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { revert InvalidState(InvalidStates.WITHDRAW_RESERVE_NOT_ZERO); } - address currentWithdrawProxy = s.epochData[s.currentEpoch].withdrawProxy; + WithdrawProxy currentWithdrawProxy = WithdrawProxy( + s.epochData[s.currentEpoch].withdrawProxy + ); - if (currentWithdrawProxy != address(0)) { - if ( - WithdrawProxy(currentWithdrawProxy).getFinalAuctionEnd() > - block.timestamp - ) { + if (address(currentWithdrawProxy) != address(0)) { + if (currentWithdrawProxy.getFinalAuctionEnd() > block.timestamp) { revert InvalidState( InvalidStates.LIQUIDATION_ACCOUNTANT_FINAL_AUCTION_OPEN ); @@ -228,14 +281,14 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { // split funds from previous WithdrawProxy with PublicVault if hasn't been already if (s.currentEpoch != 0) { - address previousWithdrawProxy = s - .epochData[s.currentEpoch - 1] - .withdrawProxy; + WithdrawProxy previousWithdrawProxy = WithdrawProxy( + s.epochData[s.currentEpoch - 1].withdrawProxy + ); if ( - previousWithdrawProxy != address(0) && - WithdrawProxy(previousWithdrawProxy).getFinalAuctionEnd() != 0 + address(previousWithdrawProxy) != address(0) && + previousWithdrawProxy.getFinalAuctionEnd() != 0 ) { - WithdrawProxy(previousWithdrawProxy).claim(); + previousWithdrawProxy.claim(); } } @@ -247,9 +300,8 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { s.liquidationWithdrawRatio = 0; // check if there are LPs withdrawing this epoch - address withdrawProxy = getWithdrawProxy(s.currentEpoch); - if ((withdrawProxy != address(0))) { - uint256 proxySupply = WithdrawProxy(withdrawProxy).totalSupply(); + if ((address(currentWithdrawProxy) != address(0))) { + uint256 proxySupply = currentWithdrawProxy.totalSupply(); unchecked { s.liquidationWithdrawRatio = proxySupply @@ -257,15 +309,13 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { .safeCastTo88(); } - if (currentWithdrawProxy != address(0)) { - WithdrawProxy(currentWithdrawProxy).setWithdrawRatio( - s.liquidationWithdrawRatio - ); + if (address(currentWithdrawProxy) != address(0)) { + currentWithdrawProxy.setWithdrawRatio(s.liquidationWithdrawRatio); } uint256 expected = 0; - if (currentWithdrawProxy != address(0)) { - expected = WithdrawProxy(currentWithdrawProxy).getExpected(); + if (address(currentWithdrawProxy) != address(0)) { + expected = currentWithdrawProxy.getExpected(); } unchecked { @@ -299,7 +349,6 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { { return interfaceId == type(IPublicVault).interfaceId || - interfaceId == type(IVault).interfaceId || interfaceId == type(ERC4626Cloned).interfaceId || interfaceId == type(ERC4626).interfaceId || interfaceId == type(ERC20).interfaceId || @@ -311,24 +360,26 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { if (s.currentEpoch > uint64(0)) { // check the available balance to be withdrawn - uint256 withdrawBalance = ERC20(underlying()).balanceOf(address(this)); - - // prevent transfer of more assets then are available - if (s.withdrawReserve <= withdrawBalance) { - withdrawBalance = s.withdrawReserve; - s.withdrawReserve = 0; - } else { - unchecked { - s.withdrawReserve -= uint88(withdrawBalance); - } - } + address currentWithdrawProxy = s .epochData[s.currentEpoch - 1] .withdrawProxy; // prevents transfer to a non-existent WithdrawProxy // withdrawProxies are indexed by the epoch where they're deployed if (currentWithdrawProxy != address(0)) { - ERC20(underlying()).safeTransfer(currentWithdrawProxy, withdrawBalance); + uint256 withdrawBalance = ERC20(asset()).balanceOf(address(this)); + + // prevent transfer of more assets then are available + if (s.withdrawReserve <= withdrawBalance) { + withdrawBalance = s.withdrawReserve; + s.withdrawReserve = 0; + } else { + unchecked { + s.withdrawReserve -= uint88(withdrawBalance); + } + } + + ERC20(asset()).safeTransfer(currentWithdrawProxy, withdrawBalance); WithdrawProxy(currentWithdrawProxy).increaseWithdrawReserveReceived( withdrawBalance ); @@ -421,13 +472,25 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { * @notice Computes the implied value of this PublicVault. This includes interest payments that have not yet been made. * @return The implied value for this PublicVault. */ - function totalAssets() public view virtual override returns (uint256) { + function totalAssets() + public + view + virtual + override(ERC4626Cloned) + returns (uint256) + { VaultData storage s = _loadStorageSlot(); uint256 delta_t = block.timestamp - s.last; return uint256(s.slope).mulDivDown(delta_t, 1) + uint256(s.yIntercept); } - function totalSupply() public view virtual override returns (uint256) { + function totalSupply() + public + view + virtual + override(IERC20, ERC20Cloned) + returns (uint256) + { return _loadERC20Slot()._totalSupply + _loadStorageSlot().strategistUnclaimedShares; @@ -531,13 +594,13 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { } /** - * @notice TODO - * @param auctionWindow The auction duration. + * @notice + * @param maxAuctionWindow The max possible auction duration. * @param params AfterLiquidation data. * @return withdrawProxyIfNearBoundary The address of the WithdrawProxy to set the payee to if the liquidation is triggered near an epoch boundary. */ function updateVaultAfterLiquidation( - uint256 auctionWindow, + uint256 maxAuctionWindow, AfterLiquidationParams calldata params ) public returns (address withdrawProxyIfNearBoundary) { require(msg.sender == address(LIEN_TOKEN())); // can only be called by router @@ -558,14 +621,15 @@ contract PublicVault is Vault, IPublicVault, ERC4626Cloned { _decreaseEpochLienCount(s, lienEpoch); uint256 timeToEnd = timeToEpochEnd(lienEpoch); - if (timeToEnd <= auctionWindow) { + if (timeToEnd < maxAuctionWindow) { _deployWithdrawProxyIfNotDeployed(s, lienEpoch); withdrawProxyIfNearBoundary = s.epochData[lienEpoch].withdrawProxy; } + if (withdrawProxyIfNearBoundary != address(0)) { WithdrawProxy(withdrawProxyIfNearBoundary).handleNewLiquidation( params.newAmount, - auctionWindow + maxAuctionWindow ); } } diff --git a/src/Vault.sol b/src/Vault.sol index 0bfcf591..7d2853bc 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -18,7 +18,6 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; import {IERC165} from "core/interfaces/IERC165.sol"; -import {IVault} from "core/interfaces/IVault.sol"; import {ITokenBase} from "core/interfaces/ITokenBase.sol"; import {AstariaVaultBase} from "core/AstariaVaultBase.sol"; @@ -27,51 +26,71 @@ import {ILienToken} from "core/interfaces/ILienToken.sol"; import {LienToken} from "core/LienToken.sol"; import {VaultImplementation} from "core/VaultImplementation.sol"; +import {IERC4626} from "core/interfaces/IERC4626.sol"; /** * @title Vault */ -contract Vault is AstariaVaultBase, VaultImplementation, IVault { +contract Vault is AstariaVaultBase, VaultImplementation { using SafeTransferLib for ERC20; - function name() public view override returns (string memory) { - return string(abi.encodePacked("AST-Vault-", ERC20(underlying()).symbol())); + function name() + public + view + virtual + override(AstariaVaultBase, VaultImplementation) + returns (string memory) + { + return string(abi.encodePacked("AST-Vault-", ERC20(asset()).symbol())); } - function symbol() public view override returns (string memory) { + function symbol() + public + view + virtual + override(AstariaVaultBase, VaultImplementation) + returns (string memory) + { return - string( - abi.encodePacked("AST-V", owner(), "-", ERC20(underlying()).symbol()) - ); + string(abi.encodePacked("AST-V", owner(), "-", ERC20(asset()).symbol())); + } + + function supportsInterface(bytes4 interfaceId) + public + pure + virtual + returns (bool) + { + return false; } function deposit(uint256 amount, address receiver) public virtual - override returns (uint256) { VIData storage s = _loadVISlot(); - require(s.allowList[msg.sender]); - ERC20(underlying()).safeTransferFrom( - address(msg.sender), - address(this), - amount + require( + s.allowList[msg.sender] || + (msg.sender == address(ROUTER()) && s.allowList[receiver]) ); + ERC20(asset()).safeTransferFrom(address(msg.sender), address(this), amount); return amount; } function withdraw(uint256 amount) external { - ERC20(underlying()).safeTransferFrom( - address(this), - address(msg.sender), - amount - ); + require(msg.sender == owner()); + ERC20(asset()).safeTransferFrom(address(this), address(msg.sender), amount); } function disableAllowList() external pure override(VaultImplementation) { //invalid action allowlist must be enabled for private vaults - revert(); + revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); + } + + function enableAllowList() external pure override(VaultImplementation) { + //invalid action allowlist must be enabled for private vaults + revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } function modifyAllowList(address depositor, bool enabled) @@ -80,6 +99,6 @@ contract Vault is AstariaVaultBase, VaultImplementation, IVault { override(VaultImplementation) { //invalid action private vautls can only be the owner or strategist - revert(); + revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); } } diff --git a/src/VaultImplementation.sol b/src/VaultImplementation.sol index f7e6a5e5..645e4f06 100644 --- a/src/VaultImplementation.sol +++ b/src/VaultImplementation.sol @@ -16,7 +16,6 @@ import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; -import {IVault} from "core/interfaces/IVault.sol"; import {CollateralLookup} from "core/libraries/CollateralLookup.sol"; @@ -41,35 +40,32 @@ abstract contract VaultImplementation is using CollateralLookup for address; using FixedPointMathLib for uint256; + function name() public view virtual override returns (string memory); + + function symbol() public view virtual override returns (string memory); + bytes32 constant VI_SLOT = - keccak256("xyz.astaria.core.VaultImplementation.storage.location"); - address public delegate; //account connected to the daemon - bool public allowListEnabled; - uint256 public depositCap; - - struct VIData { - address delegate; - bool allowListEnabled; - uint256 depositCap; - mapping(address => bool) allowList; - } + keccak256("xyz.astaria.VaultImplementation.storage.location"); - mapping(address => bool) public allowList; - event NewLien( - bytes32 strategyRoot, - address tokenContract, - uint256 tokenId, - uint256 amount - ); + function getStrategistNonce() external view returns (uint32) { + return _loadVISlot().strategistNonce; + } - event NewVault(address appraiser, address vault); + function incrementNonce() external { + VIData storage s = _loadVISlot(); + if (msg.sender != owner() && msg.sender != s.delegate) { + revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); + } + s.strategistNonce++; + emit NonceUpdated(s.strategistNonce); + } /** * @notice modify the deposit cap for the vault * @param newCap The deposit cap. */ function modifyDepositCap(uint256 newCap) public onlyOwner { - depositCap = newCap; + _loadVISlot().depositCap = newCap.safeCastTo88(); } function _loadVISlot() internal pure returns (VIData storage vi) { @@ -89,14 +85,21 @@ abstract contract VaultImplementation is virtual onlyOwner { - allowList[depositor] = enabled; + _loadVISlot().allowList[depositor] = enabled; } /** * @notice disable the allowlist for the vault */ function disableAllowList() external virtual onlyOwner { - allowListEnabled = false; + _loadVISlot().allowListEnabled = false; + } + + /** + * @notice enable the allowl ist for the vault + */ + function enableAllowList() external virtual onlyOwner { + _loadVISlot().allowListEnabled = true; } /** @@ -112,12 +115,25 @@ abstract contract VaultImplementation is } modifier whenNotPaused() { - if (IAstariaRouter(ROUTER()).paused()) { - revert("protocol is paused"); + if (ROUTER().paused()) { + revert InvalidRequest(InvalidRequestReason.PAUSED); + } + + if (_loadVISlot().isShutdown) { + revert InvalidRequest(InvalidRequestReason.SHUTDOWN); } _; } + function getShutdown() external view returns (bool) { + return _loadVISlot().isShutdown; + } + + function shutdown() external onlyOwner { + _loadVISlot().isShutdown = true; + emit VaultShutdown(); + } + function domainSeparator() public view virtual returns (bytes32) { return keccak256( @@ -125,63 +141,54 @@ abstract contract VaultImplementation is keccak256( "EIP712Domain(string version,uint256 chainId,address verifyingContract)" ), - keccak256("0"), + keccak256("0"), //version block.chainid, address(this) ) ); } + bytes32 public constant STRATEGY_TYPEHASH = + 0x679f3933bd13bd2e4ec6e9cde341ede07736ad7b635428a8a211e9cccb4393b0; + /* * @notice encodes the data for a 712 signature * @param tokenContract The address of the token contract * @param tokenId The id of the token * @param amount The amount of the token */ - - // cast k "StrategyDetails(uint256 nonce,uint256 deadline,bytes32 root)" - bytes32 private constant STRATEGY_TYPEHASH = - 0x679f3933bd13bd2e4ec6e9cde341ede07736ad7b635428a8a211e9cccb4393b0; - function encodeStrategyData( IAstariaRouter.StrategyDetails calldata strategy, bytes32 root - ) public view returns (bytes memory) { + ) external view returns (bytes memory) { + VIData storage s = _loadVISlot(); + return _encodeStrategyData(s, strategy, root); + } + + function _encodeStrategyData( + VIData storage s, + IAstariaRouter.StrategyDetails calldata strategy, + bytes32 root + ) internal view returns (bytes memory) { bytes32 hash = keccak256( - abi.encode( - STRATEGY_TYPEHASH, - IAstariaRouter(ROUTER()).strategistNonce(strategy.strategist), - strategy.deadline, - root - ) + abi.encode(STRATEGY_TYPEHASH, s.strategistNonce, strategy.deadline, root) ); return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), hash); } - struct InitParams { - address delegate; - bool allowListEnabled; - address[] allowList; - uint256 depositCap; // max amount of tokens that can be deposited - } - function init(InitParams calldata params) external virtual { require(msg.sender == address(ROUTER())); - VIData storage vi; - bytes32 slot = VI_SLOT; - assembly { - vi.slot := slot - } + VIData storage s = _loadVISlot(); if (params.delegate != address(0)) { - vi.delegate = params.delegate; + s.delegate = params.delegate; } - depositCap = params.depositCap; + s.depositCap = params.depositCap.safeCastTo88(); if (params.allowListEnabled) { - vi.allowListEnabled = true; + s.allowListEnabled = true; for (uint256 i = 0; i < params.allowList.length; i++) { - vi.allowList[params.allowList[i]] = true; + s.allowList[params.allowList[i]] = true; } } } @@ -191,10 +198,11 @@ abstract contract VaultImplementation is _; } - function setDelegate(address delegate_) public onlyOwner { - allowList[delegate] = false; - allowList[delegate_] = true; - delegate = delegate_; + function setDelegate(address delegate_) external onlyOwner { + VIData storage s = _loadVISlot(); + s.allowList[s.delegate] = false; + s.allowList[delegate_] = true; + s.delegate = delegate_; } /** @@ -212,23 +220,16 @@ abstract contract VaultImplementation is IAstariaRouter.Commitment calldata params, address receiver ) internal view { - if ( - params.lienRequest.amount > ERC20(underlying()).balanceOf(address(this)) - ) { - revert InvalidRequest(InvalidRequestReason.INSUFFICIENT_FUNDS); - } uint256 collateralId = params.tokenContract.computeId(params.tokenId); ERC721 CT = ERC721(address(COLLATERAL_TOKEN())); + address holder = CT.ownerOf(collateralId); address operator = CT.getApproved(collateralId); - address holder = ERC721(address(COLLATERAL_TOKEN())).ownerOf(collateralId); - if ( msg.sender != holder && receiver != holder && receiver != operator && - receiver != recipient() && - !IAstariaRouter(ROUTER()).isValidVault(receiver) + !ROUTER().isValidVault(receiver) ) { if (operator != address(0)) { require(operator == receiver); @@ -236,10 +237,11 @@ abstract contract VaultImplementation is require(CT.isApprovedForAll(holder, receiver)); } } - + VIData storage s = _loadVISlot(); address recovered = ecrecover( keccak256( - encodeStrategyData( + _encodeStrategyData( + s, params.lienRequest.strategy, params.lienRequest.merkle.root ) @@ -248,11 +250,12 @@ abstract contract VaultImplementation is params.lienRequest.r, params.lienRequest.s ); - if (recovered != params.lienRequest.strategy.strategist) { - revert InvalidRequest(InvalidRequestReason.INVALID_SIGNATURE); - } - if (recovered != owner() && recovered != delegate) { - revert InvalidRequest(InvalidRequestReason.INVALID_STRATEGIST); + if ( + recovered != owner() && recovered != s.delegate && recovered != address(0) + ) { + revert IVaultImplementation.InvalidRequest( + InvalidRequestReason.INVALID_SIGNATURE + ); } } @@ -296,12 +299,6 @@ abstract contract VaultImplementation is params.lienRequest.amount, slopeAddition ); - emit NewLien( - params.lienRequest.merkle.root, - params.tokenContract, - params.tokenId, - params.lienRequest.amount - ); } /** @@ -315,25 +312,26 @@ abstract contract VaultImplementation is uint8 position, IAstariaRouter.Commitment calldata incomingTerms, ILienToken.Stack[] calldata stack - ) external whenNotPaused { + ) + external + whenNotPaused + returns (ILienToken.Stack[] memory, ILienToken.Stack memory) + { (uint256 owed, uint256 buyout) = IAstariaRouter(ROUTER()) .LIEN_TOKEN() .getBuyout(stack[position]); - if (buyout > ERC20(underlying()).balanceOf(address(this))) { - revert InvalidRequest(InvalidRequestReason.INSUFFICIENT_FUNDS); + if (buyout > ERC20(asset()).balanceOf(address(this))) { + revert IVaultImplementation.InvalidRequest( + InvalidRequestReason.INSUFFICIENT_FUNDS + ); } _validateCommitment(incomingTerms, recipient()); - ERC20(underlying()).safeApprove( - address(IAstariaRouter(ROUTER()).TRANSFER_PROXY()), - buyout - ); + ERC20(asset()).safeApprove(address(ROUTER().TRANSFER_PROXY()), buyout); - LienToken lienToken = LienToken( - address(IAstariaRouter(ROUTER()).LIEN_TOKEN()) - ); + LienToken lienToken = LienToken(address(ROUTER().LIEN_TOKEN())); if ( recipient() != address(this) && @@ -342,21 +340,20 @@ abstract contract VaultImplementation is lienToken.setApprovalForAll(recipient(), true); } - ILienToken.Lien memory newLien = ROUTER().validateCommitment(incomingTerms); - - lienToken.buyoutLien( - ILienToken.LienActionBuyout({ - incoming: incomingTerms, - position: position, - encumber: ILienToken.LienActionEncumber({ - collateralId: collateralId, - amount: incomingTerms.lienRequest.amount, - receiver: recipient(), - lien: newLien, - stack: stack + return + lienToken.buyoutLien( + ILienToken.LienActionBuyout({ + incoming: incomingTerms, + position: position, + encumber: ILienToken.LienActionEncumber({ + collateralId: collateralId, + amount: incomingTerms.lienRequest.amount, + receiver: recipient(), + lien: ROUTER().validateCommitment(incomingTerms), + stack: stack + }) }) - }) - ); + ); } /** @@ -387,25 +384,22 @@ abstract contract VaultImplementation is uint256 slope ) { - (newLienId, stack, slope) = IAstariaRouter(ROUTER()).requestLienPosition( - c, - recipient() - ); - + _validateCommitment(c, receiver); + (newLienId, stack, slope) = ROUTER().requestLienPosition(c, recipient()); uint256 payout = _handleProtocolFee(c.lienRequest.amount); - ERC20(underlying()).safeTransfer(receiver, payout); + ERC20(asset()).safeTransfer(receiver, payout); } function _handleProtocolFee(uint256 amount) internal returns (uint256) { - address feeTo = IAstariaRouter(ROUTER()).feeTo(); + address feeTo = ROUTER().feeTo(); bool feeOn = feeTo != address(0); if (feeOn) { - uint256 fee = IAstariaRouter(ROUTER()).getProtocolFee(amount); + uint256 fee = ROUTER().getProtocolFee(amount); unchecked { amount -= fee; } - ERC20(underlying()).safeTransfer(feeTo, fee); + ERC20(asset()).safeTransfer(feeTo, fee); } return amount; } diff --git a/src/WithdrawProxy.sol b/src/WithdrawProxy.sol index 0e8fe386..6ab749ad 100644 --- a/src/WithdrawProxy.sol +++ b/src/WithdrawProxy.sol @@ -17,9 +17,12 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {ERC4626Cloned} from "gpl/ERC4626-Cloned.sol"; import {WithdrawVaultBase} from "core/WithdrawVaultBase.sol"; -import {ITokenBase} from "core/interfaces/ITokenBase.sol"; +import {IWithdrawProxy} from "core/interfaces/IWithdrawProxy.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; -import {PublicVault} from "./PublicVault.sol"; +import {PublicVault} from "core/PublicVault.sol"; +import {IERC20Metadata} from "core/interfaces/IERC20Metadata.sol"; +import {IERC4626} from "core/interfaces/IERC4626.sol"; + /** * @title WithdrawProxy * @notice This contract collects funds for liquidity providers who are exiting. When a liquidity provider is the first @@ -29,6 +32,7 @@ import {PublicVault} from "./PublicVault.sol"; * of the next epoch. * */ + contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { using SafeTransferLib for ERC20; using FixedPointMathLib for uint256; @@ -42,9 +46,9 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { ); bytes32 constant WITHDRAW_PROXY_SLOT = - keccak256("xyz.astaria.WithdrawProxy.storage.location"); // TODO change + keccak256("xyz.astaria.WithdrawProxy.storage.location"); - struct WPStorage { + struct WPStorage { uint88 withdrawRatio; uint88 expected; // Expected value of auctioned NFTs. yIntercept (virtual assets) of a PublicVault are not modified on liquidation, only once an auction is completed. uint40 finalAuctionEnd; // when this is deleted, we know the final auction is over @@ -59,8 +63,26 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { } error InvalidState(InvalidStates); - function totalAssets() public view override returns (uint256) { - return ERC20(underlying()).balanceOf(address(this)); + function decimals() public pure override returns (uint8) { + return 18; + } + + function asset() + public + pure + override(ERC4626Cloned, WithdrawVaultBase) + returns (address) + { + return super.asset(); + } + + function totalAssets() + public + view + override(ERC4626Cloned, IERC4626) + returns (uint256) + { + return ERC20(asset()).balanceOf(address(this)); } /** @@ -70,13 +92,11 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { function name() public view - override(ITokenBase, WithdrawVaultBase) + override(IERC20Metadata, WithdrawVaultBase) returns (string memory) { return - string( - abi.encodePacked("AST-WithdrawVault-", ERC20(underlying()).symbol()) - ); + string(abi.encodePacked("AST-WithdrawVault-", ERC20(asset()).symbol())); } /** @@ -86,13 +106,11 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { function symbol() public view - override(ITokenBase, WithdrawVaultBase) + override(IERC20Metadata, WithdrawVaultBase) returns (string memory) { return - string( - abi.encodePacked("AST-W", owner(), "-", ERC20(underlying()).symbol()) - ); + string(abi.encodePacked("AST-W", owner(), "-", ERC20(asset()).symbol())); } /** @@ -100,9 +118,40 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { * @param receiver The receiver of the Withdraw Tokens. * @param shares The number of shares to mint. */ - function mint(address receiver, uint256 shares) public virtual { + function mint(uint256 shares, address receiver) + public + virtual + override(ERC4626Cloned, IERC4626) + returns (uint256 assets) + { require(msg.sender == owner(), "only owner can mint"); _mint(receiver, shares); + return shares; + } + + function deposit(uint256 assets, address receiver) + public + virtual + override(ERC4626Cloned, IERC4626) + returns (uint256 shares) + { + revert NotSupported(); + } + + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual override(ERC4626Cloned, IERC4626) returns (uint256 shares) { + WPStorage storage s = _loadSlot(); + // If auction funds have been collected to the WithdrawProxy + // but the PublicVault hasn't claimed its share, too much money will be sent to LPs + if (s.finalAuctionEnd != 0) { + // if finalAuctionEnd is 0, no auctions were added + revert InvalidState(InvalidStates.NOT_CLAIMED); + } + + return super.withdraw(assets, receiver, owner); } /** @@ -116,15 +165,25 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { uint256 shares, address receiver, address owner - ) public virtual override returns (uint256 assets) { + ) public virtual override(ERC4626Cloned, IERC4626) returns (uint256 assets) { WPStorage storage s = _loadSlot(); // If auction funds have been collected to the WithdrawProxy // but the PublicVault hasn't claimed its share, too much money will be sent to LPs - if (s.finalAuctionEnd != 0) { // if finalAuctionEnd is 0, no auctions were added + if (s.finalAuctionEnd != 0) { + // if finalAuctionEnd is 0, no auctions were added revert InvalidState(InvalidStates.NOT_CLAIMED); } - super.redeem(shares, receiver, owner); + return super.redeem(shares, receiver, owner); + } + + function supportsInterface(bytes4 interfaceId) + external + view + virtual + returns (bool) + { + return interfaceId == type(IWithdrawProxy).interfaceId; } function _loadSlot() internal pure returns (WPStorage storage s) { @@ -161,7 +220,7 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { function claim() public { WPStorage storage s = _loadSlot(); - if(s.finalAuctionEnd == 0) { + if (s.finalAuctionEnd == 0) { revert InvalidState(InvalidStates.CANT_CLAIM); } @@ -176,24 +235,32 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { } uint256 transferAmount = 0; - uint256 balance = ERC20(underlying()).balanceOf(address(this)) - s.withdrawReserveReceived; + uint256 balance = ERC20(asset()).balanceOf(address(this)) - + s.withdrawReserveReceived; if (balance < s.expected) { PublicVault(VAULT()).decreaseYIntercept( - (s.expected - balance).mulWadDown(1e18 - s.withdrawRatio) + (s.expected - balance).mulWadDown( + 10**ERC20(asset()).decimals() - s.withdrawRatio + ) ); } if (s.withdrawRatio == uint256(0)) { - ERC20(underlying()).safeTransfer(VAULT(), balance); + ERC20(asset()).safeTransfer(VAULT(), balance); } else { - transferAmount = uint256(s.withdrawRatio).mulDivDown(balance, 1e18); + transferAmount = uint256(s.withdrawRatio).mulDivDown( + balance, + 10**ERC20(asset()).decimals() + ); unchecked { balance -= transferAmount; } - ERC20(underlying()).safeTransfer(VAULT(), balance); + if (balance > 0) { + ERC20(asset()).safeTransfer(VAULT(), balance); + } } s.finalAuctionEnd = 0; @@ -210,11 +277,11 @@ contract WithdrawProxy is ERC4626Cloned, WithdrawVaultBase { returns (uint256) { require(msg.sender == VAULT()); - uint256 balance = ERC20(underlying()).balanceOf(address(this)); + uint256 balance = ERC20(asset()).balanceOf(address(this)); if (amount > balance) { amount = balance; } - ERC20(underlying()).safeTransfer(withdrawProxy, amount); + ERC20(asset()).safeTransfer(withdrawProxy, amount); return amount; } @@ -232,17 +299,17 @@ 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 finalAuctionTimestamp 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. + * @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 finalAuctionTimestamp + uint256 finalAuctionDelta ) public { require(msg.sender == VAULT()); WPStorage storage s = _loadSlot(); unchecked { s.expected += newLienExpectedValue.safeCastTo88(); - s.finalAuctionEnd = finalAuctionTimestamp.safeCastTo40(); + s.finalAuctionEnd = (block.timestamp + finalAuctionDelta).safeCastTo40(); } } } diff --git a/src/WithdrawVaultBase.sol b/src/WithdrawVaultBase.sol index 98f8bf79..87f9535b 100644 --- a/src/WithdrawVaultBase.sol +++ b/src/WithdrawVaultBase.sol @@ -9,11 +9,13 @@ */ pragma solidity ^0.8.17; -import {ERC4626Base} from "core/ERC4626Base.sol"; import {IRouterBase} from "core/interfaces/IRouterBase.sol"; import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; +import {IWithdrawProxy} from "core/interfaces/IWithdrawProxy.sol"; +import {IERC4626} from "core/interfaces/IERC4626.sol"; +import {Clone} from "clones-with-immutable-args/Clone.sol"; -abstract contract WithdrawVaultBase is ERC4626Base, IRouterBase { +abstract contract WithdrawVaultBase is Clone, IWithdrawProxy { function name() public view virtual returns (string memory); function symbol() public view virtual returns (string memory); @@ -30,13 +32,7 @@ abstract contract WithdrawVaultBase is ERC4626Base, IRouterBase { return _getArgAddress(21); } - function underlying() - public - pure - virtual - override(ERC4626Base) - returns (address) - { + function asset() public pure virtual override(IERC4626) returns (address) { return _getArgAddress(41); } @@ -44,7 +40,7 @@ abstract contract WithdrawVaultBase is ERC4626Base, IRouterBase { return _getArgAddress(61); } - function CLAIMABLE_EPOCH() public pure returns (uint256) { + function CLAIMABLE_EPOCH() public pure returns (uint64) { return _getArgUint64(81); } } diff --git a/src/actions/UNIV3/ClaimFees.sol b/src/actions/UNIV3/ClaimFees.sol new file mode 100644 index 00000000..473b986b --- /dev/null +++ b/src/actions/UNIV3/ClaimFees.sol @@ -0,0 +1,30 @@ +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"; + +contract ClaimFees is IFlashAction { + address public immutable positionManager; + + constructor(address positionManager_) { + positionManager = positionManager_; + } + + function onFlashAction( + IFlashAction.Underlying calldata asset, + bytes calldata data + ) external override returns (bytes32) { + address receiver = abi.decode(data, (address)); + IV3PositionManager(positionManager).collect( + IV3PositionManager.CollectParams( + asset.tokenId, + receiver, + type(uint128).max, + type(uint128).max + ) + ); + ERC721(asset.token).transferFrom(address(this), msg.sender, asset.tokenId); + return keccak256("FlashAction.onFlashAction"); + } +} diff --git a/src/interfaces/IAstariaRouter.sol b/src/interfaces/IAstariaRouter.sol index f8e2c815..bbe5f742 100644 --- a/src/interfaces/IAstariaRouter.sol +++ b/src/interfaces/IAstariaRouter.sol @@ -12,20 +12,51 @@ pragma solidity ^0.8.17; import {IERC721} from "core/interfaces/IERC721.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; -import {IVault} from "gpl/ERC4626-Cloned.sol"; +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 "./IBeacon.sol"; +import {IBeacon} from "core/interfaces/IBeacon.sol"; +import {IERC4626RouterBase} from "gpl/interfaces/IERC4626RouterBase.sol"; interface IAstariaRouter is IPausable, IBeacon { + enum FileType { + FeeTo, + LiquidationFee, + ProtocolFee, + StrategistFee, + MinInterestBPS, + MinEpochLength, + MaxEpochLength, + MinInterestRate, + MaxInterestRate, + BuyoutFee, + MinDurationIncrease, + BuyoutInterestWindow, + AuctionWindow, + StrategyValidator, + AuctionHouse, + Implementation, + CollateralToken, + LienToken, + TransferProxy + } + + struct File { + FileType what; + bytes data; + } + + event FileUpdated(FileType what, bytes data); + error UnsupportedFile(); + struct RouterStorage { //slot 1 - uint32 minInterestBPS; // was uint64 uint32 auctionWindow; + uint32 auctionWindowBuffer; uint32 liquidationFeeNumerator; uint32 liquidationFeeDenominator; uint32 maxEpochLength; @@ -39,20 +70,20 @@ interface IAstariaRouter is IPausable, IBeacon { ITransferProxy TRANSFER_PROXY; //20 IAuctionHouse AUCTION_HOUSE; //20 address feeTo; //20 - address guardian; //20 address BEACON_PROXY_IMPLEMENTATION; //20 uint88 maxInterestRate; //6 - uint32 strategistFeeNumerator; //4 - uint32 strategistFeeDenominator; + uint32 minInterestBPS; // was uint64 + mapping(uint8 => address) strategyValidators; //slot 3 + + address guardian; //20 uint32 buyoutFeeNumerator; uint32 buyoutFeeDenominator; + uint32 strategistFeeDenominator; + uint32 strategistFeeNumerator; //4 uint32 minDurationIncrease; uint32 buyoutInterestWindow; - mapping(uint32 => address) strategyValidators; mapping(uint8 => address) implementations; //A strategist can have many deployed vaults - mapping(address => uint32) strategistNonce; mapping(address => address) vaults; } @@ -63,6 +94,7 @@ interface IAstariaRouter is IPausable, IBeacon { } enum LienRequestType { + DEACTIVATED, UNIQUE, COLLECTION, UNIV3_LIQUIDITY @@ -70,7 +102,6 @@ interface IAstariaRouter is IPausable, IBeacon { struct StrategyDetails { uint8 version; - address strategist; uint256 deadline; address vault; } @@ -83,7 +114,6 @@ interface IAstariaRouter is IPausable, IBeacon { struct NewLienRequest { StrategyDetails strategy; ILienToken.Stack[] stack; - uint8 nlrType; bytes nlrDetails; MerkleData merkle; uint256 amount; @@ -98,8 +128,6 @@ interface IAstariaRouter is IPausable, IBeacon { NewLienRequest lienRequest; } - function strategistNonce(address strategist) external view returns (uint256); - /** * @notice Validates the incoming commitment * @param commitment The commitment proofs and requested loan data for each loan. @@ -173,7 +201,7 @@ interface IAstariaRouter is IPausable, IBeacon { function maxInterestRate() external view returns (uint256); - function getAuctionWindow() external view returns (uint256); + function getAuctionWindow(bool includeBuffer) external view returns (uint256); function getStrategistFee(uint256) external view returns (uint256); @@ -185,13 +213,6 @@ interface IAstariaRouter is IPausable, IBeacon { function getBuyoutInterestWindow() external view returns (uint32); - /** - * @notice Lend to a PublicVault. - * @param vault The address of the PublicVault. - * @param amount The amount to lend. - */ - function lendToVault(IVault vault, uint256 amount) external; - /** * @notice Liquidate a CollateralToken that has defaulted on one of its liens. * @param collateralId The ID of the CollateralToken. @@ -208,6 +229,8 @@ interface IAstariaRouter is IPausable, IBeacon { function isValidVault(address) external view returns (bool); + function file(File calldata incoming) external; + function isValidRefinance( ILienToken.Lien calldata newLien, uint8 position, @@ -226,8 +249,18 @@ interface IAstariaRouter is IPausable, IBeacon { */ function endAuction(uint256 tokenId) external; - event Liquidation(uint256 collateralId, uint256 position, uint256 reserve); - event NewVault(address appraiser, address vault); + event Liquidation( + uint256 collateralId, + uint256 position, + uint256 reserve, + uint256[] fee + ); + event NewVault( + address strategist, + address delegate, + address vault, + uint8 vaultType + ); error InvalidEpochLength(uint256); error InvalidRefinanceRate(uint256); diff --git a/src/interfaces/IAstariaVaultBase.sol b/src/interfaces/IAstariaVaultBase.sol index 40cfcfbf..a254deba 100644 --- a/src/interfaces/IAstariaVaultBase.sol +++ b/src/interfaces/IAstariaVaultBase.sol @@ -1,12 +1,11 @@ pragma solidity ^0.8.16; -import {IERC4626Base} from "./IERC4626Base.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"; -interface IAstariaVaultBase is IERC4626Base, IRouterBase { +interface IAstariaVaultBase is IRouterBase { function owner() external view returns (address); function COLLATERAL_TOKEN() external view returns (ICollateralToken); diff --git a/src/interfaces/ICollateralToken.sol b/src/interfaces/ICollateralToken.sol index a6161998..76941d20 100644 --- a/src/interfaces/ICollateralToken.sol +++ b/src/interfaces/ICollateralToken.sol @@ -33,11 +33,21 @@ interface ICollateralToken is IERC721 { //mapping of a security token hook for an nft's token contract address mapping(address => address) securityHooks; } + enum FileType { + NotSupported, + AstariaRouter, + AuctionHouse, + SecurityHook, + FlashEnabled + } + struct File { - bytes32 what; + FileType what; bytes data; } + event FileUpdated(FileType what, bytes data); + /** * @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. @@ -57,10 +67,9 @@ interface ICollateralToken is IERC721 { * @param collateralId The ID of the CollateralToken wrapping the NFT. * @return The address and tokenId of the underlying NFT. */ - function getUnderlying(uint256 collateralId) - external - view - returns (address, uint256); + function getUnderlying( + uint256 collateralId + ) external view returns (address, uint256); /** * @notice Unlocks the NFT for a CollateralToken and sends it to a specified address. @@ -80,8 +89,8 @@ interface ICollateralToken is IERC721 { uint256 assetId, address indexed to ); - event FileUpdated(bytes32 indexed what, bytes data); + error UnsupportedFile(); error InvalidCollateral(); error InvalidSender(); error InvalidCollateralState(InvalidCollateralStates); diff --git a/src/interfaces/IERC20Metadata.sol b/src/interfaces/IERC20Metadata.sol new file mode 100644 index 00000000..8ea6e0c6 --- /dev/null +++ b/src/interfaces/IERC20Metadata.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.8.17; +import {IERC20} from "core/interfaces/IERC20.sol"; + +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} diff --git a/src/interfaces/IERC4626.sol b/src/interfaces/IERC4626.sol new file mode 100644 index 00000000..6e2b73a0 --- /dev/null +++ b/src/interfaces/IERC4626.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (interfaces/IERC4626.sol) + +pragma solidity ^0.8.17; +import {IERC20} from "core/interfaces/IERC20.sol"; +import {IERC20Metadata} from "core/interfaces/IERC20Metadata.sol"; + +/** + * @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in + * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. + * + * _Available since v4.7._ + */ +interface IERC4626 is IERC20, IERC20Metadata { + event Deposit( + address indexed sender, + address indexed owner, + uint256 assets, + uint256 shares + ); + + event Withdraw( + address indexed sender, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /** + * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. + * + * - MUST be an ERC-20 token contract. + * - MUST NOT revert. + */ + function asset() external view returns (address assetTokenAddress); + + /** + * @dev Returns the total amount of the underlying asset that is “managed” by Vault. + * + * - SHOULD include any compounding that occurs from yield. + * - MUST be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT revert. + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToShares(uint256 assets) + external + view + returns (uint256 shares); + + /** + * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToAssets(uint256 shares) + external + view + returns (uint256 assets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver, + * through a deposit call. + * + * - MUST return a limited value if receiver is subject to some deposit limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. + * - MUST NOT revert. + */ + function maxDeposit(address receiver) + external + view + returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit + * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called + * in the same transaction. + * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the + * deposit would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewDeposit(uint256 assets) + external + view + returns (uint256 shares); + + /** + * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * deposit execution, and are accounted for during deposit. + * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function deposit(uint256 assets, address receiver) + external + returns (uint256 shares); + + /** + * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call. + * - MUST return a limited value if receiver is subject to some mint limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted. + * - MUST NOT revert. + */ + function maxMint(address receiver) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call + * in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the + * same transaction. + * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint + * would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by minting. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint + * execution, and are accounted for during mint. + * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function mint(uint256 shares, address receiver) + external + returns (uint256 assets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the + * Vault, through a withdraw call. + * + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw + * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if + * called + * in the same transaction. + * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though + * the withdrawal would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewWithdraw(uint256 assets) + external + view + returns (uint256 shares); + + /** + * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * withdraw execution, and are accounted for during withdraw. + * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares); + + /** + * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, + * through a redeem call. + * + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call + * in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the + * same transaction. + * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the + * redemption would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by redeeming. + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * redeem execution, and are accounted for during redeem. + * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets); +} diff --git a/src/interfaces/IERC4626Base.sol b/src/interfaces/IERC4626Base.sol index c386024c..df8a1285 100644 --- a/src/interfaces/IERC4626Base.sol +++ b/src/interfaces/IERC4626Base.sol @@ -3,5 +3,5 @@ pragma solidity ^0.8.16; import {ITokenBase} from "core/interfaces/ITokenBase.sol"; interface IERC4626Base is ITokenBase { - function underlying() external view returns (address); + function asset() external view returns (address); } diff --git a/src/interfaces/IERC721.sol b/src/interfaces/IERC721.sol index 86dae051..dba04563 100644 --- a/src/interfaces/IERC721.sol +++ b/src/interfaces/IERC721.sol @@ -1,6 +1,6 @@ pragma solidity >=0.8.0; -import {IERC165} from "./IERC165.sol"; +import {IERC165} from "core/interfaces/IERC165.sol"; interface IERC721 is IERC165 { event Transfer(address indexed from, address indexed to, uint256 indexed id); diff --git a/src/interfaces/ILienToken.sol b/src/interfaces/ILienToken.sol index ae950735..0af24764 100644 --- a/src/interfaces/ILienToken.sol +++ b/src/interfaces/ILienToken.sol @@ -18,8 +18,22 @@ import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {ITransferProxy} from "core/interfaces/ITransferProxy.sol"; interface ILienToken is IERC721 { + enum FileType { + NotSupported, + AuctionHouse, + CollateralToken, + AstariaRouter + } + + struct File { + FileType what; + bytes data; + } + + event FileUpdated(FileType what, bytes data); + struct LienStorage { - uint256 maxLiens; + uint8 maxLiens; address WETH; ITransferProxy TRANSFER_PROXY; IAuctionHouse AUCTION_HOUSE; @@ -42,19 +56,19 @@ interface ILienToken is IERC721 { } struct Lien { - Details details; - bytes32 strategyRoot; - uint256 collateralId; - address vault; - address token; + address token; //20 + address vault; //20 + bytes32 strategyRoot; //32 + uint256 collateralId; //32 + Details details; //32 * 4 } struct Point { - uint256 lienId; - uint88 amount; - uint8 position; - uint40 last; - uint40 end; + uint88 amount; //11 + uint8 position; //1 + uint40 last; //5 + uint40 end; //5 + uint256 lienId; //32 } struct Stack { @@ -109,7 +123,7 @@ interface ILienToken is IERC721 { uint256 collateralId, uint256 auctionWindow, ILienToken.Stack[] memory stack - ) external returns (uint256 reserve, uint256[] memory); + ) external returns (uint256 reserve, AuctionStack[] memory); /** * @notice Computes and returns the buyout amount for a Lien. @@ -126,8 +140,10 @@ interface ILienToken is IERC721 { * @param collateralId The ID for the underlying CollateralToken. * @param remainingLiens The IDs for the unpaid liens */ - function removeLiens(uint256 collateralId, uint256[] memory remainingLiens) - external; + function removeLiens( + uint256 collateralId, + AuctionStack[] memory remainingLiens + ) external; /** * @notice Removes all liens for a given CollateralToken. @@ -209,6 +225,11 @@ interface ILienToken is IERC721 { external returns (Stack[] memory newStack); + struct AuctionStack { + uint256 lienId; + uint40 end; + } + /** * @notice Make a payment for the debt against a CollateralToken for a specific lien. * @param stack the stack to repay @@ -218,11 +239,11 @@ interface ILienToken is IERC721 { * @return the amount of the payment that was applied to the lien */ function makePaymentAuctionHouse( - uint256[] memory stack, + AuctionStack[] memory stack, uint256 collateralId, uint256 paymentAmount, address payer - ) external returns (uint256[] memory, uint256); + ) external returns (ILienToken.AuctionStack[] memory, uint256); function getMaxPotentialDebtForCollateral(ILienToken.Stack[] memory) external @@ -245,19 +266,32 @@ interface ILienToken is IERC721 { /** * @notice Sets addresses for the AuctionHouse, CollateralToken, and AstariaRouter contracts to use. - * @param what The identifier for what is being filed. - * @param data The encoded address data to be decoded and filed. + * @param file The incoming file to handle */ - function file(bytes32 what, bytes calldata data) external; - - event AddLien(uint256 indexed collateralId, uint256 lienId, uint8 position); - event LienStackUpdated(uint256 indexed collateralId, Stack[] stack); - event RemovedLien(uint256 indexed collateralId, uint8 position); + function file(File calldata file) external; + + event AddLien( + uint256 indexed collateralId, + uint8 position, + uint256 indexed lienId, + Stack stack + ); + enum StackAction { + CLEAR, + ADD, + REMOVE, + REPLACE + } + event LienStackUpdated( + uint256 indexed collateralId, + uint8 position, + StackAction action, + uint8 stackLength + ); event RemovedLiens(uint256 indexed collateralId); event Payment(uint256 indexed lienId, uint256 amount); event BuyoutLien(address indexed buyer, uint256 lienId, uint256 buyout); event PayeeChanged(uint256 indexed lienId, address indexed payee); - event File(bytes32 indexed what, bytes data); error UnsupportedFile(); error InvalidBuyoutDetails(uint256 lienMaxAmount, uint256 owed); @@ -269,6 +303,7 @@ interface ILienToken is IERC721 { COLLATERAL_AUCTION, COLLATERAL_NOT_DEPOSITED, LIEN_NO_DEBT, + EXPIRED_LIEN, DEBT_LIMIT, MAX_LIENS } diff --git a/src/interfaces/IPublicVault.sol b/src/interfaces/IPublicVault.sol index 7ce7f065..16e4041c 100644 --- a/src/interfaces/IPublicVault.sol +++ b/src/interfaces/IPublicVault.sol @@ -10,10 +10,10 @@ pragma solidity ^0.8.17; -import {IERC165} from "./IERC165.sol"; -import {IVault} from "core/interfaces/IVault.sol"; +import {IERC165} from "core/interfaces/IERC165.sol"; +import {IVaultImplementation} from "core/interfaces/IVaultImplementation.sol"; -interface IPublicVault is IERC165, IVault { +interface IPublicVault is IVaultImplementation { struct EpochData { uint64 liensOpenForEpoch; address withdrawProxy; diff --git a/src/interfaces/IRouterBase.sol b/src/interfaces/IRouterBase.sol index 21141122..76e6664f 100644 --- a/src/interfaces/IRouterBase.sol +++ b/src/interfaces/IRouterBase.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.17; -import {IAstariaRouter} from "./IAstariaRouter.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; interface IRouterBase { function ROUTER() external view returns (IAstariaRouter); diff --git a/src/interfaces/IStrategyValidator.sol b/src/interfaces/IStrategyValidator.sol index b37a18c1..cd001c84 100644 --- a/src/interfaces/IStrategyValidator.sol +++ b/src/interfaces/IStrategyValidator.sol @@ -10,9 +10,8 @@ pragma solidity ^0.8.17; -import {IAstariaRouter} from "../interfaces/IAstariaRouter.sol"; -import {ILienToken} from "../interfaces/IAstariaRouter.sol"; -import {IStrategyValidator} from "../interfaces/IStrategyValidator.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; +import {ILienToken} from "core/interfaces/IAstariaRouter.sol"; interface IStrategyValidator { function validateAndParse( @@ -20,5 +19,5 @@ interface IStrategyValidator { address borrower, address collateralTokenContract, uint256 collateralTokenId - ) external returns (bytes32, ILienToken.Details memory); + ) external view returns (bytes32, ILienToken.Details memory); } diff --git a/src/interfaces/IVault.sol b/src/interfaces/IVault.sol deleted file mode 100644 index 04272fb9..00000000 --- a/src/interfaces/IVault.sol +++ /dev/null @@ -1,5 +0,0 @@ -pragma solidity ^0.8.16; - -interface IVault { - function deposit(uint256, address) external returns (uint256); -} diff --git a/src/interfaces/IVaultImplementation.sol b/src/interfaces/IVaultImplementation.sol index 720fce2e..1ab3b5f3 100644 --- a/src/interfaces/IVaultImplementation.sol +++ b/src/interfaces/IVaultImplementation.sol @@ -11,17 +11,81 @@ pragma solidity ^0.8.17; import {ILienToken} from "core/interfaces/ILienToken.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; +import {IAstariaVaultBase} from "core/interfaces/IAstariaVaultBase.sol"; +import {IERC165} from "core/interfaces/IERC165.sol"; -interface IVaultImplementation { +interface IVaultImplementation is IAstariaVaultBase, IERC165 { enum InvalidRequestReason { + NO_AUTHORITY, INVALID_SIGNATURE, - INVALID_STRATEGIST, INVALID_COMMITMENT, INVALID_AMOUNT, INSUFFICIENT_FUNDS, INVALID_RATE, - INVALID_POTENTIAL_DEBT + INVALID_POTENTIAL_DEBT, + SHUTDOWN, + PAUSED } - error InvalidRequest(InvalidRequestReason); + error InvalidRequest(InvalidRequestReason reason); + + struct InitParams { + address delegate; + bool allowListEnabled; + address[] allowList; + uint256 depositCap; // max amount of tokens that can be deposited + } + + struct VIData { + uint32 strategistNonce; + uint88 depositCap; + address delegate; + bool allowListEnabled; + bool isShutdown; + mapping(address => bool) allowList; + } + + event NonceUpdated(uint32 nonce); + + event IncrementNonce(uint32 nonce); + + event VaultShutdown(); + + function getShutdown() external view returns (bool); + + function shutdown() external; + + function incrementNonce() external; + + function commitToLien( + IAstariaRouter.Commitment calldata params, + address receiver + ) external returns (uint256 lienId, ILienToken.Stack[] memory stack); + + function buyoutLien( + uint256 collateralId, + uint8 position, + IAstariaRouter.Commitment calldata incomingTerms, + ILienToken.Stack[] calldata stack + ) external returns (ILienToken.Stack[] memory, ILienToken.Stack memory); + + function recipient() external view returns (address); + + function setDelegate(address delegate_) external; + + function init(InitParams calldata params) external; + + function encodeStrategyData( + IAstariaRouter.StrategyDetails calldata strategy, + bytes32 root + ) external view returns (bytes memory); + + function domainSeparator() external view returns (bytes32); + + function modifyDepositCap(uint256 newCap) external; + + function getStrategistNonce() external view returns (uint32); + + function STRATEGY_TYPEHASH() external view returns (bytes32); } diff --git a/src/interfaces/IWithdrawProxy.sol b/src/interfaces/IWithdrawProxy.sol new file mode 100644 index 00000000..c65dc48c --- /dev/null +++ b/src/interfaces/IWithdrawProxy.sol @@ -0,0 +1,36 @@ +pragma solidity ^0.8.17; +import {IERC165} from "core/interfaces/IERC165.sol"; +import {IERC4626} from "core/interfaces/IERC4626.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; +import {IRouterBase} from "core/interfaces/IRouterBase.sol"; + +interface IWithdrawProxy is IRouterBase, IERC165, IERC4626 { + function owner() external pure returns (address); + + function VAULT() external pure returns (address); + + function CLAIMABLE_EPOCH() external pure returns (uint64); + + function setWithdrawRatio(uint256 liquidationWithdrawRatio) external; + + function handleNewLiquidation( + uint256 newLienExpectedValue, + uint256 finalAuctionDelta + ) external; + + function drain(uint256 amount, address withdrawProxy) + external + returns (uint256); + + function claim() external; + + function increaseWithdrawReserveReceived(uint256 amount) external; + + function getExpected() external view returns (uint256); + + function getWithdrawRatio() external view returns (uint256); + + function getFinalAuctionEnd() external view returns (uint256); + + error NotSupported(); +} diff --git a/src/scripts/deployments/Deploy.sol b/src/scripts/deployments/Deploy.sol index 3672c6cf..141e6296 100644 --- a/src/scripts/deployments/Deploy.sol +++ b/src/scripts/deployments/Deploy.sol @@ -27,10 +27,12 @@ import {CollateralToken} from "core/CollateralToken.sol"; import {LienToken} from "core/LienToken.sol"; import {AstariaRouter} from "core/AstariaRouter.sol"; -import {Vault, PublicVault} from "core/PublicVault.sol"; +import {Vault} from "core/Vault.sol"; +import {PublicVault} from "core/PublicVault.sol"; import {TransferProxy} from "core/TransferProxy.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; import {ILienToken} from "core/interfaces/ILienToken.sol"; import {WithdrawProxy} from "core/WithdrawProxy.sol"; @@ -221,16 +223,21 @@ contract Deploy is Script { ) ) ); - ICollateralToken.File[] memory ctfiles = new ICollateralToken.File[](2); + + 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); ctfiles[0] = ICollateralToken.File({ - what: "setAstariaRouter", + what: ICollateralToken.FileType.AstariaRouter, data: abi.encode(address(ASTARIA_ROUTER)) }); - ctfiles[1] = ICollateralToken.File({ - what: "setAuctionHouse", - data: abi.encode(address(AUCTION_HOUSE)) - }); COLLATERAL_TOKEN.fileBatch(ctfiles); emit Deployed(address(AUCTION_HOUSE)); diff --git a/src/scripts/deployments/strategies/CollectionStrategy.sol b/src/scripts/deployments/strategies/CollectionStrategy.sol index 42f7836e..390a7566 100644 --- a/src/scripts/deployments/strategies/CollectionStrategy.sol +++ b/src/scripts/deployments/strategies/CollectionStrategy.sol @@ -9,23 +9,24 @@ */ pragma solidity ^0.8.17; import {Script} from "forge-std/Script.sol"; -import {AstariaRouter} from "core/AstariaRouter.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; + import {CollectionValidator} from "core/strategies/CollectionValidator.sol"; import {AstariaStack} from "../AstariaStack.sol"; contract CollectionStrategy is AstariaStack { - AstariaRouter router; + IAstariaRouter router; function run() external { - router = AstariaRouter(ROUTER_ADDR); + router = IAstariaRouter(ROUTER_ADDR); vm.startBroadcast(msg.sender); CollectionValidator validator = new CollectionValidator(); router.file( - AstariaRouter.File( - bytes32("setStrategyValidator"), - abi.encode(uint8(1), address(validator)) + IAstariaRouter.File( + IAstariaRouter.FileType.StrategyValidator, + abi.encode(validator.VERSION_TYPE(), address(validator)) ) ); diff --git a/src/scripts/deployments/strategies/UniqueStrategy.sol b/src/scripts/deployments/strategies/UniqueStrategy.sol index 465022c6..37e04e7f 100644 --- a/src/scripts/deployments/strategies/UniqueStrategy.sol +++ b/src/scripts/deployments/strategies/UniqueStrategy.sol @@ -10,23 +10,23 @@ pragma solidity ^0.8.17; import {Script} from "forge-std/Script.sol"; -import {AstariaRouter} from "../../../AstariaRouter.sol"; import {UniqueValidator} from "../../../strategies/UniqueValidator.sol"; import {AstariaStack} from "../AstariaStack.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; contract UniqueStrategy is AstariaStack { - AstariaRouter router; + IAstariaRouter router; function run() external { - router = AstariaRouter(ROUTER_ADDR); + router = IAstariaRouter(ROUTER_ADDR); vm.startBroadcast(msg.sender); UniqueValidator validator = new UniqueValidator(); router.file( - AstariaRouter.File( - bytes32("setStrategyValidator"), - abi.encode(uint8(0), address(validator)) + IAstariaRouter.File( + IAstariaRouter.FileType.StrategyValidator, + abi.encode(validator.VERSION_TYPE(), address(validator)) ) ); diff --git a/src/scripts/deployments/strategies/V3Strategy.sol b/src/scripts/deployments/strategies/V3Strategy.sol index c7cad2ee..47f1156e 100644 --- a/src/scripts/deployments/strategies/V3Strategy.sol +++ b/src/scripts/deployments/strategies/V3Strategy.sol @@ -1,16 +1,16 @@ pragma solidity ^0.8.17; import {Script} from "forge-std/Script.sol"; -import {AstariaRouter} from "core/AstariaRouter.sol"; import {V3SecurityHook} from "core/security/V3SecurityHook.sol"; import {UNI_V3Validator} from "core/strategies/UNI_V3Validator.sol"; import {AstariaStack} from "../AstariaStack.sol"; import {CollateralToken} from "core/CollateralToken.sol"; import {ICollateralToken} from "core/interfaces/ICollateralToken.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; contract V3Strategy is AstariaStack { function run() external { - AstariaRouter router = AstariaRouter(ROUTER_ADDR); + IAstariaRouter router = IAstariaRouter(ROUTER_ADDR); CollateralToken ct = CollateralToken(COLLATERAL_TOKEN_ADDR); vm.startBroadcast(msg.sender); @@ -18,9 +18,9 @@ contract V3Strategy is AstariaStack { UNI_V3Validator V3Validator = new UNI_V3Validator(); router.file( - AstariaRouter.File( - bytes32("setStrategyValidator"), - abi.encode(uint8(2), address(V3Validator)) + IAstariaRouter.File( + IAstariaRouter.FileType.StrategyValidator, + abi.encode(V3Validator.VERSION_TYPE(), address(V3Validator)) ) ); @@ -30,7 +30,7 @@ contract V3Strategy is AstariaStack { ct.file( ICollateralToken.File( - bytes32("setSecurityHook"), + ICollateralToken.FileType.SecurityHook, abi.encode( address(0xC36442b4a4522E871399CD717aBDD847Ab11FE88), //v3 nft address address(V3_SECURITY_HOOK) diff --git a/src/strategies/CollectionValidator.sol b/src/strategies/CollectionValidator.sol index 94218c73..ea548aef 100644 --- a/src/strategies/CollectionValidator.sol +++ b/src/strategies/CollectionValidator.sol @@ -12,9 +12,9 @@ pragma solidity ^0.8.17; import {ERC721} from "solmate/tokens/ERC721.sol"; -import {IAstariaRouter} from "../interfaces/IAstariaRouter.sol"; -import {ILienToken} from "../interfaces/ILienToken.sol"; -import {IStrategyValidator} from "../interfaces/IStrategyValidator.sol"; +import {IAstariaRouter} from "core/interfaces/IAstariaRouter.sol"; +import {ILienToken} from "core/interfaces/ILienToken.sol"; +import {IStrategyValidator} from "core/interfaces/IStrategyValidator.sol"; interface ICollectionValidator is IStrategyValidator { struct Details { @@ -26,6 +26,8 @@ interface ICollectionValidator is IStrategyValidator { } contract CollectionValidator is ICollectionValidator { + uint8 public constant VERSION_TYPE = uint8(2); + function getLeafDetails(bytes memory nlrDetails) public pure @@ -55,6 +57,9 @@ contract CollectionValidator is ICollectionValidator { { ICollectionValidator.Details memory cd = getLeafDetails(params.nlrDetails); + if (cd.version != VERSION_TYPE) { + revert("invalid type"); + } if (cd.borrower != address(0)) { require( borrower == cd.borrower, diff --git a/src/strategies/UNI_V3Validator.sol b/src/strategies/UNI_V3Validator.sol index d3341077..c486cb74 100644 --- a/src/strategies/UNI_V3Validator.sol +++ b/src/strategies/UNI_V3Validator.sol @@ -21,13 +21,16 @@ import {IV3PositionManager} from "core/interfaces/IV3PositionManager.sol"; interface IUNI_V3Validator is IStrategyValidator { struct Details { uint8 version; - address token; - address[] assets; + address lp; + address borrower; + address token0; + address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint128 minLiquidity; - address borrower; + uint256 amount0Min; + uint256 amount1Min; ILienToken.Details lien; } } @@ -35,22 +38,20 @@ interface IUNI_V3Validator is IStrategyValidator { contract UNI_V3Validator is IUNI_V3Validator { using CollateralLookup for address; + uint8 public constant VERSION_TYPE = uint8(3); + IV3PositionManager V3_NFT_POSITION_MGR = IV3PositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); - 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)); } @@ -67,6 +68,9 @@ contract UNI_V3Validator is IUNI_V3Validator { { IUNI_V3Validator.Details memory details = getLeafDetails(params.nlrDetails); + if (details.version != VERSION_TYPE) { + revert("invalid type"); + } if (details.borrower != address(0)) { require( borrower == details.borrower, @@ -75,7 +79,7 @@ contract UNI_V3Validator is IUNI_V3Validator { } //ensure its also the correct token - require(details.token == collateralTokenContract, "invalid token contract"); + require(details.lp == collateralTokenContract, "invalid token contract"); ( , @@ -88,17 +92,22 @@ 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"); } + require( - details.assets[0] == token0 && details.assets[1] == token1, + 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" diff --git a/src/strategies/UniqueValidator.sol b/src/strategies/UniqueValidator.sol index 1d4567dc..a77c8dff 100644 --- a/src/strategies/UniqueValidator.sol +++ b/src/strategies/UniqueValidator.sol @@ -27,19 +27,17 @@ interface IUniqueValidator is IStrategyValidator { } contract UniqueValidator is IUniqueValidator { - function getLeafDetails(bytes memory nlrDetails) - public - pure - returns (Details memory) - { + uint8 public constant VERSION_TYPE = uint8(1); + + 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); } @@ -55,6 +53,9 @@ contract UniqueValidator is IUniqueValidator { returns (bytes32 leaf, ILienToken.Details memory ld) { Details memory cd = getLeafDetails(params.nlrDetails); + if (cd.version != VERSION_TYPE) { + revert("invalid type"); + } if (cd.borrower != address(0)) { require( diff --git a/src/test/AstariaTest.t.sol b/src/test/AstariaTest.t.sol index a76d3318..d3328aea 100644 --- a/src/test/AstariaTest.t.sol +++ b/src/test/AstariaTest.t.sol @@ -25,7 +25,7 @@ import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; -import {IVault, VaultImplementation} from "../VaultImplementation.sol"; +import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; @@ -39,6 +39,41 @@ contract AstariaTest is TestHelpers { using CollateralLookup for address; using SafeCastLib for uint256; + event NonceUpdated(uint32 nonce); + event VaultShutdown(); + + function testVaultShutdown() public { + address publicVault = _createPublicVault({ + epochLength: 10 days, // 10 days + strategist: strategistTwo, + delegate: strategistOne + }); + + vm.expectEmit(true, true, true, true); + emit VaultShutdown(); + vm.startPrank(strategistTwo); + VaultImplementation(publicVault).shutdown(); + vm.stopPrank(); + assert(VaultImplementation(publicVault).getShutdown()); + } + + function testIncrementNonceAsStrategistAndDelegate() public { + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + vm.expectEmit(true, true, true, true); + emit NonceUpdated(1); + vm.prank(strategistOne); + VaultImplementation(privateVault).incrementNonce(); + + vm.expectEmit(true, true, true, true); + emit NonceUpdated(2); + vm.prank(strategistTwo); + VaultImplementation(privateVault).incrementNonce(); + } + function testBasicPublicVaultLoan() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); @@ -132,10 +167,9 @@ contract AstariaTest is TestHelpers { uint256 vaultTokenBalance = IERC20(publicVault).balanceOf(address(1)); - // _signalWithdrawAtFutureEpoch(address(1), publicVault, uint64(1)); _signalWithdraw(address(1), publicVault); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy( + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy( PublicVault(publicVault).getCurrentEpoch() ); @@ -157,7 +191,7 @@ contract AstariaTest is TestHelpers { ); vm.stopPrank(); assertEq( - ERC20(PublicVault(publicVault).underlying()).balanceOf(address(1)), + ERC20(PublicVault(publicVault).asset()).balanceOf(address(1)), 50 ether ); } @@ -285,18 +319,18 @@ contract AstariaTest is TestHelpers { _signalWithdraw(address(1), publicVault4); _signalWithdraw(address(1), publicVault5); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy( + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy( PublicVault(publicVault).getCurrentEpoch() ); assertEq(vaultTokenBalance, IERC20(withdrawProxy).balanceOf(address(1))); - vm.warp(block.timestamp + 14 days); // end of loan + skip(14 days); // end of loan ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); _bid(address(2), collateralId, 33 ether); - skip(1 days); // epoch boundary + skip(WithdrawProxy(withdrawProxy).getFinalAuctionEnd()); // epoch boundary PublicVault(publicVault).processEpoch(); @@ -312,8 +346,7 @@ contract AstariaTest is TestHelpers { address(1) ); vm.stopPrank(); - // assertEq(WETH9.balanceOf(address(1)), 50410958904104000000); - // assertEq(WETH9.balanceOf(address(1)), 50575342465745600000); + assertEq(WETH9.balanceOf(address(1)), 50287680745810395000); } function testBuyoutLien() public { @@ -441,32 +474,29 @@ contract AstariaTest is TestHelpers { function testCollateralTokenFileSetup() public { bytes memory astariaRouterAddr = abi.encode(address(0)); - // COLLATERAL_TOKEN.file( - // ICollateralToken.File(bytes32("setAstariaRouter"), astariaRouterAddr) - // ); - // assert(COLLATERAL_TOKEN.ASTARIA_ROUTER() == IAstariaRouter(address(0))); bytes memory securityHook = abi.encode(address(0), address(0)); COLLATERAL_TOKEN.file( - ICollateralToken.File(bytes32("setSecurityHook"), securityHook) + ICollateralToken.File( + ICollateralToken.FileType.SecurityHook, + securityHook + ) ); assert(COLLATERAL_TOKEN.securityHooks(address(0)) == address(0)); - - vm.expectRevert("unsupported/file"); - COLLATERAL_TOKEN.file(ICollateralToken.File(bytes32("Andrew Redden"), "")); } function testLienTokenFileSetup() public { bytes memory auctionHouseAddr = abi.encode(address(0)); - LIEN_TOKEN.file(bytes32("setAuctionHouse"), auctionHouseAddr); + 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(bytes32("setCollateralToken"), collateralIdAddr); + LIEN_TOKEN.file( + ILienToken.File(ILienToken.FileType.CollateralToken, collateralIdAddr) + ); assert(LIEN_TOKEN.COLLATERAL_TOKEN() == ICollateralToken(address(0))); - - vm.expectRevert("unsupported/file"); - COLLATERAL_TOKEN.file(ICollateralToken.File(bytes32("Justin Bram"), "")); } function testEpochProcessionMultipleActors() public { @@ -506,7 +536,7 @@ contract AstariaTest is TestHelpers { _repay(stack1, 0, 100 ether, address(this)); _warpToEpochEnd(publicVault); //after epoch end - uint256 balance = ERC20(PublicVault(publicVault).underlying()).balanceOf( + uint256 balance = ERC20(PublicVault(publicVault).asset()).balanceOf( publicVault ); PublicVault(publicVault).processEpoch(); @@ -532,7 +562,6 @@ contract AstariaTest is TestHelpers { address alice = address(1); address bob = address(2); TestNFT nft = new TestNFT(6); - // mintAndDeposit(address(nft), uint256(5)); uint256 tokenId = uint256(5); address tokenContract = address(nft); address publicVault = _createPublicVault({ @@ -541,23 +570,98 @@ contract AstariaTest is TestHelpers { epochLength: 14 days }); - _lendToVault(Lender({addr: bob, amountToLend: 50 ether}), publicVault); + _lendToVault(Lender({addr: bob, amountToLend: 150 ether}), publicVault); (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, - lienDetails: standardLienDetails, - amount: 10 ether, + 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"); + assertEq( + address(this), + ERC721(tokenContract).ownerOf(tokenId), + "liquidator did not receive NFT" + ); + } + + function testAuctionEnd() 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); + skip(4 days); + ASTARIA_ROUTER.endAuction(collateralId); + assertEq(nft.ownerOf(tokenId), address(2), "the owner is not the bidder"); + } + + function testAuctionEndNoBids() 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); + skip(4 days); + ASTARIA_ROUTER.endAuction(collateralId); + PublicVault(publicVault).processEpoch(); + assertEq( + nft.ownerOf(tokenId), + address(this), + "the owner is not the bidder" + ); } function _cancelAuction(uint256 auctionId, address sender) internal { @@ -565,7 +669,7 @@ contract AstariaTest is TestHelpers { vm.deal(sender, reserve); vm.startPrank(sender); WETH9.deposit{value: reserve}(); - WETH9.approve(address(TRANSFER_PROXY), reserve * 2); + WETH9.approve(address(TRANSFER_PROXY), reserve); ASTARIA_ROUTER.cancelAuction(auctionId); vm.stopPrank(); } diff --git a/src/test/ForkedTesting.t.sol b/src/test/ForkedTesting.t.sol new file mode 100644 index 00000000..b935ae83 --- /dev/null +++ b/src/test/ForkedTesting.t.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: UNLICENSED + +/** + * __ ___ __ + * /\ /__' | /\ |__) | /\ + * /~~\ .__/ | /~~\ | \ | /~~\ + * + * Copyright (c) Astaria Labs, Inc + */ + +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; + +import {Authority} from "solmate/auth/Auth.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; +import { + MultiRolesAuthority +} from "solmate/auth/authorities/MultiRolesAuthority.sol"; + +import { + IERC1155Receiver +} from "openzeppelin/token/ERC1155/IERC1155Receiver.sol"; +import {Strings} from "openzeppelin/utils/Strings.sol"; + +import {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"; +import {ILienToken} from "../interfaces/ILienToken.sol"; +import {IPublicVault} from "../interfaces/IPublicVault.sol"; +import {CollateralToken, IFlashAction} from "../CollateralToken.sol"; +import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; +import {VaultImplementation} from "../VaultImplementation.sol"; +import {IVaultImplementation} from "../interfaces/IVaultImplementation.sol"; +import {LienToken} from "../LienToken.sol"; +import {PublicVault} from "../PublicVault.sol"; +import {TransferProxy} from "../TransferProxy.sol"; +import {WithdrawProxy} from "../WithdrawProxy.sol"; + +import {Strings2} from "./utils/Strings2.sol"; + +import "./TestHelpers.t.sol"; +import {ClaimFees} from "../actions/UNIV3/ClaimFees.sol"; + +contract ForkedTesting is TestHelpers { + using FixedPointMathLib for uint256; + using CollateralLookup for address; + + address constant V3_NFT_ADDRESS = + address(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); // todo get real nft address + + function _hijackNFT(address nft, uint256 tokenId) internal { + address holder = ERC721(nft).ownerOf(tokenId); + vm.startPrank(holder); + ERC721(nft).transferFrom(holder, address(this), tokenId); + vm.stopPrank(); + } + + //run with blocknumber 15919113 + //matic weth pair + function testClaimFeesAgainstV3Liquidity() public { + address tokenContract = V3_NFT_ADDRESS; + // fork mainnet on this block 15934974 + uint256 tokenId = uint256(349999); + _hijackNFT(tokenContract, tokenId); + + ClaimFees claimFees = new ClaimFees(V3_NFT_ADDRESS); + + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + _lendToVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + address[] memory assets; + { + ( + , + , + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + , + , + uint128 tokensOwed0, + uint128 tokensOwed1 + ) = IV3PositionManager(tokenContract).positions(tokenId); + + assets = new address[](2); + assets[0] = token0; + assets[1] = token1; + _commitToV3Lien({ + params: V3LienParams({ + assets: assets, + fee: fee, + borrower: address(0), + tickLower: tickLower, + tickUpper: tickUpper, + liquidity: liquidity, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + amount0Min: tokensOwed0, + amount1Min: tokensOwed1, + tokenId: tokenId, + details: standardLienDetails + }), + vault: privateVault, + amount: 10 ether, + stack: new ILienToken.Stack[](0), + isFirstLien: true + }); + } + + COLLATERAL_TOKEN.file( + ICollateralToken.File( + ICollateralToken.FileType.FlashEnabled, + abi.encode(V3_NFT_ADDRESS, true) + ) + ); + + uint256 balance0Before = IERC20(assets[0]).balanceOf(address(this)); + uint256 balance1Before = IERC20(assets[1]).balanceOf(address(this)); + + COLLATERAL_TOKEN.flashAction( + IFlashAction(claimFees), + tokenContract.computeId(tokenId), + abi.encode(address(this)) + ); + assert(IERC20(assets[0]).balanceOf(address(this)) > balance0Before); + assert(IERC20(assets[1]).balanceOf(address(this)) > balance1Before); + } +} diff --git a/src/test/IntegrationTest.t.sol b/src/test/IntegrationTest.t.sol index 90373df1..da4d03ea 100644 --- a/src/test/IntegrationTest.t.sol +++ b/src/test/IntegrationTest.t.sol @@ -25,7 +25,7 @@ import {IAuctionHouse} from "gpl/interfaces/IAuctionHouse.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; -import {IVault, VaultImplementation} from "../VaultImplementation.sol"; +import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; @@ -173,13 +173,23 @@ contract IntegrationTest is TestHelpers { vm.warp(block.timestamp + 11 days); uint256 collateralId = tokenContract.computeId(tokenId); + emit log_named_address( + "first lien payee before liq", + LIEN_TOKEN.getPayee(stack[0].point.lienId) + ); ASTARIA_ROUTER.liquidate(collateralId, uint8(3), stack); - + 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); address[5] memory withdrawProxies; for (uint256 i = 0; i < lienSize; i++) { - withdrawProxies[i] = PublicVault(publicVaults[i]).getWithdrawProxy(0); + withdrawProxies[i] = address( + PublicVault(publicVaults[i]).getWithdrawProxy(0) + ); } assertTrue(withdrawProxies[0] == address(0)); // 3 days from epoch end assertTrue(withdrawProxies[1] != address(0)); // 2 days from epoch end @@ -189,12 +199,14 @@ contract IntegrationTest is TestHelpers { assertEq( WETH9.balanceOf(publicVaults[0]), - PublicVault(publicVaults[0]).totalAssets() + PublicVault(publicVaults[0]).totalAssets(), + "Incorrect WETH balance" ); vm.warp(block.timestamp + 2 days); for (uint256 i = 1; i < lienSize; i++) { + vm.warp(WithdrawProxy(withdrawProxies[i]).getFinalAuctionEnd()); PublicVault(publicVaults[i]).processEpoch(); } @@ -202,30 +214,34 @@ contract IntegrationTest is TestHelpers { WithdrawProxy(withdrawProxies[i]).claim(); } - assertEq(WETH9.balanceOf(withdrawProxies[1]), 0); - assertEq(WETH9.balanceOf(withdrawProxies[2]), 0); - assertEq(WETH9.balanceOf(withdrawProxies[3]), 0); - assertEq(WETH9.balanceOf(withdrawProxies[4]), 0); + assertEq(WETH9.balanceOf(withdrawProxies[1]), 0, "proxy 1 invalid"); + assertEq(WETH9.balanceOf(withdrawProxies[2]), 0, "proxy 2 invalid"); + assertEq(WETH9.balanceOf(withdrawProxies[3]), 0, "proxy 3 invalid"); + assertEq(WETH9.balanceOf(withdrawProxies[4]), 0, "proxy 4 invalid"); assertEq( WETH9.balanceOf(publicVaults[1]), - PublicVault(publicVaults[1]).totalAssets() + PublicVault(publicVaults[1]).totalAssets(), + "vault 1 invalid" ); assertEq( WETH9.balanceOf(publicVaults[2]), - PublicVault(publicVaults[2]).totalAssets() + PublicVault(publicVaults[2]).totalAssets(), + "vault 2 invalid" ); assertEq( WETH9.balanceOf(publicVaults[3]), - PublicVault(publicVaults[3]).totalAssets() + PublicVault(publicVaults[3]).totalAssets(), + "vault 3 invalid" ); assertEq( WETH9.balanceOf(publicVaults[4]), - PublicVault(publicVaults[4]).totalAssets() + PublicVault(publicVaults[4]).totalAssets(), + "vault 4 invalid" ); } - function testBorrowerReservePriceCancellationTest() public { + function testBorrowerReservePriceCancellationTest() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -262,6 +278,10 @@ contract IntegrationTest is TestHelpers { vm.warp(block.timestamp + 4 days); ASTARIA_ROUTER.endAuction(collateralId); - assertEq(ERC721(tokenContract).ownerOf(tokenId), address(3), "Bidder address does not own NFT"); + 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 46cd6cc0..62f57c47 100644 --- a/src/test/RevertTesting.t.sol +++ b/src/test/RevertTesting.t.sol @@ -33,7 +33,8 @@ import {ILienToken} from "../interfaces/ILienToken.sol"; import {IPublicVault} from "../interfaces/IPublicVault.sol"; import {CollateralToken, IFlashAction} from "../CollateralToken.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; -import {IVault, VaultImplementation} from "../VaultImplementation.sol"; +import {VaultImplementation} from "../VaultImplementation.sol"; +import {IVaultImplementation} from "../interfaces/IVaultImplementation.sol"; import {LienToken} from "../LienToken.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; @@ -47,6 +48,66 @@ contract RevertTesting is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; + function testFailRandomAccountIncrementNonce() public { + address privateVault = _createPublicVault({ + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 10 days + }); + + vm.expectRevert(abi.encodePacked("InvalidRequest(0)")); + VaultImplementation(privateVault).incrementNonce(); + assertEq( + VaultImplementation(privateVault).getStrategistNonce(), + uint32(0), + "vault was incremented, when it shouldn't be" + ); + } + + function testFailInvalidSignature() public { + TestNFT nft = new TestNFT(3); + address tokenContract = address(nft); + uint256 tokenId = uint256(1); + address privateVault = _createPrivateVault({ + strategist: strategistOne, + delegate: strategistTwo + }); + + _lendToVault( + Lender({addr: strategistOne, amountToLend: 50 ether}), + privateVault + ); + + IAstariaRouter.Commitment memory terms = _generateValidTerms({ + vault: privateVault, + strategist: strategistOne, + strategistPK: strategistRoguePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: standardLienDetails, + amount: 10 ether, + stack: new ILienToken.Stack[](0) + }); + + ERC721(tokenContract).safeTransferFrom( + address(this), + address(COLLATERAL_TOKEN), + tokenId, + "" + ); + + COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); + + uint256 balanceOfBefore = ERC20(privateVault).balanceOf(address(this)); + vm.expectRevert(abi.encodePacked("InvalidRequest(1)")); + VaultImplementation(privateVault).commitToLien(terms, address(this)); + assertEq( + balanceOfBefore, + ERC20(privateVault).balanceOf(address(this)), + "balance changed" + ); + } + // Only strategists for PrivateVaults can supply capital function testFailSoloLendNotAppraiser() public { TestNFT nft = new TestNFT(3); diff --git a/src/test/TestHelpers.t.sol b/src/test/TestHelpers.t.sol index 3a6fb436..6f11a3a8 100644 --- a/src/test/TestHelpers.t.sol +++ b/src/test/TestHelpers.t.sol @@ -48,15 +48,18 @@ import { import {V3SecurityHook} from "../security/V3SecurityHook.sol"; import {CollateralToken} from "../CollateralToken.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; -import {IVault, VaultImplementation} from "../VaultImplementation.sol"; +import {IPublicVault} from "core/interfaces/IPublicVault.sol"; +import {VaultImplementation} from "../VaultImplementation.sol"; import {LienToken} from "../LienToken.sol"; import {TransferProxy} from "../TransferProxy.sol"; -import {Vault, PublicVault} from "../PublicVault.sol"; +import {PublicVault} from "../PublicVault.sol"; +import {Vault} from "../Vault.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import {BeaconProxy} from "../BeaconProxy.sol"; - +import {IERC721Receiver} from "core/interfaces/IERC721Receiver.sol"; +import {IERC4626} from "core/interfaces/IERC4626.sol"; string constant weth9Artifact = "src/test/WETH9.json"; interface IWETH9 is IERC20 { @@ -73,7 +76,7 @@ contract TestNFT is MockERC721 { } } -contract TestHelpers is Test { +contract TestHelpers is Test, IERC721Receiver { using CollateralLookup for address; using Strings2 for bytes; using SafeCastLib for uint256; @@ -81,8 +84,10 @@ contract TestHelpers is Test { 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 address strategistOne = vm.addr(strategistOnePK); address strategistTwo = vm.addr(strategistTwoPK); + address strategistRogue = vm.addr(strategistRoguePK); address borrower = vm.addr(0x1341); address bidderOne = vm.addr(0x1342); @@ -91,6 +96,13 @@ contract TestHelpers is Test { string private checkpointLabel; uint256 private checkpointGasLeft = 1; // Start the slot warm. + ILienToken.Details public blueChipDetails = + ILienToken.Details({ + maxAmount: 150 ether, + rate: (uint256(1e16) * 150) / (365 days), + duration: 10 days, + maxPotentialDebt: 0 ether + }); ILienToken.Details public standardLienDetails = ILienToken.Details({ maxAmount: 50 ether, @@ -121,6 +133,7 @@ contract TestHelpers is Test { duration: 25 days, maxPotentialDebt: 51 ether }); + ILienToken.Details public refinanceLienDetails4 = ILienToken.Details({ maxAmount: 50 ether, @@ -211,13 +224,13 @@ contract TestHelpers is Test { CollateralToken.File[] memory ctfiles = new CollateralToken.File[](2); ctfiles[0] = ICollateralToken.File({ - what: "setAstariaRouter", + what: ICollateralToken.FileType.AstariaRouter, data: abi.encode(address(ASTARIA_ROUTER)) }); address UNI_V3_NFT = address(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); ctfiles[1] = ICollateralToken.File({ - what: bytes32("setSecurityHook"), + what: ICollateralToken.FileType.SecurityHook, data: abi.encode(UNI_V3_NFT, address(V3_SECURITY_HOOK)) }); @@ -230,41 +243,47 @@ contract TestHelpers is Test { //strategy univ3 UNI_V3Validator UNIV3_LIQUIDITY_STRATEGY_VALIDATOR = new UNI_V3Validator(); - AstariaRouter.File[] memory files = new AstariaRouter.File[](3); + IAstariaRouter.File[] memory files = new IAstariaRouter.File[](3); - files[0] = AstariaRouter.File( - bytes32("setStrategyValidator"), - abi.encode(uint8(0), address(UNIQUE_STRATEGY_VALIDATOR)) + files[0] = IAstariaRouter.File( + IAstariaRouter.FileType.StrategyValidator, + abi.encode(uint8(1), address(UNIQUE_STRATEGY_VALIDATOR)) ); - files[1] = AstariaRouter.File( - bytes32("setStrategyValidator"), - abi.encode(uint8(1), address(COLLECTION_STRATEGY_VALIDATOR)) + files[1] = IAstariaRouter.File( + IAstariaRouter.FileType.StrategyValidator, + abi.encode(uint8(2), address(COLLECTION_STRATEGY_VALIDATOR)) ); - files[2] = AstariaRouter.File( - bytes32("setStrategyValidator"), - abi.encode(uint8(2), address(UNIV3_LIQUIDITY_STRATEGY_VALIDATOR)) + files[2] = IAstariaRouter.File( + IAstariaRouter.FileType.StrategyValidator, + abi.encode(uint8(3), address(UNIV3_LIQUIDITY_STRATEGY_VALIDATOR)) ); ASTARIA_ROUTER.fileBatch(files); - files = new AstariaRouter.File[](1); + files = new IAstariaRouter.File[](1); - files[0] = AstariaRouter.File( - bytes32("setAuctionHouse"), + files[0] = IAstariaRouter.File( + IAstariaRouter.FileType.AuctionHouse, abi.encode(address(AUCTION_HOUSE)) ); ASTARIA_ROUTER.fileGuardian(files); LIEN_TOKEN.file( - bytes32("setAuctionHouse"), - abi.encode(address(AUCTION_HOUSE)) + ILienToken.File( + ILienToken.FileType.AuctionHouse, + abi.encode(address(AUCTION_HOUSE)) + ) ); LIEN_TOKEN.file( - bytes32("setCollateralToken"), - abi.encode(address(COLLATERAL_TOKEN)) + ILienToken.File( + ILienToken.FileType.CollateralToken, + abi.encode(address(COLLATERAL_TOKEN)) + ) ); LIEN_TOKEN.file( - bytes32("setAstariaRouter"), - abi.encode(address(ASTARIA_ROUTER)) + ILienToken.File( + ILienToken.FileType.AstariaRouter, + abi.encode(address(ASTARIA_ROUTER)) + ) ); _setupRolesAndCapabilities(); @@ -291,11 +310,7 @@ contract TestHelpers is Test { LienToken.createLien.selector, true ); - // MRA.setRoleCapability( - // uint8(UserRoles.ASTARIA_ROUTER), - // CollateralToken.auctionVault.selector, - // true - // ); + MRA.setRoleCapability( uint8(UserRoles.ASTARIA_ROUTER), TRANSFER_PROXY.tokenTransferFrom.selector, @@ -341,6 +356,15 @@ contract TestHelpers is Test { MRA.setUserRole(address(LIEN_TOKEN), uint8(UserRoles.LIEN_TOKEN), true); } + function onERC721Received( + address operator_, + address from_, + uint256 tokenId_, + bytes calldata data_ + ) external pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } + // wrap NFT in a CollateralToken function _depositNFT(address tokenContract, uint256 tokenId) internal { ERC721(tokenContract).safeTransferFrom( @@ -420,7 +444,7 @@ contract TestHelpers is Test { ) internal returns (bytes32 rootHash, bytes32[] memory merkleProof) { string[] memory inputs = new string[](4); inputs[0] = "node"; - inputs[1] = "./scripts/loanProofGenerator.js"; + inputs[1] = "./dist/loanProofGenerator.js"; if (requestType == IAstariaRouter.LienRequestType.UNIQUE) { IUniqueValidator.Details memory terms = abi.decode( @@ -475,6 +499,59 @@ contract TestHelpers is Test { }); } + function _executeCommitments(IAstariaRouter.Commitment[] memory commitments) + internal + returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) + { + COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); + return ASTARIA_ROUTER.commitToLiens(commitments); + } + + struct V3LienParams { + address strategist; + uint256 strategistPK; + address tokenContract; + uint256 tokenId; + address borrower; + address[] assets; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + ILienToken.Details details; + } + + function _commitToV3Lien( + V3LienParams memory params, + address vault, + uint256 amount, + ILienToken.Stack[] memory stack, + bool isFirstLien + ) + internal + returns (uint256[] memory lienIds, ILienToken.Stack[] memory newStack) + { + IAstariaRouter.Commitment memory terms = _generateValidV3Terms({ + params: params, + amount: amount, + vault: vault, + stack: stack + }); + + if (isFirstLien) { + ERC721(params.tokenContract).setApprovalForAll( + address(ASTARIA_ROUTER), + true + ); + } + IAstariaRouter.Commitment[] + memory commitments = new IAstariaRouter.Commitment[](1); + commitments[0] = terms; + return _executeCommitments({commitments: commitments}); + } + function _commitToLien( address vault, // address of deployed Vault address strategist, @@ -503,13 +580,93 @@ contract TestHelpers is Test { if (isFirstLien) { ERC721(tokenContract).setApprovalForAll(address(ASTARIA_ROUTER), true); } - IAstariaRouter.Commitment[] memory commitments = new IAstariaRouter.Commitment[](1); commitments[0] = terms; + return _executeCommitments({commitments: commitments}); + } - COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); - return ASTARIA_ROUTER.commitToLiens(commitments); + function _generateEncodedStrategyData( + address vault, + uint256 deadline, + bytes32 root + ) internal view returns (bytes memory) { + bytes32 hash = keccak256( + abi.encode( + VaultImplementation(vault).STRATEGY_TYPEHASH(), + VaultImplementation(vault).getStrategistNonce(), + deadline, + root + ) + ); + return + abi.encodePacked( + bytes1(0x19), + bytes1(0x01), + VaultImplementation(vault).domainSeparator(), + hash + ); + } + + function _generateValidV3Terms( + V3LienParams memory params, + uint256 amount, // requested amount + address vault, + ILienToken.Stack[] memory stack + ) internal returns (IAstariaRouter.Commitment memory) { + bytes memory validatorDetails = abi.encode( + IUNI_V3Validator.Details({ + version: uint8(3), + lp: params.tokenContract, + token0: params.assets[0], + token1: params.assets[1], + fee: params.fee, + tickLower: params.tickLower, + tickUpper: params.tickUpper, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + minLiquidity: params.liquidity, + borrower: params.borrower, + lien: params.details + }) + ); + + ( + bytes32 rootHash, + bytes32[] memory merkleProof + ) = _generateLoanMerkleProof2({ + requestType: IAstariaRouter.LienRequestType.UNIV3_LIQUIDITY, + data: validatorDetails + }); + + // setup 712 signature + + IAstariaRouter.StrategyDetails memory strategyDetails = IAstariaRouter + .StrategyDetails({ + version: uint8(0), + deadline: block.timestamp + 10 days, + vault: vault + }); + + bytes32 termHash = keccak256( + _generateEncodedStrategyData(vault, strategyDetails.deadline, rootHash) + ); + return + _generateTerms( + GenTerms({ + nlrType: uint8(IAstariaRouter.LienRequestType.UNIV3_LIQUIDITY), + tokenContract: params.tokenContract, + tokenId: params.tokenId, + termHash: termHash, + rootHash: rootHash, + pk: params.strategistPK, + strategyDetails: strategyDetails, + validatorDetails: validatorDetails, + amount: amount, + merkleProof: merkleProof, + stack: stack + }) + ); } function _generateValidTerms( @@ -545,13 +702,12 @@ contract TestHelpers is Test { IAstariaRouter.StrategyDetails memory strategyDetails = IAstariaRouter .StrategyDetails({ version: uint8(0), - strategist: strategist, deadline: block.timestamp + 10 days, vault: vault }); bytes32 termHash = keccak256( - VaultImplementation(vault).encodeStrategyData(strategyDetails, rootHash) + _generateEncodedStrategyData(vault, strategyDetails.deadline, rootHash) ); return _generateTerms( @@ -563,6 +719,7 @@ contract TestHelpers is Test { pk: strategistPK, strategyDetails: strategyDetails, validatorDetails: validatorDetails, + nlrType: uint8(IAstariaRouter.LienRequestType.UNIQUE), amount: amount, merkleProof: merkleProof, stack: stack @@ -579,6 +736,7 @@ contract TestHelpers is Test { IAstariaRouter.StrategyDetails strategyDetails; ILienToken.Stack[] stack; bytes validatorDetails; + uint8 nlrType; bytes32[] merkleProof; uint256 amount; } @@ -595,7 +753,6 @@ contract TestHelpers is Test { tokenId: params.tokenId, lienRequest: IAstariaRouter.NewLienRequest({ strategy: params.strategyDetails, - nlrType: uint8(IAstariaRouter.LienRequestType.UNIQUE), // TODO support others? nlrDetails: params.validatorDetails, merkle: IAstariaRouter.MerkleData({ root: params.rootHash, @@ -619,8 +776,14 @@ contract TestHelpers is Test { vm.deal(lender.addr, lender.amountToLend); vm.startPrank(lender.addr); WETH9.deposit{value: lender.amountToLend}(); - WETH9.approve(vault, lender.amountToLend); - IVault(vault).deposit(lender.amountToLend, lender.addr); + WETH9.approve(address(TRANSFER_PROXY), lender.amountToLend); + //min slippage on the deposit + ASTARIA_ROUTER.depositToVault( + IERC4626(vault), + lender.addr, + lender.amountToLend, + uint256(0) + ); vm.stopPrank(); } @@ -701,21 +864,23 @@ contract TestHelpers is Test { uint256 vaultTokenBalance = IERC20(publicVault).balanceOf(lender); vm.startPrank(lender); - ERC20(publicVault).safeApprove(publicVault, type(uint256).max); - PublicVault(publicVault).redeemFutureEpoch({ + ERC20(publicVault).safeApprove(address(TRANSFER_PROXY), vaultTokenBalance); + ASTARIA_ROUTER.redeemFutureEpoch({ + vault: IPublicVault(publicVault), shares: vaultTokenBalance, receiver: lender, - owner: lender, epoch: epoch }); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy(epoch); + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy( + epoch + ); assertEq( - IERC20(withdrawProxy).balanceOf(lender), + withdrawProxy.balanceOf(lender), vaultTokenBalance, "Incorrect number of WithdrawTokens minted" ); - ERC20(withdrawProxy).safeApprove(address(this), type(uint256).max); + ERC20(address(withdrawProxy)).safeApprove(address(this), type(uint256).max); vm.stopPrank(); } diff --git a/src/test/WithdrawTesting.t.sol b/src/test/WithdrawTesting.t.sol index 43287cfe..39d2de5d 100644 --- a/src/test/WithdrawTesting.t.sol +++ b/src/test/WithdrawTesting.t.sol @@ -8,7 +8,6 @@ * Copyright (c) Astaria Labs, Inc */ - pragma solidity ^0.8.17; import "forge-std/Test.sol"; @@ -27,7 +26,7 @@ import {IPublicVault} from "core/interfaces/IPublicVault.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; -import {IVault, VaultImplementation} from "../VaultImplementation.sol"; +import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; @@ -44,7 +43,6 @@ contract WithdrawTest is TestHelpers { // One LP, one lien that's liquidated with no bids, so withdrawing LP does not receive anything from WithdrawProxy function testWithdrawLiquidatedNoBids() public { TestNFT nft = new TestNFT(1); - // _mintAndDeposit(address(nft), 1); address tokenContract = address(nft); uint256 tokenId = uint256(0); @@ -84,8 +82,6 @@ contract WithdrawTest is TestHelpers { ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); - // _bid(address(2), collateralId, 1 ether); - vm.warp(block.timestamp + 2 days); // end of auction AUCTION_HOUSE.endAuction(0); @@ -94,12 +90,10 @@ contract WithdrawTest is TestHelpers { PublicVault(publicVault).processEpoch(); PublicVault(publicVault).transferWithdrawReserve(); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); assertEq( - WithdrawProxy(withdrawProxy).previewRedeem( - ERC20(withdrawProxy).balanceOf(address(1)) - ), + withdrawProxy.previewRedeem(withdrawProxy.balanceOf(address(1))), 0 ); } @@ -158,31 +152,30 @@ contract WithdrawTest is TestHelpers { _signalWithdraw(address(1), publicVault); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy( + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy( PublicVault(publicVault).getCurrentEpoch() ); - vm.warp(block.timestamp + 14 days); + skip(14 days); ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack1); ASTARIA_ROUTER.liquidate(collateralId2, uint8(0), stack2); // TODO test this _bid(address(3), collateralId, 5 ether); _bid(address(3), collateralId2, 20 ether); - - _warpToEpochEnd(publicVault); // epoch boundary - + vm.warp(withdrawProxy.getFinalAuctionEnd()); + emit log_named_uint("finalAuctionEnd", block.timestamp); PublicVault(publicVault).processEpoch(); - vm.warp(block.timestamp + 13 days); + skip(13 days); - WithdrawProxy(withdrawProxy).claim(); + withdrawProxy.claim(); PublicVault(publicVault).transferWithdrawReserve(); vm.startPrank(address(1)); - WithdrawProxy(withdrawProxy).redeem( - IERC20(withdrawProxy).balanceOf(address(1)), + withdrawProxy.redeem( + withdrawProxy.balanceOf(address(1)), address(1), address(1) ); @@ -193,13 +186,13 @@ contract WithdrawTest is TestHelpers { PublicVault(publicVault).getCurrentEpoch() ); - _warpToEpochEnd(publicVault); + uint256 finalAuctionEnd = withdrawProxy.getFinalAuctionEnd(); PublicVault(publicVault).processEpoch(); PublicVault(publicVault).transferWithdrawReserve(); vm.startPrank(address(2)); - WithdrawProxy(withdrawProxy).redeem( - IERC20(withdrawProxy).balanceOf(address(2)), + withdrawProxy.redeem( + withdrawProxy.balanceOf(address(2)), address(2), address(2) ); @@ -294,17 +287,17 @@ contract WithdrawTest is TestHelpers { ASTARIA_ROUTER.liquidate(collateralId1, uint8(0), stacks[0]); - - address withdrawProxy1 = PublicVault(publicVault).getWithdrawProxy(0); + WithdrawProxy withdrawProxy1 = PublicVault(publicVault).getWithdrawProxy(0); assertEq( LIEN_TOKEN.getPayee(lienId1), - withdrawProxy1, + address(withdrawProxy1), "First lien not pointing to first WithdrawProxy" ); _bid(address(3), collateralId1, 20 ether); + vm.warp(withdrawProxy1.getFinalAuctionEnd()); PublicVault(publicVault).processEpoch(); // epoch 0 processing vm.warp(block.timestamp + 14 days); @@ -313,11 +306,11 @@ contract WithdrawTest is TestHelpers { ASTARIA_ROUTER.liquidate(collateralId2, uint8(0), stacks[1]); - address withdrawProxy2 = PublicVault(publicVault).getWithdrawProxy(1); + WithdrawProxy withdrawProxy2 = PublicVault(publicVault).getWithdrawProxy(1); assertEq( LIEN_TOKEN.getPayee(lienId2), - withdrawProxy2, + address(withdrawProxy2), "Second lien not pointing to second WithdrawProxy" ); @@ -325,21 +318,21 @@ contract WithdrawTest is TestHelpers { PublicVault(publicVault).transferWithdrawReserve(); - WithdrawProxy(withdrawProxy1).claim(); + withdrawProxy1.claim(); - WithdrawProxy(withdrawProxy1).redeem( - IERC20(withdrawProxy1).balanceOf(address(1)), + withdrawProxy1.redeem( + withdrawProxy1.balanceOf(address(1)), address(1), address(1) ); - + vm.warp(withdrawProxy2.getFinalAuctionEnd()); PublicVault(publicVault).processEpoch(); PublicVault(publicVault).transferWithdrawReserve(); - - WithdrawProxy(withdrawProxy2).claim(); // TODO maybe 2 - WithdrawProxy(withdrawProxy2).redeem( - IERC20(withdrawProxy2).balanceOf(address(2)), + + withdrawProxy2.claim(); // TODO maybe 2 + withdrawProxy2.redeem( + withdrawProxy2.balanceOf(address(2)), address(2), address(2) ); @@ -349,25 +342,25 @@ contract WithdrawTest is TestHelpers { "PublicVault should have 0 assets" ); assertEq( - WETH9.balanceOf(PublicVault(publicVault).getWithdrawProxy(0)), + WETH9.balanceOf(address(PublicVault(publicVault).getWithdrawProxy(0))), 0, "WithdrawProxy 0 should have 0 assets" ); assertEq( - WETH9.balanceOf(PublicVault(publicVault).getWithdrawProxy(1)), + WETH9.balanceOf(address(PublicVault(publicVault).getWithdrawProxy(1))), 0, "WithdrawProxy 1 should have 0 assets" ); assertEq( WETH9.balanceOf(address(1)), - 50575342941392479750, + 50636986777008079750, "LPs have different amounts" ); assertEq( WETH9.balanceOf(address(2)), - 51150685407138079750, + 51212329242753679750, "LPs have different amounts" ); } @@ -492,12 +485,15 @@ contract WithdrawTest is TestHelpers { "PublicVault yIntercept calculation incorrect" ); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); - assertTrue(WETH9.balanceOf(withdrawProxy) != 0, "WITHDRAWPROXY IS 0"); + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); + assertTrue( + WETH9.balanceOf(address(withdrawProxy)) != 0, + "WITHDRAWPROXY IS 0" + ); vm.startPrank(address(1)); - WithdrawProxy(withdrawProxy).redeem( - IERC20(withdrawProxy).balanceOf(address(1)), + withdrawProxy.redeem( + withdrawProxy.balanceOf(address(1)), address(1), address(1) ); @@ -523,13 +519,16 @@ contract WithdrawTest is TestHelpers { ); // WithdrawProxy(withdrawProxy).claim(); - address withdrawProxy2 = PublicVault(publicVault).getWithdrawProxy(1); - WithdrawProxy(withdrawProxy2).claim(); - assertTrue(WETH9.balanceOf(withdrawProxy2) != 0, "WITHDRAWPROXY 2 IS 0"); + WithdrawProxy withdrawProxy2 = PublicVault(publicVault).getWithdrawProxy(1); + withdrawProxy2.claim(); + assertTrue( + WETH9.balanceOf(address(withdrawProxy2)) != 0, + "WITHDRAWPROXY 2 IS 0" + ); vm.startPrank(address(3)); - WithdrawProxy(withdrawProxy2).redeem( - IERC20(withdrawProxy2).balanceOf(address(3)), + withdrawProxy2.redeem( + withdrawProxy2.balanceOf(address(3)), address(3), address(3) ); @@ -595,7 +594,7 @@ contract WithdrawTest is TestHelpers { _bid(address(3), collateralId1, 20 ether); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); vm.expectRevert( abi.encodeWithSelector( @@ -603,7 +602,7 @@ contract WithdrawTest is TestHelpers { WithdrawProxy.InvalidStates.PROCESS_EPOCH_NOT_COMPLETE ) ); - WithdrawProxy(withdrawProxy).claim(); + withdrawProxy.claim(); uint256 collateralId2 = tokenContract.computeId(tokenId2); ASTARIA_ROUTER.liquidate(collateralId2, 0, stacks[1]); @@ -615,8 +614,9 @@ contract WithdrawTest is TestHelpers { WithdrawProxy.InvalidStates.PROCESS_EPOCH_NOT_COMPLETE ) ); - WithdrawProxy(withdrawProxy).claim(); + withdrawProxy.claim(); + skip(withdrawProxy.getFinalAuctionEnd()); PublicVault(publicVault).processEpoch(); assertEq( @@ -635,14 +635,14 @@ contract WithdrawTest is TestHelpers { "Incorrect PublicVault withdrawRatio calculation after epoch 0" ); - WithdrawProxy(withdrawProxy).claim(); + withdrawProxy.claim(); PublicVault(publicVault).transferWithdrawReserve(); assertEq(WETH9.balanceOf(publicVault), 0, "PublicVault balance not 0"); vm.startPrank(address(1)); - WithdrawProxy(withdrawProxy).redeem( - IERC20(withdrawProxy).balanceOf(address(1)), + withdrawProxy.redeem( + withdrawProxy.balanceOf(address(1)), address(1), address(1) ); @@ -714,9 +714,9 @@ contract WithdrawTest is TestHelpers { ); PublicVault(publicVault).transferWithdrawReserve(); - address withdrawProxy1 = PublicVault(publicVault).getWithdrawProxy(0); - WithdrawProxy(withdrawProxy1).redeem( - IERC20(withdrawProxy1).balanceOf(address(1)), + WithdrawProxy withdrawProxy1 = PublicVault(publicVault).getWithdrawProxy(0); + withdrawProxy1.redeem( + withdrawProxy1.balanceOf(address(1)), address(1), address(1) ); @@ -780,9 +780,9 @@ contract WithdrawTest is TestHelpers { "Incorrect PublicVault withdrawReserve calculation after epoch 1" ); PublicVault(publicVault).transferWithdrawReserve(); - address withdrawProxy2 = PublicVault(publicVault).getWithdrawProxy(1); - WithdrawProxy(withdrawProxy2).redeem( - IERC20(withdrawProxy2).balanceOf(address(2)), + WithdrawProxy withdrawProxy2 = PublicVault(publicVault).getWithdrawProxy(1); + withdrawProxy2.redeem( + withdrawProxy2.balanceOf(address(2)), address(2), address(2) ); @@ -805,9 +805,9 @@ contract WithdrawTest is TestHelpers { // create a PublicVault with a 14-day epoch address publicVault = _createPublicVault({ - strategist: strategistOne, - delegate: strategistTwo, - epochLength: 14 days + strategist: strategistOne, + delegate: strategistTwo, + epochLength: 14 days }); // lend 50 ether to the PublicVault as address(1) @@ -821,35 +821,60 @@ contract WithdrawTest is TestHelpers { // borrow 10 eth against the dummy NFT (, ILienToken.Stack[] memory stack) = _commitToLien({ - vault: publicVault, - strategist: strategistOne, - strategistPK: strategistOnePK, - tokenContract: tokenContract, - tokenId: tokenId, - lienDetails: details, - amount: 10 ether, - isFirstLien: true + vault: publicVault, + strategist: strategistOne, + strategistPK: strategistOnePK, + tokenContract: tokenContract, + tokenId: tokenId, + lienDetails: details, + amount: 10 ether, + isFirstLien: true }); vm.warp(block.timestamp + 13 days); uint256 collateralId = tokenContract.computeId(tokenId); - assertEq(LIEN_TOKEN.getOwed(stack[0]), uint192(10534246575335200000), "Incorrect lien interest"); + assertEq( + LIEN_TOKEN.getOwed(stack[0]), + uint192(10534246575335200000), + "Incorrect lien interest" + ); + ASTARIA_ROUTER.liquidate(collateralId, uint8(0), stack); _bid(address(3), collateralId, 5 ether); + WithdrawProxy withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); - _warpToEpochEnd(publicVault); + 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"); + assertEq( + address(this), + COLLATERAL_TOKEN.ownerOf(collateralId), + "liquidator did not receive NFT" + ); - address withdrawProxy = PublicVault(publicVault).getWithdrawProxy(0); - WithdrawProxy(withdrawProxy).claim(); + withdrawProxy.claim(); - assertEq(WETH9.balanceOf(publicVault), 44350000000000000000, "Incorrect PublicVault balance"); - assertEq(PublicVault(publicVault).getYIntercept(), 44350000000000000000, "Incorrect PublicVault YIntercept"); - assertEq(PublicVault(publicVault).totalAssets(), 44350000000000000000, "Incorrect PublicVault totalAssets()"); - assertEq(PublicVault(publicVault).getSlope(), 0, "Incorrect PublicVault slope"); + assertEq( + WETH9.balanceOf(publicVault), + 44350000000000000000, + "Incorrect PublicVault balance" + ); + assertEq( + PublicVault(publicVault).getYIntercept(), + 44350000000000000000, + "Incorrect PublicVault YIntercept" + ); + assertEq( + PublicVault(publicVault).totalAssets(), + 44350000000000000000, + "Incorrect PublicVault totalAssets()" + ); + assertEq( + PublicVault(publicVault).getSlope(), + 0, + "Incorrect PublicVault slope" + ); } } diff --git a/src/utils/Pausable.sol b/src/utils/Pausable.sol index f5783fbc..5bf9c6fa 100644 --- a/src/utils/Pausable.sol +++ b/src/utils/Pausable.sol @@ -17,6 +17,8 @@ interface IPausable { * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is IPausable { + bytes32 constant PAUSE_SLOT = + keccak256("xyz.astaria.AstariaRouter.Pausable.storage.location"); /** * @dev Emitted when the pause is triggered by `account`. */ @@ -27,20 +29,22 @@ abstract contract Pausable is IPausable { */ event Unpaused(address account); - bool private _paused; + struct PauseStorage { + bool _paused; + } - /** - * @dev Initializes the contract in unpaused state. - */ - constructor() { - _paused = false; + function _loadPauseSlot() internal pure returns (PauseStorage storage ps) { + bytes32 loc = PAUSE_SLOT; + assembly { + ps.slot := loc + } } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { - return _paused; + return _loadPauseSlot()._paused; } /** @@ -75,7 +79,7 @@ abstract contract Pausable is IPausable { * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { - _paused = true; + _loadPauseSlot()._paused = true; emit Paused(msg.sender); } @@ -87,7 +91,7 @@ abstract contract Pausable is IPausable { * - The contract must be paused. */ function _unpause() internal virtual whenPaused { - _paused = false; + _loadPauseSlot()._paused = false; emit Unpaused(msg.sender); } } diff --git a/tsconfig.json b/tsconfig.json index 520906f5..d77674ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "compilerOptions": { - "target": "es2018", + "target": "es2021", "module": "commonjs", "strict": true, "esModuleInterop": true, "outDir": "dist", "declaration": true }, - "include": ["./scripts", "./typechain"], - "files": ["./hardhat.config.ts"] + "include": ["./scripts/loanProofGenerator.ts"], + "exclude": ["node_modules/*", "lib/*", "dist","typechain", "typechainabi"] } diff --git a/yarn.lock b/yarn.lock index c34ddbf8..4db9bb30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,13 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@dethcrypto/eth-sdk-client@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@dethcrypto/eth-sdk-client/-/eth-sdk-client-0.1.6.tgz#900c0b541523a9c77db0ad9608e74480ef6b4913" @@ -913,6 +920,24 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@metamask/eth-sig-util@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz" @@ -981,16 +1006,6 @@ "@types/sinon-chai" "^3.2.3" "@types/web3" "1.0.19" -"@openzeppelin/contracts@^4.6.0": - version "4.6.0" - resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.6.0.tgz" - integrity sha512-8vi4d50NNya/bQqCmaVzvHNmwHvS0OBKb7HNtuNwEE3scXWrP31fKQoGxNMT+KbzmrNZzatE3QK5p2gFONI/hg== - -"@rari-capital/solmate@^6.2.0": - version "6.2.0" - resolved "https://registry.npmjs.org/@rari-capital/solmate/-/solmate-6.2.0.tgz" - integrity sha512-g94F+Ra9ixyJyNgvnOIufNjUz488uEG0nxIEEtJ7+g+tA1XGUupRB2kB5b+VO7WYO26RNOVD2fW6xE4e14iWpg== - "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.npmjs.org/@resolver-engine/core/-/core-0.3.3.tgz" @@ -1184,6 +1199,26 @@ "@truffle/interface-adapter" "^0.5.16" web3 "1.5.3" +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + "@typechain/ethers-v5@^10.0.0": version "10.1.0" resolved "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-10.1.0.tgz" @@ -1243,7 +1278,7 @@ "@types/node" "*" "@types/responselike" "*" -"@types/chai@*", "@types/chai@^4.2.21": +"@types/chai@*": version "4.3.1" resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz" integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ== @@ -1328,11 +1363,6 @@ dependencies: "@types/node" "*" -"@types/mocha@^9.0.0": - version "9.1.1" - resolved "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz" - integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== - "@types/node-fetch@^2.5.5": version "2.6.1" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz" @@ -1587,6 +1617,11 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.3.1: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^6.0.7: version "6.4.2" resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz" @@ -1597,6 +1632,11 @@ acorn@^7.4.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.4.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + address@^1.0.1: version "1.2.0" resolved "https://registry.npmjs.org/address/-/address-1.2.0.tgz" @@ -1736,6 +1776,11 @@ anymatch@~3.1.1, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" @@ -1855,11 +1900,6 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz" @@ -2912,19 +2952,6 @@ cbor@^5.0.2: bignumber.js "^9.0.1" nofilter "^1.0.4" -chai@^4.2.0: - version "4.3.6" - resolved "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz" - integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^3.0.1" - get-func-name "^2.0.0" - loupe "^2.3.1" - pathval "^1.1.1" - type-detect "^4.0.5" - chalk@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" @@ -2963,11 +2990,6 @@ chardet@^0.7.0: resolved "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= -check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= - checkpoint-store@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz" @@ -3413,6 +3435,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-fetch@^2.1.0, cross-fetch@^2.1.1: version "2.2.6" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.6.tgz" @@ -3552,13 +3579,6 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -deep-eql@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" - integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== - dependencies: - type-detect "^4.0.0" - deep-equal@~1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz" @@ -3704,6 +3724,11 @@ diff@5.0.0: resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz" @@ -5353,11 +5378,6 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= - get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" @@ -7055,13 +7075,6 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loupe@^2.3.1: - version "2.3.4" - resolved "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz" - integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== - dependencies: - get-func-name "^2.0.0" - lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" @@ -7108,6 +7121,11 @@ ltgt@~2.1.1: resolved "https://registry.npmjs.org/ltgt/-/ltgt-2.1.3.tgz" integrity sha1-EIUaBtmWS5cReEQcI8nlJpjuzjQ= +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + map-age-cleaner@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" @@ -8191,11 +8209,6 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== - pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9: version "3.1.2" resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz" @@ -10040,6 +10053,25 @@ ts-generator@^0.1.1: resolve "^1.8.1" ts-essentials "^1.0.0" +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" @@ -10103,11 +10135,6 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0, type-detect@^4.0.5: - version "4.0.8" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" @@ -10417,6 +10444,11 @@ uuid@^8.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" @@ -11354,6 +11386,11 @@ yargs@^4.7.1: y18n "^3.2.1" yargs-parser "^2.4.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"