Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

How to Architect a Contract That Supports Multiple Payment Tokens

A technical guide to designing a secure token sale contract that accepts contributions in ETH, stablecoins (USDC, DAI), and other ERC-20 tokens using Chainlink oracles for consistent valuation.
Chainscore © 2026
introduction
INTRODUCTION

How to Architect a Contract That Supports Multiple Payment Tokens

A guide to designing flexible smart contracts that can accept payments in ETH, stablecoins, and custom ERC20 tokens.

Smart contracts that only accept the native chain token (like ETH) are inherently limited. To maximize user adoption and utility, a contract should support multiple payment tokens. This architectural pattern is essential for DeFi protocols, NFT marketplaces, and subscription services, allowing users to pay with their preferred asset. The core challenge is managing token approvals, secure value transfers, and maintaining a consistent internal accounting system regardless of the token used.

The foundation is the ERC20 standard and its transferFrom function. Your contract must be designed to receive tokens, not just ether. This requires a two-step process: first, the user must grant your contract an allowance via approve, then your contract calls transferFrom to move the tokens. A critical security consideration is to always follow the checks-effects-interactions pattern and use a pull-over-push strategy for payments to prevent reentrancy attacks when dealing with arbitrary ERC20 tokens.

A robust implementation involves creating an internal accounting abstraction. Instead of tracking balances in multiple currencies directly, you can standardize values using an oracle or a fixed price feed to convert all incoming payments into a single internal accounting unit (e.g., USD value). For example, you might accept USDC, DAI, and WETH, using a Chainlink price feed to normalize WETH to a USD equivalent before crediting the user's internal balance.

Your contract's architecture should include a whitelist or registry of approved payment tokens. This prevents the acceptance of malicious or illiquid tokens. The function to handle a payment should: 1) verify the token is in the approved list, 2) safely transfer the tokens from the user to the contract using safeTransferFrom (from OpenZeppelin's SafeERC20 library), and 3) update the user's internal balance based on the received amount and the token's current value.

Here is a simplified code snippet for a payment handler function:

solidity
function payWithToken(IERC20 token, uint256 amount) external {
    require(isApprovedToken[address(token)], "Token not approved");
    uint256 valueInUsd = getValueInUsd(address(token), amount);
    token.safeTransferFrom(msg.sender, address(this), amount);
    userBalance[msg.sender] += valueInUsd;
}

This pattern separates the payment logic from the business logic, making the system more maintainable and secure.

Finally, consider gas efficiency and user experience. Batch operations for approvals (using permit for ERC2612 tokens) and aggregating transactions can reduce costs. Always ensure your contract can handle fee-on-transfer and rebasing tokens by checking the balance before and after transfer. By architecting for multiple tokens from the start, you build a more composable and future-proof application ready for a multi-chain, multi-asset ecosystem.

prerequisites
PREREQUISITES

How to Architect a Contract That Supports Multiple Payment Tokens

This guide outlines the foundational knowledge required to design a smart contract that can accept payments in multiple ERC-20 tokens, a common requirement for modern DeFi and NFT applications.

Before building, you must understand the core Ethereum standards involved. Your contract will primarily interact with the ERC-20 token standard (EIP-20). You need to know how to call its key functions: balanceOf(address), transferFrom(address, address, uint256), and allowance(address, address). The contract will act as a spender, requiring users to grant it an allowance before it can transfer tokens on their behalf. You should also be familiar with the concept of token decimals, as amounts in contracts are handled in the token's smallest unit (e.g., Wei for ETH, but 10^18 for a typical 18-decimal token).

Solidity proficiency is essential. You'll need to work with address and IERC20 interface types, manage state variables to track accepted tokens, and implement functions that use safeTransferFrom. Understanding function modifiers (like onlyOwner) for administrative controls and error handling with require statements or custom errors (Solidity >=0.8.4) is crucial for security. A basic grasp of events for logging transactions (PaymentReceived) is also recommended for good practice.

You must decide on your contract's architecture for token support. The simplest model is a whitelist approach, where the contract owner can add token addresses to an approved list (e.g., a mapping(address => bool) public acceptedTokens). All payment logic then checks against this mapping. Alternatively, for more complex systems like decentralized exchanges, you might need a router pattern that quotes prices via an oracle or DEX aggregator. For this guide, we will focus on the whitelist model.

Security considerations are paramount. A major risk is reentrancy; always use the Checks-Effects-Interactions pattern, especially when transferring external tokens. Consider using OpenZeppelin's SafeERC20 library for safe transfers, which prevents failures with non-compliant tokens. You must also validate token addresses to prevent locking funds in unrecoverable contracts. Never assume a token's value; external price feeds are needed for value-based logic.

Finally, set up your development environment. You will need Node.js, a package manager like npm or yarn, and a development framework such as Hardhat or Foundry. These tools allow you to compile Solidity, run tests against a local blockchain (e.g., Hardhat Network), and deploy to testnets. Having test ERC-20 tokens (like those from OpenZeppelin's contracts) to simulate payments with USDC, DAI, or WETH is necessary for proper testing.

key-concepts
MULTI-TOKEN CONTRACTS

Core Architectural Concepts

Design patterns and strategies for building smart contracts that can accept, manage, and interact with multiple ERC-20 tokens.

04

Implementing a Fee Discount System

Offer fee discounts for payments in a specific protocol token or stablecoin. This requires a tiered fee structure and a mechanism to calculate equivalent value. Steps:

  1. Use an oracle (e.g., Chainlink) to get the USD price of the payment token and the protocol token.
  2. Calculate the USD value of the payment.
  3. Apply a base fee percentage (e.g., 0.3%).
  4. If paying with the protocol token, apply a discount (e.g., 50%, resulting in a 0.15% fee).
  5. Deduct the calculated fee amount from the payment. This incentivizes token utility while maintaining revenue.
0.05%
Typical Discounted Fee
2 Oracles
Minimum Required
architecture-overview
SYSTEM ARCHITECTURE OVERVIEW

How to Architect a Contract That Supports Multiple Payment Tokens

Designing a smart contract to accept multiple ERC-20 tokens requires careful planning around token validation, price oracles, and fee management. This guide outlines the core architectural patterns.

The primary challenge in multi-token architecture is handling non-native assets. Unlike a contract that only accepts its chain's native coin (e.g., ETH, MATIC), you must account for token standards, decimal precision, and approval workflows. The foundational step is to define a whitelist of accepted ERC-20 token addresses. This list should be controlled by a privileged role (like an owner or DAO) and stored in a mapping: mapping(address => bool) public acceptedTokens;. Functions that process payments must first check require(acceptedTokens[tokenAddress], "Token not accepted");.

For services with fixed-price fees, you need a reliable source for token exchange rates. You cannot assume a 1:1 value ratio between different tokens. Integrate a decentralized oracle like Chainlink Price Feeds to fetch real-time prices. Your contract should store the corresponding feed address for each whitelisted token. The payment logic will then: 1) Get the USD price of the payment token, 2) Get the USD price of your contract's preferred base currency (e.g., USDC), 3) Calculate the required payment amount: requiredAmount = (usdFee * 10**paymentTokenDecimals) / paymentTokenPrice. Always use safeTransferFrom for the final transfer.

A robust architecture separates concerns. Use an abstract PaymentModule contract that handles all token validation, oracle queries, and transfers. Your main business logic contract then inherits from or references this module. This pattern improves upgradability and security. Consider implementing a fee treasury that automatically swaps diverse income tokens to a single stablecoin via a decentralized aggregator like 1inch, reducing protocol-owned volatility. Always include a function to rescue accidentally sent tokens or unsupported ERC-20s to avoid permanent lockups.

Security audits are critical for payment systems. Common vulnerabilities include: - Oracle manipulation: Use reputable oracles with multiple data sources. - Decimal precision errors: Perform all calculations in the highest precision before dividing. - Reentrancy: Use the Checks-Effects-Interactions pattern and OpenZeppelin's ReentrancyGuard. - Front-running: Consider implementing a commit-reveal scheme for volatile token payments. Test extensively with forked mainnet environments using tools like Foundry or Hardhat to simulate real price feeds and token behaviors.

For advanced use cases like accepting ERC-777 or other token standards with hooks, extra caution is required due to potential reentrancy in the token transfer call itself. The recommended practice is to treat them as ERC-20s by using the standard transferFrom function if available, or to integrate a dedicated module like OpenZeppelin's ERC777Holder. Document clearly for users which token standards are supported. The final architecture should be gas-efficient for users, minimize protocol risk, and provide clear administrative controls for managing the token whitelist and price feed configurations.

