Skip to content

cartesi/rollups-contracts

Repository files navigation

Smart Contracts for Cartesi Rollups

This repository contains the on-chain part of Cartesi Rollups.

If you are interested in taking a look at the off-chain part, please, head over to cartesi/rollups-node.

Table of contents

Dependencies

Basic setup

This repository contains git submodules. In order to properly initialize them, please, run the following command.

git submodule update --init --recursive

This repository uses Yarn to manage JavaScript dependencies. In order to install them, please, run the following commands.

cd onchain/rollups
yarn install

Tests

If you plan to run the Forge tests, there still some setup left to do. Assuming you are on the onchain/rollups directory, and that Docker Engine is running on the background, you may run the following command. This command will build the Cartesi Machine image necessary to build the proofs.

yarn proofs:setup

Now, you may run the tests!

yarn test

From this point on, after any change in the source code, you can update the proofs before running the tests again with the following command.

yarn proofs:update

Documentation

ℹ️ Check the official Cartesi Rollups documentation website.

Cartesi Rollups is supported by several smart contracts, each with clear responsibilities and well-defined interfaces. The modules are depicted in the diagram below. The yellow boxes correspond to the core contracts, and the blue boxes correspond to externally-owned accounts (EOAs) and other contracts.

Loading
graph TD
    classDef core fill:#ffe95a,color:#000
    classDef external fill:#85b4ff,color:#000
    classDef hasLink text-decoration: underline

    InputBox[Input Box]:::core
    CartesiDApp[Cartesi DApp]:::core
    CartesiDAppFactory[Cartesi DApp Factory]:::core
    EtherPortal[Ether Portal]:::core
    ERC20Portal[ERC-20 Portal]:::core
    ERC721Portal[ERC-721 Portal]:::core
    ERC1155SinglePortal[ERC-1155 Single Transfer Portal]:::core
    ERC1155BatchPortal[ERC-1155 Batch Transfer Portal]:::core
    DAppAddressRelay[DApp Address Relay]:::core
    Consensus:::external

    ERC20[Any ERC-20 token]:::external
    ERC721[Any ERC-721 token]:::external
    ERC1155[Any ERC-1155 token]:::external
    DAppOwner[Cartesi DApp Owner]:::external
    Anyone1[Anyone]:::external
    Anyone2[Anyone]:::external
    Anyone3[Anyone]:::external

    Anyone1 -- executeVoucher --> CartesiDApp
    Anyone1 -. validateNotice .-> CartesiDApp
    Anyone1 -- newApplication --> CartesiDAppFactory
    DAppOwner -- migrateToConsensus ---> CartesiDApp
    CartesiDApp -. getClaim .-> Consensus
    CartesiDApp -- withdrawEther --> CartesiDApp
    CartesiDAppFactory == creates ==> CartesiDApp
    Anyone2 -- addInput -------> InputBox
    Anyone2 -- depositEther ---> EtherPortal
    EtherPortal -- "Ether transfer" ----> Anyone3
    EtherPortal -- addInput -----> InputBox
    Anyone2 -- depositERC20Tokens ---> ERC20Portal
    ERC20Portal -- transferFrom ----> ERC20
    ERC20Portal -- addInput -----> InputBox
    Anyone2 -- depositERC721Token ---> ERC721Portal
    ERC721Portal -- safeTransferFrom ----> ERC721
    ERC721Portal -- addInput -----> InputBox
    Anyone2 -- depositSingleERC1155Token ---> ERC1155SinglePortal
    ERC1155SinglePortal -- safeTransferFrom ----> ERC1155
    ERC1155SinglePortal -- addInput -----> InputBox
    Anyone2 -- depositBatchERC1155Token ---> ERC1155BatchPortal
    ERC1155BatchPortal -- safeBatchTransferFrom ----> ERC1155
    ERC1155BatchPortal -- addInput -----> InputBox
    Anyone2 -- relayDAppAddress ---> DAppAddressRelay
    DAppAddressRelay -- addInput -----> InputBox

    class ERC20,ERC721,ERC1155 hasLink
    click ERC20 href "https://eips.ethereum.org/EIPS/eip-20"
    click ERC721 href "https://eips.ethereum.org/EIPS/eip-721"
    click ERC1155 href "https://eips.ethereum.org/EIPS/eip-1155"

Input Box

This module is the one responsible for receiving inputs from users that want to interact with DApps. For each DApp, the module keeps an append-only list of hashes. Each hash is derived from the input and some metadata, such as the input sender, and the block timestamp. All the data needed to reconstruct a hash is available forever on-chain. As a result, one does not need to trust data providers in order to sync the off-chain machine with the latest input. Note that this module is completely permissionless, and we leave the off-chain machine to judge whether an input is valid or not.

Cartesi DApp

