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 diff --git a/foundry.toml b/foundry.toml index e7d9861..9b62284 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,9 +8,17 @@ 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"] + +[fuzz] +runs = 256 + +[invariant] +runs = 256 [rpc_endpoints] base = "https://mainnet.base.org" diff --git a/test/ExecutorV1.t.sol b/test/ExecutorV1.t.sol new file mode 100644 index 0000000..3b43a2e --- /dev/null +++ b/test/ExecutorV1.t.sol @@ -0,0 +1,207 @@ +// 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/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 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); + + vm.startPrank(executor1); + usdc.approve(address(executor), amount); + + vm.expectEmit(true, false, false, false); + emit ExecutionCompleted(poolId, amount, 0); + + 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 { + vm.prank(address(executor)); + uint256 poolId = splitBase.createPool(); + + vm.prank(address(executor)); + 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 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(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); + } + + 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..c912db0 --- /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/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), ""); + } +} diff --git a/test/SplitBaseV1.t.sol b/test/SplitBaseV1.t.sol new file mode 100644 index 0000000..6c1f214 --- /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/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), ""); + } +} 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; + } +}