BlockchainSolidity10 min readUpdated

Stablecoins on ERC20: How USDC, USDT, and DAI Work

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Stablecoins on ERC20: How USDC, USDT, and DAI Work

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.

Five step flow diagram showing a corporate treasury wiring fiat to Circle, Circle confirming reserves, the FiatTokenV2 contract minting USDC to the customer, optional secondary market trading, and the redeem path that burns USDC and releases fiat back to the user.
The fiat backed stablecoin mint and burn cycle. Every USDC in circulation maps to a dollar held in a partner bank.

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.

solidity
// 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.

solidity
// Address freeze hook — every transfer routes through this
modifier notBlacklisted(address _account) {
    require(!_blacklisted[_account], &quot;FiatToken: account is blacklisted&quot;);
    _;
}

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.

solidity
// 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.

Three column architecture comparison: fiat backed stablecoin on the left with a bank, issuer, and ERC20 mint flow; crypto collateralized stablecoin in the middle with Maker vaults, the Vat ledger, and DaiJoin minting DAI; algorithmic stablecoin on the right with a paired token rebase loop, marked as historically unstable.
Three stablecoin architectures. The ERC20 token surface is identical. The peg machinery behind it is completely different.

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.

Production ERC20 stablecoin contract surfaces, summarized.
PropertyUSDCUSDTDAI
BackingUS dollar bank reservesMixed reserves, attestedCrypto collateral in Maker vaults
Mint authorityPer minter allowance systemSingle owner walletDaiJoin adapter, triggered by vaults
Burn authorityPer minterOwnerDaiJoin adapter on repay
BlacklistYes, per addressYes, per addressNo
Pause switchYes, contract wideYes, contract wideNo global pause
Upgrade patternTransparent proxyDirect address swap on chain configImmutable token, upgrades happen in the surrounding system
Decimal places6618
Audited complexityHigher — proxy plus implementationMedium — single fileHigh — 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.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View blockchain development serviceSee ChainTrust case study

Related service

Blockchain Development

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →