MultiToken

1. Summary

MultiToken is a solidity library unifying ERC20, ERC721 & ERC1155 transfers by wrapping them into a MultiToken Asset struct.

MultiToken supports CryptoKitties.

  • If you want to use the MultiToken library in your solidity project, you can install our NPM package.

  • Run: npm install @pwnfinance/multitoken

3. Contract Details

  • MultiToken.sol contract is written in Solidity version 0.8.0 but can be compiled with any Solidity compiler version 0.8.*

Features

The library defines a token asset as a struct of token identifiers. It wraps transfer, allowance and balance check calls of the following token standards:

Unifying the function calls used within the PWN context so we don't have to worry about handling these token standards individually.

Functions

transferAssetFrom

Overview

Function for transfer calls on various token interfaces.

This function takes two arguments:

  • Asset memoryasset - Asset struct defining all necessary context of a token

  • addresssource - Source address

  • addressdest - Destination address

Implementation

function transferAssetFrom(Asset memory asset, address source, address dest) internal {
    _transferAssetFrom(asset, source, dest, false);
}
safeTransferAssetFrom

Overview

Function for safe transfer calls on various token interfaces.

This function takes two arguments:

  • Asset memoryasset - Asset struct defining all necessary context of a token

  • addresssource - Source address

  • addressdest - Destination address

Implementation

function safeTransferAssetFrom(Asset memory asset, address source, address dest) internal {
    _transferAssetFrom(asset, source, dest, true);
}
getTransferAmount

Overview

Getter function to get the maximum amount of the supplied asset that can be transferred.

This function takes one argument:

  • Asset memoryasset - Asset struct defining all necessary context of a token

Implementation

function getTransferAmount(Asset memory asset) internal pure returns (uint256) {
    if (asset.category == Category.ERC20)
        return asset.amount;
    else if (asset.category == Category.ERC1155 && asset.amount > 0)
        return asset.amount;
    else // Return 1 for ERC721, CryptoKitties and ERC1155 used as NFTs (amount = 0)
        return 1;
}
transferAssetFromCalldata

Overview

Function for transfer calls on various token interfaces that can also handle calldata.

This function takes two arguments:

  • Asset memoryasset - Asset struct defining all necessary context of a token

  • addresssource - Account/address that should initiate the transfer

  • addressdest - Destination address

  • boolfromSender - Boolean defining if msg.sender is the same as source

Implementation

function transferAssetFromCalldata(Asset memory asset, address source, address dest, bool fromSender) pure internal returns (bytes memory) {
    return _transferAssetFromCalldata(asset, source, dest, fromSender, false);
}
safeTransferAssetFromCalldata

Overview

Function for safe transfer calls on various token interfaces that can also handle calldata.

This function takes two arguments:

  • Asset memoryasset - Asset struct defining all necessary context of a token

  • addresssource - Account/address that should initiate the transfer

  • addressdest - Destination address

  • boolfromSender - Boolean defining if msg.sender is the same as source

Implementation

function safeTransferAssetFromCalldata(Asset memory asset, address source, address dest, bool fromSender) pure internal returns (bytes memory) {
    return _transferAssetFromCalldata(asset, source, dest, fromSender, true);
}
permit

Overview

Wrapper function to allow grating approval using permit signature for ERC-20 (see EIP-2612).

This function takes four arguments:

  • Asset memoryasset - Asset struct defining all necessary context of a token

  • addressowner - Address that signed the permit

  • addressspender - Address that is getting the approval to transfer the _asset

  • bytes memorypermitData - The Permit data itself. The data must include the permit deadline (uint256) and permit signature. The signature can be standard (65 bytes) or compact (64 bytes) defined in EIP-2098. Lastly, the deadline and signature should be pack encoded together.

Implementation

function permit(Asset memory asset, address owner, address spender, bytes memory permitData) internal {
    if (asset.category == Category.ERC20) {

        // Parse deadline and permit signature parameters
        uint256 deadline;
        bytes32 r;
        bytes32 s;
        uint8 v;

        // Parsing signature parameters used from OpenZeppelins ECDSA library
        // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/83277ff916ac4f58fec072b8f28a252c1245c2f1/contracts/utils/cryptography/ECDSA.sol

        // Deadline (32 bytes) + standard signature data (65 bytes) -> 97 bytes
        if (permitData.length == 97) {
            assembly {
                deadline := mload(add(permitData, 0x20))
                r := mload(add(permitData, 0x40))
                s := mload(add(permitData, 0x60))
                v := byte(0, mload(add(permitData, 0x80)))
            }
        }
        // Deadline (32 bytes) + compact signature data (64 bytes) -> 96 bytes
        else if (permitData.length == 96) {
            bytes32 vs;

            assembly {
                deadline := mload(add(permitData, 0x20))
                r := mload(add(permitData, 0x40))
                vs := mload(add(permitData, 0x60))
            }

            s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
            v = uint8((uint256(vs) >> 255) + 27);
        } else {
            revert("MultiToken::Permit: Invalid permit length");
        }

        // Call permit with parsed parameters
        IERC20Permit(asset.assetAddress).permit(owner, spender, asset.amount, deadline, v, r, s);

    } else {
        // Currently supporting only ERC20 signed approvals via ERC2612
        revert("MultiToken::Permit: Unsupported category");
    }
}
balanceOf

