Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: ERC721 NFT Contract #250

Open
wants to merge 38 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9703d23
Add ERC721 initial impl
0xNeshi Oct 16, 2024
41bd832
Rename token.cairo->erc721.cairo
0xNeshi Oct 16, 2024
cdc5a9a
Simplify contract
0xNeshi Oct 16, 2024
286e8c4
Implement mint and burn
0xNeshi Oct 16, 2024
0b1bb65
refactor erc721
0xNeshi Oct 17, 2024
eda9fc9
rename back contract->erc721
0xNeshi Oct 17, 2024
446f735
Set up tests
0xNeshi Oct 17, 2024
7a3afc7
Add all getter tests
0xNeshi Oct 17, 2024
fbb07af
Return contract_address from deploy
0xNeshi Oct 17, 2024
15fa3ae
Add approve tests
0xNeshi Oct 17, 2024
dceff79
Add transfer_from tests
0xNeshi Oct 17, 2024
f5ed044
Add safe_transfer_from tests
0xNeshi Oct 17, 2024
9e7ab1d
Add mint tests
0xNeshi Oct 17, 2024
4cb576f
Add burn tests
0xNeshi Oct 17, 2024
282fe6b
Add internal tests
0xNeshi Oct 17, 2024
e5e84e9
Move interfaces into interfaces.cairo + fix build errors
0xNeshi Oct 17, 2024
6bf7d66
fix tests
0xNeshi Oct 17, 2024
69bc00a
Fix approvals update in transfer_from
0xNeshi Oct 17, 2024
09b01b7
Remove redundant build-external-contracts section from Scarb.toml
0xNeshi Oct 18, 2024
9e79fb3
Move snforge_std dep to dev deps
0xNeshi Oct 18, 2024
20b34b1
Set edition to workspace version
0xNeshi Oct 18, 2024
aa3c4a2
Revert edition to specific version + remove [lib]
0xNeshi Oct 18, 2024
fef0cd3
Merge remote-tracking branch 'origin/main' into erc721
0xNeshi Oct 24, 2024
0650e1e
Update edition to point to workspace
0xNeshi Oct 24, 2024
4d8e6e8
merge with upstream/dev
0xNeshi Dec 7, 2024
4e9607c
fix erc20 url to OZ one
0xNeshi Dec 7, 2024
b92ae56
Add comment encouring devs to read the EIP
0xNeshi Dec 7, 2024
d30b450
add optional Metadata & Enumerable interfaces
0xNeshi Dec 7, 2024
e8307c6
make mint & burn internal
0xNeshi Dec 7, 2024
b049308
add comment for safe_transfer_from
0xNeshi Dec 7, 2024
c5c8bb1
implement burn and mint as additional interfaces
0xNeshi Dec 7, 2024
815118b
fix tests
0xNeshi Dec 7, 2024
3fc8031
add comment above metadata & enumerable
0xNeshi Dec 7, 2024
65ffa82
fix links in erc721.md
0xNeshi Dec 7, 2024
f6983ba
refactor interfaces.cairo
0xNeshi Dec 7, 2024
c8ac726
move erc721 route below erc20
0xNeshi Dec 7, 2024
de7ba30
remove redundant newline from interfaces.cairo
0xNeshi Dec 7, 2024
9541678
fix test_safe_transfer_from_to_non_receiver
0xNeshi Dec 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ version = "0.1.0"
name = "erc20"
version = "0.1.0"

[[package]]
name = "erc721"
version = "0.1.0"
dependencies = [
"openzeppelin",
"snforge_std",
]

[[package]]
name = "errors"
version = "0.1.0"
Expand Down
1 change: 1 addition & 0 deletions listings/applications/erc721/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
17 changes: 17 additions & 0 deletions listings/applications/erc721/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "erc721"
version.workspace = true
edition.workspace = true

[dependencies]
starknet.workspace = true
openzeppelin.workspace = true

[dev-dependencies]
assert_macros.workspace = true
snforge_std.workspace = true

[scripts]
test.workspace = true

[[target.starknet-contract]]
193 changes: 193 additions & 0 deletions listings/applications/erc721/src/erc721.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#[starknet::contract]
pub mod ERC721 {
use core::num::traits::Zero;
use starknet::get_caller_address;
use starknet::ContractAddress;
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
use openzeppelin_introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait};
use erc721::interfaces::{
IERC721, IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID
};

#[storage]
pub struct Storage {
pub owners: Map<u256, ContractAddress>,
pub balances: Map<ContractAddress, u256>,
pub approvals: Map<u256, ContractAddress>,
pub operator_approvals: Map<(ContractAddress, ContractAddress), bool>,
}

#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Transfer: Transfer,
Approval: Approval,
ApprovalForAll: ApprovalForAll,
}

#[derive(Drop, starknet::Event)]
pub struct Transfer {
pub from: ContractAddress,
pub to: ContractAddress,
pub token_id: u256
}

