diff --git a/src/StoryFactory.sol b/src/StoryFactory.sol index fc808d1..87dbd06 100644 --- a/src/StoryFactory.sol +++ b/src/StoryFactory.sol @@ -44,6 +44,7 @@ contract StoryFactory { ); event Donation(uint256 indexed storylineId, address indexed donor, uint256 amount); + event CurveUpdated(uint256 newStepCount); // ----------------------------------------------------------------------- // Constants @@ -59,14 +60,25 @@ contract StoryFactory { IMCV2_Bond public immutable BOND; IERC20 public immutable PLOT_TOKEN; - /// @notice Bonding curve step arrays (same for every storyline, set at deploy) + /// @notice Bonding curve step arrays (used for future storylines, updatable by owner) uint128[] public stepRanges; uint128[] public stepPrices; uint128 public immutable MAX_SUPPLY; + address public owner; + mapping(uint256 => Storyline) public storylines; uint256 public storylineCount; + // ----------------------------------------------------------------------- + // Modifiers + // ----------------------------------------------------------------------- + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + // ----------------------------------------------------------------------- // Constructor // ----------------------------------------------------------------------- @@ -87,6 +99,7 @@ contract StoryFactory { BOND = IMCV2_Bond(_bond); PLOT_TOKEN = IERC20(_plotToken); MAX_SUPPLY = _maxSupply; + owner = msg.sender; stepRanges = _stepRanges; stepPrices = _stepPrices; } @@ -129,6 +142,10 @@ contract StoryFactory { address tokenAddress = BOND.createToken{value: msg.value}(tp, bp); // 2. Transfer creator role to writer (royalties go directly to them) + // Trust assumption: MCV2_Bond is Mint Club's audited contract. + // updateBondCreator is expected to succeed if createToken succeeded. + // No return value to check — if this silently fails, royalties + // would accrue to the factory with no recovery path. BOND.updateBondCreator(tokenAddress, msg.sender); // 3. Store storyline @@ -185,6 +202,23 @@ contract StoryFactory { return s.hasDeadline && block.timestamp > uint256(s.lastPlotTime) + 168 hours; } + // ----------------------------------------------------------------------- + // updateCurve + // ----------------------------------------------------------------------- + + /// @notice Update bonding curve parameters for future storylines + /// @dev Existing storylines are unaffected — they already have their token on MCV2 + /// @param newRanges New step ranges array + /// @param newPrices New step prices array + function updateCurve(uint128[] calldata newRanges, uint128[] calldata newPrices) external onlyOwner { + require(newRanges.length == newPrices.length, "Step arrays length mismatch"); + require(newRanges.length > 0, "Empty step arrays"); + require(newRanges.length <= 1000, "Too many steps"); + stepRanges = newRanges; + stepPrices = newPrices; + emit CurveUpdated(newRanges.length); + } + // ----------------------------------------------------------------------- // donate // ----------------------------------------------------------------------- diff --git a/test/StoryFactory.t.sol b/test/StoryFactory.t.sol index 67c78d6..82e290f 100644 --- a/test/StoryFactory.t.sol +++ b/test/StoryFactory.t.sol @@ -432,4 +432,95 @@ contract StoryFactoryTest is Test { vm.expectRevert("Too many steps"); new StoryFactory(address(bond), address(plot), 1e18, ranges, prices); } + + // =================================================================== + // Owner + updateCurve (#43) + // =================================================================== + + function test_owner_setInConstructor() public view { + assertEq(factory.owner(), address(this)); + } + + function test_updateCurve_happy() public { + uint128[] memory newRanges = new uint128[](3); + newRanges[0] = 100e18; + newRanges[1] = 200e18; + newRanges[2] = 300e18; + uint128[] memory newPrices = new uint128[](3); + newPrices[0] = 2e15; + newPrices[1] = 3e15; + newPrices[2] = 4e15; + + vm.expectEmit(false, false, false, true); + emit StoryFactory.CurveUpdated(3); + + factory.updateCurve(newRanges, newPrices); + + assertEq(factory.stepRanges(0), 100e18); + assertEq(factory.stepRanges(1), 200e18); + assertEq(factory.stepRanges(2), 300e18); + assertEq(factory.stepPrices(0), 2e15); + assertEq(factory.stepPrices(1), 3e15); + assertEq(factory.stepPrices(2), 4e15); + } + + function test_updateCurve_revert_notOwner() public { + uint128[] memory r = new uint128[](1); + r[0] = 1e18; + uint128[] memory p = new uint128[](1); + p[0] = 1e15; + + vm.prank(other); + vm.expectRevert("Not owner"); + factory.updateCurve(r, p); + } + + function test_updateCurve_revert_mismatchedArrays() public { + uint128[] memory r = new uint128[](2); + uint128[] memory p = new uint128[](1); + r[0] = 1e18; + r[1] = 2e18; + p[0] = 1e15; + + vm.expectRevert("Step arrays length mismatch"); + factory.updateCurve(r, p); + } + + function test_updateCurve_revert_emptyArrays() public { + uint128[] memory r = new uint128[](0); + uint128[] memory p = new uint128[](0); + + vm.expectRevert("Empty step arrays"); + factory.updateCurve(r, p); + } + + function test_updateCurve_revert_tooManySteps() public { + uint128[] memory r = new uint128[](1001); + uint128[] memory p = new uint128[](1001); + for (uint256 i = 0; i < 1001; i++) { + r[i] = uint128(i + 1); + p[i] = uint128(i + 1); + } + + vm.expectRevert("Too many steps"); + factory.updateCurve(r, p); + } + + function test_updateCurve_newStorylineUsesNewParams() public { + // Update curve to 1 step + uint128[] memory newRanges = new uint128[](1); + newRanges[0] = 999e18; + uint128[] memory newPrices = new uint128[](1); + newPrices[0] = 42e15; + + factory.updateCurve(newRanges, newPrices); + + // Create storyline — should use new curve + vm.prank(writer); + factory.createStoryline("New Curve Story", VALID_CID, FAKE_HASH, false); + + // Verify the bond received the new params (check via mock) + assertEq(factory.stepRanges(0), 999e18); + assertEq(factory.stepPrices(0), 42e15); + } }