Using non-custodial smart contracts to process ERC20 payments at scale
Table of Contents
— How Coinbase Commerce works — 30k foot overview
— Non-custodial by design
— Naïve solution — Forwarding contracts
— Reducing transaction sizes
— Optimizing for off-chain whenever possible
— Minimizing deployment costs
Coinbase Commerce’s mission is to be the easiest way for businesses to accept cryptocurrencies. We launched in February of 2018 supporting BTC, ETH, LTC, and BCH, making it simple for anyone to start accepting cryptocurrencies in a couple of minutes. While our merchants love the ability to instantly transact with customers anywhere in the world, many have expressed concerns with the volatility of cryptocurrencies given fiat-denominated business costs.
Unlike traditional cryptocurrencies, stablecoins such as USD Coin (USDC) are explicitly designed to avoid this volatility. We released USDC support a couple months ago, enabling a volatility-free way for our merchants to accept cryptocurrencies. USDC is backed one-to-one by US dollars, giving it a stable price and making it a great option for commerce. USDC is implemented as an ERC20 token, living on the Ethereum blockchain. Here, I will discuss the approach we took to support USDC on our platform. This post is aimed for anyone who is interested in the internals of Coinbase Commerce. It is particularly useful for dapp developers that might find the ideas and implementations around create2 and minimal proxy contracts relevant to their work.
How Coinbase Commerce works — 30k foot overview
When a customer wants to pay to a Coinbase Commerce merchant, we create a charge object that keeps track of what the customer wants to buy, how much it costs, the blockchain address they need to pay to, and other metadata. We generate a unique blockchain address for each charge, which serves as a canonical identifier for the charge and the payments made to it. When a customer pays a charge, they do so by sending funds from any wallet or exchange to one of those addresses. By continuously monitoring the blockchain for payments to these addresses, we know if the customer paid the charge and how much they paid, which drives the charge’s completion and the sending of the appropriate webhooks and emails.
Our workflow — generating a unique address, having the customer pay to that address, and having our system detect that payment on the blockchain — is only one way a blockchain payments processor can be engineered. Other workflows are based on the JSON Payment Protocol, where the customer’s wallet sends an RPC call to the processor’s servers and exchange the necessary info to conduct the payment. Yet another approach would be to use the ETH’s Web3 interface to ask the customer to sign a custom, non-standard transaction. While these approaches have unique benefits, for us it was very important to keep the simple, address-only interface. A vast majority of our customers use an exchange or a hosted wallet, and therefore can only send funds to an address — they do not have the option to initiate RPC calls or sign custom transactions. It was therefore paramount for us that our solution cannot require anything but simple address that everyone can use and send their payments to.
Non-custodial by design
Part of Coinbase Commerce’s value proposition is that we operate as a non-custodial service, meaning that we do not act as a middle man and that all transactions are directly between the customers and our merchants. Being non-custodial means that no one (including Coinbase Commerce) can prevent the movement of funds once a payment has been initiated. We never let any private keys hit our servers, and therefore we do not have the ability to censor or revert a transaction. There are some tradeoffs that come with being a non-custodial solution. On the positive side, this means that:
- It increases the decentralization
- It increases interoperability with other wallets and tools
- It lowers the risks of using the service
However, there are some downsides that we’ve tried to mitigate:
- Accidental loss of private keys (recoverable if you use the encrypted backup feature)
- Hard to automate certain activities such as refunds or scheduled withdrawals
Given this context on how Coinbase Commerce works, let’s dive into the design and implementation of our USDC solution. I will start with a naïve solution that conveys the general idea, and iteratively address its short-comings, ultimately getting to the production version we use today at Coinbase Commerce. I’ll introduce a couple of techniques — factory pattern, create2 opcode, minimal proxy contracts — which make deploying ETH contracts feasible at scale. These techniques could be used in a variety of business settings.
Naïve solution — Forwarding contracts
Our solution is based on the following idea: We create smart contracts that can accept funds as if they were ordinary accounts, with the sole functionality of forwarding the accepted funds to a fixed, predetermined address that belongs to the merchant. That way, the customers can still use the simple address-only interface to pay, and the merchants can be certain that their funds cannot be misused by anyone. In fact, both the customers and the merchants might not be aware that a smart contract logic is involved at all! Here is one such Forwarder contract:
While the contract achieves our requirements, creating a production system out of these Forwarders is infeasible. For each merchant who has enabled USDC payments, we would need to preemptively deploy multiple forwarders to the blockchain to ensure that there is an available pool of addresses when a customer initiates a payment flow. After we detect a payment to one of the deployed contracts, we (or the merchant) can call the flush function to forward the tokens to the merchant’s destination and finalize the movement of funds.
However, managing these pools is quite expensive (each contract creation requires gas fees to be published on the blockchain), very cumbersome and causes unnecessary congestion of the network, making them a non-starter for any production setting.
Reducing transaction sizes
The cost of an Ethereum transaction is primarily tied with the complexity of the logic executed in that transaction — the more CPU time a transaction needs to process its logic, the more gas the transaction will consume. However, for contracts that perform repetitive, simple tasks, the transaction costs are dominated by the space used by the transaction, rather than its runtime logic.
The factory pattern significantly helps reduce the cost of these transactions. Instead of encoding the same logic multiple times when deploying a new Forwarder, we can deploy a single ForwarderFactory that knows how to instantiate new Forwarders. We no longer need to duplicate the full source code every time we deploy a Forwarder — instead we can encode only a pointer to the already deployed code. This results in 47% reduction of gas cost; and indeed, the naïve transaction with its full implementation weighs 1783 bytes, where the factory transaction that contains only the function arguments weighs only 175 bytes.
Optimizing for off-chain whenever possible
A byproduct of the factory that deploys the Forwarders is that we are now able to compute the addresses of the Forwarders before we deploy them to the blockchain. This enables the same user experience as preemptively deploying a forwarding contract for each merchant, without incurring the expense of deploying these contracts on the blockchain.
In Ethereum, the addresses of the created contracts are deterministic — they can be computed from the sender’s address (i.e. the account that created the contract, in our case the factory) and the sender’s nonce (a counter tracking how many transactions originated from that account). However this does not work well in practice because the nonce is an ever increasing global counter. If a customer pays to the 100th generated address, we’d still need to deploy the initial 99 contracts so we can deploy the expected, 100th Forwarder, and we’d need to do that in a determined order. Gaps like these, when we’d need to deploy contracts just to get to the expected nonce, are inevitable because customers might create a charge but never pay a charge.
This is a problem that has plagued the Ethereum community for a long time and has been on the wishlist for many dapp developers. Fortunately, the introduction of the create2 opcode in the Constantinople network upgrade addressed this issue. Unlike create, which uses the ever-increasing sender’s nonce, create2 uses an argument-specified salt. This enables a number of workflows that were impossible or impractical before — we can now simulate complex workflows completely off-chain; without needing to track any state beyond the self-generated salt. We can safely interact with the blockchain only when we need to do an on-chain settlement. Here is how we can use the create2 opcode to deploy a Forwarder.
Note that now initForwarder takes additional salt where before its functionality was performed by the implicit nonce. This allows us to explicitly decide which contracts to deploy on the blockchain, so if the 100th customer pays but the initial 99 do not, we can deploy the 100th forwarder in the sequence without needing to deploy the initial 99 first.
It’s informative to see the code that calculates the addresses:
In addition to the use of a salt, a crucial safety feature in the address generation algorithm is the use of the bytecode. Having the bytecode as one of the arguments ensures that it is impossible to deploy the “wrong” contract at the indicated address. For our example, this implies that there is exactly one combination of destination and salt that can lead to a contract being deployed at a computed address. It is impossible for a malicious attacker to deploy a contract of their own logic or their destination to an address we already computed. Therefore, we can present these non-deployed addresses to the customers and be certain that an attacker could not out-race us by deploying a contract of their own choosing to that particular address.
This enables novel workflows like the one we use at Commerce. It is now possible to create and simulate immutable workflows fully off-chain, and only do the “settlement” stage on the blockchain, after the customer has already paid for their invoice.
Minimizing deployment costs
We can further reduce the cost of our solution by using another novel technique called minimal proxy contracts. The minimal proxy contracts allow the deployment of a contract that is an exact copy of another existing contract, but is extremely light-weight to deploy. They represent the symlinks of the Ethereum blockchain — they have their own address, but they defer all of their functionality to the original contract they point to. These proxy contracts include carefully crafted, “magic” bytecode that changes the execution stack and uses the delegatecall opcode to delegate the implementation to the target contract. In our case, instead of deploying a new Forwarder whenever a customer pays to the same merchant, we can deploy a single Forwarder for that merchant and use its clones from that point onwards. Using clones reduces the gas cost by further 61% over the factory solution.
You can see the contracts we use in production here. The link hints at a couple of the downsides of the approach we have taken:
- Reduced privacy due to the singleton factory
- Increasing the minimal settlement time due to the extra flush call
The factory, create2 opcode, and the minimal proxy contracts are patterns that can be applied for a wide range of circumstances. At Coinbase Commerce, we applied these patterns to make the movement of funds as simple and cheap as possible, but any other workflow that needs to simulate interactions with a number of contracts while only deploying a subset could benefit from them.
A lot of patterns in blockchain engineering enable novel workflows, but these patterns come with their own gotchas and surprises. As a young ecosystem, its documentation and best practices lag behind the advancements made at the protocol level.
The USDC forwarding is a great example of our daily work on crypto technologies as well as the interesting challenges of building complex, production systems. If you enjoy working in a fun, high energy environment and want to work on making accepting cryptocurrencies easy, then checkout all open positions here. We’d love to hear from you.
This website contains links to third-party websites or other content for information purposes only (“Third-Party Sites”). The Third-Party Sites are not under the control of Coinbase, Inc., and its affiliates (“Coinbase”), and Coinbase is not responsible for the content of any Third-Party Site, including without limitation any link contained in a Third-Party Site, or any changes or updates to a Third-Party Site. Coinbase is not responsible for webcasting or any other form of transmission received from any Third-Party Site. Coinbase is providing these links to you only as a convenience, and the inclusion of any link does not imply endorsement, approval or recommendation by Coinbase of the site or any association with its operators.
All images provided herein are by Coinbase.