Crosschain

Crosschain

EeseeAssetHub / EeseeAssetSpoke

!WARNING! While our EeseeAssetHub / EeseeAssetSpoke contracts support fee-on-transfer tokens, please never use ERC20s with deflationary mechanics, or you will lose them.

Architechure

Eesee utilizes crosschain architecture to save gas for our users. We use Chainlink CCIP or Axelar to do crosschain communications and wrap assets between all supported chains.

For our architecture we use the hub and spoke model, where EeseeAssetHub acts as a hub, and EeseeAssetSpokes on multiple EVM chains act as spokes. Because of this, it is easy for us to add or change EeseeAssetSpokes without having centralized access control.

EeseeAssetSpoke

EeseeAssetSpoke is used to wrap ERC721/ERC1155/ERC20/Native tokens to ERC1155 tokens on L2 chain. It can also be used to call arbitrary functions on L2 chain. This can be used to create new lots in eesee from any chain on L2. EeseeAssetSpoke acts as a vault, securely and trustlessly holding bridged assets until their corresponding wrapped asset on L2 is unwraped and burned.

EeseeAssetHub

EeseeAssetHub is a hub contract deployed on L2 chain that receives messages from EeseeAssetSpokes. EeseeAssetHub is also an ERC1155 contract, which mints new tokens whennever it receives a message.

Note that EeseeAssetHub does not have means of access control for received messages, which means anyone can mint tokens through it (even if they are not backed up by real liquidity). However, all tokens have the sender address and a chain stored in their tokenURI, which allows identifying counterfeit tokens minted by unauthentic senders.

!WARNING! Never send any tokens to EeseeAssetHub contract from an EOA, or by contract in a separate transaction or they will be lost. Only send funds to it if you spend them by calling unwrap() function in the same transaction.

Royalties

If an asset you are wrapping includes royalties, EeseeAssetSpoke, in conjunction with EeseeAssetHub, ensures that the wrapped ERC1155 tokens also collect the same amount of royalties for the same addresses, but on a different blockchain. However, please be aware that due to the nature of cross-chain messaging, it is not possible to update the royalties on the destination chain after the wrapping process has taken place. Therefore, if the royalties of the assets on the source chain are updated, the royalties of their wrapped counterparts on the destination chain may become different until the asset is unwrapped and wrapped back.

Bridged ERC-721 and ERC-1155 tokens from EeseeAssetSpoke to EeseeAssetHub will maintain the same token royalty data on the hub chain as what is listed on the spoke chain. However, the royalty receiver on the spoke chain may be unable to access the royalty fees on the hub chain. Specifically, if the royalty receiver is a contract on the spoke chain, there is a high probability that the exact contract is not deployed at the same address on the hub chain. In this case, the royalty fees are unclaimable by the royalty receiver, or the royalty fees are not collected at all during the token sale.

A General Flow

Wrapping

If a user intends to wrap and send their asset to L2 chain, they can call wrap function in EeseeAssetSpoke and pass the following parameters:

  • Asset[] assets - Array of assets to lock in this contract and wrap to L2 chain as ERC1155 tokens. To calculate tokenId before it was minted, simply call getTokenIdL2(Asset asset) function in EeseeAssetSpoke or getTokenId(AddressHashWithSource assetHashWithSource) in EeseeAssetHub.

  • Call[] crosschainCalls - Address + bytes payload of calls to make on the destination chain.

  • address fallbackRecipient - In case crosschainCalls revert on the destination chain, this is the address that will receive the tokens instead.

  • bytes additionalData - Additional data required for crosschain message. For Chainlink this would be abi.encode(['uint256'], [gasLimit]). For Axelar - 0x.

After the CCIP or Axelar has finished bridging the assets, it executes all functions provided in crosschainCalls and transfers all leftover tokens to a fallbackRecipient.

Unwrapping

Now, if a user wants to unwrap their tokens back, they have to call unwrap function with the following parameters:

  • uint256[] tokenIds - Array of tokenIds to burn and unwrap. All tokens must have the same source. Note that the caller must either own those tokens, or have them transfered to EeseeAssetHub contract beforehand. !WARNING! Never send any tokens to this contract from an EOA, or by contract in a separate transaction or they will be lost. Only send funds to this contract if you spend them by calling unwrap function in the same transaction.

  • uint256[] amounts - Amounts of tokens to burn and unwrap.

  • address recipient - The recipient of assets on destination chain.

  • bytes additionalData - Additional data required for crosschain message. For Chainlink this would be abi.encode(['uint256'], [gasLimit]). For Axelar - 0x.

