Example 1: Atomic Swap on Plasma Next

Programmability support in the Plasma Next Mainnet is currently under development. The contents of this document cannot be tested on the Mainnet at present. Please wait until the support is fully implemented.

In this example, we consider a scenario where Alice, who holds ETH on Plasma Next, conducts an Atomic Swap with Bob, who holds DAI on the same platform. The flow of ETH and DAI follows the diagram below.

Here, we discuss the ZKPTLC for transfers from Alice to the operator. The ZKPTLC for transfers from the operator to Bob is exactly the same. The ZKPTLC for transfers from Alice to the operator is conditioned on two airdrops: one is an airdrop of ETH from the operator to Bob, and the other is an airdrop of DAI from the operator to Alice. Illustrated, this would look as follows: compared to Plasma Next's Default ZKPTLC, there is one additional condition related to the airdrops.

The instance of the ZKPTLC must be bound to the transfer from Operator to Bob and the transfer from Operator to Alice. To generalize, let's call them transfer1 and transfer2, respectively. The instance should be the hash values of these two transfers.

function computeInstance(
  ITransfer.Transfer memory transfer1,
  ITransfer.Transfer memory transfer2
) external pure returns (bytes32) {
  bytes32 tc1 = transfer1.transferCommitment();
  bytes32 tc2 = transfer2.transferCommitment();
  return keccak256(abi.encodePacked(tc1, tc2));
}

We use the same _verifyExistence function as the default ZKPTLC. This internal function verifies that the given transfer exists in Plasma Next.

function _verifyExistence(
    ITransfer.Transfer memory transfer,
    IMerkleProof.EvidenceWithMerkleProof memory proof
) internal view {
    if (transfer.transferCommitment() != proof.leaf.transferCommitment) {
        revert("Transfer commitment does not match");
    }
    IRootManager(rootManagerAddress).verifyEvidenceMerkleProof(proof);
}

Let's implement VerifyCondition. The witness consists of transfer1, transfer2, and their corresponding settlement proofs proof1, proof2. Decode the witness from bytes, ensure it matches the instance, and then pass it through the _verifyExistence function we implemented earlier.

struct Witness {
    ITransfer.Transfer transfer1;
    ITransfer.Transfer transfer2;
    IMerkleProof.EvidenceWithMerkleProof proof1;
    IMerkleProof.EvidenceWithMerkleProof proof2;
}

function verifyCondition(
    bytes32 instance,
    bytes memory witness
) external view {
    Witness memory w = abi.decode(witness, (Witness));
    bytes32 expectedInstance = computeInstance(w.transfer1, w.transfer2);
    if (instance != expectedInstance) {
        revert("Invalid instance");
    }
    _verifyExistence(w.transfer1, w.proof1);
    _verifyExistence(w.transfer2, w.proof2);
}

The entire contract is as follows.

The entire contract of Atomic swap on Plasma Next
/// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {IRootManager} from "../root-manager/IRootManager.sol";
import {ITransfer} from "../common-interface/ITransfer.sol";
import {IMerkleProof} from "../common-interface/IMerkleProof.sol";
import {TransferLib} from "../utils/TransferLib.sol";

contract TwoWaySwap {
    using TransferLib for ITransfer.Transfer;

    address public rootManagerAddress;

    constructor(address _rootManagerAddress) {
        rootManagerAddress = _rootManagerAddress;
    }

    function computeInstance(
        ITransfer.Transfer memory transfer1,
        ITransfer.Transfer memory transfer2
    ) public pure returns (bytes32) {
        bytes32 tc1 = transfer1.transferCommitment();
        bytes32 tc2 = transfer2.transferCommitment();
        return keccak256(abi.encodePacked(tc1, tc2));
    }

    function _verifyExistence(
        ITransfer.Transfer memory transfer,
        IMerkleProof.EvidenceWithMerkleProof memory proof
    ) internal view {
        if (transfer.transferCommitment() != proof.leaf.transferCommitment) {
            revert("Transfer commitment does not match");
        }
        IRootManager(rootManagerAddress).verifyEvidenceMerkleProof(proof);
    }

    struct Witness {
        ITransfer.Transfer transfer1;
        ITransfer.Transfer transfer2;
        IMerkleProof.EvidenceWithMerkleProof proof1;
        IMerkleProof.EvidenceWithMerkleProof proof2;
    }

    function verifyCondition(
        bytes32 instance,
        bytes memory witness
    ) external view {
        Witness memory w = abi.decode(witness, (Witness));
        bytes32 expectedInstance = computeInstance(w.transfer1, w.transfer2);
        if (instance != expectedInstance) {
            revert("Invalid instance");
        }
        _verifyExistence(w.transfer1, w.proof1);
        _verifyExistence(w.transfer2, w.proof2);
    }
}

Last updated