Deploying and Interacting with EVM Contracts

Writing smart contract code is only the beginning if you’re trying to build anything onchain. You’ll need to deploy the contract onchain where you can interact with it and build further.

Deploying smart contracts on EVM (Ethereum Virtual Machine) chains is easy, and with the right tools, it gets easier. This article will walk you through deploying Solidity smart contracts on any EVM chain with Foundry and interacting with them with a client library.

Getting Started with Deploying Smart Contracts

You’ll learn how to deploy smart contracts by deploying a batch transfer smart contract. You’ll find this useful for airdropping tokens to multiple addresses. You’ll deploy the contract on the Base network and interact with it via the Geth (the Go Ethereum implementation) framework.

First, let’s set you up with the tools you’ll need to deploy smart contracts. There are many tools on the market, but after some research, I found Foundry attractive.

Foundry is a suite of tools for Ethereum development written in Rust, and you can run it anywhere. Install it with this command:

curl -L <https://foundry.paradigm.xyz> | bash

Once the installation is complete, execute the foundryup command to complete the installation.

foundryup

The foundryup command will set up forge (the smart contract development tool) and cast (a CLI tool for interacting with Ethereum).

Now, create a new directory for your project on your computer and initialize a new project with Foundry with this command:

forge init airdroped

Now, open up the airdroped project in your code editor of choice. You should have this setup with a README.md that provides rudimentary Foundry documentation.

Publishing Smart Contracts to EVM Networks

Create a Solidity file in the src directory for your smart contract and paste this program.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}

contract BatchTransfer {
    function batchTransfer(
        address token,
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external {
        require(recipients.length == amounts.length, "Array lengths do not match");

        for (uint256 i = 0; i < recipients.length; i++) {
            require(
                IERC20(token).transferFrom(msg.sender, recipients[i], amounts[i]),
                "Transfer failed"
            );
        }
    }
}

The IERC20 interface defines the signature for the transferFrom function, which lets the contract transfer tokens on behalf of users after they approve it.

The BatchTransfer contract handles ERC20 token transfers to multiple recipients in one transaction. The batch transfer function in the contract takes in the user’s address, the list of recipients, and the corresponding amounts they should receive. If the arrays have corresponding lengths, the contract transfers the specified amounts to the transferFrom function.

In the client implementation, the sender address must approve the token transfer for the contract to spend the tokens; that’s the only overhead, but the same amount in gas will be paid for any transfer amount.

Execute this command to deploy the contract on your EVM-compatible network of choice over the forge command.

forge create --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> <CONTRACT_PATH>:<CONTRACT_NAME>

The command compiles the specified smart contract, signs the deployment transaction with the private key for authentication, and sends it to the blockchain via the RPC URL, registering the contract at a unique address where you can interact.

Add a --broadcast flag to the command to broadcast it to the network and deploy the contract onchain.

forge create --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> <CONTRACT_PATH>:<CONTRACT_NAME> --broadcast

The command should return the transaction hash, deployment address, and other details when executing it. Head to a block explorer on the chain where you’ve deployed the contract to verify the deployment.

Congratulations! You’ve successfully deployed/published a smart contract. Now, you’ll need to interact with the contract using a client library.

Interacting with Smart Contracts

You can use Ethereum-supported client libraries to interact with smart contracts deployed on any EVM chain. In this tutorial, you’ll learn with Geth (Go-Ethereum) the official client implementation of Ethereum.

First, execute these commands in a new Go project to add go-ethereum as a dependency and install abigen, a command line tool for transpiling smart contract ABIs into Go interactive methods.

go get github.com/ethereum/go-ethereum
go install github.com/ethereum/go-ethereum/cmd/abigen@latest

You may also need to install the Solidity compiler if you don’t have the ABI and you need to generate it from the Solidity program

Execute this command to generate the ABI from the Solidity program after installing the Solidity compiler.

// code to transpile to Go
solc --abi --bin --optimize --overwrite -o build/ <filename>.sol

Here’s the ABI of the BatchTransfer smart contract.

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address[]",
        "name": "recipients",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "amounts",
        "type": "uint256[]"
      }
    ],
    "name": "batchTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Now, execute this command to generate the Go bindings for the smart contract’s functions.

abigen --abi <path-to-abi> --bin <path-to-bytecode> --pkg <package-name> --out <output-file.go>

You can view the Go code generated by executing the command in the same directory as your ABI. Do not edit the file; if you do, all changes will be lost.

