Skip to content

Commit 569c624

Browse files
authored
Merge pull request #70 from maticnetwork/batch-withdraw-and-metadata-transfer
Batch deposit/ withdraw MintableERC721 & transfer arbitrary metadata for ERC721
2 parents c3a725a + 701a93b commit 569c624

17 files changed

+521
-55
lines changed

artifacts/ChildERC721.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

artifacts/ChildMintableERC721.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

artifacts/DummyERC721.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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"}]}
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"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setTokenMetadata","outputs":[],"stateMutability":"nonpayable","type":"function"}]}

contracts/child/ChildToken/ChildERC721.sol

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ contract ChildERC721 is
1818
// limit batching of tokens due to gas limit restrictions
1919
uint256 public constant BATCH_LIMIT = 20;
2020

21-
event WithdrawnBatch(
22-
address indexed user,
23-
uint256[] tokenIds
24-
);
21+
event WithdrawnBatch(address indexed user, uint256[] tokenIds);
22+
event TransferWithMetadata(address indexed from, address indexed to, uint256 indexed tokenId, bytes metaData);
2523

2624
constructor(
2725
string memory name_,
@@ -98,4 +96,41 @@ contract ChildERC721 is
9896
}
9997
emit WithdrawnBatch(_msgSender(), tokenIds);
10098
}
99+
100+
/**
101+
* @notice called when user wants to withdraw token back to root chain with arbitrary metadata
102+
* @dev Should handle withraw by burning user's token.
103+
*
104+
* This transaction will be verified when exiting on root chain
105+
*
106+
* @param tokenId tokenId to withdraw
107+
*/
108+
function withdrawWithMetadata(uint256 tokenId) external {
109+
110+
require(_msgSender() == ownerOf(tokenId), "ChildERC721: INVALID_TOKEN_OWNER");
111+
112+
// Encoding metadata associated with tokenId & emitting event
113+
emit TransferWithMetadata(_msgSender(), address(0), tokenId, this.encodeTokenMetadata(tokenId));
114+
115+
_burn(tokenId);
116+
117+
}
118+
119+
/**
120+
* @notice This method is supposed to be called by client when withdrawing token with metadata
121+
* and pass return value of this function as second paramter of `withdrawWithMetadata` method
122+
*
123+
* It can be overridden by clients to encode data in a different form, which needs to
124+
* be decoded back by them correctly during exiting
125+
*
126+
* @param tokenId Token for which URI to be fetched
127+
*/
128+
function encodeTokenMetadata(uint256 tokenId) external view virtual returns (bytes memory) {
129+
130+
// You're always free to change this default implementation
131+
// and pack more data in byte array which can be decoded back
132+
// in L1
133+
return abi.encode(tokenURI(tokenId));
134+
135+
}
101136
}

contracts/child/ChildToken/ChildMintableERC721.sol

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ contract ChildMintableERC721 is
1717
bytes32 public constant DEPOSITOR_ROLE = keccak256("DEPOSITOR_ROLE");
1818
mapping (uint256 => bool) public withdrawnTokens;
1919

20+
// limit batching of tokens due to gas limit restrictions
21+
uint256 public constant BATCH_LIMIT = 20;
22+
23+
event WithdrawnBatch(address indexed user, uint256[] tokenIds);
2024
event TransferWithMetadata(address indexed from, address indexed to, uint256 indexed tokenId, bytes metaData);
2125

