diff --git a/script/E2ETest.s.sol b/script/E2ETest.s.sol index 8ebdc89..50e146a 100644 --- a/script/E2ETest.s.sol +++ b/script/E2ETest.s.sol @@ -18,7 +18,7 @@ contract E2ETest is Script { // ----------------------------------------------------------------------- // Base mainnet addresses // ----------------------------------------------------------------------- - StoryFactory constant FACTORY = StoryFactory(0xc278F4099298118efA8dF30DF0F4876632571948); + StoryFactory constant FACTORY = StoryFactory(0x27B4FCf333f29a3865b3B76ea00C955D7b64BD0F); IERC20 constant PL_TEST = IERC20(0xF8A2C39111FCEB9C950aAf28A9E34EBaD99b85C1); IMCV2_Bond constant BOND = IMCV2_Bond(0xc5a076cad94176c2996B32d8466Be1cE757FAa27); @@ -434,11 +434,45 @@ contract E2ETest is Script { console.log("[F5] Buy/sell royalty diff PASS cost=%d refund=%d", buyCost, sellRefund); scenariosPassed++; + // F6: updateCurve by owner + create storyline with new curve + uint128[] memory newRanges = new uint128[](2); + newRanges[0] = 500_000e18; + newRanges[1] = 1_000_000e18; + uint128[] memory newPrices = new uint128[](2); + newPrices[0] = 2e15; // 0.002 PL_TEST (double the original) + newPrices[1] = 2e18; + FACTORY.updateCurve(newRanges, newPrices); + console.log("[F6] updateCurve by owner PASS"); + + uint256 idF6 = _verifyF6(); + // Serialize edge case storyline IDs string memory fKey = "edgeCasesF"; vm.serializeUint(fKey, "f1StorylineId", idF1); vm.serializeAddress(fKey, "f1Token", tokenF1); - string memory fJson = vm.serializeUint(fKey, "f2StorylineId", idF2); + vm.serializeUint(fKey, "f2StorylineId", idF2); + string memory fJson = vm.serializeUint(fKey, "f6StorylineId", idF6); vm.serializeString(resultsJson, "edgeCasesF", fJson); } + + function _verifyF6() internal returns (uint256 idF6) { + idF6 = FACTORY.createStoryline{value: creationFee}("Updated Curve Story", CID_46, HASH_A, false); + require(idF6 > 0, "F6: failed to create with new curve"); + + // Prove the new curve is used: buy 1 token, cost should reflect new price (2e15) + (, address tokenF6,,,) = FACTORY.storylines(idF6); + IERC20Extended storyTokenF6 = IERC20Extended(tokenF6); + storyTokenF6.approve(address(BOND), type(uint256).max); + uint256 balBefore = PL_TEST.balanceOf(deployer); + BOND.mint(tokenF6, 1e18, type(uint256).max, deployer); + uint256 f6Cost = balBefore - PL_TEST.balanceOf(deployer); + // New curve first step is 2e15 + 1% royalty. Original was 1e15 + 1%. + // So cost should be > 1.5e15 (proving new curve was applied) + require(f6Cost > 1.5e15, "F6: cost too low, new curve not applied"); + console.log("[F6] Create+buy after updateCurve PASS cost=%d (new curve)", f6Cost); + scenariosPassed += 2; + + // Sell back to clean up + BOND.burn(tokenF6, 1e18, 0, deployer); + } } diff --git a/script/E2ETestReverts.s.sol b/script/E2ETestReverts.s.sol index 0bfabba..e825643 100644 --- a/script/E2ETestReverts.s.sol +++ b/script/E2ETestReverts.s.sol @@ -11,7 +11,7 @@ import {IERC20} from "../src/interfaces/IERC20.sol"; /// under --broadcast. Run with `forge script` (no --broadcast flag). /// Requires the main E2ETest to have run first (needs existing storylines). contract E2ETestReverts is Script { - StoryFactory constant FACTORY = StoryFactory(0xc278F4099298118efA8dF30DF0F4876632571948); + StoryFactory constant FACTORY = StoryFactory(0x27B4FCf333f29a3865b3B76ea00C955D7b64BD0F); IERC20 constant PL_TEST = IERC20(0xF8A2C39111FCEB9C950aAf28A9E34EBaD99b85C1); IMCV2_Bond constant BOND = IMCV2_Bond(0xc5a076cad94176c2996B32d8466Be1cE757FAa27); @@ -145,6 +145,44 @@ contract E2ETestReverts is Script { scenariosPassed++; } + // E10: chainPlot with empty title (new validation from #42) + vm.prank(deployer); + try FACTORY.chainPlot(idA1, "", CID_46, HASH_A) { + revert("E10: should have reverted"); + } catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256("Empty title"), "E10: wrong revert reason"); + console.log('[E10] Empty title in chainPlot reverts PASS "Empty title"'); + scenariosPassed++; + } + + // E11: createStoryline with zero hash + try FACTORY.createStoryline("Test", CID_46, bytes32(0), false) { + revert("E11: should have reverted"); + } catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256("Empty hash"), "E11: wrong revert reason"); + console.log('[E11] Zero hash in create reverts PASS "Empty hash"'); + scenariosPassed++; + } + + // E12: chainPlot with zero hash + vm.prank(deployer); + try FACTORY.chainPlot(idA1, "Test", CID_46, bytes32(0)) { + revert("E12: should have reverted"); + } catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256("Empty hash"), "E12: wrong revert reason"); + console.log('[E12] Zero hash in chainPlot reverts PASS "Empty hash"'); + scenariosPassed++; + } + + // E13: updateCurve by non-owner + try FACTORY.updateCurve(new uint128[](1), new uint128[](1)) { + revert("E13: should have reverted"); + } catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256("Not owner"), "E13: wrong revert reason"); + console.log('[E13] Non-owner updateCurve reverts PASS "Not owner"'); + scenariosPassed++; + } + // ===== F3: Zero creation fee ===== console.log(""); console.log("--- Group F: Edge Cases (reverts) ---"); @@ -156,6 +194,66 @@ contract E2ETestReverts is Script { scenariosPassed++; } + // ===== Group G: hasSunset view ===== + console.log(""); + console.log("--- Group G: hasSunset ---"); + + // G1: hasSunset on active storyline (no deadline) — should be false + bool sunset1 = FACTORY.hasSunset(idA1); + require(!sunset1, "G1: hasSunset should be false for no-deadline storyline"); + console.log("[G1] hasSunset (no deadline) = false PASS"); + scenariosPassed++; + + // G2: hasSunset on expired deadline storyline (A1 has hasDeadline=true) + // Warp past 168h from the last plot time — hasSunset should return true + vm.warp(block.timestamp + 169 hours); + bool sunset2 = FACTORY.hasSunset(idA1); + require(sunset2, "G2: hasSunset should be true after deadline expired"); + console.log("[G2] hasSunset (expired deadline) PASS"); + scenariosPassed++; + + // ===== Group H: Constructor validations (simulation only) ===== + console.log(""); + console.log("--- Group H: Constructor validations ---"); + + uint128[] memory r1 = new uint128[](1); + r1[0] = 1e18; + uint128[] memory p1 = new uint128[](1); + p1[0] = 1e15; + + // H1: Zero bond address + try new StoryFactory(address(0), address(1), 1e18, r1, p1) { + revert("H1: should have reverted"); + } catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256("Zero bond address"), "H1: wrong revert reason"); + console.log('[H1] Zero bond address reverts PASS "Zero bond address"'); + scenariosPassed++; + } + + // H2: Zero token address + try new StoryFactory(address(1), address(0), 1e18, r1, p1) { + revert("H2: should have reverted"); + } catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256("Zero token address"), "H2: wrong revert reason"); + console.log('[H2] Zero token address reverts PASS "Zero token address"'); + scenariosPassed++; + } + + // H3: Too many steps (>1000) + uint128[] memory bigR = new uint128[](1001); + uint128[] memory bigP = new uint128[](1001); + for (uint256 i = 0; i < 1001; i++) { + bigR[i] = uint128(i + 1); + bigP[i] = uint128(i + 1); + } + try new StoryFactory(address(1), address(1), 1e18, bigR, bigP) { + revert("H3: should have reverted"); + } catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256("Too many steps"), "H3: wrong revert reason"); + console.log('[H3] >1000 steps reverts PASS "Too many steps"'); + scenariosPassed++; + } + console.log(""); console.log("=== ALL REVERT TESTS PASSED ==="); console.log("Scenarios passed:", scenariosPassed);