This guide will walk you through how your Portal Wallets work with ERC20 tokens, how to interface with ERC20 Token Contracts, and how to use the Portal Provider to transfer ERC20 tokens.
What are ERC20 tokens?
ERC20 is an Ethereum standard that defines a unified interface for standard token behavior in a smart contract. This allows for custom tokens to be created beyond the native token for that chain (ETH in the case of Ethereum). Examples of ERC20 tokens include stablecoins like USDT & USDC or Defi protocol tokens like UNI.
The ERC20 standard defines just a few Solidity functions that these contracts must implement:
In order to send or check the balance of your ERC20 tokens you must interact with the smart contract. Depending on if those functions are Read-Only or State Changing you interact with these functions in two different ways.
Read-Only Functions
Notice that these functions (shown as their Solidity function signatures) all contain the view keyword. This means that these functions can be invoked using eth_call RPC method and do not change the state of the blockchain. This also means that calling these functions does not require gas.
These functions do not contain view keyword, which means they do change the state of the blockchain. This requires sending a signed transaction to the blockchain, along with gas, to update the global state. These functions are invoked using the eth_sendTransaction RPC method.
The Portal SDK supports the processing of web3 transactions using the Portal Provider. Because of this, working with ERC20 tokens using your Portal Wallet is very similar to working with ETH itself. The main difference is that you'll need to interface with the ERC20 token's contract in order to build the appropriate transaction before signing with the Portal Provider.
This can be thought of in three basic steps:
Gathering inputs for generating a transaction
Using Web3.swift to generate an ERC20 transaction
Using Provider to sign and send the transaction
Gathering inputs for generating a transaction
In this example, we'll be looking at transferring ERC20 tokens from your Portal Wallet to another wallet (sending ERC20 funds to another wallet). In order to accomplish this, you'll need your application to manage certain inputs required to generate the transaction: the receiver's address, and the token address for the ERC20 token being sent.
In this case, we'll use Uniswap (UNI) as an example, so we'll be using the tokenAddress for UNI (0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984).
Using Web3.swift to generate an ERC20 transaction
The Web3.swift library allows you to easily interface with token contracts to build the appropriate transaction payload for an ERC20 transaction without needing to manually build eth_call requests. We'll be using it for this purpose in this example.
The following code demonstrates how to use the Web3.swift library to access an instance of the UNI Contract and generating a basic send transaction.
In order to use Web3.swift in your application, you'll need to add the package as a dependency. More details on adding Web3.swift to your project can be found here.
importWeb3importWeb3ContractABI// Get the Gateway URL from the Portal Providerguardlet gatewayUrl = portal?.getRpcUrl(forChainId:"eip155:11155111")else {// Add application logic to handle this scenarioreturn}// Get the Portal Wallet addressguardlet address = portal?.address else {// Add application logic to handle this scenarioreturn}// Create an instance of Web3.swiftlet web3 =Web3(rpcURL: gatewayUrl)// Get an instance of the UNI Contract to generate transactionslet contract = web3.eth.Contract( type: GenericERC20Contract.self, address:"0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"// UNI token address)// Get the correct decimal conversion for the UNI smart contractlet conversionFactor: Double=1e18let value =BigUInt(amount * conversionFactor)// Generate the ERC20 transaction using the UNI Contractguardlet tx = contract .transfer(// eip55 is dependent on the wallet receiving funds to:try EthereumAddress(hex: toAddress, eip55:false) value: value) .createTransaction( nonce:0, // Here you'd want to provide the actual nonce for this transaction gasPrice: EthereumQuantity(quantity:21.gwei), maxFeePerGas:nil, maxPriorityFeePerGas:nil, gasLimit:100_000,// eip55 will be false for Portal MPC Wallets from:try EthereumAddress(hex: address, eip55:false), value:0, // This is the value of the native token, not the ERC20 amount accessList: [:], transactionType: .legacy)else { // Add application logic to handle this scenarioreturn }
Using Portal to sign and send the transaction
Once you've generated a transaction using the token Contract, you can use it to create a request using your Portal instance to sign and send the transaction.
Task {do {// Ethereum Sepolialet chainId ="eip155:11155111"// CAIP-2 format.// Obtain the eip155 address of the user's wallet.guardlet eip155Address =await portal.getAddress(chainId)else {throw PortalExampleAppError.addressNotFound() }// Create the transaction object.let transaction = ["from": eip155Address,"to":"0xRecipientAddress", // Sending the tx to the smart contract to execute the smart contract function call in the data field"gasPrice": tx.gasPrice.hex(),"value":"0x0","data": tx.data.hex(), "nonce": tx.nonce.hex() ]// Make the eth_sendTransaction request.let requestResponse =tryawait portal.request( chainId, withMethod: .eth_sendTransaction, andParams: [transaction]) }}
The sendERC20Token() function
Now that we've covered the steps required to sign and send a transaction using ERC20 tokens, let's look at an example of a full function for sending ERC20 tokens to another wallet.
funcsendERC20Token(tokenAddress: String, toAddress: String, amount: Int64) throws {// Get the Gateway URL from the Portal Providerguardlet gatewayUrl = portal?.getRpcUrl(forChainId:"eip155:11155111")else {return }// Ensure the Portal Address is setguardlet address = portal?.address else {// Probably throw an error herereturn }// Initialize Web3let web3 =Web3(rpcURL: gatewayUrl)do {// Transform the receiver addresslet receiverAddress =tryEthereumAddress(hex: toAddress, eip55:false)// eip55 is dependent on the wallet receiving funds// Transfor the sender addresslet senderAddress =tryEthereumAddress(hex: address, eip55:false)// eip55 will be false for Portal MPC Wallets// Transform the token addresslet tokenContractAddress =tryEthereumAddress(hex: tokenAddress, eip55:true)// eip55 will be true for Token Contracts// Initialize the token contractlet contract = web3.eth.Contract( type: GenericERC20Contract.self, address: tokenContractAddress)// Get the correct decimal conversion for the UNI smart contractlet conversionFactor: Double=1e18// In the case of UNI it is 18.let value =BigUInt(amount * conversionFactor)// Generate the transaction for Portal to executeguardlet tx = contract .transfer(to: receiverAddress, value: value) .createTransaction( nonce:0, // Here you'd want to provide the actual nonce for this transaction gasPrice: EthereumQuantity(quantity:21.gwei), maxFeePerGas:nil, maxPriorityFeePerGas:nil, gasLimit:100_000, from: senderAddress, value:0, accessList: [:], transactionType: .legacy)else { return }Task {do {// Ethereum Sepolialet chainId ="eip155:11155111"// CAIP-2 format.// Obtain the eip155 address of the user's wallet.guardlet eip155Address =await portal.getAddress(chainId)else {throw PortalExampleAppError.addressNotFound() }// Create the transaction object.let transaction = ["from": eip155Address,"to": tokenAddress,"gasPrice": tx.gasPrice.hex(),"value":"0x0","data": tx.data.hex(), "nonce": tx.nonce.hex() ]// Make the eth_sendTransaction request.let requestResponse =tryawait portal.request( chainId, withMethod: .eth_sendTransaction, andParams: [transaction]) } } } catch {// Handle errors }}