Arbitrum Stylus logo

Stylus by Example

ERC-721

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.

src/erc721.rs

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}

lib.rs

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}

Cargo.toml

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"