Design of Generic Predicate Contracts

Status : Draft

Abstract : Proposing generic & simpler design of Predicate Contracts for ERC20, ERC721, ERC1155, which will be able to provide all of standard, mintable, burnable functionalities, while also allowing full-duplex arbitrary data passing during deposit & withdraw.

Motivation : Currently we’ve standard, mintable, burnable predicates for each of ERC20, ERC721, ERC1155, which allows developers to deposit, withdraw, mint on L2, burn on L2. For implementing single & batch withdraw of ERC721, ERC1155 tokens, predicates expect several event signatures in L2 transaction, which makes predicate logic repetitive & complex. A subset of those predicates also support uni-directional arbitrary data passing i.e. during exit.

Following proposed design can be used for writing generic predicates which will support all of aforementioned functionalities ( at cost of lower complexity ) along with bi-directional arbitrary data passing - giving developers more freedom in building multi-chain applications.

Specification : Predicates are expected to implement following interface

interface ITokenPredicate {

    function lockTokens(
        address depositor,
        address depositReceiver,
        address rootToken,
        bytes calldata depositData
    ) external;

    function exitTokens(
        address sender,
        address rootToken,
        bytes calldata logRLPList
    ) external;

}

GenericERC20Predicate’s lockTokens method is expected to deserialise depositData as below, where

  • amount : Tokens being deposited
  • data : Arbitrary length, byte serialised, application specific data
(uint256 amount, bytes memory data) = abi.decode(depositData, (uint256, bytes))

exitTokens expects to see only one event signature in logRLPList, otherwise it’ll revert

ExitERC20(address to, uint256 amount, bytes data)

It processes exit event as per following logic

if (to == address(0)) {
    // burnability
    ERC20.burn(address(this), amount, data);
    return;
}

uint256 balance = ERC20.balanceOf(address(this))
if (balance < amount) {
    // mintability
    ERC20.mint(address(this), amount - balance);
}

ERC20.transfer(to, amount, data);

GenericERC721Predicate allows batch deposit, while expecting depositData in form

  • ids : Array of token ids being deposited
  • data : Arbitrary length, byte serialised, application specific data
(uint256[] memory ids, bytes memory data) = abi.decode(depositData, (uint256[], bytes))

Same as GenericERC20Predicate, it supports only one event signature in exitTokens method

ExitERC721(address to, uint256[] ids, bytes data)

Logic flow to be followed during exit is quite same as GenericERC20Predicate

if (to == address(0)) {
    // burnability
    ERC721.burnBatch(address(this), ids, data);
    return;
}

for (uint256 i = 0; i < ids.length; i++) {
	if(ERC721.ownerOf(ids[i]) != address(this)) {
        // mintability
		ERC721.mint(address(this), ids[i]);
	}
}

ERC721.transferBatch(to, ids, data);

Very similarly GenericERC1155Predicate allows arbitrary data passing during batch deposit, where it deserialises depositData as below

(uint256[] memory ids, uint256[] memory amounts, bytes memory data) = abi.decode(depositData, (uint256[], uint256[], bytes))

Its exitTokens method only expects to see event signature

ExitERC1155(address to, uint256[] ids, uint256[] amounts, bytes data)

Otherwise it simply reverts.

Exit happens by following rule

if (to == address(0)) {
    // burnability
    ERC1155.burnBatch(address(this), ids, data);
    return;
}

for (uint256 i = 0; i < ids.length; i++) {

    uint256 balance = ERC1155.balanceOf(ids[i], address(this))
    if(balance < amounts[i]) {
		// mintability
        ERC1155.mint(address(this), amounts[i] - balance);
	}
}

ERC1155.transferBatch(to, ids, amounts, data);

Rationale : This predicate design attempts to lower cognitive load on both predicate developers & predicate users by encapsulating all functionalities into single predicate for each token type, while expecting predicate users to implement a subset of methods ( only those they need to ) on respective L1, L2 tokens.

Backwards Compatibility : These new predicates are incompatible with already mapped tokens, so they can’t just be remapped to use new predicates. Potential rewrite of those L1, L2 tokens are required for using new functionalities.

Security Considerations : This design doesn’t loosen security assumptions anyhow, it allows exiting from L2 only when supported event signature is produced & that event log is included in receiptRootHash submitted by L2 validators during checkpointing.

To make it clear, as this proposal attempts to simplify cross-chain communicating smart contract implementation, which lives on much higher level than validators, it doesn’t anyhow affect validator workflow.