Skip to main content

Command Palette

Search for a command to run...

[TWIL] Week of May 5, 2024

Updated
3 min read
[TWIL] Week of May 5, 2024

This week, I focused on fixing and refactoring smart contracts to lay the foundation for introducing royalties for artists and collectors. Let me briefly share a few things I've learned.

USDC Permit Domain Name

USDC supports ERC-2612, enabling USDC holders to approve a payout without requiring a native token as a transaction fee. I decided to utilize this feature for setting up an NFT purchasing process, but I faced an error message stating EIP2612: invalid signature on the mainnet. It functioned correctly on the testnet, so I wasn't certain about the cause of this problem until I examined their smart contracts on both the testnet and mainnet Etherscans:

If you select the name dropdown, you'll observe that Sepolia Etherscan presents "USDC" while the mainnet Etherscan displays "USD Coin". This distinction is crucial because the permit method utilizes the name field to authenticate the USDC holder's signature on-chain. My issue stemmed from using a constant string "USDC" instead of retrieving it directly from the contract. Therefore, if you encounter a similar problem, ensure to provide the accurate name based on the blockchain type.

const name = await getUsdcName(chainId);
const signature = await signTypedData({
    domain: {
      name,
      version,
      chainId,
      verifyingContract,
    },
    // ...
});

async function getUsdcName(chainId: number) {
    // Fetch the name value from the USDC contract
}

Typechain

Since I started developing smart contracts, I've been using Hardhat as a tool for deploying and managing smart contracts. However, it wasn't until this week that I discovered typechain. This amazing library automatically creates TypeScript types for each smart contract, making it very simple to know which types are required when working with smart contracts. One way to use this library is by creating a helper function for deploying a smart contract:

import { ethers } from "hardhat";
import { SmartContractA__factory } from "/path/to/typechain/dir";

async function deploySmartContractA(
    deployer: HardhatEthersSigner,
    ...params: Paramters<SmartContractA__factory>
) {
    const smartContractA = await new SmartContractA__factory(owner)
        .deploy(...params);
    await smartContractA.waitForDeployment();
    return smartContractA;
}

async test() {
    const [owner] = await ethers.getSigners();
    const args = [];
    const smartContractA = await deploySmartContractA(owner, ...args);

    // Assume SmartContractA has setOwner(address owner) external method
    await smartContractA.setOwner(owner.address);

    // This will be caught by a linter
    // since the argument type is already known
    await smartContractA.setOwner(owner);
}

Now you know precisely which types are needed for each method!

Smart Contract Size Limit

As I added more methods and tested a smart contract, I noticed all my test cases failing because of the same problem - ProviderError: Error: Transaction reverted: trying to deploy a contract whose code is too large. I soon realized that deploying the smart contract used up more than the default block gas limit (30,000,000). This meant I had to split it into multiple contracts.

I chose to create another contract with a specific purpose: a storage contract. It will store all the on-chain states related to a similar context, along with the getters and setters, but nothing more. Other contracts can access these states by calling the getters and setters through composition. Essentially, the storage contract functions like a database. Here's a simplified example:

contract Storage is OwnableUpgradeable {
    address public nft;

    function initialize(address nftAddr) public initializer {
        __Ownable_init();
        nft = nftAddr;
    }

    function setNft(address nftAddr) external onlyOwner {
        require(
            nftAddr != address(0),
            "NFT address cannot be the zero address"
        );
        nft = nftAddr;
    }
}

contract PaymentController is 
    OwnableUpgradeable, 
    ReentrancyGuardUpgradeable 
{
    Storage public controllerStorage;

    function initialize(address storageAddr) public initializer {
        __Ownable_init();
        controllerStorage = Storage(storageAddr);
    }

    function setControllerStorage(
        Storage newStorage
    ) external onlyOwner {
        require(
            address(newStorage) != address(0),
            "New storage address cannot be the zero address"
        );
        controllerStorage = newStorage;
    }

    function payNftOwner() external nonReentrant {
      require(address(this).balance >= 1 ether, "Not enough balance");
      address nftAddr = controllerStorage.nft();
      uint256 nftBalance = IERC721(nftAddr).balanceOf(msg.sender);
      if (nftBalance > 0) {
          (bool paid, ) = payable(msg.sender).call{value: 1 ether}("");
          require(paid, "Failed to pay");
      }
    }
}

As you can see, I connected these two contracts using composition instead of inheritance to make the Storage contract's states reusable.

More from this blog

// Sean's SWE Journey

19 posts

Love to learn, develop and share new ideas 👨‍💻