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
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ libs = ["lib"]
solc = "0.8.28"
evm_version = "cancun"

fs_permissions = [{ access = "read-write", path = "e2e-results.json" }]

[rpc_endpoints]
base = "https://mainnet.base.org"
base_sepolia = "https://sepolia.base.org"
129 changes: 3 additions & 126 deletions script/E2ETest.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ interface IERC20Extended is IERC20 {
function totalSupply() external view returns (uint256);
}

/// @title E2ETest - Full StoryFactory lifecycle on Base mainnet
/// @notice Groups A-F: story lifecycle, trading, donations, royalties,
/// validation barriers, and edge cases. Outputs results to e2e-results.json.
/// @title E2ETest - Broadcastable StoryFactory lifecycle on Base mainnet
/// @notice Groups A-D, F (happy paths only). Outputs results to e2e-results.json.
/// Revert-validation tests live in E2ETestReverts.s.sol (simulation only).
contract E2ETest is Script {
// -----------------------------------------------------------------------
// Base mainnet addresses
Expand Down Expand Up @@ -113,11 +113,6 @@ contract E2ETest is Script {
// ===== Group D: Royalties =====
_groupD();

// ===== Group E: Validation Barriers =====
vm.stopBroadcast();
_groupE();
vm.startBroadcast(deployerKey);

// ===== Group F: Edge Cases =====
_groupF();

Expand Down Expand Up @@ -390,116 +385,6 @@ contract E2ETest is Script {

// Serialize royalty results
vm.serializeUint(resultsJson, "royaltiesClaimed", royaltyClaimed);

// D2: Claim again - should revert with MCV2_Royalty__NothingToClaim()
try BOND.claimRoyalties(address(PL_TEST)) {
revert("D2: should have reverted on empty claim");
} catch {
console.log("[D2] Empty claim reverts PASS (MCV2_Royalty__NothingToClaim)");
scenariosPassed++;
}
}

// ===================================================================
// Group E: Validation Barriers (Expected Reverts)
// ===================================================================

function _groupE() internal {
console.log("");
console.log("--- Group E: Validation Barriers ---");

// E1: Empty title
try FACTORY.createStoryline("", CID_46, HASH_A, false) {
revert("E1: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Empty title"), "E1: wrong revert reason");
console.log('[E1] Empty title reverts PASS "Empty title"');
scenariosPassed++;
}

// E2: CID too short (2 chars)
try FACTORY.createStoryline("Test", "Qm", HASH_A, false) {
revert("E2: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E2: wrong revert reason");
console.log('[E2] Short CID reverts PASS "Invalid CID"');
scenariosPassed++;
}

// E3: CID too long (101 chars)
try FACTORY.createStoryline(
"Test",
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi1234567890abcdefghijklmnopqrstuvwxyz12345X",
HASH_A,
false
) {
revert("E3: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E3: wrong revert reason");
console.log('[E3] Long CID reverts PASS "Invalid CID"');
scenariosPassed++;
}

// E4: chainPlot from non-writer address
// Outside broadcast, msg.sender is the script contract (not the deployer/writer)
try FACTORY.chainPlot(idA1, "Unauthorized", CID_46, HASH_A) {
revert("E4: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Not writer"), "E4: wrong revert reason");
console.log('[E4] Non-writer chainPlot reverts PASS "Not writer"');
scenariosPassed++;
}

// E5: Zero donation
try FACTORY.donate(idA1, 0) {
revert("E5: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Zero amount"), "E5: wrong revert reason");
console.log('[E5] Zero donation reverts PASS "Zero amount"');
scenariosPassed++;
}

// E6: Donate to non-existent storyline
try FACTORY.donate(999999, 1) {
revert("E6: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Storyline does not exist"), "E6: wrong revert reason");
console.log('[E6] Non-existent storyline reverts PASS "Storyline does not exist"');
scenariosPassed++;
}

// E7: Donate without approval (from script contract, which has no approval)
try FACTORY.donate(idA1, 1 ether) {
revert("E7: should have reverted");
} catch {
console.log("[E7] No approval reverts PASS (ERC-20 transferFrom failed)");
scenariosPassed++;
}

// E8: chainPlot with CID < 46 chars (prank as deployer to pass writer check)
vm.prank(deployer);
try FACTORY.chainPlot(idA1, "Test", "QmShortCID1234567890123456789012345678901234", HASH_A) {
revert("E8: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E8: wrong revert reason");
console.log('[E8] Short CID in chainPlot reverts PASS "Invalid CID"');
scenariosPassed++;
}

// E9: chainPlot with CID > 100 chars (prank as deployer to pass writer check)
vm.prank(deployer);
try FACTORY.chainPlot(
idA1,
"Test",
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi1234567890abcdefghijklmnopqrstuvwxyz12345X",
HASH_A
) {
revert("E9: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E9: wrong revert reason");
console.log('[E9] Long CID in chainPlot reverts PASS "Invalid CID"');
scenariosPassed++;
}
}

// ===================================================================
Expand All @@ -522,14 +407,6 @@ contract E2ETest is Script {
console.log("[F2] CID exact max (100 chars) PASS storylineId=%d", idF2);
scenariosPassed++;

// F3: createStoryline with msg.value = 0 should revert (creation fee required)
try FACTORY.createStoryline("Zero Fee Story", CID_46, HASH_A, false) {
revert("F3: should have reverted without creation fee");
} catch {
console.log("[F3] Zero fee reverts PASS (MCV2_Bond__InvalidCreationFee)");
scenariosPassed++;
}

// F4: chainPlot with empty title (title not validated in chainPlot) — use F1's storyline
FACTORY.chainPlot(idF1, "", CID_46, HASH_B);
(,, uint256 pc,,,) = FACTORY.storylines(idF1);
Expand Down
158 changes: 158 additions & 0 deletions script/E2ETestReverts.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Script, console} from "forge-std/Script.sol";
import {StoryFactory} from "../src/StoryFactory.sol";
import {IMCV2_Bond} from "../src/interfaces/IMCV2_Bond.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";

