From 1f4175ba729fbbab58538cd3ac02b5459e014c86 Mon Sep 17 00:00:00 2001 From: golem Date: Mon, 17 Aug 2026 16:22:30 -0600 Subject: [PATCH] feat: add valueless mock USDC --- src/MockUSDC.sol | 18 ++++++++++++++ test/MockUSDC.t.sol | 60 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/MockUSDC.sol create mode 100644 test/MockUSDC.t.sol diff --git a/src/MockUSDC.sol b/src/MockUSDC.sol new file mode 100644 index 0000000..96ca297 --- /dev/null +++ b/src/MockUSDC.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.35; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @notice Educational mock token with no monetary value. Never use as real USDC. +contract MockUSDC is ERC20, Ownable { + constructor(address initialOwner) ERC20("Mock USD Coin", "mUSDC") Ownable(initialOwner) {} + + function decimals() public pure override returns (uint8) { + return 6; + } + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } +} diff --git a/test/MockUSDC.t.sol b/test/MockUSDC.t.sol new file mode 100644 index 0000000..028c1a0 --- /dev/null +++ b/test/MockUSDC.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.35; + +import {Test} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {MockUSDC} from "../src/MockUSDC.sol"; + +contract MockUSDCTest is Test { + MockUSDC internal token; + address internal constant STRANGER = address(0xBEEF); + address internal constant RECIPIENT = address(0xCAFE); + address internal constant SPENDER = address(0xD00D); + + function setUp() public { + token = new MockUSDC(address(this)); + } + + function testMetadataUsesSixDecimalMockUSDC() public view { + assertEq(token.name(), "Mock USD Coin"); + assertEq(token.symbol(), "mUSDC"); + assertEq(token.decimals(), 6); + } + + function testInitialSupplyIsZero() public view { + assertEq(token.totalSupply(), 0); + } + + function testMintMintsToRecipientWhenCalledByOwner() public { + token.mint(RECIPIENT, 1_250_000); + + assertEq(token.totalSupply(), 1_250_000); + assertEq(token.balanceOf(RECIPIENT), 1_250_000); + } + + function testMintRevertsWhenCalledByNonOwner() public { + vm.prank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, STRANGER)); + token.mint(RECIPIENT, 1); + } + + function testTransferMovesMintedBalance() public { + token.mint(address(this), 1_250_000); + + token.transfer(RECIPIENT, 250_000); + + assertEq(token.balanceOf(address(this)), 1_000_000); + assertEq(token.balanceOf(RECIPIENT), 250_000); + } + + function testApproveSetsAllowanceForSpender() public { + token.approve(SPENDER, 750_000); + + assertEq(token.allowance(address(this), SPENDER), 750_000); + } + + function testConstructorRevertsForZeroInitialOwner() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0))); + new MockUSDC(address(0)); + } +}