A Cartesi DApp contract, just like any other contract on Ethereum, has a unique address. With this address, a DApp can hold ownership over digital assets on the base layer like Ether, ERC-20 tokens, and NFTs. In the next sections, we'll explain how DApps are able to receive assets through portals, and perform arbitrary message calls, such as asset transfers, through vouchers.

Since there is no access control to execute a voucher, the caller must also provide a proof that such voucher was generated by the off-chain machine. This proof is checked on-chain against a claim, that is provided by the DApp's consensus. Therefore, a DApp must trust its consensus to only provide valid claims. However, if the consensus goes inactive or rogue, the DApp owner can migrate to a new consensus. In summary, DApp users must trust the DApp owner to choose a trustworthy consensus.

Cartesi DApp Factory

The Cartesi DApp Factory allows anyone to deploy Cartesi DApp contracts with a simple function call, costing only 3.5% more gas than deploying the DApp contract directly. It also provides greater convenience to the deployer, and security to users and validators, as they know the bytecode could not have been altered maliciously.

Portals

Portals, as the name suggests, are used to safely teleport assets from the base layer to the execution layer. It works in the following way. First, for some types of assets, the user has to allow the portal to deduct the asset(s) from their account. Second, the user tells the portal to transfer the asset(s) from their account to some DApp's account. The portal then adds an input to the DApp's input box to inform the machine of the transfer that just took place in the base layer. Finally, the off-chain machine is made aware of the transfer through the input sent by the portal. Note that the machine must know the address of the portal beforehand in order to validate such input.

The DApp developer can choose to do whatever they want with this information. For example, they might choose to create a wallet for each user in the execution layer, where assets can be managed at a much lower cost through inputs that are understood by the Linux logic. In this sense, one could think of the DApp contract as a wallet, owned by the off-chain machine. Anyone can deposit assets there but only the DApp—through vouchers—can decide on withdrawals.

The withdrawal process is quite simple from the user's perspective. Typically, the user would first send an input to the DApp requesting the withdrawal, which would then get processed and interpreted off-chain. If all goes well, the machine should generate a voucher that, once executed, transfers the asset(s) to the rightful recipient.

Currently, we support the following types of assets:

Input encodings for deposits

As explained above, in order to teleport an asset from the base layer to the execution layer, you need the corresponding portal to add an input to the DApp's input box, which will then be interpreted and validated by the off-chain machine. To do that, the machine will need to decode the input payload.

The input payloads for deposits are always specified as packed ABI-encoded parameters, as detailed below. In Solidity, packed ABI-encoding is denoted by abi.encodePacked(...) and standard ABI-encoded is denoted by abi.encode(...).

Asset Packed ABI-encoded payload fields Standard ABI-encoded payload fields
Ether
  • address sender,
  • uint256 value,
  • bytes execLayerData
none
ERC-20
  • bool success,
  • address token,
  • address sender,
  • uint256 amount,
  • bytes execLayerData
none
ERC-721
  • address token,
  • address sender,
  • uint256 tokenId,
  • standard ABI-encoded fields...
  • bytes baseLayerData,
  • bytes execLayerData
ERC-1155 (single)
  • address token,
  • address sender,
  • uint256 tokenId,
  • uint256 value,
  • standard ABI-encoded fields...
  • bytes baseLayerData,
  • bytes execLayerData
ERC-1155 (batch)
  • address token,
  • address sender,
  • standard ABI-encoded fields...
  • uint256[] tokenIds,
  • uint256[] values,
  • bytes baseLayerData,
  • bytes execLayerData

As an example, the deposit of 100 Wei (of Ether) sent by address 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 with data 0xabcd would result in the following input payload:

0xf39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000064abcd

Vouchers

Vouchers allow DApps in the execution layer to interact with contracts in the base layer through message calls. They are emitted by the off-chain machine, and executed by anyone in the base layer. Each voucher is composed of a destination address and a payload. In the case of vouchers destined to Solidity contracts, the payload generally encodes a function call.

A voucher can only be executed once the DApp's consensus submits a claim containing it. They can be executed in any order. Although the DApp contract is indifferent to the content of the voucher being executed, it enforces some sanity checks before allowing its execution. First, it checks whether the voucher has been successfully executed already. Second, it ensures that the voucher has been emitted by the off-chain machine, by requiring a validity proof.

Because of their generality, vouchers can be used in a wide range of applications: from withdrawing funds to providing liquidity in a DeFi protocol. Typically, DApps use vouchers to withdraw assets. Below, we show how vouchers can be used to withdraw several types of assets. You can find more information about a particular function by clicking on the 📄 emoji near it.

Asset Destination Function signature
Ether DApp contract withdrawEther(address,uint256) 📄
ERC-20 Token contract transfer(address,uint256) 📄
ERC-20 Token contract transferFrom(address,address,uint256) 📄 1
ERC-721 Token contract safeTransferFrom(address,address,uint256) 📄
ERC-721 Token contract safeTransferFrom(address,address,uint256,bytes) 📄 2
ERC-1155 Token contract safeTransferFrom(address,address,uint256,uint256,data) 📄
ERC-1155 Token contract safeBatchTransferFrom(address,address,uint256[],uint256[],data) 📄 3