Overview

Function for checking balances on various token interfaces.

This function takes two arguments:

  • Asset memoryasset - Asset struct defining all necessary context of a token

  • addresstarget - Target address to be checked

Implementation

function balanceOf(Asset memory asset, address target) internal view returns (uint256) {
    if (asset.category == Category.ERC20) {
        return IERC20(asset.assetAddress).balanceOf(target);

    } else if (asset.category == Category.ERC721) {
        return IERC721(asset.assetAddress).ownerOf(asset.id) == target ? 1 : 0;

    } else if (asset.category == Category.ERC1155) {
        return IERC1155(asset.assetAddress).balanceOf(target, asset.id);

    } else if (asset.category == Category.CryptoKitties) {
        return ICryptoKitties(asset.assetAddress).ownerOf(asset.id) == target ? 1 : 0;

    } else {
        revert("MultiToken: Unsupported category");
    }
}
approveAsset

Overview

Function for approving calls on various token interfaces.

This function takes two arguments:

  • Asset memoryasset - Asset struct defining all necessary context of a token

  • addresstarget - Target address to be checked

Implementation

function approveAsset(Asset memory asset, address target) internal {
    if (asset.category == Category.ERC20) {
        IERC20(asset.assetAddress).safeApprove(target, asset.amount);

    } else if (asset.category == Category.ERC721) {
        IERC721(asset.assetAddress).approve(target, asset.id);

    } else if (asset.category == Category.ERC1155) {
        IERC1155(asset.assetAddress).setApprovalForAll(target, true);

    } else if (asset.category == Category.CryptoKitties) {
        ICryptoKitties(asset.assetAddress).approve(target, asset.id);

    } else {
        revert("MultiToken: Unsupported category");
    }
}
isValid

Overview

A view function to check the ID and amount values are valid for the asset category in the provided asset.

This function takes one argument:

  • Asset memoryasset - Asset struct defining all necessary context of a token

Implementation

function isValid(Asset memory asset) internal view returns (bool) {
    if (asset.category == Category.ERC20) {
        // Check format
        if (asset.id != 0)
            return false;

        // ERC20 has optional ERC165 implementation
        if (asset.assetAddress.supportsERC165()) {
            // If ERC20 implements ERC165, it has to return true for its interface id
            return asset.assetAddress.supportsERC165InterfaceUnchecked(ERC20_INTERFACE_ID);

        } else {
            // In case token doesn't implement ERC165, its safe to assume that provided category is correct,
            // because any other category have to implement ERC165.

            // Check that asset address is contract
            // Tip: asset address will return code length 0, if this code is called from the asset constructor
            return asset.assetAddress.code.length > 0;
        }

    } else if (asset.category == Category.ERC721) {
        // Check format
        if (asset.amount != 0)
            return false;

        // Check it's ERC721 via ERC165
        return asset.assetAddress.supportsInterface(ERC721_INTERFACE_ID);

    } else if (asset.category == Category.ERC1155) {
        // Check it's ERC1155 via ERC165
        return asset.assetAddress.supportsInterface(ERC1155_INTERFACE_ID);

    } else if (asset.category == Category.CryptoKitties) {
        // Check format
        if (asset.amount != 0)
            return false;

        // Check it's CryptoKitties via ERC165
        return asset.assetAddress.supportsInterface(CRYPTO_KITTIES_INTERFACE_ID);

    } else {
        revert("MultiToken: Unsupported category");
    }
}
isSameAs

Overview

A view function to compare two assets (ignoring their amounts). Returns true if the assets are the same.

This function takes two arguments:

  • Asset memoryasset - Asset struct defining the first token to compare

  • Asset memoryotherAsset - Asset struct defining the second token to compare

Implementation

function isSameAs(Asset memory asset, Asset memory otherAsset) internal pure returns (bool) {
    return
        asset.category == otherAsset.category &&
        asset.assetAddress == otherAsset.assetAddress &&
        asset.id == otherAsset.id;
}

Asset struct

Each asset is defined by the Asset struct and has the following properties:

TypeNameComment

category

Corresponding asset category

address

assetAddress

Address of the token contract defining the asset

uint256

id

TokenID of an NFT or 0

uint256

amount

Amount of fungible tokens or 0 -> 1

Events

The MultiToken library does not define any events or custom errors.

Last updated