#[derive(Drop, starknet::Event)]
pub struct Approval {
pub owner: ContractAddress,
pub approved: ContractAddress,
pub token_id: u256
}

#[derive(Drop, starknet::Event)]
pub struct ApprovalForAll {
pub owner: ContractAddress,
pub operator: ContractAddress,
pub approved: bool
}

pub mod Errors {
pub const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID';
pub const INVALID_ACCOUNT: felt252 = 'ERC721: invalid account';
pub const INVALID_OPERATOR: felt252 = 'ERC721: invalid operator';
pub const UNAUTHORIZED: felt252 = 'ERC721: unauthorized caller';
pub const INVALID_RECEIVER: felt252 = 'ERC721: invalid receiver';
pub const INVALID_SENDER: felt252 = 'ERC721: invalid sender';
pub const SAFE_TRANSFER_FAILED: felt252 = 'ERC721: safe transfer failed';
pub const ALREADY_MINTED: felt252 = 'ERC721: token already minted';
}

#[abi(embed_v0)]
impl ERC721 of IERC721<ContractState> {
fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress {
self._require_owned(token_id)
}

fn balance_of(self: @ContractState, owner: ContractAddress) -> u256 {
assert(!owner.is_zero(), Errors::INVALID_ACCOUNT);
self.balances.read(owner)
}

fn set_approval_for_all(
ref self: ContractState, operator: ContractAddress, approved: bool
) {
assert(!operator.is_zero(), Errors::INVALID_OPERATOR);
let owner = get_caller_address();
self.operator_approvals.write((owner, operator), approved);
self.emit(ApprovalForAll { owner, operator, approved });
}

fn approve(ref self: ContractState, approved: ContractAddress, token_id: u256) {
let owner = self._require_owned(token_id);
let caller = get_caller_address();
assert(
caller == owner || self.is_approved_for_all(owner, caller), Errors::UNAUTHORIZED
);

self.approvals.write(token_id, approved);
self.emit(Approval { owner, approved, token_id });
}

fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress {
self._require_owned(token_id);
self.approvals.read(token_id)
}

fn transfer_from(
ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256
) {
let previous_owner = self._require_owned(token_id);
assert(from == previous_owner, Errors::INVALID_SENDER);
assert(!to.is_zero(), Errors::INVALID_RECEIVER);
assert(
self._is_approved_or_owner(from, get_caller_address(), token_id),
Errors::UNAUTHORIZED
);

self.balances.write(from, self.balances.read(from) - 1);
self.balances.write(to, self.balances.read(to) + 1);
self.owners.write(token_id, to);
self.approvals.write(token_id, Zero::zero());

self.emit(Transfer { from, to, token_id });
}

fn safe_transfer_from(
ref self: ContractState,
from: ContractAddress,
to: ContractAddress,
token_id: u256,
data: Span<felt252>
) {
Self::transfer_from(ref self, from, to, token_id);
assert(
self._check_on_erc721_received(from, to, token_id, data),
Errors::SAFE_TRANSFER_FAILED
);
}

fn is_approved_for_all(
self: @ContractState, owner: ContractAddress, operator: ContractAddress
) -> bool {
self.operator_approvals.read((owner, operator))
}

fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) {
assert(!to.is_zero(), Errors::INVALID_RECEIVER);
assert(self.owners.read(token_id).is_zero(), Errors::ALREADY_MINTED);

self.balances.write(to, self.balances.read(to) + 1);
self.owners.write(token_id, to);

self.emit(Transfer { from: Zero::zero(), to, token_id });
}

fn burn(ref self: ContractState, token_id: u256) {
let owner = self._require_owned(token_id);

self.balances.write(owner, self.balances.read(owner) - 1);

self.owners.write(token_id, Zero::zero());
self.approvals.write(token_id, Zero::zero());

self.emit(Transfer { from: owner, to: Zero::zero(), token_id });
}
0xNeshi marked this conversation as resolved.
Show resolved Hide resolved
}

#[generate_trait]
pub impl InternalImpl of InternalTrait {
fn _require_owned(self: @ContractState, token_id: u256) -> ContractAddress {
let owner = self.owners.read(token_id);
assert(!owner.is_zero(), Errors::INVALID_TOKEN_ID);
owner
}

fn _is_approved_or_owner(
self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256
) -> bool {
!spender.is_zero()
&& (owner == spender
|| self.is_approved_for_all(owner, spender)
|| spender == self.get_approved(token_id))
}

fn _check_on_erc721_received(
self: @ContractState,
from: ContractAddress,
to: ContractAddress,
token_id: u256,
data: Span<felt252>
) -> bool {
let src5_dispatcher = ISRC5Dispatcher { contract_address: to };

if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) {
IERC721ReceiverDispatcher { contract_address: to }
.on_erc721_received(
get_caller_address(), from, token_id, data
) == IERC721_RECEIVER_ID
} else {
src5_dispatcher.supports_interface(openzeppelin_account::interface::ISRC6_ID)
}
}
}
}
43 changes: 43 additions & 0 deletions listings/applications/erc721/src/interfaces.cairo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to also add the optional ERC721Metadata an ERC721Enumerable interfaces

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the interfaces, no implementation?

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use starknet::ContractAddress;

