Lock membership defaults to USDC 100 and add ERC20 settlement tests

This commit is contained in:
Joshua 2026-02-18 13:13:44 -08:00
parent 0718198924
commit 4f7977c3fd
5 changed files with 94 additions and 8 deletions

View File

@ -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

View File

@ -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)

47
contracts/MockERC20.sol Normal file
View File

@ -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;
}
}

View File

@ -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."
}

View File

@ -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"
);
});
});