Advanced Flow

Transfer wrapped asset to recipient via CCIP.

! Untested code, use with caution !


// Asset we would like to wrap and create lot with. Note that it needs to be already approved for use in eeseeAssetSpoke.
const asset = {
    token: NFT.address,
    tokenID: 1,
    amount: 1,
    assetType: 0,
    data: "0x"
}


// Get TokenID of wrapped asset on L2 chain.
const tokenIDL2 = await eeseeAssetSpoke.getTokenIdL2(asset)

// Transfer NFT to a recipient
transferNFTEncodedData = eeseeAssetHub.interface.encodeFunctionData('safeTransferFrom', [eeseeAssetHub.address, recipient, tokenIDL2, asset.amount, "0x"])
call = {
    target: eeseeAssetHub.address,
    callData: transferNFTEncodedData
}

gasLimit = 1000000 // Gas Limit for a call on destination chain
additionalData = abi.encode(['uint256'], [gasLimit])
await eeseeAssetSpoke.wrap(
    [asset],
    [call],
    recipient, // fallback recipient
    additionalData,
    {from: signer.address, value: gasPaid} // Gas can be calculated using simulations with eeseeAssetHub.ccipReceive().
)

List asset on eesee after wrapping it via CCIP

Users can utilize crosschain functionality together with Eesee.sol smart contract.

Example: Wrap asset and create lot in Eesee with it. ! Untested code, use with caution !


// Asset we would like to wrap and create lot with. Note that it needs to be already approved for use in eeseeAssetSpoke.
const asset = {
    token: NFT.address,
    tokenID: 1,
    amount: 1,
    assetType: 0,
    data: "0x"
}


// Get TokenID of wrapped asset on L2 chain.
const tokenIDL2 = await eeseeAssetSpoke.getTokenIdL2(asset)

// Describe the call that will be executed on L2 chain.
// First, we need to have wrapped NFT approved for use in Eesee contract.
approveNFTEncodedData = eeseeAssetHub.interface.encodeFunctionData('setApprovalForAll', [
    eesee.address, true
])
call1 = {
    target: eeseeAssetHub.address,
    callData: approveNFTEncodedData
}
// Then, we construct eesee createLot calldata.
createLotsEncodedData = eesee.interface.encodeFunctionData('createLots', [
    [{token: eeseeAssetHub.address, tokenID: tokenIDL2, amount: 1, assetType: 0, data:'0x'}], 
    [{totalTickets: 100, ticketPrice: 2, duration: 86400, owner: recipient, signer: eeseeAssetHub.address, signatureData: "0x"}],
    fee // Fee for eesee, usually 600
])
call2 = {
    target: eesee.address,
    callData: createLotsEncodedData
}

gasLimit = 1000000 // Gas Limit for a call on destination chain
additionalData = abi.encode(['uint256'], [gasLimit])
await eeseeAssetSpoke.wrap(
    [asset],
    [call1, call2],
    recipient, // fallback recipient
    additionalData,
    {from: signer.address, value: gasPaid} // Gas can be calculated using simulations with eeseeAssetHub.ccipReceive().
)

Collect and Unwrap via CCIP

Users can also collect assets from eesee and unwrap them to another chain in a single transaction by utilizing eesee's multicall functionality together with callExternal function.

Example: Claim asset and unwrap it to the source chain. ! Untested code, use with caution !

// Create multicall data.
// Receive asset for lot with id "2" and send it to eeseeAssetHub.
first = eesee.interface.encodeFunctionData('receiveAssets', [[2], eeseeAssetHub.address]);

gasLimit = 1000000 // Gas Limit for a call on destination chain
additionalData = abi.encode(['uint256'], [gasLimit])
// Create calldata for unwrap
unwrap = eeseeAssetHub.interface.encodeFunctionData('unwrap', [
    [99], // Id of token transfered from receiveAssets
    [1], // Amount f tokens transfered from receiveAssets
    recipient,
    additionalData
]);
// unwrap using callExternal function
second = eesee.interface.encodeFunctionData('callExternal', [eeseeAssetHub.address, unwrap]);

// Call multicall
await eeesee.multicall([first, second], {value: gasPrice}) // gasPaid will be spent fully on CCIP gas fees.

