From 4f7977c3fdb32da4e743e2b2532fffd3f2076338 Mon Sep 17 00:00:00 2001 From: Joshua Date: Wed, 18 Feb 2026 13:13:44 -0800 Subject: [PATCH] Lock membership defaults to USDC 100 and add ERC20 settlement tests --- .env.example | 6 ++-- README.md | 4 +-- contracts/MockERC20.sol | 47 ++++++++++++++++++++++++++ deploy/membership-deploy.template.json | 5 ++- test/EdutHumanMembership.test.js | 40 ++++++++++++++++++++++ 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 contracts/MockERC20.sol diff --git a/.env.example b/.env.example index 28dae5b..bcbe8d6 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,9 @@ BASE_MAINNET_RPC_URL=https://mainnet.base.org TREASURY_WALLET=0x0000000000000000000000000000000000000000 -# Use zero address for native ETH pricing. -MINT_CURRENCY_ADDRESS=0x0000000000000000000000000000000000000000 -MINT_AMOUNT_ATOMIC=5000000000000000 +# Base Sepolia USDC by default (override per network as needed). +MINT_CURRENCY_ADDRESS=0x036CbD53842c5426634e7929541eC2318f3dCf7e +MINT_AMOUNT_ATOMIC=100000000 # Optional: write deployment metadata JSON (address, tx hash, params). # DEPLOY_OUTPUT_PATH=./deploy/membership-deploy.sepolia.json diff --git a/README.md b/README.md index e1e0c0e..bf1311f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Features: 1. One-time soulbound human token mint. 2. Sponsor mint support (`mintMembership(recipient)` can be paid by inviter/company wallet). -3. Owner-configurable flat mint price (`updateMintPrice`), launch default can stay flat `$5` equivalent. +3. Owner-configurable flat mint price (`updateMintPrice`), launch default is fixed `100 USDC` (6 decimals). 4. Membership status lifecycle (`ACTIVE/SUSPENDED/REVOKED`) for runtime gates. 5. Treasury address control for settlement routing. @@ -40,7 +40,7 @@ Copy `.env.example` values into your shell/session before deploy: 1. `DEPLOYER_PRIVATE_KEY` 2. `BASE_SEPOLIA_RPC_URL` / `BASE_MAINNET_RPC_URL` 3. `TREASURY_WALLET` -4. `MINT_CURRENCY_ADDRESS` (`0x000...000` for ETH) +4. `MINT_CURRENCY_ADDRESS` (USDC token contract on target chain) 5. `MINT_AMOUNT_ATOMIC` 6. `DEPLOY_OUTPUT_PATH` (optional) diff --git a/contracts/MockERC20.sol b/contracts/MockERC20.sol new file mode 100644 index 0000000..204266b --- /dev/null +++ b/contracts/MockERC20.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract MockERC20 { + string public name; + string public symbol; + uint8 public immutable decimals; + + 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); + + constructor(string memory name_, string memory symbol_, uint8 decimals_) { + name = name_; + symbol = symbol_; + decimals = decimals_; + } + + function mint(address to, uint256 value) external { + balanceOf[to] += value; + emit Transfer(address(0), to, value); + } + + function approve(address spender, uint256 value) external returns (bool) { + allowance[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); + return true; + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed < value) { + return false; + } + uint256 bal = balanceOf[from]; + if (bal < value) { + return false; + } + allowance[from][msg.sender] = allowed - value; + balanceOf[from] = bal - value; + balanceOf[to] += value; + emit Transfer(from, to, value); + return true; + } +} diff --git a/deploy/membership-deploy.template.json b/deploy/membership-deploy.template.json index 972e9bb..5d27ee4 100644 --- a/deploy/membership-deploy.template.json +++ b/deploy/membership-deploy.template.json @@ -3,11 +3,10 @@ "chain_id": 84532, "deployer": "0x0000000000000000000000000000000000000000", "treasury_wallet": "0x0000000000000000000000000000000000000000", - "mint_currency_address": "0x0000000000000000000000000000000000000000", - "mint_amount_atomic": "5000000000000000", + "mint_currency_address": "0x036CbD53842c5426634e7929541eC2318f3dCf7e", + "mint_amount_atomic": "100000000", "membership_contract": "0x0000000000000000000000000000000000000000", "deployment_tx_hash": "0x", "deployed_at": "2026-02-18T00:00:00Z", "notes": "Fill from deployment output and keep under change control." } - diff --git a/test/EdutHumanMembership.test.js b/test/EdutHumanMembership.test.js index b5228d4..f9c687c 100644 --- a/test/EdutHumanMembership.test.js +++ b/test/EdutHumanMembership.test.js @@ -78,4 +78,44 @@ describe("EdutHumanMembership", function () { expect(await contract.membershipStatus(recipient.address)).to.equal(2); }); + + it("supports ERC20 settlement path for USDC-style pricing", async function () { + const [deployer, treasury, sponsor, recipient] = await ethers.getSigners(); + const tokenFactory = await ethers.getContractFactory("MockERC20", deployer); + const usdc = await tokenFactory.deploy("Mock USDC", "USDC", 6); + await usdc.deployed(); + + const membershipFactory = await ethers.getContractFactory("EdutHumanMembership", deployer); + const usdcPrice = ethers.BigNumber.from("100000000"); // 100 USDC with 6 decimals + const contract = await membershipFactory.deploy(treasury.address, usdc.address, usdcPrice); + await contract.deployed(); + + await (await usdc.mint(sponsor.address, usdcPrice)).wait(); + await (await usdc.connect(sponsor).approve(contract.address, usdcPrice)).wait(); + + const before = await usdc.balanceOf(treasury.address); + await (await contract.connect(sponsor).mintMembership(recipient.address)).wait(); + const after = await usdc.balanceOf(treasury.address); + + expect(after.sub(before).eq(usdcPrice)).to.equal(true); + expect(await contract.hasMembership(recipient.address)).to.equal(true); + }); + + it("fails ERC20 mint without allowance", async function () { + const [deployer, treasury, sponsor, recipient] = await ethers.getSigners(); + const tokenFactory = await ethers.getContractFactory("MockERC20", deployer); + const usdc = await tokenFactory.deploy("Mock USDC", "USDC", 6); + await usdc.deployed(); + + const membershipFactory = await ethers.getContractFactory("EdutHumanMembership", deployer); + const usdcPrice = ethers.BigNumber.from("100000000"); + const contract = await membershipFactory.deploy(treasury.address, usdc.address, usdcPrice); + await contract.deployed(); + + await (await usdc.mint(sponsor.address, usdcPrice)).wait(); + await expectRevertWithCustomError( + contract.connect(sponsor).mintMembership(recipient.address), + "PaymentTransferFailed" + ); + }); });