2226
constructor(
@@ -44,20 +48,34 @@ contract ChildMintableERC721 is
4448
/**
4549
* @notice called when token is deposited on root chain
4650
* @dev Should be callable only by ChildChainManager
47-
* Should handle deposit by minting the required tokenId for user
51+
* Should handle deposit by minting the required tokenId(s) for user
4852
* Should set `withdrawnTokens` mapping to `false` for the tokenId being deposited
4953
* Minting can also be done by other functions
5054
* @param user user address for whom deposit is being done
51-
* @param depositData abi encoded tokenId
55+
* @param depositData abi encoded tokenIds. Batch deposit also supported.
5256
*/
5357
function deposit(address user, bytes calldata depositData)
5458
external
5559
override
5660
only(DEPOSITOR_ROLE)
5761
{
58-
uint256 tokenId = abi.decode(depositData, (uint256));
59-
withdrawnTokens[tokenId] = false;
60-
_mint(user, tokenId);
62+
63+
// deposit single
64+
if (depositData.length == 32) {
65+
uint256 tokenId = abi.decode(depositData, (uint256));
66+
withdrawnTokens[tokenId] = false;
67+
_mint(user, tokenId);
68+
69+
// deposit batch
70+
} else {
71+
uint256[] memory tokenIds = abi.decode(depositData, (uint256[]));
72+
uint256 length = tokenIds.length;
73+
for (uint256 i; i < length; i++) {
74+
withdrawnTokens[tokenIds[i]] = false;
75+
_mint(user, tokenIds[i]);
76+
}
77+
}
78+
6179
}
6280

6381
/**
@@ -73,15 +91,41 @@ contract ChildMintableERC721 is
7391
_burn(tokenId);
7492
}
7593

94+
/**
95+
* @notice called when user wants to withdraw multiple tokens back to root chain
96+
* @dev Should burn user's tokens. This transaction will be verified when exiting on root chain
97+
* @param tokenIds tokenId list to withdraw
98+
*/
99+
function withdrawBatch(uint256[] calldata tokenIds) external {
100+
101+
uint256 length = tokenIds.length;
102+
require(length <= BATCH_LIMIT, "ChildMintableERC721: EXCEEDS_BATCH_LIMIT");
103+
104+
// Iteratively burn ERC721 tokens, for performing
105+
// batch withdraw
106+
for (uint256 i; i < length; i++) {
107+
108+
uint256 tokenId = tokenIds[i];
109+
110+
require(_msgSender() == ownerOf(tokenId), string(abi.encodePacked("ChildMintableERC721: INVALID_TOKEN_OWNER ", tokenId)));
111+
withdrawnTokens[tokenId] = true;
112+
_burn(tokenId);
113+
114+
}
115+
116+
// At last emit this event, which will be used
117+
// in MintableERC721 predicate contract on L1
118+
// while verifying burn proof
119+
emit WithdrawnBatch(_msgSender(), tokenIds);
120+
121+
}
122+
76123
/**
77124
* @notice called when user wants to withdraw token back to root chain with token URI
78125
* @dev Should handle withraw by burning user's token.
79126
* Should set `withdrawnTokens` mapping to `true` for the tokenId being withdrawn
80127
* This transaction will be verified when exiting on root chain
81128
*
82-
* Before calling this function, you may want calling `encodeTokenMetadata`
83-
* and get metadata to be transferred from L2 to L1 during exit
84-
*
85129
* @param tokenId tokenId to withdraw
86130
*/
87131
function withdrawWithMetadata(uint256 tokenId) external {

contracts/root/RootToken/DummyERC721.sol

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ pragma solidity 0.6.6;
22

33
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
44
import {NativeMetaTransaction} from "../../common/NativeMetaTransaction.sol";
5+
import {IRootERC721} from "./IRootERC721.sol";
56
import {ContextMixin} from "../../common/ContextMixin.sol";
67

78
contract DummyERC721 is
89
ERC721,
910
NativeMetaTransaction,
11+
IRootERC721,
1012
ContextMixin
1113
{
1214
constructor(string memory name_, string memory symbol_)
@@ -20,6 +22,30 @@ contract DummyERC721 is
2022
_mint(_msgSender(), tokenId);
2123
}
2224

25+
/**
26+
* If you're attempting to bring metadata associated with token
27+
* from L2 to L1, you must implement this method
28+
*
29+
* To be invoked when attempting to exit ERC721 with metadata from L2
30+
*
31+
* `data` is nothing but arbitrary byte array which
32+
* is brought in L1, by event emitted in L2, during withdraw
33+
*
34+
* Make sure this method is always callable by Predicate contract
35+
* who will invoke it when attempting to exit with metadata
36+
*/
37+
function setTokenMetadata(uint256 tokenId, bytes calldata data) external override {
38+
// This function should decode metadata obtained from L2
39+
// and attempt to set it for this `tokenId`
40+
//
41+
// Following is just a default implementation, feel
42+
// free to define your own encoding/ decoding scheme
43+
// for L2 -> L1 token metadata transfer
44+
string memory uri = abi.decode(data, (string));
45+
46+
_setTokenURI(tokenId, uri);
47+
}
48+
2349
function _msgSender()
2450
internal
2551
override

0 commit comments

Comments
 (0)