Section 01 · Introduction
Why stablecoins are the most important ERC20 use case
More than 80 percent of stablecoin supply lives on ERC20 contracts. The standard does the heavy lifting; the interesting engineering is in the extensions.
Quick answer
How do stablecoins use ERC20? A stablecoin is an ERC20 token whose price is engineered to stay near a target — usually one US dollar. The ERC20 interface gives every stablecoin the same transfer, approve, and balanceOf surface. The peg logic lives outside the interface: fiat backed coins like USDC mint and burn against bank reserves, while DAI mints against on chain collateral inside MakerDAO vaults.
Open the contract for USDC, USDT, or DAI on Etherscan and the first thing you notice is how much code sits on top of the ERC20 interface. The plain standard is roughly 80 lines. The real production contracts run into the thousands.
That extra surface is where the engineering decisions live. Who can mint. Who can burn. Who can freeze a balance. Whether the contract can be paused or upgraded. Whether transfers go through a fee hook. Each of those decisions shapes how the stablecoin behaves under stress.
If you are integrating a stablecoin into a payment rail, a lending market, or an on chain settlement system, the brand on the token is almost beside the point. What matters is the contract surface you are agreeing to call into. This post walks through three representative stablecoins — USDC, USDT, and DAI — and shows the exact ERC20 extensions each one ships.
The ERC20 surface area is the boring part
Every stablecoin implements transfer, approve, transferFrom, balanceOf, and totalSupply the same way. The interesting differences are in mint, burn, blacklist, pause, and upgrade. If your integration only reads ERC20 functions you are leaving real operational risk uncovered.
Section 02 · Mint and Burn
How fiat backed stablecoins create supply
USDC and USDT both follow the same shape: a centralized issuer mints when a bank wire confirms, burns when a user redeems back to fiat.
When a corporate treasury wires one million dollars to Circle, three things happen. The fiat lands in a partner bank. Circle confirms the reserves. A minter wallet calls the mint function on the FiatTokenV2 contract, which adds one million USDC to the recipient address and bumps total supply by the same amount.
The redeem path runs in reverse. The user transfers USDC back to Circle. Circle calls burn on the contract, which subtracts the balance and reduces total supply. The bank releases fiat to the user. The on chain supply tracks the off chain reserve dollar for dollar.
The mint function itself is a thin wrapper around the ERC20 internal balance update. The interesting code is the access control and the minter allowance system that prevents any single key from inflating supply unilaterally.
// Simplified mint flow used by USDC (FiatTokenV2_2)
function mint(address _to, uint256 _amount)
external
whenNotPaused
onlyMinters
notBlacklisted(msg.sender)
notBlacklisted(_to)
returns (bool)
{
require(_to != address(0), "ERC20: mint to the zero address");
require(_amount > 0, "FiatToken: mint amount not greater than 0");
uint256 mintingAllowedAmount = minterAllowed[msg.sender];
require(_amount <= mintingAllowedAmount, "FiatToken: mint amount exceeds minterAllowance");
totalSupply_ = totalSupply_ + _amount;
_setBalance(_to, _balanceOf(_to) + _amount);
minterAllowed[msg.sender] = mintingAllowedAmount - _amount;
emit Mint(msg.sender, _to, _amount);
emit Transfer(address(0), _to, _amount);
return true;
}The minterAllowed mapping is what keeps the system safe from a compromised minter key. Circle assigns each minter wallet a finite allowance. The wallet can mint up to that allowance and then needs to be topped up by a higher privileged controller. A leaked minter key can drain its current allowance but not infinitely inflate supply.
USDT's mint is even more centralized
Tether's contract uses a single owner wallet for minting, no per minter allowance system. Operationally it has been fine. Architecturally it is a single point of failure. If you are designing a stablecoin yourself, the USDC minter allowance pattern is the better starting point.
Section 03 · Blacklist and Pause
The hooks that can freeze your integration
Every production fiat backed stablecoin ships a blacklist function and a pause switch. These are the two surfaces most integrators forget to model.
Regulators expect stablecoin issuers to comply with sanctions lists. The way that compliance ships in code is a blacklist mapping plus a modifier injected into the transfer path. Once an address is blacklisted, every transfer touching it reverts.
// Address freeze hook — every transfer routes through this
modifier notBlacklisted(address _account) {
require(!_blacklisted[_account], "FiatToken: account is blacklisted");
_;
}
function blacklist(address _account) external onlyBlacklister {
_blacklisted[_account] = true;
emit Blacklisted(_account);
}
// Override of ERC20 transfer adds the gate at the contract layer
function _transfer(address from, address to, uint256 value)
internal
override
notBlacklisted(from)
notBlacklisted(to)
{
super._transfer(from, to, value);
}That modifier sits on transfer, transferFrom, mint, and burn. So if your protocol holds USDC on behalf of a user and either the sender or the receiver gets blacklisted between transactions, the funds sit frozen. You cannot move them. You cannot return them. The only path forward is the issuer removing the blacklist entry.
The pause function is more dramatic. It freezes every transfer on the entire contract until a controller wallet unpauses it. USDC was paused once in production, during the bank stress event that briefly broke the peg. Lending markets that had not modeled a paused USDC behaved unpredictably for a few hours.
Model the freeze path in your integration tests
Before you ship a contract that holds another party's stablecoin, write a test that simulates a blacklist or pause and confirms your contract degrades gracefully. The cheapest time to find a stuck balance is in CI, not on mainnet.
Read the controller wallet hierarchy
Etherscan exposes the minter, blacklister, pauser, and owner roles for every major stablecoin. Knowing which multisig holds which role tells you the realistic blast radius of an operational mistake at the issuer.
Document the upgrade proxy
USDC sits behind a transparent upgradeable proxy. The implementation has been upgraded multiple times. Your security review needs to track the upgrade trail, not just the current implementation.
Section 04 · DAI Architecture
How DAI mints on chain without a bank
DAI is also an ERC20 token. The difference is what backs it. Instead of a bank deposit, every DAI in circulation maps to overcollateralized crypto inside a MakerDAO vault.
A user opens a vault, deposits ETH or another approved collateral, and draws DAI against it up to a collateralization ratio of around 150 percent. The DAI token itself is dead simple — it is the surrounding system that does the work.
The Vat is the central accounting ledger. Urns are individual vaults. Ilks are the collateral types. Adapters called Joins move ERC20 tokens in and out of the Vat. The DaiJoin adapter is the one piece that mints and burns DAI on the ERC20 contract proper.
// MakerDAO Vault open + draw (very simplified, real flow uses CDP Manager)
function openVault(bytes32 ilk, uint256 wadCollateral, uint256 wadDai) external {
// 1. Deposit collateral into the GemJoin adapter
gemJoin.join(address(this), wadCollateral);
// 2. Adjust the urn (vault) inside the Vat: add collateral, draw debt
vat.frob(
ilk,
address(this), // urn
address(this), // collateral source
address(this), // dai destination
int256(wadCollateral),
int256(wadDai)
);
// 3. Pull DAI out through the DaiJoin adapter
daiJoin.exit(msg.sender, wadDai);
}When a user repays DAI plus the stability fee, the flow reverses. The DaiJoin adapter burns the returned DAI, the Vat reduces the urn debt, and the GemJoin releases the collateral. If the collateral price drops below the safety threshold, keepers liquidate the vault by buying the collateral at a discount in exchange for the DAI debt.
DAI is the only one with no off chain step
Both USDC and USDT depend on a bank confirming a deposit before new supply is minted. DAI never leaves the chain. That is its strongest property and its biggest constraint at scale — DAI supply is gated by how much crypto collateral users are willing to lock.
Section 05 · Comparison
USDC, USDT, and DAI side by side
The interface is the same. The risk profile, the operational hooks, and the contract complexity are all different.
| Property | USDC | USDT | DAI |
|---|---|---|---|
| Backing | US dollar bank reserves | Mixed reserves, attested | Crypto collateral in Maker vaults |
| Mint authority | Per minter allowance system | Single owner wallet | DaiJoin adapter, triggered by vaults |
| Burn authority | Per minter | Owner | DaiJoin adapter on repay |
| Blacklist | Yes, per address | Yes, per address | No |
| Pause switch | Yes, contract wide | Yes, contract wide | No global pause |
| Upgrade pattern | Transparent proxy | Direct address swap on chain config | Immutable token, upgrades happen in the surrounding system |
| Decimal places | 6 | 6 | 18 |
| Audited complexity | Higher — proxy plus implementation | Medium — single file | High — full Maker stack |
For a payment rail, the lower complexity of USDT is appealing until you remember the single owner key. For a programmable money primitive that needs no off chain trust, DAI is the only candidate. For broad merchant acceptance and clear regulatory posture, USDC is the default. Most production protocols end up integrating two or three of these because each user segment leans different.
Section 06 · Integration
How to integrate a stablecoin without surprises
The mistakes that show up at midnight on mainnet are usually integration mistakes, not protocol mistakes. Three patterns prevent most of them.
Never assume 18 decimals
USDC and USDT use 6 decimals. DAI uses 18. If your contract does any arithmetic involving a stablecoin amount, read decimals() from the token and scale. The number of production incidents traced to hard coded decimal assumptions is embarrassing.
Treat transfer as fallible
A blacklist hit will revert the transfer. A pause will revert. A failed external call inside a custom hook will revert. Your integration needs to handle the revert path explicitly — surface it to the user, queue a retry, or hold the position.
Use SafeERC20 for return value checks
USDT's transfer does not return a boolean in the way the standard expects. OpenZeppelin's SafeERC20 wrapper normalizes this. If you call transfer directly on USDT from a contract that expects the standard return, your call may revert in some compiler versions.
The deeper integration patterns — multi stablecoin routing, peg break circuit breakers, atomic redemption flows — are protocol design questions. They sit on top of the ERC20 surface but are not part of it. The ERC standards explainer covers the wider ERC family that stablecoins increasingly compose with.
Section 07 · FAQ
Common questions about ERC20 stablecoins
The questions that come up most often when integrators read the actual contracts.
Is USDC an ERC20 token?
Yes. USDC implements the full ERC20 interface — transfer, approve, transferFrom, balanceOf, allowance, totalSupply. The contract adds mint, burn, blacklist, and pause hooks on top, plus an upgradeable proxy pattern. The token uses 6 decimals rather than the more common 18.
How does a stablecoin contract mint new tokens?
For fiat backed stablecoins, a privileged minter wallet calls a mint function that increases total supply and credits a recipient address. The mint function is gated by an access control role and, in USDC's case, by a per minter allowance to limit blast radius. For DAI, minting happens through the DaiJoin adapter when a user draws against a collateralized vault.
Why can USDC blacklist your address?
USDC ships a blacklist function so the issuer can comply with sanctions enforcement. A blacklister wallet adds an address to a mapping. Every transfer routes through a modifier that checks the mapping and reverts if either party is blacklisted. Once frozen, the only way to move funds is for the issuer to remove the entry.
What is the difference between USDC and USDT contracts?
Both are ERC20 fiat backed stablecoins with 6 decimals. USDC sits behind a transparent upgradeable proxy and uses a per minter allowance system. USDT is a more monolithic contract with a single owner wallet for mint and burn. USDC has more operational machinery; USDT has less but concentrates trust more aggressively in one key.
Can a stablecoin be paused?
Most fiat backed stablecoins, including USDC and USDT, ship a pause switch that a privileged wallet can flip to freeze every transfer on the contract. DAI has no global pause because the token itself is immutable; pausing happens at the surrounding MakerDAO system level instead.
Section 08 · Next Steps
Build the integration with the contract surface in view
Stablecoins are the most battle tested ERC20 deployment in production. They are also the most operationally complex. Treat the contract as the spec.
If you are designing a payment rail, a custody system, or a lending protocol that holds user stablecoin balances, the right starting point is to read the actual contract source for every token you plan to support. Map the privileged roles. Write integration tests that simulate the freeze and pause paths. Document the blast radius.
We help teams ship production grade smart contract systems that integrate with major stablecoins, including the operational surface most tutorials skip. If you are evaluating a stablecoin integration for an exchange, treasury, or DeFi protocol, the contract review is the cheapest insurance you can buy.