Here’s a function that interacts with the batch transfer smart contact.

// BatchTransferAssets performs a batch transfer of assets on Ethereum.
func BatchTransferAssets(privateKeyHex, tokenAddress, contractAddress string, recipients []string, amounts []*big.Int, chainID *big.Int, rpcURL string) error {
    // Connect to the Ethereum client
    client, err := ethclient.Dial(rpcURL)
    if err != nil {
        return fmt.Errorf("failed to connect to Ethereum client: %w", err)
    }

    privateKey, err := crypto.HexToECDSA(privateKeyHex)
    if err != nil {
        return fmt.Errorf("failed to parse private key: %w", err)
    }

    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    if !ok {
        return errors.New("error casting public key to ECDSA")
    }

    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    if err != nil {
        return fmt.Errorf("failed to get nonce: %w", err)
    }
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        return fmt.Errorf("failed to get gas price: %w", err)
    }

    auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
    if err != nil {
        return fmt.Errorf("failed to create transaction signer: %w", err)
    }

    auth.Nonce = big.NewInt(int64(nonce))
    auth.Value = big.NewInt(0)      // Amount to send in wei (0 for token transfer)
    auth.GasLimit = uint64(3000000) // Gas limit
    auth.GasPrice = gasPrice

    batchContract, err := NewBatchtransfer(common.HexToAddress(contractAddress), client)
    if err != nil {
        return fmt.Errorf("failed to create contract instance: %w", err)
    }

    recipientAddresses := make([]common.Address, len(recipients))
    for i, addr := range recipients {
        recipientAddresses[i] = common.HexToAddress(addr)
    }

    tx, err := batchContract.BatchTransfer(auth, common.HexToAddress(tokenAddress), recipientAddresses, amounts)
    if err != nil {
        return fmt.Errorf("batch transfer failed: %w", err)
    }

    fmt.Printf("Batch transfer initiated. Transaction hash: %s\\n", tx.Hash().Hex())
    return nil
}

The function connects to Ethereum, signs a transaction, interacts with a batch transfer smart contract to send tokens to multiple recipients, and prints the transaction hash once it is successful.

The NewBatchtransfer creates a new instance of the smart contract, and the BatchTransfer method performs the batch transfer of tokens, taking the transaction signer (auth), the token contract address (tokenAddress), the list of recipient addresses (recipient addresses), and the list of amounts (amounts) to transfer.

Here’s a main function with the BatchTransferAssets function call and all variables instantiated properly

func main() {
    // Replace these values with your own configurations
    privateKeyHex := "YOUR_PRIVATE_KEY"                       // Replace with your private key
    tokenAddress := "0xTokenContractAddressHere"             // Replace with your token contract address
    contractAddress := "0xBatchTransferContractAddressHere"  // Replace with your batch transfer contract address
    rpcURL := "<https://mainnet.infura.io/v3/YOUR_INFURA_KEY>" // Replace with your Ethereum RPC endpoint
    chainID := big.NewInt(8453)                                // Mainnet ChainID

    // Define recipients and amounts
    recipients := []string{
        "0xRecipientAddress1",
        "0xRecipientAddress2",
        "0xRecipientAddress3",
    }

    amounts := []*big.Int{
        big.NewInt(1000000000000000000), // 1 token (assuming 18 decimals)
        big.NewInt(2000000000000000000), // 2 tokens
        big.NewInt(1500000000000000000), // 1.5 tokens
    }

    // Call the batch transfer function
    err := BatchTransferAssets(privateKeyHex, tokenAddress, contractAddress, recipients, amounts, chainID, rpcURL)
    if err != nil {
        log.Fatalf("Batch transfer failed: %v", err)
    }
}

On running the program, if everything is right, this is the output you should expect.

The code and processes in this article are excerpts from the airdroped project on GitHub. Feel free to play around with them and enjoy.

Conclusion

You’ve learned how to deploy smart contracts to the 60+ EVM L1 and L2 and how to interact with them from client implementations like Geth.

Your next steps could be anything from building more complex batch transfer mechanisms across other EVM and non-EVM compatible chains to building other smart contracts; whichever you choose, remember you can always lean back on the Web3Afrika community of devs if you get stuck.

0
Subscribe to my newsletter

Read articles from Ukeje Chukwemeriwo Goodness directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ukeje Chukwemeriwo Goodness
Ukeje Chukwemeriwo Goodness

Mechanical Engineering Student. Interested in Computational Sciences, Human Philosophy and Psychology.