ERC1155 Multi-Token¶
Warning
This is example code for learning purposes. Do not use in production without thorough review and testing.
ERC1155 supports both fungible and non-fungible tokens in a single contract. This implementation includes ownership and pause functionality.
1#pragma version >0.3.10
2
3###########################################################################
4## THIS IS EXAMPLE CODE, NOT MEANT TO BE USED IN PRODUCTION! CAVEAT EMPTOR!
5###########################################################################
6
7"""
8@dev example implementation of ERC-1155 non-fungible token standard ownable, with approval, OPENSEA compatible (name, symbol)
9@author Dr. Pixel (github: @Doc-Pixel)
10"""
11
12############### imports ###############
13from ethereum.ercs import IERC165
14
15############### variables ###############
16# maximum items in a batch call. Set to 128, to be determined what the practical limits are.
17BATCH_SIZE: constant(uint256) = 128
18
19# callback number of bytes
20CALLBACK_NUMBYTES: constant(uint256) = 4096
21
22# URI length set to 300.
23MAX_URI_LENGTH: constant(uint256) = 300
24# for uint2str / dynamic URI
25MAX_DYNURI_LENGTH: constant(uint256) = 78
26# for the .json extension on the URL
27MAX_EXTENSION_LENGTH: constant(uint256) = 5
28
29MAX_URL_LENGTH: constant(uint256) = MAX_URI_LENGTH+MAX_DYNURI_LENGTH+MAX_EXTENSION_LENGTH # dynamic URI status
30dynamicUri: bool
31
32# the contract owner
33# not part of the core spec but a common feature for NFT projects
34owner: public(address)
35
36# pause status True / False
37# not part of the core spec but a common feature for NFT projects
38paused: public(bool)
39
40# the contracts URI to find the metadata
41baseuri: String[MAX_URI_LENGTH]
42contractURI: public(String[MAX_URI_LENGTH])
43
44# Name and symbol are not part of the ERC1155 standard. For opensea compatibility
45name: public(String[128])
46symbol: public(String[16])
47
48# Interface IDs
49ERC165_INTERFACE_ID: constant(bytes4) = 0x01ffc9a7
50ERC1155_INTERFACE_ID: constant(bytes4) = 0xd9b67a26
51ERC1155_INTERFACE_ID_METADATA: constant(bytes4) = 0x0e89341c
52
53# mappings
54
55# Mapping from token ID to account balances
56balanceOf: public(HashMap[address, HashMap[uint256, uint256]])
57
58# Mapping from account to operator approvals
59isApprovedForAll: public( HashMap[address, HashMap[address, bool]])
60
61############### events ###############
62event Paused:
63 # Emits a pause event with the address that paused the contract
64 account: address
65
66event unPaused:
67 # Emits an unpause event with the address that paused the contract
68 account: address
69
70event OwnershipTransferred:
71 # Emits smart contract ownership transfer from current to new owner
72 previousOwner: address
73 newOwner: address
74
75event TransferSingle:
76 # Emits on transfer of a single token
77 operator: indexed(address)
78 fromAddress: indexed(address)
79 to: indexed(address)
80 id: uint256
81 value: uint256
82
83event TransferBatch:
84 # Emits on batch transfer of tokens. the ids array correspond with the values array by their position
85 operator: indexed(address) # indexed
86 fromAddress: indexed(address)
87 to: indexed(address)
88 ids: DynArray[uint256, BATCH_SIZE]
89 values: DynArray[uint256, BATCH_SIZE]
90
91event ApprovalForAll:
92 # This emits when an operator is enabled or disabled for an owner. The operator manages all tokens for an owner
93 account: indexed(address)
94 operator: indexed(address)
95 approved: bool
96
97event URI:
98 # This emits when the URI gets changed
99 value: String[MAX_URI_LENGTH]
100 id: indexed(uint256)
101
102############### interfaces ###############
103implements: IERC165
104
105interface IERC1155Receiver:
106 def onERC1155Received(
107 operator: address,
108 sender: address,
109 id: uint256,
110 amount: uint256,
111 data: Bytes[CALLBACK_NUMBYTES],
112 ) -> bytes32: payable
113 def onERC1155BatchReceived(
114 operator: address,
115 sender: address,
116 ids: DynArray[uint256, BATCH_SIZE],
117 amounts: DynArray[uint256, BATCH_SIZE],
118 data: Bytes[CALLBACK_NUMBYTES],
119 ) -> bytes4: payable
120
121interface IERC1155MetadataURI:
122 def uri(id: uint256) -> String[MAX_URI_LENGTH]: view
123
124############### functions ###############
125
126@deploy
127def __init__(name: String[128], symbol: String[16], uri: String[MAX_URI_LENGTH], contractUri: String[MAX_URI_LENGTH]):
128 """
129 @dev contract initialization on deployment
130 @dev will set name and symbol, interfaces, owner and URI
131 @dev self.paused will default to false
132 @param name the smart contract name
133 @param symbol the smart contract symbol
134 @param uri the new uri for the contract
135 """
136 self.name = name
137 self.symbol = symbol
138 self.owner = msg.sender
139 self.baseuri = uri
140 self.contractURI = contractUri
141
142## contract status ##
143@external
144def pause():
145 """
146 @dev Pause the contract, checks if the caller is the owner and if the contract is paused already
147 @dev emits a pause event
148 @dev not part of the core spec but a common feature for NFT projects
149 """
150 assert self.owner == msg.sender, "Ownable: caller is not the owner"
151 assert not self.paused, "the contract is already paused"
152 self.paused = True
153 log Paused(account=msg.sender)
154
155@external
156def unpause():
157 """
158 @dev Unpause the contract, checks if the caller is the owner and if the contract is paused already
159 @dev emits an unpause event
160 @dev not part of the core spec but a common feature for NFT projects
161 """
162 assert self.owner == msg.sender, "Ownable: caller is not the owner"
163 assert self.paused, "the contract is not paused"
164 self.paused = False
165 log unPaused(account=msg.sender)
166
167## ownership ##
168@external
169def transferOwnership(newOwner: address):
170 """
171 @dev Transfer the ownership. Checks for contract pause status, current owner and prevent transferring to
172 @dev zero address
173 @dev emits an OwnershipTransferred event with the old and new owner addresses
174 @param newOwner The address of the new owner.
175 """
176 assert not self.paused, "The contract has been paused"
177 assert self.owner == msg.sender, "Ownable: caller is not the owner"
178 assert newOwner != self.owner, "This account already owns the contract"
179 assert newOwner != empty(address), "Transfer to the zero address not allowed. Use renounceOwnership() instead."
180 oldOwner: address = self.owner
181 self.owner = newOwner
182 log OwnershipTransferred(previousOwner=oldOwner, newOwner=newOwner)
183
184@external
185def renounceOwnership():
186 """
187 @dev Transfer the ownership to the zero address, this will lock the contract
188 @dev emits an OwnershipTransferred event with the old and new zero owner addresses
189 """
190 assert not self.paused, "The contract has been paused"
191 assert self.owner == msg.sender, "Ownable: caller is not the owner"
192 oldOwner: address = self.owner
193 self.owner = empty(address)
194 log OwnershipTransferred(previousOwner=oldOwner, newOwner=empty(address))
195
196@external
197@view
198def balanceOfBatch(accounts: DynArray[address, BATCH_SIZE], ids: DynArray[uint256, BATCH_SIZE]) -> DynArray[uint256,BATCH_SIZE]: # uint256[BATCH_SIZE]:
199 """
200 @dev check the balance for an array of specific IDs and addresses
201 @dev will return an array of balances
202 @dev Can also be used to check ownership of an ID
203 @param accounts a dynamic array of the addresses to check the balance for
204 @param ids a dynamic array of the token IDs to check the balance
205 """
206 assert len(accounts) == len(ids), "ERC1155: accounts and ids length mismatch"
207 batchBalances: DynArray[uint256, BATCH_SIZE] = []
208 j: uint256 = 0
209 for i: uint256 in ids:
210 batchBalances.append(self.balanceOf[accounts[j]][i])
211 j += 1
212 return batchBalances
213
214## mint ##
215@external
216def mint(receiver: address, id: uint256, amount:uint256):
217 """
218 @dev mint one new token with a certain ID
219 @dev this can be a new token or "topping up" the balance of a non-fungible token ID
220 @param receiver the account that will receive the minted token
221 @param id the ID of the token
222 @param amount of tokens for this ID
223 """
224 assert not self.paused, "The contract has been paused"
225 assert self.owner == msg.sender, "Only the contract owner can mint"
226 assert receiver != empty(address), "Can not mint to ZERO ADDRESS"
227 operator: address = msg.sender
228 self.balanceOf[receiver][id] += amount
229 log TransferSingle(operator=operator, fromAddress=empty(address), to=receiver, id=id, value=amount)
230
231
232@external
233def mintBatch(receiver: address, ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE]):
234 """
235 @dev mint a batch of new tokens with the passed IDs
236 @dev this can be new tokens or "topping up" the balance of existing non-fungible token IDs in the contract
237 @param receiver the account that will receive the minted token
238 @param ids array of ids for the tokens
239 @param amounts amounts of tokens for each ID in the ids array
240 """
241 assert not self.paused, "The contract has been paused"
242 assert self.owner == msg.sender, "Only the contract owner can mint"
243 assert receiver != empty(address), "Can not mint to ZERO ADDRESS"
244 assert len(ids) == len(amounts), "ERC1155: ids and amounts length mismatch"
245 operator: address = msg.sender
246
247 for i: uint256 in range(BATCH_SIZE):
248 if i >= len(ids):
249 break
250 self.balanceOf[receiver][ids[i]] += amounts[i]
251
252 log TransferBatch(operator=operator, fromAddress=empty(address), to=receiver, ids=ids, values=amounts)
253
254## burn ##
255@external
256def burn(id: uint256, amount: uint256):
257 """
258 @dev burn one or more token with a certain ID
259 @dev the amount of tokens will be deducted from the holder's balance
260 @param id the ID of the token to burn
261 @param amount of tokens to burnfor this ID
262 """
263 assert not self.paused, "The contract has been paused"
264 assert self.balanceOf[msg.sender][id] > 0 , "caller does not own this ID"
265 self.balanceOf[msg.sender][id] -= amount
266 log TransferSingle(operator=msg.sender, fromAddress=msg.sender, to=empty(address), id=id, value=amount)
267
268@external
269def burnBatch(ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE]):
270 """
271 @dev burn a batch of tokens with the passed IDs
272 @dev this can be burning non fungible tokens or reducing the balance of existing non-fungible token IDs in the contract
273 @dev inside the loop ownership will be checked for each token. We can not burn tokens we do not own
274 @param ids array of ids for the tokens to burn
275 @param amounts array of amounts of tokens for each ID in the ids array
276 """
277 assert not self.paused, "The contract has been paused"
278 assert len(ids) == len(amounts), "ERC1155: ids and amounts length mismatch"
279 operator: address = msg.sender
280
281 for i: uint256 in range(BATCH_SIZE):
282 if i >= len(ids):
283 break
284 self.balanceOf[msg.sender][ids[i]] -= amounts[i]
285
286 log TransferBatch(operator=msg.sender, fromAddress=msg.sender, to=empty(address), ids=ids, values=amounts)
287
288## approval ##
289@external
290def setApprovalForAll(owner: address, operator: address, approved: bool):
291 """
292 @dev set an operator for a certain NFT owner address
293 @param owner the NFT owner address
294 @param operator the operator address
295 @param approved approve or disapprove
296 """
297 assert owner == msg.sender, "You can only set operators for your own account"
298 assert not self.paused, "The contract has been paused"
299 assert owner != operator, "ERC1155: setting approval status for self"
300 self.isApprovedForAll[owner][operator] = approved
301 log ApprovalForAll(account=owner, operator=operator, approved=approved)
302
303@external
304def safeTransferFrom(sender: address, receiver: address, id: uint256, amount: uint256, bytes: bytes32):
305 """
306 @dev transfer token from one address to another.
307 @param sender the sending account (current owner)
308 @param receiver the receiving account
309 @param id the token id that will be sent
310 @param amount the amount of tokens for the specified id
311 """
312 assert not self.paused, "The contract has been paused"
313 assert receiver != empty(address), "ERC1155: transfer to the zero address"
314 assert sender != receiver
315 assert sender == msg.sender or self.isApprovedForAll[sender][msg.sender], "Caller is neither owner nor approved operator for this ID"
316 assert self.balanceOf[sender][id] > 0 , "caller does not own this ID or ZERO balance"
317 operator: address = msg.sender
318 self.balanceOf[sender][id] -= amount
319 self.balanceOf[receiver][id] += amount
320 log TransferSingle(operator=operator, fromAddress=sender, to=receiver, id=id, value=amount)
321
322@external
323def safeBatchTransferFrom(sender: address, receiver: address, ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE], _bytes: bytes32):
324 """
325 @dev transfer tokens from one address to another.
326 @param sender the sending account
327 @param receiver the receiving account
328 @param ids a dynamic array of the token ids that will be sent
329 @param amounts a dynamic array of the amounts for the specified list of ids.
330 """
331 assert not self.paused, "The contract has been paused"
332 assert receiver != empty(address), "ERC1155: transfer to the zero address"
333 assert sender != receiver
334 assert sender == msg.sender or self.isApprovedForAll[sender][msg.sender], "Caller is neither owner nor approved operator for this ID"
335 assert len(ids) == len(amounts), "ERC1155: ids and amounts length mismatch"
336 operator: address = msg.sender
337 for i: uint256 in range(BATCH_SIZE):
338 if i >= len(ids):
339 break
340 id: uint256 = ids[i]
341 amount: uint256 = amounts[i]
342 self.balanceOf[sender][id] -= amount
343 self.balanceOf[receiver][id] += amount
344
345 log TransferBatch(operator=operator, fromAddress=sender, to=receiver, ids=ids, values=amounts)
346
347# URI #
348@external
349def setURI(uri: String[MAX_URI_LENGTH]):
350 """
351 @dev set the URI for the contract
352 @param uri the new uri for the contract
353 """
354 assert not self.paused, "The contract has been paused"
355 assert self.baseuri != uri, "new and current URI are identical"
356 assert msg.sender == self.owner, "Only the contract owner can update the URI"
357 self.baseuri = uri
358 log URI(value=uri, id=0)
359
360@external
361def toggleDynUri(status: bool):
362 """
363 @dev toggle dynamic URI
364 @param status true for dynamic false for static
365 """
366 assert msg.sender == self.owner
367 assert status != self.dynamicUri, "already in desired state"
368 self.dynamicUri = status
369
370@view
371@external
372def uri(id: uint256) -> String[MAX_URL_LENGTH]:
373 """
374 @dev retrieve the uri. Adds requested ID when dynamic URI is active
375 @param id NFT ID to retrieve the uri for.
376 """
377 if self.dynamicUri:
378 return concat(self.baseuri, uint2str(id), '.json')
379 else:
380 return self.baseuri
381
382# URI #
383@external
384def setContractURI(contractUri: String[MAX_URI_LENGTH]):
385 """
386 @dev set the contractURI for the contract. points to collection metadata file
387 @dev This function is opensea specific and is required to properly show collection metadata and image
388 @param contractUri the new urcontractUri for the contract
389 """
390 assert not self.paused, "The contract has been paused"
391 assert self.contractURI != contractUri, "new and current URI are identical"
392 assert msg.sender == self.owner, "Only the contract owner can update the URI"
393 self.contractURI = contractUri
394 log URI(value=contractUri, id=0)
395
396@view
397@external
398def supportsInterface(interfaceId: bytes4) -> bool:
399 """
400 @dev Returns True if the interface is supported
401 @param interfaceId bytes4 interface identifier
402 """
403 return interfaceId in [
404 ERC165_INTERFACE_ID,
405 ERC1155_INTERFACE_ID,
406 ERC1155_INTERFACE_ID_METADATA,
407 ]
Features beyond the base ERC1155 standard:
Ownable: Only the owner can mint tokens
Pausable: Owner can pause all transfers
Batch operations:
mintBatch,burnBatch,safeBatchTransferFromDynamic URI: Optional per-token metadata URIs
The BATCH_SIZE constant (128) limits array sizes for gas predictability—a Vyper requirement.