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, safeBatchTransferFrom

  • Dynamic URI: Optional per-token metadata URIs

The BATCH_SIZE constant (128) limits array sizes for gas predictability—a Vyper requirement.