ARCHITECTURE COMPARISON

Price Oracle Options for Token Conversion

A comparison of common oracle solutions for determining token prices in a multi-token payment contract.

Feature / MetricChainlink Data FeedsUniswap V3 TWAPCustom On-Chain DEX Oracle

Oracle Type

Decentralized Network

Time-Weighted Average Price

Instant Spot Price

Update Frequency

~1 sec - 1 min

Configurable (e.g., 30 min)

Per-block (on trade)

Primary Use Case

High-value, secure price data

Resistance to short-term manipulation

Extreme gas efficiency for niche pairs

Security Model

Decentralized node network with cryptoeconomic security

Relies on DEX liquidity depth and TWAP window

Depends on the security of the underlying DEX pool

Gas Cost for Query

~70k - 100k gas

~150k - 300k+ gas (computes TWAP)

~20k - 50k gas (simple read)

Supported Assets

1000+ major tokens across chains

Any token with a Uniswap V3 pool

Only tokens in the specific DEX pool

Manipulation Resistance

High (multi-node consensus)

High (with sufficient TWAP period)

Low (vulnerable to flash loan attacks)

Implementation Complexity

Medium (integrate AggregatorV3Interface)

High (manage observation arrays, cardinality)

Low (call pool's price function)

step-by-step-implementation
IMPLEMENTATION GUIDE

How to Architect a Contract That Supports Multiple Payment Tokens

This guide details the architectural patterns and security considerations for building a smart contract that can accept payments in multiple ERC-20 tokens, from basic whitelisting to advanced price oracle integration.

A multi-token payment contract allows users to pay for services, mint NFTs, or purchase goods using various ERC-20 tokens instead of just the native chain currency (e.g., ETH). The core architectural challenge is managing exchange rates and ensuring the contract securely holds and accounts for diverse assets. The simplest pattern involves a whitelist of approved tokens and a fixed price mapping set by the contract owner. For example, you might set that 1 USDC or 1 DAI equals 1 unit of your service, while requiring 1500 UNI for the same unit. This approach is straightforward but inflexible, as manual price updates cannot track volatile market movements.

For dynamic, market-accurate pricing, you must integrate a decentralized price oracle. Instead of hardcoding values, your contract queries an oracle like Chainlink to get the current ETH/USD price and the Token/USD price for each payment option. The payable amount in a given token is then calculated on-chain: tokenAmount = (usdPriceOfItem * 10**tokenDecimals) / usdPriceOfToken. This calculation must account for different decimal precision (e.g., USDC has 6, DAI has 18) to prevent rounding errors. Always use oracle.latestRoundData() and check for stale data with a heartbeat threshold to avoid using outdated prices that could be exploited.

Security is paramount when handling external tokens. Never use a user-provided token address without checks; an attacker could pass a malicious contract. Maintain an owner-managed mapping like mapping(address => bool) public approvedTokens;. When a payment is made, the contract must call token.transferFrom(msg.sender, address(this), amount). This requires the user to have first called token.approve() on your contract, granting it an allowance. Your function should verify the transfer was successful by checking the return value (for compliant tokens) or using a safeTransferFrom wrapper from OpenZeppelin's SafeERC20 library, which ensures compatibility with all ERC-20 variations.

The contract must also robustly handle the received funds. A common pattern is to immediately swap all non-native tokens into a canonical vault asset (like WETH) via a decentralized exchange aggregator such as Uniswap V3 or 1inch. This consolidates treasury management and reduces exposure to multiple token volatilities. Alternatively, you can accumulate each token in separate balances and allow the owner to withdraw them periodically. Implement a pull-over-push pattern for withdrawals to avoid gas race conditions and reentrancy risks; instead of sending tokens automatically, let authorized addresses call a withdrawToken(address token) function.

Finally, consider gas efficiency and user experience. Performing oracle calls and DEX swaps within the same transaction as the payment can become prohibitively expensive. For complex flows, a two-transaction process or a meta-transaction relay might be better. Always emit clear events like TokenPaymentReceived(address user, address token, uint256 amount, uint256 itemId) for off-chain tracking. Thoroughly test your contract with forked mainnet simulations using tools like Foundry or Hardhat, especially edge cases involving fee-on-transfer tokens, rebasing tokens, and oracle downtime.

security-considerations
DESIGN PATTERNS

How to Architect a Contract That Supports Multiple Payment Tokens

A guide to implementing secure, gas-efficient smart contracts that accept multiple ERC-20 tokens, covering architecture, price oracles, and critical security considerations.

Supporting multiple payment tokens in a smart contract, such as for a marketplace or subscription service, introduces complexity beyond handling native ETH. The primary architectural decision is whether to use a pull-based or push-based payment model. In a pull model, you grant the contract an allowance to transfer tokens on the user's behalf using transferFrom. This is common for DEXes. In a push model, the user directly transfers tokens to the contract using transfer. The pull model offers better UX for repeated interactions but requires careful allowance management to prevent front-running and requires two transactions (approve, then execute).

A core challenge is obtaining reliable, on-chain prices for different token pairs. You must integrate a decentralized oracle like Chainlink Price Feeds. Never use a single DEX pool's spot price, as it is vulnerable to manipulation. For a contract selling an item for $100, you would query the ETH/USD feed and the USDC/USD feed to calculate the correct amount of each token. Store the oracle address and the payment token's decimals in a mapping (e.g., mapping(address => TokenConfig)) for validation. Always implement circuit breakers to pause operations if oracle data is stale or the price deviates beyond a sanity-checked threshold.

Security considerations are paramount. Use the Checks-Effects-Interactions pattern rigorously, updating internal state before making external token transfers. For pull models, be aware of the ERC-20 approval race condition: a user could change an allowance between the time you check it and use it. Mitigate this by either using the allowance sent with the function call via IERC20(token).transferFrom(msg.sender, address(this), amount) or employing a pull-payment wrapper. Reentrancy is a risk with some ERC-20 tokens that make callbacks; use a nonReentrant modifier from OpenZeppelin. Finally, implement a robust withdrawal pattern for the contract owner to securely retrieve accumulated fees in various tokens, preventing them from being locked.

MULTI-TOKEN CONTRACTS

Frequently Asked Questions

Common questions and solutions for developers implementing smart contracts that accept multiple payment tokens, covering architecture, security, and gas optimization.

The most secure and gas-efficient architecture uses a pull-over-push payment pattern. Instead of the contract pulling tokens from users (which requires an approval per token), users approve and transfer tokens to the contract in a single transaction. The contract then validates the transfer.

Core Components:

  1. A mapping to track accepted token addresses and their price oracles.
  2. A function that uses IERC20(token).transferFrom(msg.sender, address(this), amount).
  3. A modifier or internal check to verify the token is in the accepted list.

This pattern minimizes contract state changes and avoids the need for the contract to call transferFrom on behalf of users, which can be a security risk.

conclusion-next-steps
ARCHITECTURE REVIEW

Conclusion and Next Steps

This guide has outlined the core patterns for building a smart contract that can accept multiple payment tokens, balancing flexibility, security, and gas efficiency.

You now have a foundational understanding of the primary architectural approaches: the Registry Pattern for maximum flexibility with on-chain price feeds, the Whitelist Pattern for controlled simplicity, and the Aggregator Pattern for optimal user experience via DEX routing. The choice depends on your protocol's needs—whether you prioritize governance control, gas cost minimization, or supporting a vast array of assets. Each pattern introduces distinct trade-offs in upgradeability, security surface, and operational complexity that must be evaluated.

For production deployment, rigorous testing is non-negotiable. Beyond standard unit tests, you must implement integration tests that simulate price feed updates from oracles like Chainlink, test swaps through aggregators like 1inch, and verify fee collection in various ERC-20 tokens. Use a forked mainnet environment (with Foundry or Hardhat) to test with real token contracts and liquidity conditions. Security audits focusing on the payment module's interaction with price oracles and external routers are essential before mainnet launch.

Looking forward, consider how this payment system integrates with broader protocol mechanics. Will accrued protocol fees in various tokens be automatically converted to a stablecoin via a treasury manager contract? How does the design accommodate potential ERC-7579 modular smart accounts? Explore existing implementations in protocols like Uniswap v3 (which uses a hardcoded list) or Lens Protocol (which employs a fee module) for real-world reference. The next step is to prototype your chosen design, measure gas costs, and iterate based on feedback.

How to Architect a Multi-Token Sale Contract (ETH, USDC, DAI) | ChainScore Guides