Skip to content

Commit

Permalink
Merge pull request #70 from maticnetwork/batch-withdraw-and-metadata-…
Browse files Browse the repository at this point in the history
…transfer

Batch deposit/ withdraw MintableERC721 & transfer arbitrary metadata for ERC721
  • Loading branch information
itzmeanjan committed Mar 12, 2021
2 parents c3a725a + 701a93b commit 569c624
Show file tree
Hide file tree
Showing 17 changed files with 521 additions and 55 deletions.
2 changes: 1 addition & 1 deletion artifacts/ChildERC721.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifacts/ChildMintableERC721.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifacts/DummyERC721.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"abi":[{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"userAddress","type":"address"},{"indexed":false,"internalType":"address payable","name":"relayerAddress","type":"address"},{"indexed":false,"internalType":"bytes","name":"functionSignature","type":"bytes"}],"name":"MetaTransactionExecuted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"ERC712_VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"userAddress","type":"address"},{"internalType":"bytes","name":"functionSignature","type":"bytes"},{"internalType":"bytes32","name":"sigR","type":"bytes32"},{"internalType":"bytes32","name":"sigS","type":"bytes32"},{"internalType":"uint8","name":"sigV","type":"uint8"}],"name":"executeMetaTransaction","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getDomainSeperator","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"nonce","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"}]}
{"abi":[{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"userAddress","type":"address"},{"indexed":false,"internalType":"address payable","name":"relayerAddress","type":"address"},{"indexed":false,"internalType":"bytes","name":"functionSignature","type":"bytes"}],"name":"MetaTransactionExecuted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"ERC712_VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"userAddress","type":"address"},{"internalType":"bytes","name":"functionSignature","type":"bytes"},{"internalType":"bytes32","name":"sigR","type":"bytes32"},{"internalType":"bytes32","name":"sigS","type":"bytes32"},{"internalType":"uint8","name":"sigV","type":"uint8"}],"name":"executeMetaTransaction","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getDomainSeperator","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"nonce","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setTokenMetadata","outputs":[],"stateMutability":"nonpayable","type":"function"}]}
43 changes: 39 additions & 4 deletions contracts/child/ChildToken/ChildERC721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ contract ChildERC721 is
// limit batching of tokens due to gas limit restrictions
uint256 public constant BATCH_LIMIT = 20;

event WithdrawnBatch(
address indexed user,
uint256[] tokenIds
);
event WithdrawnBatch(address indexed user, uint256[] tokenIds);
event TransferWithMetadata(address indexed from, address indexed to, uint256 indexed tokenId, bytes metaData);

constructor(
string memory name_,
Expand Down Expand Up @@ -98,4 +96,41 @@ contract ChildERC721 is
}
emit WithdrawnBatch(_msgSender(), tokenIds);
}

/**
* @notice called when user wants to withdraw token back to root chain with arbitrary metadata
* @dev Should handle withraw by burning user's token.
*
* This transaction will be verified when exiting on root chain
*
* @param tokenId tokenId to withdraw
*/
function withdrawWithMetadata(uint256 tokenId) external {

require(_msgSender() == ownerOf(tokenId), "ChildERC721: INVALID_TOKEN_OWNER");

// Encoding metadata associated with tokenId & emitting event
emit TransferWithMetadata(_msgSender(), address(0), tokenId, this.encodeTokenMetadata(tokenId));

_burn(tokenId);

}

/**
* @notice This method is supposed to be called by client when withdrawing token with metadata
* and pass return value of this function as second paramter of `withdrawWithMetadata` method
*
* It can be overridden by clients to encode data in a different form, which needs to
* be decoded back by them correctly during exiting
*
* @param tokenId Token for which URI to be fetched
*/
function encodeTokenMetadata(uint256 tokenId) external view virtual returns (bytes memory) {

// You're always free to change this default implementation
// and pack more data in byte array which can be decoded back
// in L1
return abi.encode(tokenURI(tokenId));

}
}
60 changes: 52 additions & 8 deletions contracts/child/ChildToken/ChildMintableERC721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ contract ChildMintableERC721 is
bytes32 public constant DEPOSITOR_ROLE = keccak256("DEPOSITOR_ROLE");
mapping (uint256 => bool) public withdrawnTokens;

// limit batching of tokens due to gas limit restrictions
uint256 public constant BATCH_LIMIT = 20;

event WithdrawnBatch(address indexed user, uint256[] tokenIds);
event TransferWithMetadata(address indexed from, address indexed to, uint256 indexed tokenId, bytes metaData);

