Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions script/E2ETest.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}
100 changes: 99 additions & 1 deletion script/E2ETestReverts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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) ---");
Expand All @@ -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);
Expand Down