Mock stablecoin contracts and a small ethers v6 helper layer so integration tests can exercise real-world quirks (USDT’s missing return value, USDC blacklisting, fee-on-transfer, proxies, pausing, permit, and so on) without hitting mainnet.
What this is: a local / CI testing toolkit.
What this is not: audited custody, issuance, or anything you would ship to a production chain as “the” stablecoin.
- Node.js ≥ 18
- Hardhat (this repo uses Hardhat for compile + tests)
- ethers v6 (peer to how you use Hardhat in your project)
Deploy helpers expect a Signer with a provider (e.g. await ethers.getSigner()). A bare provider cannot deploy.
cd stablecoinkit
npm install
npm run compile # builds Solidity artifacts under artifacts/
npm run build # optional: TypeScript → dist/
npm testdeployStablecoins reads compiled artifacts from artifacts/. If you see “Artifact not found”, run npm run compile first.
- Add this package to your workspace (git submodule, monorepo path, or copy), or publish it and depend on it like any npm package.
- Run
npm run compileinstablecoinkit(or ensureartifacts/is present). - From your Hardhat tests or scripts, import from the built output or source (depending on how you wire TypeScript).
Minimal pattern:
import { deployStablecoins, createFaucet, snapshot } from "stablecoinkit"; // or "../path/to/stablecoinkit/src"
const signer = await ethers.getSigner();
const coins = await deployStablecoins(signer, ["usdc", "usdt", "dai"], {
owner: await signer.getAddress(),
seedOwnerAmountHuman: "1_000_000", // optional: seed owner after deploy
// usdcUseProxy: true, // optional: USDC behind ERC1967 proxy
});
await coins.usdc!.mint(alice.address, "5000"); // human amount; 6 decimals handled inside
await coins.dai!.mint(alice.address, "5000"); // 18 decimals
await coins.usdc!.blacklist(badActor.address);
await coins.usdt!.setTransferFee(50); // basis points (0.5%)
await coins.usdt!.pause();
const faucet = createFaucet(coins, { amount: "10_000" });
await faucet.drip([wallet1, wallet2]);
const snap = await snapshot(ethers.provider);
if (snap.supported) {
// ... tests ...
await snap.restore();
}| Option | Purpose |
|---|---|
owner |
Receives initial role/supply semantics per mock; defaults to deployer. |
seedOwnerAmountHuman |
After deploy, mint this display amount (per token, per its decimals) to owner. |
usdcUseProxy |
Deploy USDC mock behind an ERC1967 proxy (MockUSDCUpgradeable). |
| Kind | Decimals | Notes (high level) |
|---|---|---|
usdc |
6 | Blacklist, pause, permit; optional proxy deployment. |
usdt |
6 | No bool return on transfer, fee bps, pause. |
dai |
18 | Permit-style flows, drip-style helpers per contract. |
frax |
18 | Collateral ratio style behavior (see MockFRAX). |
lusd |
18 | Liquity-style mock (tests include drip). |
busd |
18 | Pausable + permit-related surface (see MockBUSD). |
Feature flags and metadata live in adapters in src/types.ts (StablecoinAdapter, StablecoinFeature).
From src/index.ts:
- Deploy / faucet:
deployStablecoins,createFaucet, typesDeployStablecoinsOptions,FaucetOptions - Registry:
TokenRegistry,StablecoinSet - Adapters:
TestToken,USDCoin,USDTcoin,DAIcoin,FRAXcoin,LUSDcoin,BUSDcoin - Types:
adaptersmap,StablecoinKind,StablecoinFeature,StablecoinAdapter - Units:
parseHumanAmount - Snapshot:
snapshot,SnapshotProvider(re-exported asnetworkas well)
stablecoinkit/
├── contracts/ # MockUSDC, MockUSDT, MockDAI, MockFRAX, MockLUSD, MockBUSD, upgrades, etc.
├── src/
│ ├── factory.ts # deployStablecoins, createFaucet
│ ├── registry.ts # TokenRegistry, StablecoinSet
│ ├── types.ts # kinds, features, adapter table
│ ├── adapters/ # per-token wrappers on TestToken
│ ├── network/ # snapshot re-exports
│ └── utils/ # units, snapshot (evm_snapshot / evm_revert)
├── test/
└── dist/ # after npm run build
Generic ERC20Mock tests miss failures that only show up with:
- USDT:
transferwithout a bool return (SafeERC20 assumptions). - USDC: blacklist or pause reverting mid-flow.
- Decimals: 6 vs 18 mixed in one flow.
This kit makes those paths reproducible in your environment before mainnet.