constructor(
Expand Down Expand Up @@ -44,20 +48,34 @@ contract ChildMintableERC721 is
/**
* @notice called when token is deposited on root chain
* @dev Should be callable only by ChildChainManager
* Should handle deposit by minting the required tokenId for user
* Should handle deposit by minting the required tokenId(s) for user
* Should set `withdrawnTokens` mapping to `false` for the tokenId being deposited
* Minting can also be done by other functions
* @param user user address for whom deposit is being done
* @param depositData abi encoded tokenId
* @param depositData abi encoded tokenIds. Batch deposit also supported.
*/
function deposit(address user, bytes calldata depositData)
external
override
only(DEPOSITOR_ROLE)
{
uint256 tokenId = abi.decode(depositData, (uint256));
withdrawnTokens[tokenId] = false;
_mint(user, tokenId);

// deposit single
if (depositData.length == 32) {
uint256 tokenId = abi.decode(depositData, (uint256));
withdrawnTokens[tokenId] = false;
_mint(user, tokenId);

// deposit batch
} else {
uint256[] memory tokenIds = abi.decode(depositData, (uint256[]));
uint256 length = tokenIds.length;
for (uint256 i; i < length; i++) {
withdrawnTokens[tokenIds[i]] = false;
_mint(user, tokenIds[i]);
}
}

}

/**
Expand All @@ -73,15 +91,41 @@ contract ChildMintableERC721 is
_burn(tokenId);
}

/**
* @notice called when user wants to withdraw multiple tokens back to root chain
* @dev Should burn user's tokens. This transaction will be verified when exiting on root chain
* @param tokenIds tokenId list to withdraw
*/
function withdrawBatch(uint256[] calldata tokenIds) external {

uint256 length = tokenIds.length;
require(length <= BATCH_LIMIT, "ChildMintableERC721: EXCEEDS_BATCH_LIMIT");

// Iteratively burn ERC721 tokens, for performing
// batch withdraw
for (uint256 i; i < length; i++) {

uint256 tokenId = tokenIds[i];

require(_msgSender() == ownerOf(tokenId), string(abi.encodePacked("ChildMintableERC721: INVALID_TOKEN_OWNER ", tokenId)));
withdrawnTokens[tokenId] = true;
_burn(tokenId);

}

// At last emit this event, which will be used
// in MintableERC721 predicate contract on L1
// while verifying burn proof
emit WithdrawnBatch(_msgSender(), tokenIds);

}

/**
* @notice called when user wants to withdraw token back to root chain with token URI
* @dev Should handle withraw by burning user's token.
* Should set `withdrawnTokens` mapping to `true` for the tokenId being withdrawn
* This transaction will be verified when exiting on root chain
*
* Before calling this function, you may want calling `encodeTokenMetadata`
* and get metadata to be transferred from L2 to L1 during exit
*
* @param tokenId tokenId to withdraw
*/
function withdrawWithMetadata(uint256 tokenId) external {
Expand Down
26 changes: 26 additions & 0 deletions contracts/root/RootToken/DummyERC721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ pragma solidity 0.6.6;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {NativeMetaTransaction} from "../../common/NativeMetaTransaction.sol";
import {IRootERC721} from "./IRootERC721.sol";
import {ContextMixin} from "../../common/ContextMixin.sol";

contract DummyERC721 is
ERC721,
NativeMetaTransaction,
IRootERC721,
ContextMixin
{
constructor(string memory name_, string memory symbol_)
Expand All @@ -20,6 +22,30 @@ contract DummyERC721 is
_mint(_msgSender(), tokenId);
}

/**
* If you're attempting to bring metadata associated with token
* from L2 to L1, you must implement this method
*
* To be invoked when attempting to exit ERC721 with metadata from L2
*
* `data` is nothing but arbitrary byte array which
* is brought in L1, by event emitted in L2, during withdraw
*
* Make sure this method is always callable by Predicate contract
* who will invoke it when attempting to exit with metadata
*/
function setTokenMetadata(uint256 tokenId, bytes calldata data) external override {
// This function should decode metadata obtained from L2
// and attempt to set it for this `tokenId`
//
// Following is just a default implementation, feel
// free to define your own encoding/ decoding scheme
// for L2 -> L1 token metadata transfer
string memory uri = abi.decode(data, (string));

_setTokenURI(tokenId, uri);
}

function _msgSender()
internal
override
Expand Down
Loading

0 comments on commit 569c624

Please sign in to comment.