Example 2: NFT/FT Atomic Swap

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.

We will implement ZKPTLC on Plasma Next for Atomic Swaps between ETH on Plasma and an NFT on L1. ZKPTLC verifies that the Merkle Proof has been passed from the Operator to Bob. This allows for two transfers. One is a payment of ETH from Alice to the Operator through a payment channel. The other is the transfer of an NFT from Bob to Alice on L1. The transfer of the NFT is done using a deposit & withdraw method because if the Operator does not pass the Merkle Proof to Bob, the transfer of the NFT must be cancelled.

Let's calculate the instance. The instance must include information such as the type of NFT and details of the transaction, like who is sending to whom. It should also include a transfer on the PN from the Operator to Bob.

function computeInstance(
  address from,
  address to,
  address nftContract,
  uint256 tokenId,
  ITransfer.Transfer memory transfer
) public pure returns (bytes32) {
  bytes32 tc = transfer.transferCommitment();
  return keccak256(abi.encodePacked(tc, from, to, nftContract, tokenId));
}

We will implement the deposit function for an NFT.

function deposit(
    address to,
    address nftContract,
    uint256 tokenId,
    ITransfer.Transfer memory transfer
) external {
    bytes32 instance = computeInstance(
        msg.sender,
        to,
        nftContract,
        tokenId,
        transfer
    );
    require(instanceSetAt[instance] == 0, "Duplicate instance");
    IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
    instanceSetAt[instance] = block.timestamp;
}

Here, instanceSetAt is a mapping that manages when the deposit was made.

mapping(bytes32 => uint256) public instanceSetAt;

Withdrawal can be performed by submitting an evidenceMerkleProof which provides evidence that a transfer has been made from the Operator to Bob.

function withdraw(
    address from,
    address nftContract,
    uint256 tokenId,
    ITransfer.Transfer memory transfer,
    IMerkleProof.EvidenceWithMerkleProof memory proof
) external {
    bytes32 instance = computeInstance(
        from,
        msg.sender,
        nftContract,
        tokenId,
        transfer
    );
    require(instanceSetAt[instance] != 0, "Instance does not exist");
    instanceSetAt[instance] = 0;
    _verifyExistence(transfer, proof);
    IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
}

If a certain amount of time has elapsed since the deposit, Bob will be able to cancel. The cancellation period needs to be longer than the three-day challenge period of Plasma Next's withdraw request. This is to ensure that if the Operator withholds the Merkle Proof from Bob and challenges Alice's withdraw request at the very end of the challenge period, thus revealing the Merkle Proof to Bob, Alice can acquire Carol's NFT.

function cancell(
    address to,
    address nftContract,
    uint256 tokenId,
    ITransfer.Transfer memory transfer
) external {
    bytes32 instance = computeInstance(
        msg.sender,
        to,
        nftContract,
        tokenId,
        transfer
    );
    require(instanceSetAt[instance] != 0, "Instance does not exist");
    require(
        instanceSetAt[instance] + 4 days < block.timestamp,
        "Instance is not expired"
    );
    instanceSetAt[instance] = 0;
    IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
}

The verifyCondition simply ensures that a transfer from the Operator to Bob exists on Plasma Next, just like the default ZKPTLC.

struct Witness {
    ITransfer.Transfer transfer;
    IMerkleProof.EvidenceWithMerkleProof proof;
    address from;
    address to;
    address nftContract;
    uint256 tokenId;
}

function verifyCondition(
    bytes32 instance,
    bytes memory witness
) external view {
    Witness memory w = abi.decode(witness, (Witness));
    bytes32 expectedInstance = computeInstance(
        w.from,
        w.to,
        w.nftContract,
        w.tokenId,
        w.transfer
    );
    if (instance != expectedInstance) {
        revert("Invalid instance");
    }
    _verifyExistence(w.transfer, w.proof);
}

The entire contract is as follows.

The entire contract of NFT/FT atomic swap
/// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

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 NFTAtomicSwap {
    using TransferLib for ITransfer.Transfer;

    address public rootManagerAddress;
    mapping(bytes32 => uint256) public instanceSetAt;

    constructor(address _rootManagerAddress) {
        rootManagerAddress = _rootManagerAddress;
    }

    function computeInstance(
        address from,
        address to,
        address nftContract,
        uint256 tokenId,
        ITransfer.Transfer memory transfer
    ) public pure returns (bytes32) {
        bytes32 tc = transfer.transferCommitment();
        return keccak256(abi.encodePacked(tc, from, to, nftContract, tokenId));
    }

    function deposit(
        address to,
        address nftContract,
        uint256 tokenId,
        ITransfer.Transfer memory transfer
    ) external {
        bytes32 instance = computeInstance(
            msg.sender,
            to,
            nftContract,
            tokenId,
            transfer
        );
        require(instanceSetAt[instance] == 0, "Duplicate instance");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        instanceSetAt[instance] = block.timestamp;
    }

    function withdraw(
        address from,
        address nftContract,
        uint256 tokenId,
        ITransfer.Transfer memory transfer,
        IMerkleProof.EvidenceWithMerkleProof memory proof
    ) external {
        bytes32 instance = computeInstance(
            from,
            msg.sender,
            nftContract,
            tokenId,
            transfer
        );
        require(instanceSetAt[instance] != 0, "Instance does not exist");
        instanceSetAt[instance] = 0;
        _verifyExistence(transfer, proof);
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
    }

    function cancell(
        address to,
        address nftContract,
        uint256 tokenId,
        ITransfer.Transfer memory transfer
    ) external {
        bytes32 instance = computeInstance(
            msg.sender,
            to,
            nftContract,
            tokenId,
            transfer
        );
        require(instanceSetAt[instance] != 0, "Instance does not exist");
        require(
            instanceSetAt[instance] + 4 days < block.timestamp,
            "Instance is not expired"
        );
        instanceSetAt[instance] = 0;
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
    }

    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 transfer;
        IMerkleProof.EvidenceWithMerkleProof proof;
        address from;
        address to;
        address nftContract;
        uint256 tokenId;
    }

    function verifyCondition(
        bytes32 instance,
        bytes memory witness
    ) external view {
        Witness memory w = abi.decode(witness, (Witness));
        bytes32 expectedInstance = computeInstance(
            w.from,
            w.to,
            w.nftContract,
            w.tokenId,
            w.transfer
        );
        if (instance != expectedInstance) {
            revert("Invalid instance");
        }
        _verifyExistence(w.transfer, w.proof);
    }
}

Last updated