ERC721 Token
The ERC721 token standard defines an interface for non-fungible tokens. In contrast to ERC20 tokens, each token of an ERC721 contract is unique. One famous implementation of the standard is CryptoKitties.

Implementation in Vyper

Note that in this implementation the smallest tokenId is 0.
erc721.vy
1
# Author: Sören Steiger, twitter.com/ssteiger_
2
# License: MIT
3
4
# ERC721 Token Standard
5
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
6
7
# @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.
8
# @notice Handle the receipt of an NFT
9
# @dev The ERC721 smart contract calls this function on the recipient
10
# after a `transfer`.
11
# This function MAY throw to revert and reject the transfer.
12
# Return of other than the magic value MUST result in the transaction
13
# being reverted.
14
# Note: the contract address is always the message sender.
15
# @param _operator The address which called `safeTransferFrom` function
16
# @param _from The address which previously owned the token
17
# @param _tokenId The NFT identifier which is being transferred
18
# @param _data Additional data with no specified format
19
# @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
20
# unless throwing
21
# function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
22
contract ERC721TokenReceiver:
23
def onERC721Received(
24
_operator: address,
25
_from: address,
26
_tokenId: uint256,
27
_data: bytes[256]
28
) -> bytes32: constant
29
30
31
# EVENTS:
32
33
# @dev This emits when ownership of any NFT changes by any mechanism.
34
# This event emits when NFTs are created (`from` == 0) and destroyed
35
# (`to` == 0).
36
# Exception: during contract creation, any number of NFTs may be created
37
# and assigned without emitting Transfer.
38
# At the time of any transfer, the approved address for that NFT (if any)
39
# is reset to none.
40
Transfer: event({
41
_from: indexed(address),
42
_to: indexed(address),
43
_tokenId: indexed(uint256)
44
})
45
46
47
# NOTE: This is not part of the standard
48
Mint: event({
49
_to: indexed(address),
50
_tokenId: indexed(uint256)
51
})
52
53
54
# @dev This emits when the approved address for an NFT is changed or reaffirmed.
55
# The zero address indicates there is no approved address.
56
# When a Transfer event emits, this also indicates that the approved
57
# address for that NFT (if any) is reset to none.
58
Approval: event({
59
_owner: indexed(address),
60
_approved: indexed(address),
61
_tokenId: indexed(uint256)
62
})
63
64
65
# @dev This emits when an operator is enabled or disabled for an owner.
66
# The operator can manage all NFTs of the owner.
67
ApprovalForAll: event({
68
_owner: indexed(address),
69
_operator: indexed(address),
70
_approved: bool
71
})
72
73
74
# STATE VARIABLES:
75
76
# NOTE: This is not part of the standard
77
contractOwner: public(address)
78
# Used for token id's
79
nftSupply: uint256
80
81
# Used to keep track of the number of tokens an address holds
82
nftCount: public(map(address, uint256))
83
ownerOfNFT: public(map(uint256, address))
84
85
operatorFor: public(map(uint256, address))
86
approvedForAll: public(map(address, map(address, bool)))
87
88
# Interface detection as specified in ERC165
89
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md
90
supportedInterfaces: public(map(bytes32, bool))
91
# ERC165 interface ID's
92
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7
93
ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd
94
95
96
# METHODS:
97
@public
98
def __init__():
99
# set initial supply (used for token id's)
100
self.nftSupply = 0
101
# set supported interfaces
102
self.supportedInterfaces[ERC165_INTERFACE_ID] = True
103
self.supportedInterfaces[ERC721_INTERFACE_ID] = True
104
# set contract owner
105
# NOTE: This is not part of the standard
106
# only contractOwner can call mint()
107
self.contractOwner = msg.sender
108
109
110
@private
111
def _checkIfIsOwnerOrOperatorOrApprovedForAll(_msgSender: address, _from: address, _tokenId: uint256):
112
# Throws unless `msg.sender` is
113
# the current owner
114
isOwner: bool = self.ownerOfNFT[_tokenId] == _msgSender
115
# an authorized operator
116
isOperator: bool = self.operatorFor[_tokenId] == _msgSender
117
# or the approved address for this NFT
118
isApprovedForAll: bool = (self.approvedForAll[_from])[_msgSender]
119
assert (isOwner or isOperator or isApprovedForAll)
120
121
122
@private
123
def _setNewOwner(_currentOwner: address, _newOwner: address, _tokenId: uint256):
124
# set new owner
125
self.ownerOfNFT[_tokenId] = _newOwner
126
# updated balances
127
self.nftCount[_currentOwner] -= 1
128
self.nftCount[_newOwner] += 1
129
# reset operator
130
# TODO: what about `approvedForAll`?
131
self.operatorFor[_tokenId] = ZERO_ADDRESS
132
133
134
@private
135
def _transfer(_from: address, _to: address, _tokenId: uint256):
136
# Throws if `_from` is not the current owner.
137
assert self.ownerOfNFT[_tokenId] == _from
138
# Throws if `_to` is the zero address.
139
assert _to != ZERO_ADDRESS
140
# Throws if `_tokenId` is not a valid NFT.
141
assert self.ownerOfNFT[_tokenId] != ZERO_ADDRESS
142
# transfer to new owner
143
self._setNewOwner(_from, _to, _tokenId)
144
# log transfer
145
log.Transfer(_from, _to, _tokenId)
146
147
148
@public
149
@constant
150
def supportsInterface(_interfaceID: bytes32) -> bool:
151
# Interface detection as specified in ERC165
152
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md
153
return self.supportedInterfaces[_interfaceID]
154
155
156
# @notice Count all NFTs assigned to an owner
157
# @dev NFTs assigned to the zero address are considered invalid, and this
158
# function throws for queries about the zero address.
159
# @param _owner An address for whom to query the balance
160
# @return The number of NFTs owned by `_owner`, possibly zero
161
# function balanceOf(address _owner) external view returns (uint256);
162
@public
163
@constant
164
def balanceOf(_owner: address) -> uint256:
165
# NFTs assigned to the zero address are considered invalid, and this
166
# function throws for queries about the zero address.
167
assert _owner != ZERO_ADDRESS
168
return self.nftCount[_owner]
169
170
171
# @notice Find the owner of an NFT
172
# @dev NFTs assigned to zero address are considered invalid, and queries
173
# about them do throw.
174
# @param _tokenId The identifier for an NFT
175
# @return The address of the owner of the NFT
176
# function ownerOf(uint256 _tokenId) external view returns (address);
177
@public
178
@constant
179
def ownerOf(_tokenId: uint256) -> address:
180
# NFTs assigned to the zero address are considered invalid, and this
181
# function throws for queries about the zero address.
182
owner: address = self.ownerOfNFT[_tokenId]
183
assert owner != ZERO_ADDRESS
184
return owner
185
186
187
# @notice Transfers the ownership of an NFT from one address to another address
188
# @dev Throws unless `msg.sender` is
189
# the current owner,
190
# an authorized operator,
191
# or the approved address for this NFT.
192
# Throws if `_from` is not the current owner.
193
# Throws if `_to` is the zero address.
194
# Throws if `_tokenId` is not a valid NFT.
195
# When transfer is complete, this function checks if `_to` is
196
# a smart contract (code size > 0). If so, it calls
197
# `onERC721Received` on `_to` and throws if the return value is not
198
# `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
199
# @param _from The current owner of the NFT
200
# @param _to The new owner
201
# @param _tokenId The NFT to transfer
202
# @param data Additional data with no specified format, sent in call to `_to`
203
# function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
204
@public
205
@payable
206
def safeTransferFrom(_from: address, _to: address, _tokenId: uint256, _data: bytes[256]=""):
207
# Throws unless `msg.sender` is
208
# the current owner,
209
# an authorized operator,
210
# or the approved address for this NFT.
211
self._checkIfIsOwnerOrOperatorOrApprovedForAll(msg.sender, _from, _tokenId)
212
# transfer
213
self._transfer(_from, _to, _tokenId)
214
# When transfer is complete,
215
# this function checks if `_to` is a smart contract (code size > 0)
216
if _to.is_contract:
217
# If so, it calls `onERC721Received` on `_to` and throws if the return value is not
218
# `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
219
returnValue: bytes32 = ERC721TokenReceiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)
220
assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", bytes32)
221
222
223
# @notice Transfers the ownership of an NFT from one address to another address
224
# @dev This works identically to the other function with an extra data
225
# parameter, except this function just sets data to "".
226
# @param _from The current owner of the NFT
227
# @param _to The new owner
228
# @param _tokenId The NFT to transfer
229
# function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
230
231
232
# @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
233
# TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
234
# THEY MAY BE PERMANENTLY LOST
235
# @dev Throws unless
236
# `msg.sender` is the current owner,
237
# an authorized operator,
238
# or the approved address for this NFT.
239
# Throws if `_from` is not the current owner.
240
# Throws if `_to` is the zero address.
241
# Throws if `_tokenId` is not a valid NFT.
242
# @param _from The current owner of the NFT
243
# @param _to The new owner
244
# @param _tokenId The NFT to transfer
245
# function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
246
@public
247
@payable
248
def transferFrom(_from: address, _to: address, _tokenId: uint256):
249
# Throws unless `msg.sender` is
250
# the current owner,
251
# an authorized operator,
252
# or the approved address for this NFT.
253
self._checkIfIsOwnerOrOperatorOrApprovedForAll(msg.sender, _from, _tokenId)
254
# do transfer
255
self._transfer(_from, _to, _tokenId)
256
257
258
# @notice Change or reaffirm the approved address for an NFT
259
# @dev The zero address indicates there is no approved address.
260
# Throws unless `msg.sender` is
261
# the current NFT owner,
262
# or an authorized operator of the current owner.
263
# @param _approved The new approved NFT controller
264
# @param _tokenId The NFT to approve
265
# function approve(address _approved, uint256 _tokenId) external payable;
266
@public
267
@payable
268
def approve(_approved: address, _tokenId: uint256):
269
# Throws if _tokenId is not owned / a valid NFT
270
assert self.ownerOfNFT[_tokenId] != ZERO_ADDRESS
271
# Throws unless `msg.sender` is the current NFT owner
272
isOwner: bool = self.ownerOfNFT[_tokenId] == msg.sender
273
# or an authorized operator of the current owner.
274
isOperator: bool = self.operatorFor[_tokenId] == msg.sender
275
# TODO: does the this include approvedForAll?
276
assert (isOwner or isOperator)
277
# set new approved address
278
self.operatorFor[_tokenId] = _approved
279
# log change
280
log.Approval(msg.sender, _approved, _tokenId)
281
282
283
# @notice Enable or disable approval for a third party ("operator") to manage
284
# all of `msg.sender`'s assets
285
# @dev Emits the ApprovalForAll event.
286
# The contract MUST allow multiple operators per owner.
287
# @param _operator Address to add to the set of authorized operators
288
# @param _approved True if the operator is approved, false to revoke approval
289
# function setApprovalForAll(address _operator, bool _approved) external;
290
@public
291
def setApprovalForAll(_operator: address, _approved: bool):
292
# The contract MUST allow multiple operators per owner.
293
self.approvedForAll[msg.sender][_operator] = _approved
294
# log change
295
log.ApprovalForAll(msg.sender, _operator, _approved)
296
297
298
# @notice Get the approved address for a single NFT
299
# @dev Throws if `_tokenId` is not a valid NFT.
300
# @param _tokenId The NFT to find the approved address for
301
# @return The approved address for this NFT, or the zero address if
302
# there is none
303
# function getApproved(uint256 _tokenId) external view returns (address);
304
@public
305
@constant
306
def getApproved(_tokenId: uint256) -> address:
307
# Throws if `_tokenId` is not a valid NFT.
308
assert self.ownerOfNFT[_tokenId] != ZERO_ADDRESS
309
return self.operatorFor[_tokenId]
310
311
312
# @notice Query if an address is an authorized operator for another address
313
# @param _owner The address that owns the NFTs
314
# @param _operator The address that acts on behalf of the owner
315
# @return True if `_operator` is an approved operator for `_owner`,
316
# false otherwise
317
# function isApprovedForAll(address _owner, address _operator) external view returns (bool);
318
@public
319
@constant
320
def isApprovedForAll(_owner: address, _operator: address) -> bool:
321
return (self.approvedForAll[_owner])[_operator]
322
323
324
# NOTE: This is not part of the standard
325
@public
326
def mint() -> uint256:
327
# only contractOwner is allowed to mint
328
assert msg.sender == self.contractOwner
329
# update supply
330
tokenId: uint256 = self.nftSupply
331
self.nftSupply += 1
332
# update ownership
333
self.ownerOfNFT[tokenId] = msg.sender
334
self.nftCount[msg.sender] += 1
335
self.operatorFor[tokenId] = ZERO_ADDRESS
336
# log mint
337
log.Mint(msg.sender, tokenId)
338
return tokenId
Copied!
Last modified 2yr ago
Copy link