|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +import "./TestHelpers.t.sol"; |
| 3 | +import "./utils/SigUtils.sol"; |
| 4 | +import {Bound} from "./utils/Bound.sol"; |
| 5 | +import "murky/Merkle.sol"; |
| 6 | +import {IERC4626 as ERC4626} from "src/interfaces/IERC4626.sol"; |
| 7 | +import {ERC721TokenReceiver} from "solmate/tokens/ERC721.sol"; |
| 8 | + |
| 9 | +//import {CollateralLookup} from "src/libraries/CollateralLookup.sol"; |
| 10 | + |
| 11 | +contract AstariaFuzzTest is TestHelpers, SigUtils, Bound { |
| 12 | + PublicVault public vault; |
| 13 | + bytes32 constant NEW_LIEN_SIG = |
| 14 | + 0xd03fcb98c0b64b239ccfeed4d62fcf721b2cf2c8ded60319cd2230f80dd2536c; |
| 15 | + |
| 16 | + function setUp() public override { |
| 17 | + super.setUp(); |
| 18 | + |
| 19 | + vm.warp(100_000); |
| 20 | + |
| 21 | + vm.startPrank(strategistOne); |
| 22 | + vault = PublicVault( |
| 23 | + payable( |
| 24 | + ASTARIA_ROUTER.newPublicVault( |
| 25 | + 14 days, |
| 26 | + strategistTwo, |
| 27 | + address(WETH9), |
| 28 | + 0, |
| 29 | + false, |
| 30 | + new address[](0), |
| 31 | + uint256(0) |
| 32 | + ) |
| 33 | + ) |
| 34 | + ); |
| 35 | + vm.stopPrank(); |
| 36 | + |
| 37 | + vm.label(address(vault), "PublicVault"); |
| 38 | + vm.label(address(TRANSFER_PROXY), "TransferProxy"); |
| 39 | + } |
| 40 | + |
| 41 | + function onERC721Received( |
| 42 | + address operator, // operator_ |
| 43 | + address from, // from_ |
| 44 | + uint256 tokenId, // tokenId_ |
| 45 | + bytes calldata data // data_ |
| 46 | + ) public virtual override returns (bytes4) { |
| 47 | + return ERC721TokenReceiver.onERC721Received.selector; |
| 48 | + } |
| 49 | + |
| 50 | + struct FuzzCommit { |
| 51 | + address borrower; |
| 52 | + address lender; |
| 53 | + uint256 amount; |
| 54 | + uint40 duration; |
| 55 | + uint40 strategyDuration; |
| 56 | + FuzzTerm[] terms; |
| 57 | + uint256 termIndex; |
| 58 | + } |
| 59 | + |
| 60 | + struct FuzzTerm { |
| 61 | + uint256 tokenId; |
| 62 | + uint256 maxAmount; |
| 63 | + uint256 rate; |
| 64 | + uint256 liquidationInitialAsk; |
| 65 | + address borrower; |
| 66 | + uint40 duration; |
| 67 | + bool isBorrowerSpecific; |
| 68 | + bool isUnique; |
| 69 | + } |
| 70 | + |
| 71 | + struct LoanAssertions { |
| 72 | + uint256 borrowerBalance; |
| 73 | + uint256 vaultBalance; |
| 74 | + uint256 slope; |
| 75 | + uint256 liensOpen; |
| 76 | + uint256 shares; |
| 77 | + } |
| 78 | + |
| 79 | + function boundFuzzTerm( |
| 80 | + FuzzTerm memory term |
| 81 | + ) internal view returns (FuzzTerm memory) { |
| 82 | + term.tokenId = _boundNonZero(term.tokenId); |
| 83 | + |
| 84 | + if (term.isBorrowerSpecific) { |
| 85 | + term.borrower = _toAddress(_boundMin(_toUint(term.borrower), 100)); |
| 86 | + } else { |
| 87 | + term.borrower = address(0); |
| 88 | + } |
| 89 | + |
| 90 | + //term.duration = 3 days; |
| 91 | + term.duration = uint40(_bound(term.duration, 1 hours, 365 days)); |
| 92 | + term.maxAmount = _boundMin(term.maxAmount, vault.minDepositAmount() + 1); |
| 93 | + term.rate = _bound(term.rate, 1, ((uint256(1e16) * 200) / (365 days))); |
| 94 | + //TODO: bound lia |
| 95 | + term.liquidationInitialAsk = type(uint256).max; |
| 96 | + |
| 97 | + return term; |
| 98 | + } |
| 99 | + |
| 100 | + function willArithmeticOverflow( |
| 101 | + FuzzCommit memory commit, |
| 102 | + FuzzTerm memory term |
| 103 | + ) internal pure returns (bool) { |
| 104 | + // mulDivWad requirements |
| 105 | + unchecked { |
| 106 | + //calculateSlope |
| 107 | + if (term.rate > type(uint256).max / commit.amount) { |
| 108 | + return true; |
| 109 | + } |
| 110 | + |
| 111 | + //getOwed() |
| 112 | + if ( |
| 113 | + term.duration > type(uint256).max / term.rate || |
| 114 | + term.duration * term.rate > type(uint256).max / commit.amount |
| 115 | + ) { |
| 116 | + return true; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + return false; |
| 121 | + } |
| 122 | + |
| 123 | + function testFuzzCommitToLien(FuzzCommit memory params) public { |
| 124 | + vm.assume(params.terms.length > 1); |
| 125 | + |
| 126 | + //BOUND PARAMS |
| 127 | + params.termIndex = _bound( |
| 128 | + params.termIndex, |
| 129 | + 0, |
| 130 | + params.terms.length > 100_000 ? 99_999 : params.terms.length - 1 |
| 131 | + ); |
| 132 | + |
| 133 | + params.terms[params.termIndex] = boundFuzzTerm( |
| 134 | + params.terms[params.termIndex] |
| 135 | + ); |
| 136 | + |
| 137 | + params.strategyDuration = uint40( |
| 138 | + _boundNonZero(uint256(params.strategyDuration)) |
| 139 | + ); |
| 140 | + |
| 141 | + FuzzTerm memory term = params.terms[params.termIndex]; |
| 142 | + params.amount = _bound(params.amount, 1, term.maxAmount); |
| 143 | + |
| 144 | + if (term.isBorrowerSpecific) { |
| 145 | + params.borrower = term.borrower; |
| 146 | + } else { |
| 147 | + params.borrower = _toAddress(_boundMin(_toUint(params.borrower), 100)); |
| 148 | + } |
| 149 | + |
| 150 | + vm.assume(params.borrower != COLLATERAL_TOKEN.getConduit()); |
| 151 | + vm.assume(!willArithmeticOverflow(params, term)); |
| 152 | + |
| 153 | + //BORROWER MINT & APPROVE NFT |
| 154 | + vm.startPrank(params.borrower); |
| 155 | + |
| 156 | + TestNFT tokenContract = new TestNFT(0); |
| 157 | + tokenContract.mint(address(params.borrower), term.tokenId); |
| 158 | + |
| 159 | + tokenContract.approve(address(ASTARIA_ROUTER), term.tokenId); |
| 160 | + |
| 161 | + vm.stopPrank(); |
| 162 | + |
| 163 | + //LEND |
| 164 | + vm.deal(address(params.lender), term.maxAmount); |
| 165 | + |
| 166 | + vm.startPrank(address(params.lender)); |
| 167 | + |
| 168 | + WETH9.deposit{value: term.maxAmount}(); |
| 169 | + WETH9.approve(address(ASTARIA_ROUTER), term.maxAmount); |
| 170 | + WETH9.approve(address(TRANSFER_PROXY), term.maxAmount); |
| 171 | + |
| 172 | + ASTARIA_ROUTER.depositToVault( |
| 173 | + ERC4626(address(vault)), |
| 174 | + address(params.lender), |
| 175 | + term.maxAmount, |
| 176 | + 0 |
| 177 | + ); |
| 178 | + |
| 179 | + vm.stopPrank(); |
| 180 | + LoanAssertions memory before; |
| 181 | + { |
| 182 | + //GET STRATEGY DETAILS |
| 183 | + IAstariaRouter.StrategyDetailsParam |
| 184 | + memory strategyDetails = IAstariaRouter.StrategyDetailsParam({ |
| 185 | + version: uint8(0), |
| 186 | + deadline: block.timestamp + params.strategyDuration, |
| 187 | + vault: payable(address(vault)) |
| 188 | + }); |
| 189 | + |
| 190 | + //MERKLEIZE |
| 191 | + bytes32[] memory data = new bytes32[]( |
| 192 | + params.terms.length > 100_000 ? 100_000 : params.terms.length |
| 193 | + ); |
| 194 | + |
| 195 | + bytes memory nlrDetails; |
| 196 | + for (uint256 i = 0; i < data.length; i++) { |
| 197 | + //TODO: include other validators |
| 198 | + IUniqueValidator.Details memory validatorDetails = IUniqueValidator |
| 199 | + .Details({ |
| 200 | + version: uint8(1), |
| 201 | + token: address(tokenContract), |
| 202 | + tokenId: params.terms[i].tokenId, |
| 203 | + borrower: params.terms[i].borrower, |
| 204 | + lien: ILienToken.Details({ |
| 205 | + maxAmount: params.terms[i].maxAmount, |
| 206 | + rate: params.terms[i].rate, |
| 207 | + duration: params.terms[i].duration, |
| 208 | + maxPotentialDebt: 0, |
| 209 | + liquidationInitialAsk: params.terms[i].liquidationInitialAsk |
| 210 | + }) |
| 211 | + }); |
| 212 | + if (i == params.termIndex) { |
| 213 | + nlrDetails = abi.encode(validatorDetails); |
| 214 | + } |
| 215 | + |
| 216 | + data[i] = keccak256(abi.encode(validatorDetails)); |
| 217 | + } |
| 218 | + |
| 219 | + Merkle m = new Merkle(); |
| 220 | + //bytes32 root = m.getRoot(data); |
| 221 | + bytes32 strategyHash = getTypedDataHash( |
| 222 | + vault.domainSeparator(), |
| 223 | + EIP712Message({ |
| 224 | + nonce: vault.getStrategistNonce(), |
| 225 | + root: m.getRoot(data), |
| 226 | + deadline: block.timestamp + params.strategyDuration |
| 227 | + }) |
| 228 | + ); |
| 229 | + |
| 230 | + IAstariaRouter.NewLienRequest memory lienRequest = IAstariaRouter |
| 231 | + .NewLienRequest({ |
| 232 | + strategy: strategyDetails, |
| 233 | + nlrDetails: nlrDetails, |
| 234 | + root: m.getRoot(data), |
| 235 | + proof: m.getProof(data, params.termIndex), |
| 236 | + amount: params.amount, |
| 237 | + v: 0, |
| 238 | + r: 0, |
| 239 | + s: 0 |
| 240 | + }); |
| 241 | + |
| 242 | + (lienRequest.v, lienRequest.r, lienRequest.s) = vm.sign( |
| 243 | + strategistOnePK, |
| 244 | + strategyHash |
| 245 | + ); |
| 246 | + |
| 247 | + before = LoanAssertions({ |
| 248 | + borrowerBalance: params.borrower.balance, |
| 249 | + vaultBalance: WETH9.balanceOf(address(vault)), |
| 250 | + slope: vault.getSlope(), |
| 251 | + liensOpen: 0, |
| 252 | + shares: 0 |
| 253 | + }); |
| 254 | + |
| 255 | + (before.liensOpen, ) = vault.getEpochData( |
| 256 | + vault.getLienEpoch(uint64(block.timestamp + term.duration)) |
| 257 | + ); |
| 258 | + (, , , , , , before.shares) = vault.getPublicVaultState(); |
| 259 | + |
| 260 | + vm.prank(params.borrower); |
| 261 | + vm.recordLogs(); |
| 262 | + ASTARIA_ROUTER.commitToLien( |
| 263 | + IAstariaRouter.Commitment({ |
| 264 | + tokenContract: address(tokenContract), |
| 265 | + tokenId: term.tokenId, |
| 266 | + lienRequest: lienRequest |
| 267 | + }) |
| 268 | + ); |
| 269 | + } |
| 270 | + Vm.Log[] memory logs = vm.getRecordedLogs(); |
| 271 | + ILienToken.Stack memory stack; |
| 272 | + for (uint256 i = 0; i < logs.length; ++i) { |
| 273 | + if (logs[i].topics[0] == NEW_LIEN_SIG) { |
| 274 | + stack = abi.decode(logs[i].data, (ILienToken.Stack)); |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + assertEq(tokenContract.ownerOf(term.tokenId), address(COLLATERAL_TOKEN)); |
| 279 | + |
| 280 | + assertEq(params.borrower.balance, before.borrowerBalance + params.amount); |
| 281 | + |
| 282 | + assertEq( |
| 283 | + WETH9.balanceOf(address(vault)), |
| 284 | + before.vaultBalance - params.amount, |
| 285 | + "vault balance did not decrease as expected" |
| 286 | + ); |
| 287 | + |
| 288 | + assertEq( |
| 289 | + vault.getSlope(), |
| 290 | + before.slope + LIEN_TOKEN.calculateSlope(stack), |
| 291 | + "slope did not increase as expected" |
| 292 | + ); |
| 293 | + |
| 294 | + (uint256 liensAfter, ) = vault.getEpochData( |
| 295 | + vault.getLienEpoch(stack.point.end) |
| 296 | + ); |
| 297 | + |
| 298 | + assertEq(liensAfter, before.liensOpen + 1, "no lien opened for epoch"); |
| 299 | + |
| 300 | + assertEq( |
| 301 | + COLLATERAL_TOKEN.ownerOf( |
| 302 | + CollateralLookup.computeId(address(tokenContract), term.tokenId) |
| 303 | + ), |
| 304 | + params.borrower, |
| 305 | + "CT not transferred" |
| 306 | + ); |
| 307 | + |
| 308 | + assertEq( |
| 309 | + LIEN_TOKEN.ownerOf(uint256(keccak256(abi.encode(stack)))), |
| 310 | + vault.recipient(), |
| 311 | + "LT not issued" |
| 312 | + ); |
| 313 | + } |
| 314 | +} |
| 315 | +//Test invalid conditions |
0 commit comments