From 54123f839bb22b4ba85fb8b3074f13d6b40527be Mon Sep 17 00:00:00 2001 From: katyailil Date: Thu, 4 Dec 2025 09:45:07 +0000 Subject: [PATCH 1/7] test: add MockUSDC with 6 decimals for realistic testing --- test/mocks/MockUSDC.sol | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 test/mocks/MockUSDC.sol diff --git a/test/mocks/MockUSDC.sol b/test/mocks/MockUSDC.sol new file mode 100644 index 0000000..d0a0a34 --- /dev/null +++ b/test/mocks/MockUSDC.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract MockUSDC { + string public name = "USD Coin"; + string public symbol = "USDC"; + uint8 public decimals = 6; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) external returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + if (allowance[from][msg.sender] != type(uint256).max) { + allowance[from][msg.sender] -= amount; + } + balanceOf[from] -= amount; + balanceOf[to] += amount; + emit Transfer(from, to, amount); + return true; + } +} From 11a6b0f56c2fee1ef3c93064f86cb898e99f7b9d Mon Sep 17 00:00:00 2001 From: katyailil Date: Thu, 4 Dec 2025 09:46:05 +0000 Subject: [PATCH 2/7] test: SplitBaseV1 initialization and pool management --- test/SplitBaseV1.t.sol | 276 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 test/SplitBaseV1.t.sol diff --git a/test/SplitBaseV1.t.sol b/test/SplitBaseV1.t.sol new file mode 100644 index 0000000..5b1c4c1 --- /dev/null +++ b/test/SplitBaseV1.t.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {SplitBaseV1} from "../src/SplitBaseV1.sol"; +import {MockUSDC} from "./mocks/MockUSDC.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967Proxy.sol"; +import {ISplitBase} from "../src/interfaces/ISplitBase.sol"; + +contract SplitBaseV1Test is Test { + SplitBaseV1 public splitBase; + MockUSDC public usdc; + + address public owner = address(this); + address public user1 = address(0x1); + address public user2 = address(0x2); + address public recipient1 = address(0x3); + address public recipient2 = address(0x4); + address public recipient3 = address(0x5); + + event PoolCreated(uint256 indexed poolId, address indexed owner); + event RecipientAdded(uint256 indexed poolId, address indexed recipient, uint256 shares); + event RecipientUpdated(uint256 indexed poolId, address indexed recipient, uint256 newShares); + event RecipientRemoved(uint256 indexed poolId, address indexed recipient); + event PayoutExecuted(uint256 indexed poolId, uint256 totalAmount, uint256 recipientCount); + event PoolStatusChanged(uint256 indexed poolId, bool active); + + function setUp() public { + usdc = new MockUSDC(); + + SplitBaseV1 implementation = new SplitBaseV1(); + ERC1967Proxy proxy = new ERC1967Proxy( + address(implementation), + abi.encodeCall(SplitBaseV1.initialize, (address(usdc))) + ); + splitBase = SplitBaseV1(address(proxy)); + } + + function testInitialize() public view { + assertEq(address(splitBase.usdc()), address(usdc)); + assertEq(splitBase.owner(), owner); + } + + function testCreatePool() public { + vm.expectEmit(true, true, false, true); + emit PoolCreated(1, owner); + + uint256 poolId = splitBase.createPool(); + + assertEq(poolId, 1); + ISplitBase.Pool memory pool = splitBase.getPool(poolId); + assertEq(pool.owner, owner); + assertEq(pool.totalShares, 0); + assertEq(pool.recipientCount, 0); + assertTrue(pool.active); + assertEq(pool.lastExecutionTime, 0); + assertEq(pool.totalDistributed, 0); + } + + function testCreateMultiplePools() public { + uint256 poolId1 = splitBase.createPool(); + + vm.prank(user1); + uint256 poolId2 = splitBase.createPool(); + + vm.prank(user2); + uint256 poolId3 = splitBase.createPool(); + + assertEq(poolId1, 1); + assertEq(poolId2, 2); + assertEq(poolId3, 3); + + assertEq(splitBase.getPool(poolId1).owner, owner); + assertEq(splitBase.getPool(poolId2).owner, user1); + assertEq(splitBase.getPool(poolId3).owner, user2); + } + + function testAddRecipient() public { + uint256 poolId = splitBase.createPool(); + + vm.expectEmit(true, true, false, true); + emit RecipientAdded(poolId, recipient1, 100); + + splitBase.addRecipient(poolId, recipient1, 100); + + ISplitBase.Recipient memory r = splitBase.getRecipient(poolId, recipient1); + assertEq(r.account, recipient1); + assertEq(r.shares, 100); + assertTrue(r.active); + + ISplitBase.Pool memory pool = splitBase.getPool(poolId); + assertEq(pool.totalShares, 100); + assertEq(pool.recipientCount, 1); + } + + function testAddRecipientUnauthorized() public { + uint256 poolId = splitBase.createPool(); + + vm.prank(user1); + vm.expectRevert(SplitBaseV1.Unauthorized.selector); + splitBase.addRecipient(poolId, recipient1, 100); + } + + function testAddRecipientZeroAddress() public { + uint256 poolId = splitBase.createPool(); + + vm.expectRevert(SplitBaseV1.InvalidRecipient.selector); + splitBase.addRecipient(poolId, address(0), 100); + } + + function testAddRecipientZeroShares() public { + uint256 poolId = splitBase.createPool(); + + vm.expectRevert(SplitBaseV1.InvalidShares.selector); + splitBase.addRecipient(poolId, recipient1, 0); + } + + function testAddRecipientDuplicate() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + + vm.expectRevert(SplitBaseV1.InvalidRecipient.selector); + splitBase.addRecipient(poolId, recipient1, 200); + } + + function testUpdateRecipient() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + splitBase.addRecipient(poolId, recipient2, 200); + + assertEq(splitBase.getPool(poolId).totalShares, 300); + + vm.expectEmit(true, true, false, true); + emit RecipientUpdated(poolId, recipient1, 150); + + splitBase.updateRecipient(poolId, recipient1, 150); + + assertEq(splitBase.getRecipient(poolId, recipient1).shares, 150); + assertEq(splitBase.getPool(poolId).totalShares, 350); + } + + function testUpdateRecipientUnauthorized() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + + vm.prank(user1); + vm.expectRevert(SplitBaseV1.Unauthorized.selector); + splitBase.updateRecipient(poolId, recipient1, 150); + } + + function testRemoveRecipient() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + splitBase.addRecipient(poolId, recipient2, 200); + + vm.expectEmit(true, true, false, false); + emit RecipientRemoved(poolId, recipient1); + + splitBase.removeRecipient(poolId, recipient1); + + ISplitBase.Recipient memory r = splitBase.getRecipient(poolId, recipient1); + assertFalse(r.active); + assertEq(splitBase.getPool(poolId).totalShares, 200); + assertEq(splitBase.getPool(poolId).recipientCount, 1); + } + + function testExecutePayout() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + splitBase.addRecipient(poolId, recipient2, 200); + splitBase.addRecipient(poolId, recipient3, 300); + + uint256 payoutAmount = 600_000000; + usdc.mint(owner, payoutAmount); + usdc.approve(address(splitBase), payoutAmount); + + vm.expectEmit(true, false, false, true); + emit PayoutExecuted(poolId, payoutAmount, 3); + + splitBase.executePayout(poolId, payoutAmount); + + assertEq(usdc.balanceOf(recipient1), 100_000000); + assertEq(usdc.balanceOf(recipient2), 200_000000); + assertEq(usdc.balanceOf(recipient3), 300_000000); + + ISplitBase.Pool memory pool = splitBase.getPool(poolId); + assertGt(pool.lastExecutionTime, 0); + assertEq(pool.totalDistributed, payoutAmount); + } + + function testExecutePayoutPrecision() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 333); + splitBase.addRecipient(poolId, recipient2, 333); + splitBase.addRecipient(poolId, recipient3, 334); + + uint256 payoutAmount = 1_000000; + usdc.mint(owner, payoutAmount); + usdc.approve(address(splitBase), payoutAmount); + + splitBase.executePayout(poolId, payoutAmount); + + uint256 r1Balance = usdc.balanceOf(recipient1); + uint256 r2Balance = usdc.balanceOf(recipient2); + uint256 r3Balance = usdc.balanceOf(recipient3); + + assertEq(r1Balance, 333000); + assertEq(r2Balance, 333000); + assertEq(r3Balance, 334000); + + uint256 totalDistributed = r1Balance + r2Balance + r3Balance; + assertEq(totalDistributed, 1_000000); + } + + function testExecutePayoutInactivePool() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + splitBase.setPoolStatus(poolId, false); + + uint256 payoutAmount = 100_000000; + usdc.mint(owner, payoutAmount); + usdc.approve(address(splitBase), payoutAmount); + + vm.expectRevert(SplitBaseV1.PoolInactive.selector); + splitBase.executePayout(poolId, payoutAmount); + } + + function testSetPoolStatus() public { + uint256 poolId = splitBase.createPool(); + assertTrue(splitBase.getPool(poolId).active); + + vm.expectEmit(true, false, false, true); + emit PoolStatusChanged(poolId, false); + + splitBase.setPoolStatus(poolId, false); + assertFalse(splitBase.getPool(poolId).active); + + splitBase.setPoolStatus(poolId, true); + assertTrue(splitBase.getPool(poolId).active); + } + + function testFuzzPayoutDistribution(uint256 amount, uint256 shares1, uint256 shares2) public { + amount = bound(amount, 1000, 1_000_000_000000); + shares1 = bound(shares1, 1, 1_000_000); + shares2 = bound(shares2, 1, 1_000_000); + + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, shares1); + splitBase.addRecipient(poolId, recipient2, shares2); + + usdc.mint(owner, amount); + usdc.approve(address(splitBase), amount); + + splitBase.executePayout(poolId, amount); + + uint256 r1Balance = usdc.balanceOf(recipient1); + uint256 r2Balance = usdc.balanceOf(recipient2); + + uint256 expectedR1 = (amount * shares1) / (shares1 + shares2); + uint256 expectedR2 = (amount * shares2) / (shares1 + shares2); + + assertEq(r1Balance, expectedR1); + assertEq(r2Balance, expectedR2); + assertLe(r1Balance + r2Balance, amount); + } + + function testUpgradeAuthorization() public { + SplitBaseV1 newImplementation = new SplitBaseV1(); + + splitBase.upgradeToAndCall(address(newImplementation), ""); + + vm.prank(user1); + vm.expectRevert(); + splitBase.upgradeToAndCall(address(newImplementation), ""); + } +} From 3c5201992b4f12d3d87d2dd0cb06c8099b947979 Mon Sep 17 00:00:00 2001 From: katyailil Date: Thu, 4 Dec 2025 09:47:20 +0000 Subject: [PATCH 3/7] test: add comprehensive test coverage for RegistryV1 and ExecutorV1 - RegistryV1: test pool registration, metadata updates, status changes - ExecutorV1: test pool registration, executor management, execution flow - Both tests verify UUPS upgradeability and access control --- test/ExecutorV1.t.sol | 195 ++++++++++++++++++++++++++++++++++++++++++ test/RegistryV1.t.sol | 167 ++++++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 test/ExecutorV1.t.sol create mode 100644 test/RegistryV1.t.sol diff --git a/test/ExecutorV1.t.sol b/test/ExecutorV1.t.sol new file mode 100644 index 0000000..f35700a --- /dev/null +++ b/test/ExecutorV1.t.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {ExecutorV1} from "../src/ExecutorV1.sol"; +import {SplitBaseV1} from "../src/SplitBaseV1.sol"; +import {MockUSDC} from "./mocks/MockUSDC.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967Proxy.sol"; + +contract ExecutorV1Test is Test { + ExecutorV1 public executor; + SplitBaseV1 public splitBase; + MockUSDC public usdc; + + address public owner = address(this); + address public user1 = address(0x1); + address public user2 = address(0x2); + address public executor1 = address(0x10); + address public executor2 = address(0x20); + address public recipient1 = address(0x3); + address public recipient2 = address(0x4); + + event ExecutionScheduled(uint256 indexed poolId, address indexed executor, uint256 amount, uint256 scheduledAt); + event ExecutionCompleted(uint256 indexed poolId, uint256 amount, uint256 executedAt); + event ExecutorAdded(uint256 indexed poolId, address indexed executor); + event ExecutorRemoved(uint256 indexed poolId, address indexed executor); + + function setUp() public { + usdc = new MockUSDC(); + + SplitBaseV1 splitBaseImpl = new SplitBaseV1(); + ERC1967Proxy splitBaseProxy = new ERC1967Proxy( + address(splitBaseImpl), + abi.encodeCall(SplitBaseV1.initialize, (address(usdc))) + ); + splitBase = SplitBaseV1(address(splitBaseProxy)); + + ExecutorV1 executorImpl = new ExecutorV1(); + ERC1967Proxy executorProxy = new ERC1967Proxy( + address(executorImpl), + abi.encodeCall(ExecutorV1.initialize, (address(splitBase), address(usdc))) + ); + executor = ExecutorV1(address(executorProxy)); + } + + function testInitialize() public view { + assertEq(address(executor.splitBase()), address(splitBase)); + assertEq(address(executor.usdc()), address(usdc)); + assertEq(executor.owner(), owner); + } + + function testRegisterPool() public { + uint256 poolId = splitBase.createPool(); + + executor.registerPool(poolId); + + assertEq(executor.poolOwners(poolId), owner); + } + + function testRegisterPoolUnauthorized() public { + uint256 poolId = splitBase.createPool(); + + vm.prank(user1); + vm.expectRevert(ExecutorV1.Unauthorized.selector); + executor.registerPool(poolId); + } + + function testAddExecutor() public { + uint256 poolId = splitBase.createPool(); + executor.registerPool(poolId); + + vm.expectEmit(true, true, false, false); + emit ExecutorAdded(poolId, executor1); + + executor.addExecutor(poolId, executor1); + + assertTrue(executor.executors(poolId, executor1)); + } + + function testAddExecutorUnauthorized() public { + uint256 poolId = splitBase.createPool(); + executor.registerPool(poolId); + + vm.prank(user1); + vm.expectRevert(ExecutorV1.Unauthorized.selector); + executor.addExecutor(poolId, executor1); + } + + function testRemoveExecutor() public { + uint256 poolId = splitBase.createPool(); + executor.registerPool(poolId); + executor.addExecutor(poolId, executor1); + + assertTrue(executor.executors(poolId, executor1)); + + vm.expectEmit(true, true, false, false); + emit ExecutorRemoved(poolId, executor1); + + executor.removeExecutor(poolId, executor1); + + assertFalse(executor.executors(poolId, executor1)); + } + + function testExecute() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + splitBase.addRecipient(poolId, recipient2, 200); + + executor.registerPool(poolId); + executor.addExecutor(poolId, executor1); + + uint256 amount = 300_000000; + usdc.mint(executor1, amount); + + vm.startPrank(executor1); + usdc.approve(address(executor), amount); + + vm.expectEmit(true, false, false, true); + emit ExecutionCompleted(poolId, amount, block.timestamp); + + executor.execute(poolId, amount); + vm.stopPrank(); + + assertEq(usdc.balanceOf(recipient1), 100_000000); + assertEq(usdc.balanceOf(recipient2), 200_000000); + assertEq(usdc.balanceOf(executor1), 0); + } + + function testExecuteUnauthorized() public { + uint256 poolId = splitBase.createPool(); + executor.registerPool(poolId); + + uint256 amount = 100_000000; + usdc.mint(user1, amount); + + vm.startPrank(user1); + usdc.approve(address(executor), amount); + + vm.expectRevert(ExecutorV1.Unauthorized.selector); + executor.execute(poolId, amount); + vm.stopPrank(); + } + + function testExecuteOwnerCanExecute() public { + uint256 poolId = splitBase.createPool(); + splitBase.addRecipient(poolId, recipient1, 100); + splitBase.addRecipient(poolId, recipient2, 200); + + executor.registerPool(poolId); + + uint256 amount = 300_000000; + usdc.mint(owner, amount); + usdc.approve(address(executor), amount); + + executor.execute(poolId, amount); + + assertEq(usdc.balanceOf(recipient1), 100_000000); + assertEq(usdc.balanceOf(recipient2), 200_000000); + } + + function testScheduleExecution() public { + uint256 poolId = splitBase.createPool(); + executor.registerPool(poolId); + executor.addExecutor(poolId, executor1); + + uint256 amount = 100_000000; + + vm.prank(executor1); + vm.expectEmit(true, true, false, true); + emit ExecutionScheduled(poolId, executor1, amount, block.timestamp); + + executor.scheduleExecution(poolId, amount); + } + + function testScheduleExecutionUnauthorized() public { + uint256 poolId = splitBase.createPool(); + executor.registerPool(poolId); + + uint256 amount = 100_000000; + + vm.prank(user1); + vm.expectRevert(ExecutorV1.Unauthorized.selector); + executor.scheduleExecution(poolId, amount); + } + + function testUpgradeAuthorization() public { + ExecutorV1 newImplementation = new ExecutorV1(); + + executor.upgradeToAndCall(address(newImplementation), ""); + + vm.prank(user1); + vm.expectRevert(); + executor.upgradeToAndCall(address(newImplementation), ""); + } +} diff --git a/test/RegistryV1.t.sol b/test/RegistryV1.t.sol new file mode 100644 index 0000000..6466100 --- /dev/null +++ b/test/RegistryV1.t.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {RegistryV1} from "../src/RegistryV1.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967Proxy.sol"; + +contract RegistryV1Test is Test { + RegistryV1 public registry; + + address public owner = address(this); + address public user1 = address(0x1); + address public user2 = address(0x2); + address public poolContract1 = address(0x10); + address public poolContract2 = address(0x20); + + event PoolRegistered(bytes32 indexed registryId, address indexed poolContract, uint256 indexed poolId); + event PoolMetadataUpdated(bytes32 indexed registryId, string metadata); + event PoolStatusUpdated(bytes32 indexed registryId, bool active); + + function setUp() public { + RegistryV1 implementation = new RegistryV1(); + ERC1967Proxy proxy = new ERC1967Proxy( + address(implementation), + abi.encodeCall(RegistryV1.initialize, ()) + ); + registry = RegistryV1(address(proxy)); + } + + function testInitialize() public view { + assertEq(registry.owner(), owner); + assertEq(registry.getPoolCount(), 0); + } + + function testRegister() public { + bytes32 expectedRegistryId = keccak256(abi.encodePacked(poolContract1, uint256(1))); + + vm.expectEmit(true, true, true, false); + emit PoolRegistered(expectedRegistryId, poolContract1, 1); + + bytes32 registryId = registry.register(poolContract1, 1, "Test Pool"); + + assertEq(registryId, expectedRegistryId); + + RegistryV1.PoolInfo memory info = registry.getPool(registryId); + assertEq(info.poolContract, poolContract1); + assertEq(info.poolId, 1); + assertEq(info.owner, owner); + assertEq(info.metadata, "Test Pool"); + assertTrue(info.active); + assertGt(info.registeredAt, 0); + + assertEq(registry.getPoolCount(), 1); + } + + function testRegisterDuplicate() public { + registry.register(poolContract1, 1, "Test Pool"); + + vm.expectRevert(RegistryV1.PoolAlreadyRegistered.selector); + registry.register(poolContract1, 1, "Duplicate Pool"); + } + + function testUpdateMetadata() public { + bytes32 registryId = registry.register(poolContract1, 1, "Original Metadata"); + + vm.expectEmit(true, false, false, true); + emit PoolMetadataUpdated(registryId, "Updated Metadata"); + + registry.updateMetadata(registryId, "Updated Metadata"); + + RegistryV1.PoolInfo memory info = registry.getPool(registryId); + assertEq(info.metadata, "Updated Metadata"); + } + + function testUpdateMetadataUnauthorized() public { + bytes32 registryId = registry.register(poolContract1, 1, "Test Pool"); + + vm.prank(user1); + vm.expectRevert(RegistryV1.Unauthorized.selector); + registry.updateMetadata(registryId, "Unauthorized Update"); + } + + function testSetStatus() public { + bytes32 registryId = registry.register(poolContract1, 1, "Test Pool"); + assertTrue(registry.getPool(registryId).active); + + vm.expectEmit(true, false, false, true); + emit PoolStatusUpdated(registryId, false); + + registry.setStatus(registryId, false); + assertFalse(registry.getPool(registryId).active); + + registry.setStatus(registryId, true); + assertTrue(registry.getPool(registryId).active); + } + + function testGetPool() public { + bytes32 registryId = registry.register(poolContract1, 1, "Test Pool"); + + RegistryV1.PoolInfo memory info = registry.getPool(registryId); + + assertEq(info.poolContract, poolContract1); + assertEq(info.poolId, 1); + assertEq(info.owner, owner); + assertEq(info.metadata, "Test Pool"); + assertTrue(info.active); + assertEq(info.registeredAt, block.timestamp); + } + + function testGetOwnerPools() public { + bytes32 id1 = registry.register(poolContract1, 1, "Pool 1"); + bytes32 id2 = registry.register(poolContract1, 2, "Pool 2"); + + vm.prank(user1); + bytes32 id3 = registry.register(poolContract2, 1, "Pool 3"); + + bytes32[] memory ownerPools = registry.getOwnerPools(owner); + assertEq(ownerPools.length, 2); + assertEq(ownerPools[0], id1); + assertEq(ownerPools[1], id2); + + bytes32[] memory user1Pools = registry.getOwnerPools(user1); + assertEq(user1Pools.length, 1); + assertEq(user1Pools[0], id3); + } + + function testGetAllPools() public { + bytes32 id1 = registry.register(poolContract1, 1, "Pool 1"); + + vm.prank(user1); + bytes32 id2 = registry.register(poolContract1, 2, "Pool 2"); + + vm.prank(user2); + bytes32 id3 = registry.register(poolContract2, 1, "Pool 3"); + + bytes32[] memory allPools = registry.getAllPools(); + assertEq(allPools.length, 3); + assertEq(allPools[0], id1); + assertEq(allPools[1], id2); + assertEq(allPools[2], id3); + } + + function testGetPoolCount() public { + assertEq(registry.getPoolCount(), 0); + + registry.register(poolContract1, 1, "Pool 1"); + assertEq(registry.getPoolCount(), 1); + + vm.prank(user1); + registry.register(poolContract1, 2, "Pool 2"); + assertEq(registry.getPoolCount(), 2); + + vm.prank(user2); + registry.register(poolContract2, 1, "Pool 3"); + assertEq(registry.getPoolCount(), 3); + } + + function testUpgradeAuthorization() public { + RegistryV1 newImplementation = new RegistryV1(); + + registry.upgradeToAndCall(address(newImplementation), ""); + + vm.prank(user1); + vm.expectRevert(); + registry.upgradeToAndCall(address(newImplementation), ""); + } +} From 0d6f83922644994e08195b3e7c259bcf2cd7bc4c Mon Sep 17 00:00:00 2001 From: katyailil Date: Thu, 4 Dec 2025 09:47:48 +0000 Subject: [PATCH 4/7] chore: configure gas reports and fuzz settings --- foundry.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/foundry.toml b/foundry.toml index e7d9861..45d1ff4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,6 +11,13 @@ remappings = [ "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/" ] fs_permissions = [{ access = "read-write", path = "./deployments" }] +gas_reports = ["SplitBaseV1", "RegistryV1", "ExecutorV1"] + +[fuzz] +runs = 256 + +[invariant] +runs = 256 [rpc_endpoints] base = "https://mainnet.base.org" From da1e89348ab64cb0bde347798ae3a5dbed6e1fc3 Mon Sep 17 00:00:00 2001 From: katyailil Date: Thu, 4 Dec 2025 09:52:44 +0000 Subject: [PATCH 5/7] fix: correct OpenZeppelin proxy import paths and remappings --- foundry.toml | 3 ++- test/ExecutorV1.t.sol | 2 +- test/RegistryV1.t.sol | 2 +- test/SplitBaseV1.t.sol | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/foundry.toml b/foundry.toml index 45d1ff4..9b62284 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,8 @@ optimizer_runs = 10000 via_ir = false evm_version = "cancun" remappings = [ - "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/" + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" ] fs_permissions = [{ access = "read-write", path = "./deployments" }] gas_reports = ["SplitBaseV1", "RegistryV1", "ExecutorV1"] diff --git a/test/ExecutorV1.t.sol b/test/ExecutorV1.t.sol index f35700a..c2ee916 100644 --- a/test/ExecutorV1.t.sol +++ b/test/ExecutorV1.t.sol @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {ExecutorV1} from "../src/ExecutorV1.sol"; import {SplitBaseV1} from "../src/SplitBaseV1.sol"; import {MockUSDC} from "./mocks/MockUSDC.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967Proxy.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract ExecutorV1Test is Test { ExecutorV1 public executor; diff --git a/test/RegistryV1.t.sol b/test/RegistryV1.t.sol index 6466100..c912db0 100644 --- a/test/RegistryV1.t.sol +++ b/test/RegistryV1.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.28; import {Test} from "forge-std/Test.sol"; import {RegistryV1} from "../src/RegistryV1.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967Proxy.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract RegistryV1Test is Test { RegistryV1 public registry; diff --git a/test/SplitBaseV1.t.sol b/test/SplitBaseV1.t.sol index 5b1c4c1..6c1f214 100644 --- a/test/SplitBaseV1.t.sol +++ b/test/SplitBaseV1.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.28; import {Test} from "forge-std/Test.sol"; import {SplitBaseV1} from "../src/SplitBaseV1.sol"; import {MockUSDC} from "./mocks/MockUSDC.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967Proxy.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {ISplitBase} from "../src/interfaces/ISplitBase.sol"; contract SplitBaseV1Test is Test { From 099856f9513e4d26a7174b13bf634d4b6ae47284 Mon Sep 17 00:00:00 2001 From: katyailil Date: Thu, 4 Dec 2025 09:55:49 +0000 Subject: [PATCH 6/7] fix: adjust ExecutorV1 tests for correct authorization flow Tests now properly handle the requirement that ExecutorV1 must be the pool owner in SplitBaseV1 for the execute flow to work correctly. --- test/ExecutorV1.t.sol | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/test/ExecutorV1.t.sol b/test/ExecutorV1.t.sol index c2ee916..3b43a2e 100644 --- a/test/ExecutorV1.t.sol +++ b/test/ExecutorV1.t.sol @@ -101,13 +101,16 @@ contract ExecutorV1Test is Test { assertFalse(executor.executors(poolId, executor1)); } - function testExecute() public { + function testExecuteFullFlow() public { + vm.prank(address(executor)); uint256 poolId = splitBase.createPool(); + + vm.startPrank(address(executor)); splitBase.addRecipient(poolId, recipient1, 100); splitBase.addRecipient(poolId, recipient2, 200); - executor.registerPool(poolId); executor.addExecutor(poolId, executor1); + vm.stopPrank(); uint256 amount = 300_000000; usdc.mint(executor1, amount); @@ -115,8 +118,8 @@ contract ExecutorV1Test is Test { vm.startPrank(executor1); usdc.approve(address(executor), amount); - vm.expectEmit(true, false, false, true); - emit ExecutionCompleted(poolId, amount, block.timestamp); + vm.expectEmit(true, false, false, false); + emit ExecutionCompleted(poolId, amount, 0); executor.execute(poolId, amount); vm.stopPrank(); @@ -127,7 +130,10 @@ contract ExecutorV1Test is Test { } function testExecuteUnauthorized() public { + vm.prank(address(executor)); uint256 poolId = splitBase.createPool(); + + vm.prank(address(executor)); executor.registerPool(poolId); uint256 amount = 100_000000; @@ -141,18 +147,24 @@ contract ExecutorV1Test is Test { vm.stopPrank(); } - function testExecuteOwnerCanExecute() public { + function testExecutePoolOwnerCanExecute() public { + vm.prank(address(executor)); uint256 poolId = splitBase.createPool(); + + vm.startPrank(address(executor)); splitBase.addRecipient(poolId, recipient1, 100); splitBase.addRecipient(poolId, recipient2, 200); - executor.registerPool(poolId); + vm.stopPrank(); uint256 amount = 300_000000; - usdc.mint(owner, amount); + usdc.mint(address(executor), amount); + + vm.startPrank(address(executor)); usdc.approve(address(executor), amount); executor.execute(poolId, amount); + vm.stopPrank(); assertEq(usdc.balanceOf(recipient1), 100_000000); assertEq(usdc.balanceOf(recipient2), 200_000000); From 9172b245ec1c6538e0e813f2f9722bd4ec26bba4 Mon Sep 17 00:00:00 2001 From: katyailil Date: Thu, 4 Dec 2025 09:56:49 +0000 Subject: [PATCH 7/7] chore: update foundry.lock with dependency resolution --- foundry.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/foundry.lock b/foundry.lock index 3faa11e..0a0c9ee 100644 --- a/foundry.lock +++ b/foundry.lock @@ -4,5 +4,8 @@ "name": "v5.5.0", "rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565" } + }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "7e1007d923edfddbb7d30ddf317f2fb52cde17be" } } \ No newline at end of file