Please note that the voucher payload should be encoded according to the Ethereum ABI specification for calling contract functions. As such, it should start with the first four bytes of the Keccak-256 hash of the function signature string (as given in the table above), followed by the ABI-encoded parameter values.

As an example, the voucher for a simple ERC-20 transfer (2nd line in the table above) to address 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 with amount 100 should specify the following payload:

0xa9059cbb000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000064

DApp Address Relay

In the previous section, we showed how vouchers can be used to withdraw different types of assets. Most of those vouchers contain the address of the DApp contract, either as the destination address or as a function argument. So, the off-chain machine needs to "know" the DApp contract address at some point. If the off-chain machine knew the DApp contract address from the beginning, it would create a cyclical dependency between the initial machine state hash (also called "template hash") and the DApp contract address. This is due to the fact that the address of a DApp contract depends on its construction arguments, which include the template hash; and that the template hash is the Merkle root of the machine address space, which includes the DApp contract address.

This "chicken-and-egg" problem is circumvented by a very small permissionless contract in the base layer, the DApp Address Relay (source). Its only job is to add an input to a DApp's input box with the DApp contract address. The off-chain machine then decodes this input and stores the address somewhere for future use. Just like in the case of portals, the machine must also know the address of the relay in order to validate the origin of the input.

Notices

Notices are informational statements that can be proved by contracts in the base layer. They're emitted by the off-chain machine and contain a payload, in bytes. DApp developers are free to explore different use cases for notices, their generality and negligible cost of emission makes them a powerful tool to assist integration between DApps and contracts or even other DApps. Similar to vouchers, notices can only be proved once they've been finalized on-chain and if they're accompanied by a validity proof. A chess DApp could, for example, emit a notice informing the underlying blockchain of the winner of a tournament. While that information is not necessarily "actionable", it could be used by other applications for different purposes.

Consensus

This module is responsible for providing valid claims to DApps after reaching some form of consensus. The module's interface aims to be as generic as possible to accommodate any consensus model, since there are plenty to choose from. The way claims are encoded and stored is abstracted entirely by the interface. Implementation-wise, this is left to a History contract.

The only type of consensus that is currently implemented by Cartesi is called Authority. It is owned by a single address, who has complete power over the consensus. It is arguably the simplest consensus to implement, although quite vulnerable.

History

The sole purpose of this module is to store claims and to allow them to be retrieved later. Just as with the consensus interface, we leave much of the details open for the implementation to define.

Our only implementation of history stores claims in a very simple manner: each claim is composed of an epoch hash and a range of input indices. Each DApp has its own append-only list of claims, where ranges don't overlap, and come one after the other. As a result, one cannot overwrite past claims, or skip inputs, or claim in a non-linear order.

Dispute Resolution

Disputes occur when two validators claim different state updates to the same epoch. Because of the deterministic nature of our virtual machine and the fact that the inputs that constitute an epoch are agreed upon beforehand, conflicting claims imply dishonest behavior. When a conflict occurs, the module that mediates the interactions between both validators is the dispute resolution.

The code for rollups dispute resolution is not being published yet, but a big part of it is available on the Cartesi Rollups SDK, using the Arbitration dlib

Experimenting

To get a taste of how to use Cartesi to develop your DApp, check the following resources: See Cartesi Rollups in action with the Simple Echo Examples in C++, JavaScript, Lua, Rust and Python. To have a glimpse of how to develop your DApp locally using your favorite IDE and tools check our Host Environment in the Rollups Examples repository.

Talk with us

If you're interested in developing with Cartesi, working with the team, or hanging out in our community, don't forget to join us on Discord and follow along.

Want to stay up to date? Make sure to join our announcements channel on Telegram or follow our Twitter.

Contributing

Thank you for your interest in Cartesi! Head over to our Contributing Guidelines for instructions on how to sign our Contributors Agreement and get started with Cartesi!

Please note we have a Code of Conduct, please follow it in all your interactions with the project.

License

Note: This component currently has dependencies that are licensed under the GNU GPL, version 3, and so you should treat this component as a whole as being under the GPL version 3. But all Cartesi-written code in this component is licensed under the Apache License, version 2, or a compatible permissive license, and can be used independently under the Apache v2 license. After this component is rewritten, the entire component will be released under the Apache v2 license. The arbitration d-lib repository and all contributions are licensed under GPL 3. Please review our COPYING file.

Footnotes

  1. If the DApp owns the tokens, prefer to use transfer(address,uint256)

  2. If no data is being passed as an argument, prefer to use safeTransferFrom(address,address,uint256)

  3. If only one token is being transferred, prefer to use safeTransferFrom(address,address,uint256,uint256,data)