Centre’s USDC has become the second largest stablecoin in DeFi. While it has maintained its 1:1 peg with the US dollar well, its “blacklist” functionality comes with a heavy tax on users, amounting to, by my estimate, an extra $3.6M spent in transaction fees in December 2021 alone.
I propose a new version of USDC that preserves what I argue is the most salient feature of the blacklist--the ability to freeze funds--in a way that eliminates blacklist-related gas costs for users, which can be found in a pull request to the Centre token repo.
Deployed in august of 2018, USDC has grown to be the second largest stablecoin by market cap (next to USDT) with 44.6 billion in circulation at the time of writing. In the beginning it existed natively on Ethereum mainnet only, but has since expanded to 6 other blockchains.
USDC is managed by an organization called Centre whose founding members are Coinbase and Circle, two providers of fiat-to-cryptocurrency conversion services.
USDC has become one of the most favored stablecoins in DeFi and it’s no wonder--it has maintained its peg well and it is easily convertible 1 for 1 to dollars via Coinbase and Circle.
One area in which USDC performs poorly, however, is the cost to use it. It costs approximately 40% more in network fees to transfer USDC compared to DAI, a decentralized stablecoin maintained by MakerDAO.
Why is that? How could USDC, which implements the same ERC20 standard as DAI, cost so much more to use?
A partial explanation is that USDC is a proxy contract. This means that there is a separation between the logic of how USDC operates and the address that people interact with. This allows USDC to be upgradeable--Centre can change the logic of the contract without requiring a migration of USDC balances. However, it also requires extra steps to look up the contract where the logic is and then call it, which requires extra gas.
Another reason is that USDC costs more to use is that it has additional checks to make sure the addresses aren’t on the the blacklist. USDC allows an admin to bar specified addresses from interacting with USDC.
transfer function on USDC is called, the contract checks that both the sender and receiver aren’t on this naughty list. According to the Centre whitepaper, this blacklist is meant to protect users as well as maintain regulatory compliance.
Unfortunately for users, however, checking the blacklist comes at a cost. Each check of the blacklist costs ~2100 gas or around $0.63 a pop (see the appendix for why this is). To transfer, this list is checked twice, adding ~$1.26 to the transaction.
transferFrom (the call used when interacting with other contracts like Uniswap) requires 3 checks of the blacklist, for a total cost of up to ~6300 gas, or ~$1.89.
These costs add up. I estimate that in December 2021 alone, these blacklist checks added about $3.6M in gas fees for USDC users.
I would argue the ability to freeze funds is the only aspect of the blacklist that is worth saving. I discuss this in the context of the two stated aims of the blacklist according to Centre: regulatory compliance and protecting users.
When an address is added to the USDC blacklist, they can no longer interact with USDC directly--it can’t receive funds nor can it move funds.
If the address has an existing balance, the entity behind that address now can’t access it--that’s a real cost. However, in terms of preventing future interaction with the USDC contract, the blacklist isn’t all that binding.
Say you’re the owner of an address that gets blacklisted. If you want to continue to use USDC, you have a two options:
You would probably opt for 1, as it is easy and bears less risk for additional blacklisting, but the point is the blacklist is not a useful tool for stopping the entity behind an address from interacting with USDC. It’s a measure that is more theater than substance.
The logic is similar for contracts. You could blacklist a particular project’s contract, but contracts can be re-deployed.
The risk that bad actors are most likely concerned about is having their USDC frozen. If you lock their balance, that’s a real economic hit for which they have no recourse. If an actor with a frozen balance for some reason continues to use USDC and acquires a fresh balance, Centre could freeze the additional funds. This could even be done in an automated way.
Simply freezing balances doesn’t require an approach as heavy-handed as the blacklist, however, which I’ll discuss later in this post.
The blacklist does effectively protect users from addresses that are determined to steal users’ funds--for example, scam accounts.
However, doing this at the smart contract level is the most expensive place to do this (i.e. $3.6M per month expensive). Building warnings into wallets is a much cheaper place to help protect users from errant behavior or interacting with scam accounts, and they’re even more effective in that they can help prevent the user from spending gas on a failed transaction.
Freezing funds also still leaves recourse for users--in theory you can freeze USDC at a scam address, thereby taking that USDC out of circulation, and Centre wanted to do so, they could mint USDC for the affected users to replace that removed USDC.
In the previous section I argue that freezing funds is the sole aspect of the blacklist worth saving (or at least worth spending transaction fees on).
In my proposed USDC v3, I remove the blacklist checks from all transfer and approval functions. This would save the gas spent on these checks (the $3.6M in cost).
In its place, I introduce a new function called
freezeBalance. This function allows an admin (in this case, an address that is already given the role of blacklister) to freeze an account’s funds.
Under the hood, this sets the target account’s balance to zero, removes that amount from the total supply, and updates a separate account mapping (
frozenBalances) to reflect the funds that are frozen.
frozenBalances makes accounting for these actions easier as well as allows the admin to unfreeze balances (calling
unfreezeBalance) if the initial freeze is later deemed to have been done in error.
My ultimate hope is that my proposed version of USDC gets adopted to save users money as well as make a (very) modest dent in Ethereum’s congestion problem by reducing USDC’s footprint in Ethereum blocks.
This code for can be in a pull request to Centre’s tokens repo.
In the high gas environment that we’ve experienced on Ethereum mainnet the past two years, it’s crucial for projects behind heavily-used contracts to scrutinize gas usage very carefully.
Thanks to Noah Zinsmeister, Kelsey Dean, and Daniel Pyrathon for reviewing this post.
The most significant (though not the only) gas costs associated with checking the blacklist is the fact that it needs to read from storage (specifically looking up an address in a mapping of address to a boolean value of whether that address is blacklisted).
The cost of every action one can take in the EVM is specified in the Ethereum yellow paper. This document is updated as hard forks modify the behavior of the EVM. The relevant costs for storage access were last updated in the April 2021 Berlin hardfork due to the inclusion of EIP 2929.
The cost for a “cold sload” (“cold” meaning the storage slot has not yet been accessed in the transaction) is 2100 gas. Once a storage slot has been accessed, the cost for any subsequent access within that transaction is 100 gas.
As discussed previously, the USDC blacklist is checked twice for each
transfer--once for the sender and once for the receiver--and three times for each
transferFromonce for the receiver--once for the address calling
transferFrom (typically a contract), once for the sender, and once for the receiver.
In theory some of these addresses within a
transferFrom could be duplicates, in which case the additional read would cost only 100 gas. In practice, however, these addresses (sender, receiver, caller) will often all be different addresses, so each read will cost 2100 gas.
To put this into context, 2100 gas at $3K ETH and 100 gwei gas price costs $0.63. This may sound low, but add multiple checks per transaction and millions of transactions, the costs add up.
In addition to
transferFrom, there are two checks of the blacklist for
approve allows a separate address (say the Uniswap router or a Compound cToken contract) to spend your balance (subject to the logic of the contract, of course).
USDC checks that the address granting the approval (
msg.sender) and the address that is being granted the approval (
spender) are not on the blacklist.
While in this post I argue that the most important functionality of the blacklist can be preserved while getting rid of the blacklist checks on
transferFrom, the blacklist checks on
approve are particularly superfluous, since even if approval is granted from or to a blacklisted address, the checks on
transferFrom prevent any movement of funds.
These blacklist checks for
approve impose a cost on all users calling this function and should be removed. Blacklist checks for
approve alone cost users $346K in December 2021, though I note that December had a higher number of approvals than normal due to token mining with a Gearbox Protocol contract on December 16 (see Dune query). Excluding these the Gearbox approvals, the
approve blacklist checks cost users ~$209K.
I used Dune Analytics for data analysis mentioned in this post.
You can find the source for the cost of DAI/USDC transfers in the following queries:
For the numbers mentioned in the post, I calculated the median gas cost of a simple transfer transaction from the last week of 2021.
I calculated the USD value of the excess ETH spent on blacklist lookups in December 2021 relative to my proposed implementation in the following query.
To make the calculations tractable without timing out, I calculated the excess ETH for
transferFrom in separate queries and stored the results in tables. The queries for these tables can be found linked in the query above.
I calculate the gas spent on
SLOAD related to the blacklist, taking into account the lower cost of warm access if an address is repeated.
When deploying v3, addresses on the blacklist with frozen balances would be able to move this balance if they aren’t frozen via
freezeBalance, so it’s imperative that the deployment transaction includes this step.
I left blacklist checks on the
mint functions, since they are not very frequent called and those functions only affect Centre’s costs, not the costs of USDC users.