Predicates in Matic Plasma


#1

This post highlights the implementation details of our predicate design. Our predicate design is heavily inspired from Understanding the Generalized Plasma Architecture and we thank the plasma group for the same. We recently published our Account based MoreVP specification. The linked post is a pre-requisite to understanding this post.

Note: withdrawManager is our term for what plasma group calls the commitment contract.

Predicate for ERC20/721 token transfer

The most relevant functions in the ERC20/721 predicates are startExit and verifyDeprecation. See IPredicate.sol.

The startExit function will be invoked when an exitor wants to start a MoreVP style exit (referencing the preceding reference transactions).

function startExit(bytes calldata data, bytes calldata exitTx) external {
  referenceTxData = decode(data)

  // Verify inclusion of reference tx in checkpoint / commitment
  // returns priority which is something like that defined in minimum viable plasma (blknum * 1000000000 + txindex * 10000 + logIndex)
  // Here, logIndex is the index of the log in the tx receipt.
  priority = withdrawManager.verifyInclusion(referenceTxData)

  // validate exitTx - This may be an in-flight tx, so inclusion will not be checked
  exitAmount = processExitTx(exitTx)

  // returns the balance of the party at the end of referenceTx - this is the "youngest input" to the exitTx
  closingBalance = processReferenceTx(referenceTxData)

  // The closing balance of the exitTx should be <= the referenced balance
  require(
    closingBalance >= exitAmount,
    "Exiting with more tokens than referenced"
  );

  withdrawManager.addExitToQueue(msg.sender, token, exitAmount, priority)
}

For challenging older state transitions, the predicate exposes verifyDeprecation function.

function verifyDeprecation(bytes calldata exit, bytes calldata challengeData) external returns (bool) {
  referenceTxData = decode(challengeData)

  Verify the signature on the referenceTxData.rawTx and the fact that rawTx calls some function in the associated contract on plasma chain that deprecates the state

  // Verify inclusion of challenge tx in checkpoint / commitment
  priorityOfChallengeTx = withdrawManager.verifyInclusion(referenceTxData)

  return priorityOfChallengeTx > exit.priority
}

Finally, the challengeExit function in withdrawManager is responsible for calling predicate.verifyDeprecation and cancel the exit if it returns true. See WithdrawManager.sol.

function challengeExit(uint256 exitId, uint256 inputId, bytes calldata challengeData) external {
  PlasmaExit storage exit = exits[exitId];
  Input storage input = exit.inputs[inputId];
  require(
    exit.token != address(0x0) && input.signer != address(0x0),
    "Invalid exit or input id"
  );
  bool isChallengeValid = IPredicate(exit.predicate).verifyDeprecation(
    encodeExit(exit),
    encodeInputUtxo(inputId, input),
    challengeData
  );
  if (isChallengeValid) {
    deleteExit(exitId);
    emit ExitCancelled(exitId);
  }
}

While this makes up the crux of our ERC20Predicate.sol logic, the actual implementation is much more involved and can be found in this pull request. We invite the plasma community to review the same and leave their precious feedback here or on the PR.


#2

This looks super great!! :smile:

One question I would have is about a change we have decided to make: after thinking about it, we realized that more complex predicates may have multiple conditions for deprecation. Thus, instead of having verifyDeprecation it may be better to have the deposit contract expose a deprecateExit function which requires that msg.sender == exit.state.predicateAddress. In this model, you call the a method in the predicate contract to deprecate, so that it can expose different functions for different deprecation conditions.

Iā€™d be curious to hear your thoughts on this having worked on the verifyDeprecation model :slight_smile:


#3

Yep, we definitely require multiple deprecation conditions. So the above flow seems good to me. This pattern would also be required to achieve predicate interoperability. Let me draw an example:

Assume there are 2 Dapps that you can interact with on a plasma chain -

  1. Swap token A for token B (Dapp 1)
  2. Stake token B to get token C (Dapp 2)

Now,
Tx a. I do a swap in Dapp 1, get 5 tokens of type B
Tx b. Then, I stake the 5 B type tokens obtained in DApp 2 to get token C on the plasma chain.

Now, I start an exit from the swap tx (tx a), claiming I had 5 B tokens by invoking Predicate1.startExit(data). This exit is invalid since I have already used those tokens in the staking app (tx b). - There needs to be a mechanism to challenge this exit.

If I write the challenge logic in Predicate2.verifyDeprecation() then predicate2 will need to understand that it deprecated the state that otherwise fits under the purview of predicate1.
If I write the challenge logic in Predicate1.verifyDeprecation() then predicate1 will need to understand that state is outdated but Dapp 2 caused that to happen. These are my concerns in predicate interoperability, so having several deprecation scenarios in predicate will help.

To combat this, we might need to be able to call startExit in a predicate and verifyDeprecation in another predicate in regard to a same exit.

More generally, I might have #x, #y and #z tokens (of same type) locked in dapp1, dapp2, dapp3 respectively, so I am thinking that it should be possible to addInputUtxo to a particular exit, where inputUtxo is your balance handled by a particular predicate. See addInput in our implementation.