// ANCHOR: interface
#[starknet::interface]
pub trait IERC721<TContractState> {
fn balance_of(self: @TContractState, owner: ContractAddress) -> u256;
fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress;
fn safe_transfer_from(
ref self: TContractState,
from: ContractAddress,
to: ContractAddress,
token_id: u256,
data: Span<felt252>
);
fn transfer_from(
ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256
);
fn approve(ref self: TContractState, approved: ContractAddress, token_id: u256);
fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool);
fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress;
fn is_approved_for_all(
self: @TContractState, owner: ContractAddress, operator: ContractAddress
) -> bool;
fn mint(ref self: TContractState, to: ContractAddress, token_id: u256);
fn burn(ref self: TContractState, token_id: u256);
}
0xNeshi marked this conversation as resolved.
Show resolved Hide resolved

pub const IERC721_RECEIVER_ID: felt252 =
0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc;

#[starknet::interface]
pub trait IERC721Receiver<TContractState> {
fn on_erc721_received(
self: @TContractState,
operator: ContractAddress,
from: ContractAddress,
token_id: u256,
data: Span<felt252>
) -> felt252;
}
// ANCHOR_END: interface


6 changes: 6 additions & 0 deletions listings/applications/erc721/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod erc721;
pub mod interfaces;
mod mocks;

#[cfg(test)]
mod tests;
7 changes: 7 additions & 0 deletions listings/applications/erc721/src/mocks.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub mod account;
pub mod receiver;
pub mod non_receiver;

pub use account::AccountMock;
pub use non_receiver::NonReceiverMock;
pub use receiver::ERC721ReceiverMock;
46 changes: 46 additions & 0 deletions listings/applications/erc721/src/mocks/account.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Copied with modifications from OpenZeppelin's repo
//! https://github.com/OpenZeppelin/cairo-contracts/blob/6e60ba9310fa7953f045d0c30b343b0ffc168c14/packages/test_common/src/mocks/account.cairo

#[starknet::contract(account)]
pub mod AccountMock {
use openzeppelin_account::AccountComponent;
use openzeppelin_introspection::src5::SRC5Component;

component!(path: AccountComponent, storage: account, event: AccountEvent);
component!(path: SRC5Component, storage: src5, event: SRC5Event);

// Account
#[abi(embed_v0)]
impl SRC6Impl = AccountComponent::SRC6Impl<ContractState>;
#[abi(embed_v0)]
impl DeclarerImpl = AccountComponent::DeclarerImpl<ContractState>;
#[abi(embed_v0)]
impl DeployableImpl = AccountComponent::DeployableImpl<ContractState>;
impl AccountInternalImpl = AccountComponent::InternalImpl<ContractState>;

// SCR5
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;

#[storage]
pub struct Storage {
#[substorage(v0)]
pub account: AccountComponent::Storage,
#[substorage(v0)]
pub src5: SRC5Component::Storage
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
AccountEvent: AccountComponent::Event,
#[flat]
SRC5Event: SRC5Component::Event
}

#[constructor]
fn constructor(ref self: ContractState, public_key: felt252) {
self.account.initializer(public_key);
}
}
10 changes: 10 additions & 0 deletions listings/applications/erc721/src/mocks/non_receiver.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#[starknet::contract]
pub mod NonReceiverMock {
#[storage]
pub struct Storage {}

#[external(v0)]
fn nope(self: @ContractState) -> bool {
false
}
}
50 changes: 50 additions & 0 deletions listings/applications/erc721/src/mocks/receiver.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const SUCCESS: felt252 = 'SUCCESS';

#[starknet::contract]
pub mod ERC721ReceiverMock {
use openzeppelin_introspection::src5::SRC5Component;
use erc721::interfaces::{IERC721Receiver, IERC721_RECEIVER_ID};
use starknet::ContractAddress;

component!(path: SRC5Component, storage: src5, event: SRC5Event);

// SRC5
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
impl SRC5InternalImpl = SRC5Component::InternalImpl<ContractState>;

#[storage]
pub struct Storage {
#[substorage(v0)]
pub src5: SRC5Component::Storage
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
SRC5Event: SRC5Component::Event
}

#[constructor]
fn constructor(ref self: ContractState) {
self.src5.register_interface(IERC721_RECEIVER_ID);
}

#[abi(embed_v0)]
impl ExternalImpl of IERC721Receiver<ContractState> {
fn on_erc721_received(
self: @ContractState,
operator: ContractAddress,
from: ContractAddress,
token_id: u256,
data: Span<felt252>
) -> felt252 {
if *data.at(0) == super::SUCCESS {
IERC721_RECEIVER_ID
} else {
0
}
}
}
}
Loading