/// @title E2ETestReverts - Revert-validation tests (simulation only, no broadcast)
/// @notice Groups D2, E1-E9, F3: expected-revert scenarios that cannot run
/// 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);
IERC20 constant PL_TEST = IERC20(0xF8A2C39111FCEB9C950aAf28A9E34EBaD99b85C1);
IMCV2_Bond constant BOND = IMCV2_Bond(0xc5a076cad94176c2996B32d8466Be1cE757FAa27);

string constant CID_46 = "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
bytes32 constant HASH_A = keccak256("e2e genesis content");

uint256 scenariosPassed;

function run() external {
uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployer = vm.addr(deployerKey);

// Read storyline IDs from e2e-results.json (produced by E2ETest)
string memory json = vm.readFile("e2e-results.json");
uint256 idA1 = vm.parseJsonUint(json, ".storylineA1.storylineId");

console.log("=== E2E Revert Tests (Simulation Only) ===");
console.log("Deployer:", deployer);
console.log("Using storylineId:", idA1);
console.log("");

// ===== Group D2: Empty royalty claim =====
console.log("--- Group D: Royalties (reverts) ---");

vm.prank(deployer);
try BOND.claimRoyalties(address(PL_TEST)) {
revert("D2: should have reverted on empty claim");
} catch {
console.log("[D2] Empty claim reverts PASS (MCV2_Royalty__NothingToClaim)");
scenariosPassed++;
}

// ===== Group E: Validation Barriers =====
console.log("");
console.log("--- Group E: Validation Barriers ---");

// E1: Empty title
try FACTORY.createStoryline("", CID_46, HASH_A, false) {
revert("E1: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Empty title"), "E1: wrong revert reason");
console.log('[E1] Empty title reverts PASS "Empty title"');
scenariosPassed++;
}

// E2: CID too short (2 chars)
try FACTORY.createStoryline("Test", "Qm", HASH_A, false) {
revert("E2: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E2: wrong revert reason");
console.log('[E2] Short CID reverts PASS "Invalid CID"');
scenariosPassed++;
}

// E3: CID too long (101 chars)
try FACTORY.createStoryline(
"Test",
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi1234567890abcdefghijklmnopqrstuvwxyz12345X",
HASH_A,
false
) {
revert("E3: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E3: wrong revert reason");
console.log('[E3] Long CID reverts PASS "Invalid CID"');
scenariosPassed++;
}

// E4: chainPlot from non-writer address (script contract is not the writer)
try FACTORY.chainPlot(idA1, "Unauthorized", CID_46, HASH_A) {
revert("E4: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Not writer"), "E4: wrong revert reason");
console.log('[E4] Non-writer chainPlot reverts PASS "Not writer"');
scenariosPassed++;
}

// E5: Zero donation
try FACTORY.donate(idA1, 0) {
revert("E5: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Zero amount"), "E5: wrong revert reason");
console.log('[E5] Zero donation reverts PASS "Zero amount"');
scenariosPassed++;
}

// E6: Donate to non-existent storyline
try FACTORY.donate(999999, 1) {
revert("E6: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Storyline does not exist"), "E6: wrong revert reason");
console.log('[E6] Non-existent storyline reverts PASS "Storyline does not exist"');
scenariosPassed++;
}

// E7: Donate without approval (script contract has no approval)
try FACTORY.donate(idA1, 1 ether) {
revert("E7: should have reverted");
} catch {
console.log("[E7] No approval reverts PASS (ERC-20 transferFrom failed)");
scenariosPassed++;
}

// E8: chainPlot with CID < 46 chars (prank as deployer to pass writer check)
vm.prank(deployer);
try FACTORY.chainPlot(idA1, "Test", "QmShortCID1234567890123456789012345678901234", HASH_A) {
revert("E8: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E8: wrong revert reason");
console.log('[E8] Short CID in chainPlot reverts PASS "Invalid CID"');
scenariosPassed++;
}

// E9: chainPlot with CID > 100 chars (prank as deployer to pass writer check)
vm.prank(deployer);
try FACTORY.chainPlot(
idA1,
"Test",
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi1234567890abcdefghijklmnopqrstuvwxyz12345X",
HASH_A
) {
revert("E9: should have reverted");
} catch Error(string memory reason) {
require(keccak256(bytes(reason)) == keccak256("Invalid CID"), "E9: wrong revert reason");
console.log('[E9] Long CID in chainPlot reverts PASS "Invalid CID"');
scenariosPassed++;
}

// ===== F3: Zero creation fee =====
console.log("");
console.log("--- Group F: Edge Cases (reverts) ---");

try FACTORY.createStoryline("Zero Fee Story", CID_46, HASH_A, false) {
revert("F3: should have reverted without creation fee");
} catch {
console.log("[F3] Zero fee reverts PASS (MCV2_Bond__InvalidCreationFee)");
scenariosPassed++;
}

console.log("");
console.log("=== ALL REVERT TESTS PASSED ===");
console.log("Scenarios passed:", scenariosPassed);
}
}