Error Handling and Asset Retrieval

Our team made sure that no assets will be lost if something unexpected happens. In case there is an error during the execution of crosschainCalls on EeseeAssetHub's chain, or the crosschain call execution did not spend all minted ERC1155 tokens, then all leftover ERC1155 tokens will be sent directly to the fallbackRecipient address. In case fallbackRecipient is unable to receive ERC1155 tokens minted by EeseeAssetHub, those tokens will get transfered to a special contract called EeseeVault, where they can be retrieved later by calling function unstuck(uint256[] calldata tokenIds, address recipient) from fallbackRecipient account. In case an asset got stuck because it failed to get transfered to the recipient during EeseeAssetHub => EeseeAssetSpoke call, the recipient can reclaim it in the EeseeAssetSpoke contract by calling function unstuck(bytes32 stuckAssetHash, address recipient).

Wrapped Token Data Resolution

To save on gas for data transfers between chains, all wrapped ERC1155 tokens in EeseeAssetHub collection have the same default image and minimal data. If you would like to retrieve underlying token address, tokenId, amount and assetType, you would have to:

  1. Call uri(uint256 tokenId) in EeseeAssetHub. You can also call assetHashesWithSources(uint256 tokenId) function directly and skip 2.

  2. Decode its base64 representation.

  3. Receive sourceChain sourceSpoke and assetHash parameters from the resulting JSON.

  4. Verify sourceChain and sourceSpoke to be authentic contracts build or approved by eesee team.

  5. Call assetsStorage(bytes32 assetHash) function in sourceSpoke on the sourceChain with the assetHash from the previous step. That would produce Asset struct with all the necessary data.

  6. In case the received asset is of ERC1155 or ERC721 asset types, you will be able to call token's tokenURI() to receive the token image.

EeseeExpress

While having EeseeAssetHub and EeseeAssetSpoke allows us to transfer any type of assets between chains, the transfers can take up to 30 minutes to process, which might be problematic for cases other than simply wrapping NFTs or creating lots on eesee. To address this problem, we implemented a contract called EeseeExpress, which acts as a wrapper for SquidRouter contract. The wrapper exists to add ERC-2612 permit and to allow transferring tokens to the contract before callBridgeCall function call. Using this contract, users can transfer ERC20 tokens (without wrapping) and arbitrary data for function calls in about 2-3 minutes.

EeseeExpress allows transfering predetermined tokens between chains. If used together with other DEXes it allows the transfer of almost any token on any chain.

!WARNING! Never send any tokens to this contract from an EOA, or by contract in a separate transaction or they will be lost. Only send funds to this contract if you spend them by calling callBridgeCall function in the same transaction.

Interface

To transfer ERC20 tokens and call arbitrary data on both chains, users have to call callBridgeCall function:

  • AddressWithChain squidRouter - Struct with destination chain and the address of SquidRouter on that chain.

  • string tokenToSymbol - Token to use in this crosschain transfer.

  • TokenData tokenFrom - Token to provide to this function to get swapped for tokenTo. Includes abi-encoded permit containing approveAmount, deadline, v, r and s. Set to empty bytes to skip permit.

  • ISquidMulticall.Call[] calls - Calls to execute on this chain to swap tokenFrom to tokenTo.

  • ISquidMulticall.Call[] crosschainCalls - Calls to execute on destination chain.

  • address refundRecipient - In case calls on destination chain fail, this is the address that will receive the tokens instead.

Advanced Actions

Wrap USDC to aUSDC, transfer and unwrap back to USDC

! Untested code, use with caution !

// Source calls
// Approve all USDC transfered to SquidMulticall in USDCWrapper (./contracts/test/USDCWrapper.sol).
approveEncodedData = USDC.interface.encodeFunctionData('approve', [USDCWrapper.address, 0])
payload = abi.encode(
    ['address', 'uint256'],
    [USDC.address, 1] // Get the balance of USDC.address and insert this balance in 2nd param of 'approve'
)

call1_source = {
    callType: 1, //FullTokenBalance
    target: USDC.address,
    value: 0,
    callData: approveEncodedData,
    payload: payload,
}

// Wrap USDC to aUSDC and transfer it to SquidRouter for use. Note that this step can be changed with any DEX you like.
wrapEncodedData = USDCWrapper.interface.encodeFunctionData('wrap', [0, squidRouterL1.address])
payload = abi.encode(
    ['address', 'uint256'],
    [USDC.address, 0] // Get the balance of USDC.address and insert this balance in 1st param of 'wrap'
)

