Any contract that follows the ERC-721 standard is an ERC-721 token.
Here is the interface for ERC-721.
1interface ERC721 {
2 event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
3 event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
4 event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
5
6 function balanceOf(address _owner) external view returns (uint256);
7 function ownerOf(uint256 _tokenId) external view returns (address);
8 function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
9 function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
10 function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
11 function approve(address _approved, uint256 _tokenId) external payable;
12 function setApprovalForAll(address _operator, bool _approved) external;
13 function getApproved(uint256 _tokenId) external view returns (address);
14 function isApprovedForAll(address _owner, address _operator) external view returns (bool);
15}
1interface ERC721 {
2 event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
3 event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
4 event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
5
6 function balanceOf(address _owner) external view returns (uint256);
7 function ownerOf(uint256 _tokenId) external view returns (address);
8 function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
9 function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
10 function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
11 function approve(address _approved, uint256 _tokenId) external payable;
12 function setApprovalForAll(address _operator, bool _approved) external;
13 function getApproved(uint256 _tokenId) external view returns (address);
14 function isApprovedForAll(address _owner, address _operator) external view returns (bool);
15}
Example implementation of an ERC-721 token contract written in Rust.
1//! Implementation of the ERC-721 standard
2//!
3//! The eponymous [`Erc721`] type provides all the standard methods,
4//! and is intended to be inherited by other contract types.
5//!
6//! You can configure the behavior of [`Erc721`] via the [`Erc721Params`] trait,
7//! which allows specifying the name, symbol, and token uri.
8//!
9//! Note that this code is unaudited and not fit for production use.
10
11use alloc::{string::String, vec, vec::Vec};
12use alloy_primitives::{Address, U256, FixedBytes};
13use alloy_sol_types::sol;
14use core::{borrow::BorrowMut, marker::PhantomData};
15use stylus_sdk::{
16 abi::Bytes,
17 evm,
18 msg,
19 prelude::*
20};
21
22pub trait Erc721Params {
23 /// Immutable NFT name.
24 const NAME: &'static str;
25
26 /// Immutable NFT symbol.
27 const SYMBOL: &'static str;
28
29 /// The NFT's Uniform Resource Identifier.
30 fn token_uri(token_id: U256) -> String;
31}
32
33sol_storage! {
34 /// Erc721 implements all ERC-721 methods
35 pub struct Erc721<T: Erc721Params> {
36 /// Token id to owner map
37 mapping(uint256 => address) owners;
38 /// User to balance map
39 mapping(address => uint256) balances;
40 /// Token id to approved user map
41 mapping(uint256 => address) token_approvals;
42 /// User to operator map (the operator can manage all NFTs of the owner)
43 mapping(address => mapping(address => bool)) operator_approvals;
44 /// Total supply
45 uint256 total_supply;
46 /// Used to allow [`Erc721Params`]
47 PhantomData<T> phantom;
48 }
49}
50
51// Declare events and Solidity error types
52sol! {
53 event Transfer(address indexed from, address indexed to, uint256 indexed token_id);
54 event Approval(address indexed owner, address indexed approved, uint256 indexed token_id);
55 event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
56
57 // Token id has not been minted, or it has been burned
58 error InvalidTokenId(uint256 token_id);
59 // The specified address is not the owner of the specified token id
60 error NotOwner(address from, uint256 token_id, address real_owner);
61 // The specified address does not have allowance to spend the specified token id
62 error NotApproved(address owner, address spender, uint256 token_id);
63 // Attempt to transfer token id to the Zero address
64 error TransferToZero(uint256 token_id);
65 // The receiver address refused to receive the specified token id
66 error ReceiverRefused(address receiver, uint256 token_id, bytes4 returned);
67}
68
69/// Represents the ways methods may fail.
70#[derive(SolidityError)]
71pub enum Erc721Error {
72 InvalidTokenId(InvalidTokenId),
73 NotOwner(NotOwner),
74 NotApproved(NotApproved),
75 TransferToZero(TransferToZero),
76 ReceiverRefused(ReceiverRefused),
77}
78
79// External interfaces
80sol_interface! {
81 /// Allows calls to the `onERC721Received` method of other contracts implementing `IERC721TokenReceiver`.
82 interface IERC721TokenReceiver {
83 function onERC721Received(address operator, address from, uint256 token_id, bytes data) external returns(bytes4);
84 }
85}
86
87/// Selector for `onERC721Received`, which is returned by contracts implementing `IERC721TokenReceiver`.
88const ERC721_TOKEN_RECEIVER_ID: u32 = 0x150b7a02;
89
90// These methods aren't external, but are helpers used by external methods.
91// Methods marked as "pub" here are usable outside of the erc721 module (i.e. they're callable from lib.rs).
92impl<T: Erc721Params> Erc721<T> {
93 /// Requires that msg::sender() is authorized to spend a given token
94 fn require_authorized_to_spend(&self, from: Address, token_id: U256) -> Result<(), Erc721Error> {
95 // `from` must be the owner of the token_id
96 let owner = self.owner_of(token_id)?;
97 if from != owner {
98 return Err(Erc721Error::NotOwner(NotOwner {
99 from,
100 token_id,
101 real_owner: owner,
102 }));
103 }
104
105 // caller is the owner
106 if msg::sender() == owner {
107 return Ok(());
108 }
109
110 // caller is an operator for the owner (can manage their tokens)
111 if self.operator_approvals.getter(owner).get(msg::sender()) {
112 return Ok(());
113 }
114
115 // caller is approved to manage this token_id
116 if msg::sender() == self.token_approvals.get(token_id) {
117 return Ok(());
118 }
119
120 // otherwise, caller is not allowed to manage this token_id
121 Err(Erc721Error::NotApproved(NotApproved {
122 owner,
123 spender: msg::sender(),
124 token_id,
125 }))
126 }
127
128 /// Transfers `token_id` from `from` to `to`.
129 /// This function does check that `from` is the owner of the token, but it does not check
130 /// that `to` is not the zero address, as this function is usable for burning.
131 pub fn transfer(&mut self, token_id: U256, from: Address, to: Address) -> Result<(), Erc721Error> {
132 let mut owner = self.owners.setter(token_id);
133 let previous_owner = owner.get();
134 if previous_owner != from {
135 return Err(Erc721Error::NotOwner(NotOwner {
136 from,
137 token_id,
138 real_owner: previous_owner,
139 }));
140 }
141 owner.set(to);
142
143 // right now working with storage can be verbose, but this will change upcoming version of the Stylus SDK
144 let mut from_balance = self.balances.setter(from);
145 let balance = from_balance.get() - U256::from(1);
146 from_balance.set(balance);
147
148 let mut to_balance = self.balances.setter(to);
149 let balance = to_balance.get() + U256::from(1);
150 to_balance.set(balance);
151
152 // cleaning app the approved mapping for this token
153 self.token_approvals.delete(token_id);
154
155 evm::log(Transfer { from, to, token_id });
156 Ok(())
157 }
158
159 /// Calls `onERC721Received` on the `to` address if it is a contract.
160 /// Otherwise it does nothing
161 fn call_receiver<S: TopLevelStorage>(
162 storage: &mut S,
163 token_id: U256,
164 from: Address,
165 to: Address,
166 data: Vec<u8>,
167 ) -> Result<(), Erc721Error> {
168 if to.has_code() {
169 let receiver = IERC721TokenReceiver::new(to);
170 let received = receiver
171 .on_erc_721_received(&mut *storage, msg::sender(), from, token_id, data.into())
172 .map_err(|_e| Erc721Error::ReceiverRefused(ReceiverRefused {
173 receiver: receiver.address,
174 token_id,
175 returned: alloy_primitives::FixedBytes(0_u32.to_be_bytes()),
176 }))?
177 .0;
178
179 if u32::from_be_bytes(received) != ERC721_TOKEN_RECEIVER_ID {
180 return Err(Erc721Error::ReceiverRefused(ReceiverRefused {
181 receiver: receiver.address,
182 token_id,
183 returned: alloy_primitives::FixedBytes(received),
184 }));
185 }
186 }
187 Ok(())
188 }
189
190 /// Transfers and calls `onERC721Received`
191 pub fn safe_transfer<S: TopLevelStorage + BorrowMut<Self>>(
192 storage: &mut S,
193 token_id: U256,
194 from: Address,
195 to: Address,
196 data: Vec<u8>,
197 ) -> Result<(), Erc721Error> {
198 storage.borrow_mut().transfer(token_id, from, to)?;
199 Self::call_receiver(storage, token_id, from, to, data)
200 }
201
202 /// Mints a new token and transfers it to `to`
203 pub fn mint(&mut self, to: Address) -> Result<(), Erc721Error> {
204 let new_token_id = self.total_supply.get();
205 self.total_supply.set(new_token_id + U256::from(1u8));
206 self.transfer(new_token_id, Address::default(), to)?;
207 Ok(())
208 }
209
210 /// Burns the token `token_id` from `from`
211 /// Note that total_supply is not reduced since it's used to calculate the next token_id to mint
212 pub fn burn(&mut self, from: Address, token_id: U256) -> Result<(), Erc721Error> {
213 self.transfer(token_id, from, Address::default())?;
214 Ok(())
215 }
216}
217
218// these methods are external to other contracts
219#[public]
220impl<T: Erc721Params> Erc721<T> {
221 /// Immutable NFT name.
222 pub fn name() -> Result<String, Erc721Error> {
223 Ok(T::NAME.into())
224 }
225
226 /// Immutable NFT symbol.
227 pub fn symbol() -> Result<String, Erc721Error> {
228 Ok(T::SYMBOL.into())
229 }
230
231 /// The NFT's Uniform Resource Identifier.
232 #[selector(name = "tokenURI")]
233 pub fn token_uri(&self, token_id: U256) -> Result<String, Erc721Error> {
234 self.owner_of(token_id)?; // require NFT exist
235 Ok(T::token_uri(token_id))
236 }
237
238 /// Gets the number of NFTs owned by an account.
239 pub fn balance_of(&self, owner: Address) -> Result<U256, Erc721Error> {
240 Ok(self.balances.get(owner))
241 }
242
243 /// Gets the owner of the NFT, if it exists.
244 pub fn owner_of(&self, token_id: U256) -> Result<Address, Erc721Error> {
245 let owner = self.owners.get(token_id);
246 if owner.is_zero() {
247 return Err(Erc721Error::InvalidTokenId(InvalidTokenId { token_id }));
248 }
249 Ok(owner)
250 }
251
252 /// Transfers an NFT, but only after checking the `to` address can receive the NFT.
253 /// It includes additional data for the receiver.
254 #[selector(name = "safeTransferFrom")]
255 pub fn safe_transfer_from_with_data<S: TopLevelStorage + BorrowMut<Self>>(
256 storage: &mut S,
257 from: Address,
258 to: Address,
259 token_id: U256,
260 data: Bytes,
261 ) -> Result<(), Erc721Error> {
262 if to.is_zero() {
263 return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
264 }
265 storage
266 .borrow_mut()
267 .require_authorized_to_spend(from, token_id)?;
268
269 Self::safe_transfer(storage, token_id, from, to, data.0)
270 }
271
272 /// Equivalent to [`safe_transfer_from_with_data`], but without the additional data.
273 ///
274 /// Note: because Rust doesn't allow multiple methods with the same name,
275 /// we use the `#[selector]` macro attribute to simulate solidity overloading.
276 #[selector(name = "safeTransferFrom")]
277 pub fn safe_transfer_from<S: TopLevelStorage + BorrowMut<Self>>(
278 storage: &mut S,
279 from: Address,
280 to: Address,
281 token_id: U256,
282 ) -> Result<(), Erc721Error> {
283 Self::safe_transfer_from_with_data(storage, from, to, token_id, Bytes(vec![]))
284 }
285
286 /// Transfers the NFT.
287 pub fn transfer_from(&mut self, from: Address, to: Address, token_id: U256) -> Result<(), Erc721Error> {
288 if to.is_zero() {
289 return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
290 }
291 self.require_authorized_to_spend(from, token_id)?;
292 self.transfer(token_id, from, to)?;
293 Ok(())
294 }
295
296 /// Grants an account the ability to manage the sender's NFT.
297 pub fn approve(&mut self, approved: Address, token_id: U256) -> Result<(), Erc721Error> {
298 let owner = self.owner_of(token_id)?;
299
300 // require authorization
301 if msg::sender() != owner && !self.operator_approvals.getter(owner).get(msg::sender()) {
302 return Err(Erc721Error::NotApproved(NotApproved {
303 owner,
304 spender: msg::sender(),
305 token_id,
306 }));
307 }
308 self.token_approvals.insert(token_id, approved);
309
310 evm::log(Approval {
311 approved,
312 owner,
313 token_id,
314 });
315 Ok(())
316 }
317
318 /// Grants an account the ability to manage all of the sender's NFTs.
319 pub fn set_approval_for_all(&mut self, operator: Address, approved: bool) -> Result<(), Erc721Error> {
320 let owner = msg::sender();
321 self.operator_approvals
322 .setter(owner)
323 .insert(operator, approved);
324
325 evm::log(ApprovalForAll {
326 owner,
327 operator,
328 approved,
329 });
330 Ok(())
331 }
332
333 /// Gets the account managing an NFT, or zero if unmanaged.
334 pub fn get_approved(&mut self, token_id: U256) -> Result<Address, Erc721Error> {
335 Ok(self.token_approvals.get(token_id))
336 }
337
338 /// Determines if an account has been authorized to managing all of a user's NFTs.
339 pub fn is_approved_for_all(&mut self, owner: Address, operator: Address) -> Result<bool, Erc721Error> {
340 Ok(self.operator_approvals.getter(owner).get(operator))
341 }
342
343 /// Whether the NFT supports a given standard.
344 pub fn supports_interface(interface: FixedBytes<4>) -> Result<bool, Erc721Error> {
345 let interface_slice_array: [u8; 4] = interface.as_slice().try_into().unwrap();
346
347 if u32::from_be_bytes(interface_slice_array) == 0xffffffff {
348 // special cased in the ERC165 standard
349 return Ok(false);
350 }
351
352 const IERC165: u32 = 0x01ffc9a7;
353 const IERC721: u32 = 0x80ac58cd;
354 const IERC721_METADATA: u32 = 0x5b5e139f;
355
356 Ok(matches!(u32::from_be_bytes(interface_slice_array), IERC165 | IERC721 | IERC721_METADATA))
357 }
358}
1//! Implementation of the ERC-721 standard
2//!
3//! The eponymous [`Erc721`] type provides all the standard methods,
4//! and is intended to be inherited by other contract types.
5//!
6//! You can configure the behavior of [`Erc721`] via the [`Erc721Params`] trait,
7//! which allows specifying the name, symbol, and token uri.
8//!
9//! Note that this code is unaudited and not fit for production use.
10
11use alloc::{string::String, vec, vec::Vec};
12use alloy_primitives::{Address, U256, FixedBytes};
13use alloy_sol_types::sol;
14use core::{borrow::BorrowMut, marker::PhantomData};
15use stylus_sdk::{
16 abi::Bytes,
17 evm,
18 msg,
19 prelude::*
20};
21
22pub trait Erc721Params {
23 /// Immutable NFT name.
24 const NAME: &'static str;
25
26 /// Immutable NFT symbol.
27 const SYMBOL: &'static str;
28
29 /// The NFT's Uniform Resource Identifier.
30 fn token_uri(token_id: U256) -> String;
31}
32
33sol_storage! {
34 /// Erc721 implements all ERC-721 methods
35 pub struct Erc721<T: Erc721Params> {
36 /// Token id to owner map
37 mapping(uint256 => address) owners;
38 /// User to balance map
39 mapping(address => uint256) balances;
40 /// Token id to approved user map
41 mapping(uint256 => address) token_approvals;
42 /// User to operator map (the operator can manage all NFTs of the owner)
43 mapping(address => mapping(address => bool)) operator_approvals;
44 /// Total supply
45 uint256 total_supply;
46 /// Used to allow [`Erc721Params`]
47 PhantomData<T> phantom;
48 }
49}
50
51// Declare events and Solidity error types
52sol! {
53 event Transfer(address indexed from, address indexed to, uint256 indexed token_id);
54 event Approval(address indexed owner, address indexed approved, uint256 indexed token_id);
55 event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
56
57 // Token id has not been minted, or it has been burned
58 error InvalidTokenId(uint256 token_id);
59 // The specified address is not the owner of the specified token id
60 error NotOwner(address from, uint256 token_id, address real_owner);
61 // The specified address does not have allowance to spend the specified token id
62 error NotApproved(address owner, address spender, uint256 token_id);
63 // Attempt to transfer token id to the Zero address
64 error TransferToZero(uint256 token_id);
65 // The receiver address refused to receive the specified token id
66 error ReceiverRefused(address receiver, uint256 token_id, bytes4 returned);
67}
68
69/// Represents the ways methods may fail.
70#[derive(SolidityError)]
71pub enum Erc721Error {
72 InvalidTokenId(InvalidTokenId),
73 NotOwner(NotOwner),
74 NotApproved(NotApproved),
75 TransferToZero(TransferToZero),
76 ReceiverRefused(ReceiverRefused),
77}
78
79// External interfaces
80sol_interface! {
81 /// Allows calls to the `onERC721Received` method of other contracts implementing `IERC721TokenReceiver`.
82 interface IERC721TokenReceiver {
83 function onERC721Received(address operator, address from, uint256 token_id, bytes data) external returns(bytes4);
84 }
85}
86
87/// Selector for `onERC721Received`, which is returned by contracts implementing `IERC721TokenReceiver`.
88const ERC721_TOKEN_RECEIVER_ID: u32 = 0x150b7a02;
89
90// These methods aren't external, but are helpers used by external methods.
91// Methods marked as "pub" here are usable outside of the erc721 module (i.e. they're callable from lib.rs).
92impl<T: Erc721Params> Erc721<T> {
93 /// Requires that msg::sender() is authorized to spend a given token
94 fn require_authorized_to_spend(&self, from: Address, token_id: U256) -> Result<(), Erc721Error> {
95 // `from` must be the owner of the token_id
96 let owner = self.owner_of(token_id)?;
97 if from != owner {
98 return Err(Erc721Error::NotOwner(NotOwner {
99 from,
100 token_id,
101 real_owner: owner,
102 }));
103 }
104
105 // caller is the owner
106 if msg::sender() == owner {
107 return Ok(());
108 }
109
110 // caller is an operator for the owner (can manage their tokens)
111 if self.operator_approvals.getter(owner).get(msg::sender()) {
112 return Ok(());
113 }
114
115 // caller is approved to manage this token_id
116 if msg::sender() == self.token_approvals.get(token_id) {
117 return Ok(());
118 }
119
120 // otherwise, caller is not allowed to manage this token_id
121 Err(Erc721Error::NotApproved(NotApproved {
122 owner,
123 spender: msg::sender(),
124 token_id,
125 }))
126 }
127
128 /// Transfers `token_id` from `from` to `to`.
129 /// This function does check that `from` is the owner of the token, but it does not check
130 /// that `to` is not the zero address, as this function is usable for burning.
131 pub fn transfer(&mut self, token_id: U256, from: Address, to: Address) -> Result<(), Erc721Error> {
132 let mut owner = self.owners.setter(token_id);
133 let previous_owner = owner.get();
134 if previous_owner != from {
135 return Err(Erc721Error::NotOwner(NotOwner {
136 from,
137 token_id,
138 real_owner: previous_owner,
139 }));
140 }
141 owner.set(to);
142
143 // right now working with storage can be verbose, but this will change upcoming version of the Stylus SDK
144 let mut from_balance = self.balances.setter(from);
145 let balance = from_balance.get() - U256::from(1);
146 from_balance.set(balance);
147
148 let mut to_balance = self.balances.setter(to);
149 let balance = to_balance.get() + U256::from(1);
150 to_balance.set(balance);
151
152 // cleaning app the approved mapping for this token
153 self.token_approvals.delete(token_id);
154
155 evm::log(Transfer { from, to, token_id });
156 Ok(())
157 }
158
159 /// Calls `onERC721Received` on the `to` address if it is a contract.
160 /// Otherwise it does nothing
161 fn call_receiver<S: TopLevelStorage>(
162 storage: &mut S,
163 token_id: U256,
164 from: Address,
165 to: Address,
166 data: Vec<u8>,
167 ) -> Result<(), Erc721Error> {
168 if to.has_code() {
169 let receiver = IERC721TokenReceiver::new(to);
170 let received = receiver
171 .on_erc_721_received(&mut *storage, msg::sender(), from, token_id, data.into())
172 .map_err(|_e| Erc721Error::ReceiverRefused(ReceiverRefused {
173 receiver: receiver.address,
174 token_id,
175 returned: alloy_primitives::FixedBytes(0_u32.to_be_bytes()),
176 }))?
177 .0;
178
179 if u32::from_be_bytes(received) != ERC721_TOKEN_RECEIVER_ID {
180 return Err(Erc721Error::ReceiverRefused(ReceiverRefused {
181 receiver: receiver.address,
182 token_id,
183 returned: alloy_primitives::FixedBytes(received),
184 }));
185 }
186 }
187 Ok(())
188 }
189
190 /// Transfers and calls `onERC721Received`
191 pub fn safe_transfer<S: TopLevelStorage + BorrowMut<Self>>(
192 storage: &mut S,
193 token_id: U256,
194 from: Address,
195 to: Address,
196 data: Vec<u8>,
197 ) -> Result<(), Erc721Error> {
198 storage.borrow_mut().transfer(token_id, from, to)?;
199 Self::call_receiver(storage, token_id, from, to, data)
200 }
201
202 /// Mints a new token and transfers it to `to`
203 pub fn mint(&mut self, to: Address) -> Result<(), Erc721Error> {
204 let new_token_id = self.total_supply.get();
205 self.total_supply.set(new_token_id + U256::from(1u8));
206 self.transfer(new_token_id, Address::default(), to)?;
207 Ok(())
208 }
209
210 /// Burns the token `token_id` from `from`
211 /// Note that total_supply is not reduced since it's used to calculate the next token_id to mint
212 pub fn burn(&mut self, from: Address, token_id: U256) -> Result<(), Erc721Error> {
213 self.transfer(token_id, from, Address::default())?;
214 Ok(())
215 }
216}
217
218// these methods are external to other contracts
219#[public]
220impl<T: Erc721Params> Erc721<T> {
221 /// Immutable NFT name.
222 pub fn name() -> Result<String, Erc721Error> {
223 Ok(T::NAME.into())
224 }
225
226 /// Immutable NFT symbol.
227 pub fn symbol() -> Result<String, Erc721Error> {
228 Ok(T::SYMBOL.into())
229 }
230
231 /// The NFT's Uniform Resource Identifier.
232 #[selector(name = "tokenURI")]
233 pub fn token_uri(&self, token_id: U256) -> Result<String, Erc721Error> {
234 self.owner_of(token_id)?; // require NFT exist
235 Ok(T::token_uri(token_id))
236 }
237
238 /// Gets the number of NFTs owned by an account.
239 pub fn balance_of(&self, owner: Address) -> Result<U256, Erc721Error> {
240 Ok(self.balances.get(owner))
241 }
242
243 /// Gets the owner of the NFT, if it exists.
244 pub fn owner_of(&self, token_id: U256) -> Result<Address, Erc721Error> {
245 let owner = self.owners.get(token_id);
246 if owner.is_zero() {
247 return Err(Erc721Error::InvalidTokenId(InvalidTokenId { token_id }));
248 }
249 Ok(owner)
250 }
251
252 /// Transfers an NFT, but only after checking the `to` address can receive the NFT.
253 /// It includes additional data for the receiver.
254 #[selector(name = "safeTransferFrom")]
255 pub fn safe_transfer_from_with_data<S: TopLevelStorage + BorrowMut<Self>>(
256 storage: &mut S,
257 from: Address,
258 to: Address,
259 token_id: U256,
260 data: Bytes,
261 ) -> Result<(), Erc721Error> {
262 if to.is_zero() {
263 return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
264 }
265 storage
266 .borrow_mut()
267 .require_authorized_to_spend(from, token_id)?;
268
269 Self::safe_transfer(storage, token_id, from, to, data.0)
270 }
271
272 /// Equivalent to [`safe_transfer_from_with_data`], but without the additional data.
273 ///
274 /// Note: because Rust doesn't allow multiple methods with the same name,
275 /// we use the `#[selector]` macro attribute to simulate solidity overloading.
276 #[selector(name = "safeTransferFrom")]
277 pub fn safe_transfer_from<S: TopLevelStorage + BorrowMut<Self>>(
278 storage: &mut S,
279 from: Address,
280 to: Address,
281 token_id: U256,
282 ) -> Result<(), Erc721Error> {
283 Self::safe_transfer_from_with_data(storage, from, to, token_id, Bytes(vec![]))
284 }
285
286 /// Transfers the NFT.
287 pub fn transfer_from(&mut self, from: Address, to: Address, token_id: U256) -> Result<(), Erc721Error> {
288 if to.is_zero() {
289 return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
290 }
291 self.require_authorized_to_spend(from, token_id)?;
292 self.transfer(token_id, from, to)?;
293 Ok(())
294 }
295
296 /// Grants an account the ability to manage the sender's NFT.
297 pub fn approve(&mut self, approved: Address, token_id: U256) -> Result<(), Erc721Error> {
298 let owner = self.owner_of(token_id)?;
299
300 // require authorization
301 if msg::sender() != owner && !self.operator_approvals.getter(owner).get(msg::sender()) {
302 return Err(Erc721Error::NotApproved(NotApproved {
303 owner,
304 spender: msg::sender(),
305 token_id,
306 }));
307 }
308 self.token_approvals.insert(token_id, approved);
309
310 evm::log(Approval {
311 approved,
312 owner,
313 token_id,
314 });
315 Ok(())
316 }
317
318 /// Grants an account the ability to manage all of the sender's NFTs.
319 pub fn set_approval_for_all(&mut self, operator: Address, approved: bool) -> Result<(), Erc721Error> {
320 let owner = msg::sender();
321 self.operator_approvals
322 .setter(owner)
323 .insert(operator, approved);
324
325 evm::log(ApprovalForAll {
326 owner,
327 operator,
328 approved,
329 });
330 Ok(())
331 }
332
333 /// Gets the account managing an NFT, or zero if unmanaged.
334 pub fn get_approved(&mut self, token_id: U256) -> Result<Address, Erc721Error> {
335 Ok(self.token_approvals.get(token_id))
336 }
337
338 /// Determines if an account has been authorized to managing all of a user's NFTs.
339 pub fn is_approved_for_all(&mut self, owner: Address, operator: Address) -> Result<bool, Erc721Error> {
340 Ok(self.operator_approvals.getter(owner).get(operator))
341 }
342
343 /// Whether the NFT supports a given standard.
344 pub fn supports_interface(interface: FixedBytes<4>) -> Result<bool, Erc721Error> {
345 let interface_slice_array: [u8; 4] = interface.as_slice().try_into().unwrap();
346
347 if u32::from_be_bytes(interface_slice_array) == 0xffffffff {
348 // special cased in the ERC165 standard
349 return Ok(false);
350 }
351
352 const IERC165: u32 = 0x01ffc9a7;
353 const IERC721: u32 = 0x80ac58cd;
354 const IERC721_METADATA: u32 = 0x5b5e139f;
355
356 Ok(matches!(u32::from_be_bytes(interface_slice_array), IERC165 | IERC721 | IERC721_METADATA))
357 }
358}
1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5// Modules and imports
6mod erc721;
7
8use alloy_primitives::{U256, Address};
9/// Import the Stylus SDK along with alloy primitive types for use in our program.
10use stylus_sdk::{
11 msg, prelude::*
12};
13use crate::erc721::{Erc721, Erc721Params, Erc721Error};
14
15/// Immutable definitions
16struct StylusNFTParams;
17impl Erc721Params for StylusNFTParams {
18 const NAME: &'static str = "StylusNFT";
19 const SYMBOL: &'static str = "SNFT";
20
21 fn token_uri(token_id: U256) -> String {
22 format!("{}{}{}", "https://my-nft-metadata.com/", token_id, ".json")
23 }
24}
25
26// Define the entrypoint as a Solidity storage object. The sol_storage! macro
27// will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
28// storage slots and types.
29sol_storage! {
30 #[entrypoint]
31 struct StylusNFT {
32 #[borrow] // Allows erc721 to access StylusNFT's storage and make calls
33 Erc721<StylusNFTParams> erc721;
34 }
35}
36
37#[public]
38#[inherit(Erc721<StylusNFTParams>)]
39impl StylusNFT {
40 /// Mints an NFT
41 pub fn mint(&mut self) -> Result<(), Erc721Error> {
42 let minter = msg::sender();
43 self.erc721.mint(minter)?;
44 Ok(())
45 }
46
47 /// Mints an NFT to another address
48 pub fn mint_to(&mut self, to: Address) -> Result<(), Erc721Error> {
49 self.erc721.mint(to)?;
50 Ok(())
51 }
52
53 /// Burns an NFT
54 pub fn burn(&mut self, token_id: U256) -> Result<(), Erc721Error> {
55 // This function checks that msg::sender() owns the specified token_id
56 self.erc721.burn(msg::sender(), token_id)?;
57 Ok(())
58 }
59
60 /// Total supply
61 pub fn total_supply(&mut self) -> Result<U256, Erc721Error> {
62 Ok(self.erc721.total_supply.get())
63 }
64}
1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5// Modules and imports
6mod erc721;
7
8use alloy_primitives::{U256, Address};
9/// Import the Stylus SDK along with alloy primitive types for use in our program.
10use stylus_sdk::{
11 msg, prelude::*
12};
13use crate::erc721::{Erc721, Erc721Params, Erc721Error};
14
15/// Immutable definitions
16struct StylusNFTParams;
17impl Erc721Params for StylusNFTParams {
18 const NAME: &'static str = "StylusNFT";
19 const SYMBOL: &'static str = "SNFT";
20
21 fn token_uri(token_id: U256) -> String {
22 format!("{}{}{}", "https://my-nft-metadata.com/", token_id, ".json")
23 }
24}
25
26// Define the entrypoint as a Solidity storage object. The sol_storage! macro
27// will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
28// storage slots and types.
29sol_storage! {
30 #[entrypoint]
31 struct StylusNFT {
32 #[borrow] // Allows erc721 to access StylusNFT's storage and make calls
33 Erc721<StylusNFTParams> erc721;
34 }
35}
36
37#[public]
38#[inherit(Erc721<StylusNFTParams>)]
39impl StylusNFT {
40 /// Mints an NFT
41 pub fn mint(&mut self) -> Result<(), Erc721Error> {
42 let minter = msg::sender();
43 self.erc721.mint(minter)?;
44 Ok(())
45 }
46
47 /// Mints an NFT to another address
48 pub fn mint_to(&mut self, to: Address) -> Result<(), Erc721Error> {
49 self.erc721.mint(to)?;
50 Ok(())
51 }
52
53 /// Burns an NFT
54 pub fn burn(&mut self, token_id: U256) -> Result<(), Erc721Error> {
55 // This function checks that msg::sender() owns the specified token_id
56 self.erc721.burn(msg::sender(), token_id)?;
57 Ok(())
58 }
59
60 /// Total supply
61 pub fn total_supply(&mut self) -> Result<U256, Erc721Error> {
62 Ok(self.erc721.total_supply.get())
63 }
64}
1[package]
2name = "stylus_erc721_example"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14
15[dev-dependencies]
16tokio = { version = "1.12.0", features = ["full"] }
17ethers = "2.0"
18eyre = "0.6.8"
19
20[features]
21export-abi = ["stylus-sdk/export-abi"]
22
23[lib]
24crate-type = ["lib", "cdylib"]
25
26[profile.release]
27codegen-units = 1
28strip = true
29lto = true
30panic = "abort"
31opt-level = "s"
1[package]
2name = "stylus_erc721_example"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14
15[dev-dependencies]
16tokio = { version = "1.12.0", features = ["full"] }
17ethers = "2.0"
18eyre = "0.6.8"
19
20[features]
21export-abi = ["stylus-sdk/export-abi"]
22
23[lib]
24crate-type = ["lib", "cdylib"]
25
26[profile.release]
27codegen-units = 1
28strip = true
29lto = true
30panic = "abort"
31opt-level = "s"