call2_source = {
    callType: 1, //FullTokenBalance
    target: USDCWrapper.address,
    value: 0,
    callData: wrapEncodedData,
    payload: payload,
}

// Destination calls
// Approve all aUSDC transfered to SquidMulticall
// aUSDC - address of aUSDC on destination chain.
approveEncodedData = aUSDC.interface.encodeFunctionData('approve', [USDCWrapper.address, 0])
payload = abi.encode(
    ['address', 'uint256'],
    [aUSDC.address, 1] // Get the balance of aUSDC.address and insert this balance in 2nd param of 'approve'
)

call1_destination = {
    callType: 1, //FullTokenBalance
    target: aUSDC.address,
    value: 0,
    callData: approveEncodedData,
    payload: payload,
}

// Wrap aUSDC to USDC and transfer it all to the recipient. Note that this step can be changed with any DEX you like.
wrapEncodedData = USDCWrapper.interface.encodeFunctionData('unwrap', [0, recipient])
payload = abi.encode(
    ['address', 'uint256'],
    [aUSDC.address, 0] // Get the balance of aUSDC.address and insert this balance in 1st param of 'wrap'
)

call2_destination = {
    callType: 1, //FullTokenBalance
    target: USDCWrapper.address,
    value: 0,
    callData: wrapEncodedData,
    payload: payload,
}

await eeseeExpress.callBridgeCall(
    {
        chain: squidRouterChainL2, 
        _address: squidRouterL2.address
    },
    'aUSDC',
    {
        token: USDC.address,
        amount: 100,
        permit:'0x' // Provide your permit if needed
    },
    [call1_source, call2_source],
    [call1_destination, call2_destination],
    recipient // In case destination calls fail, transfer all aUSDC to this recipient instead.
)

Crosschain Aggregation

Remember we mentioned eesee can act as an NFT marketplace aggrgator? Well, now it can also act a crosschain aggregator using the following flow: ! Untested code, use with caution !

// Create multicall data.

// Transfer all tokens from squid to swap contract on destination chain
transferEncodedData = aUSDC.interface.encodeFunctionData('transferFrom', [squidRouterL2.address, swap.address, 0])

payload = abi.encode(
    ['address', 'uint256'],
    [aUSDC.address, 2] // Get the balance of aUSDC.address and insert this balance in 3rd param of 'transferFrom'
)

call1_destination = {
    callType: 1, //FullTokenBalance
    target: aUSDC.address,
    value: 0,
    callData: transferEncodedData,
    payload: payload,
}

// Generate swapData to swap aUSDC for the needed token on destination chain
swapData = await 1inch.swap('aUSDC', 'ETH', 1000)

// Create data to buy asset on another chain
swapCalldata = swap.interface.encodeFunctionData('swapTokensForAssets', [{
    swapData: swapData, 
    marketplaceRoutersData: [{
        router: openseaRouter.address, // EeseeOpenseaRouter on destination chain
        data: encodedBasicOrderData // abi-encoded Opensea order on destination chain
    }]
},
signer.address]); // Final receiver of assets

call2_destination = {
    callType: 0,
    target: swap.address,
    value: 0,
    callData: swapCalldata,
    payload: '0x'
}

// Create calldata for callBridgeCall
callBridgeCall = eeseeExpress.interface.encodeFunctionData('callBridgeCall', [
    {
        chain: squidRouterChainL2, 
        _address: squidRouterL2.address
    },
    'aUSDC',
    {
        token: USDC.address,
        amount: 100,
        permit:'0x' // Provide your permit if needed
    },
    [call1_source, call2_source], // See how to wrap from the previous example, or use custom DEX logic.
    [call1_destination, call2_destination], //Transfer to swap contract and swap for the needed NFT.
    recipient // In case destination calls fail, transfer all aUSDC to this recipient instead.
]);
// Receive tokens for lot with id "2" and send it to eeseeExpress.
first = eesee.interface.encodeFunctionData('receiveTokens', [[2], eeseeExpress.address]);
// callBridgeCall using callExternal function
second = eesee.interface.encodeFunctionData('callExternal', [eeseeExpress.address, callBridgeCall]);

// Call multicall
await eeesee.multicall([first, second], {value: gasPaid}) // gasPaid will be spent fully on Axelar gas fees.

Last updated