diff --git a/README.md b/README.md index d33aa8d..c237126 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ The .devcontainer container will automatically generate key pair and deploy a dev account just follow prompts ### Contract Deployment + ```bash /bin/bash /workspaces/golden-token/scripts/deploy_contract.sh ``` ## Non-VSCode Setup + ```console starkli declare target/dev/goldenToken_ERC721.sierra.json --account ./account --keystore ./keys @@ -23,3 +25,5 @@ export DAO_ADDRESS=0x020b96923a9e60f63a1829d440a03cf680768cadbc8fe737f7138025881 starkli deploy 0x007ccf8c0a9a27392a68ec91db0d8005fd6d10ce0039a0627c8e0b0af7a73d7d $GOLDEN_TOKEN_NAME $GOLDEN_TOKEN_SYMBOL $OWNER $DAO_ADDRESS --account ./account --keystore ./keys ``` +starkli declare /Users/os/Documents/code/biblio/golden-token/target/dev/golden_token_ERC721.contract_class.json --account ./account-sepolia --keystore ./keys +starkli deploy 0x07523d0f2f1b4f2d387fe5548fa7c0a7ab77f6e49241f26c6aa8682f808379b0 --account ./account-sepolia --keystore ./keys diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..dda485a --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,23 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "arcade_account" +version = "0.1.0" +source = "git+https://github.com/BibliothecaDAO/arcade-account?branch=next#c24e720d75fc3b85b878dd896ae8d0d26ce8e04d" +dependencies = [ + "openzeppelin", +] + +[[package]] +name = "golden_token" +version = "0.1.0" +dependencies = [ + "arcade_account", + "openzeppelin", +] + +[[package]] +name = "openzeppelin" +version = "0.9.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#861fc416f87addbe23a3b47f9d19ab27c10d5dc8" diff --git a/Scarb.toml b/Scarb.toml index 2e3cb29..37687a5 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -1,11 +1,11 @@ [package] -name = "goldenToken" +name = "golden_token" version = "0.1.0" [dependencies] -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.7.0" } -starknet = "2.1.1" -arcade_account = { git = "https://github.com/BibliothecaDAO/arcade-account" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.9.0" } +starknet = "2.4.0" +arcade_account = { git = "https://github.com/BibliothecaDAO/arcade-account", branch = "next" } [lib] diff --git a/account-sepolia b/account-sepolia new file mode 100644 index 0000000..20cd6c4 --- /dev/null +++ b/account-sepolia @@ -0,0 +1,14 @@ +{ + "version": 1, + "variant": { + "type": "open_zeppelin", + "version": 1, + "public_key": "0x3e851144750a495172fd41eb7d84e8c97402ed3fd680a5e7f474d9452b2c952", + "legacy": false + }, + "deployment": { + "status": "deployed", + "class_hash": "0x4c6d6cf894f8bc96bb9c525e6853e5483177841f7388f74a46cfda6f028c755", + "address": "0x3dba48aee5fd05933d333b57f8ab7caa953b97a257b910b6dd2bc21eb9e975b" + } +} diff --git a/src/lib.cairo b/src/lib.cairo index 8a15a5f..82d82d4 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,483 +1,195 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.7.0 (token/erc721/erc721.cairo) - -use starknet::ContractAddress; +// OpenZeppelin Contracts for Cairo v0.8.0 (presets/erc721.cairo) +/// # ERC721 Preset +/// +/// The ERC721 contract offers a batch-mint mechanism that +/// can only be executed once upon contract construction. #[starknet::contract] -mod ERC721 { - use core::traits::TryInto; - use array::SpanTrait; - use openzeppelin::account; - - use openzeppelin::access::ownable; - use openzeppelin::introspection::dual_src5::DualCaseSRC5; - use openzeppelin::introspection::dual_src5::DualCaseSRC5Trait; - use openzeppelin::introspection::interface::ISRC5; - use openzeppelin::introspection::interface::ISRC5Camel; - use openzeppelin::introspection::src5; - use openzeppelin::token::erc721::dual721_receiver::DualCaseERC721Receiver; - use openzeppelin::token::erc721::dual721_receiver::DualCaseERC721ReceiverTrait; - use openzeppelin::token::erc721::interface; - use option::OptionTrait; - use starknet::ContractAddress; - use starknet::{get_caller_address, get_block_timestamp}; - use zeroable::Zeroable; - use openzeppelin::token::erc20::interface::{ - IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait, IERC20CamelLibraryDispatcher - }; - use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; - - - use arcade_account::{ - account::interface::{ - IMasterControl, IMasterControlDispatcher, IMasterControlDispatcherTrait - }, - Account, ARCADE_ACCOUNT_ID - }; - const MINT_COST: u256 = 90000000000000000; +mod GoldenToken { + use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait; + use openzeppelin::access::ownable::interface::IOwnable; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::ERC721Component; + use openzeppelin::access::ownable::OwnableComponent; + + use starknet::{get_caller_address, get_block_timestamp, ContractAddress}; + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + // ERC721 + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; + #[abi(embed_v0)] + impl ERC721CamelOnly = ERC721Component::ERC721CamelOnlyImpl; + #[abi(embed_v0)] + impl ERC721MetadataCamelOnly = + ERC721Component::ERC721MetadataCamelOnlyImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; #[storage] struct Storage { - _name: felt252, - _symbol: felt252, - _owners: LegacyMap, - _balances: LegacyMap, - _token_approvals: LegacyMap, - _operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>, - _open_edition_end: u256, - _count: u256, - _open: bool, - _owner: ContractAddress, - _dao: ContractAddress, - _eth: ContractAddress + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + count: felt252, + open_edition_end: u256, + open: bool, + owner: ContractAddress, + dao: ContractAddress, + eth: ContractAddress } - #[event] #[derive(Drop, starknet::Event)] enum Event { - Transfer: Transfer, - Approval: Approval, - ApprovalForAll: ApprovalForAll, - OwnershipTransferred: OwnershipTransferred - } - - #[derive(Drop, starknet::Event)] - struct OwnershipTransferred { - previous_owner: ContractAddress, - new_owner: ContractAddress, - } - - #[derive(Drop, starknet::Event)] - struct Transfer { - from: ContractAddress, - to: ContractAddress, - token_id: u256 + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, } - #[derive(Drop, starknet::Event)] - struct Approval { - owner: ContractAddress, - approved: ContractAddress, - token_id: u256 - } - - #[derive(Drop, starknet::Event)] - struct ApprovalForAll { - owner: ContractAddress, - operator: ContractAddress, - approved: bool + mod Errors { + const UNEQUAL_ARRAYS: felt252 = 'Array lengths do not match'; } + /// Sets the token `name` and `symbol`. + /// Mints the `token_ids` tokens to `recipient` and sets + /// each token's URI. #[constructor] fn constructor( ref self: ContractState, - name: felt252, symbol: felt252, owner: ContractAddress, dao: ContractAddress, eth: ContractAddress ) { - self.initializer(name, symbol); - self._owner.write(owner); - self._dao.write(dao); - self._eth.write(eth); - } - - // - // External - // - - #[external(v0)] - impl SRC5Impl of ISRC5 { - fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { - let unsafe_state = src5::SRC5::unsafe_new_contract_state(); - src5::SRC5::SRC5Impl::supports_interface(@unsafe_state, interface_id) - } - } - - #[starknet::interface] - trait IERC721MetadataFeltArray { - fn name(self: @TState) -> felt252; - fn symbol(self: @TState) -> felt252; - fn token_uri(self: @TState, token_id: u256) -> Array; - } - - #[external(v0)] - impl ERC721MetadataImpl of IERC721MetadataFeltArray { - fn name(self: @ContractState) -> felt252 { - self._name.read() - } - - fn symbol(self: @ContractState) -> felt252 { - self._symbol.read() - } - - fn token_uri(self: @ContractState, token_id: u256) -> Array { - assert(self._exists(token_id), 'ERC721: invalid token ID'); - self._token_uri(token_id) - } - } - - #[external(v0)] - impl ERC721Impl of interface::IERC721 { - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - assert(!account.is_zero(), 'ERC721: invalid account'); - self._balances.read(account) - } - - fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { - self._owner_of(token_id) - } - - fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { - assert(self._exists(token_id), 'ERC721: invalid token ID'); - self._token_approvals.read(token_id) - } + let name: felt252 = 'GoldenToken'; + let symbol: felt252 = 'GTKN'; - fn is_approved_for_all( - self: @ContractState, owner: ContractAddress, operator: ContractAddress - ) -> bool { - self._operator_approvals.read((owner, operator)) - } - - fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { - let owner = self._owner_of(token_id); - - let caller = get_caller_address(); - assert( - owner == caller || ERC721Impl::is_approved_for_all(@self, owner, caller), - 'ERC721: unauthorized caller' - ); - self._approve(to, token_id); - } - - fn set_approval_for_all( - ref self: ContractState, operator: ContractAddress, approved: bool - ) { - self._set_approval_for_all(get_caller_address(), operator, approved) - } - - fn transfer_from( - ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 - ) { - assert( - self._is_approved_or_owner(get_caller_address(), token_id), - 'ERC721: unauthorized caller' - ); - self._transfer(from, to, token_id); - } + self.ownable.initializer(owner); + self.dao.write(dao); + self.eth.write(eth); - fn safe_transfer_from( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ) { - assert( - self._is_approved_or_owner(get_caller_address(), token_id), - 'ERC721: unauthorized caller' - ); - self._safe_transfer(from, to, token_id, data); - } + self.erc721.initializer(name, symbol); } #[starknet::interface] - trait GoldenToken { - fn mint(ref self: TState); - fn open(ref self: TState); + trait IGoldenToken { + fn mint(ref self: TContractState); + fn open(ref self: TContractState); } + const MINT_COST: u256 = 90000000000000000; const DAY: felt252 = 86400; const OPEN_EDITION_LENGTH_DAYS: u256 = 21; - #[external(v0)] - impl GoldenTokenImpl of GoldenToken { + #[abi(embed_v0)] + impl GoldenTokenImpl of IGoldenToken { fn mint(ref self: ContractState) { - let caller = get_caller_address(); - assert(self._open.read(), 'mint not open'); - assert( - get_block_timestamp().into() < self._open_edition_end.read(), - 'open edition not available' - ); + let mut current_count = self.count.read(); - let tokenId = self._count.read(); - let new_tokenId = tokenId + 1; + current_count += 1; - self._count.write(new_tokenId); - self._mint(caller, new_tokenId); + // Mint the token. + self.erc721._mint(get_caller_address(), current_count.into()); - IERC20CamelDispatcher { contract_address: self._eth.read() } - .transferFrom(caller, self._dao.read(), MINT_COST); + self.count.write(current_count); } - fn open(ref self: ContractState) { - self.assert_only_owner(); - assert(!self._open.read(), 'already open'); + self.ownable.assert_only_owner(); + + assert(!self.open.read(), 'already open'); // open and set to 3 days from now self - ._open_edition_end + .open_edition_end .write(get_block_timestamp().into() + DAY.into() * OPEN_EDITION_LENGTH_DAYS.into()); - self._open.write(true); - } - } - - // - // Internal - // - - #[generate_trait] - impl InternalImpl of InternalTrait { - fn initializer(ref self: ContractState, name_: felt252, symbol_: felt252) { - self._name.write(name_); - self._symbol.write(symbol_); - - let mut unsafe_state = src5::SRC5::unsafe_new_contract_state(); - src5::SRC5::InternalImpl::register_interface(ref unsafe_state, interface::IERC721_ID); - src5::SRC5::InternalImpl::register_interface( - ref unsafe_state, interface::IERC721_METADATA_ID - ); - } - - fn _owner_of(self: @ContractState, token_id: u256) -> ContractAddress { - let owner = self._owners.read(token_id); - match owner.is_zero() { - bool::False(()) => owner, - bool::True(()) => panic_with_felt252('ERC721: invalid token ID') - } - } - - fn _exists(self: @ContractState, token_id: u256) -> bool { - !self._owners.read(token_id).is_zero() - } - - fn _is_approved_or_owner( - self: @ContractState, spender: ContractAddress, token_id: u256 - ) -> bool { - let owner = self._owner_of(token_id); - let is_approved_for_all = ERC721Impl::is_approved_for_all(self, owner, spender); - owner == spender - || is_approved_for_all - || spender == ERC721Impl::get_approved(self, token_id) - } - - fn _approve(ref self: ContractState, to: ContractAddress, token_id: u256) { - let owner = self._owner_of(token_id); - assert(owner != to, 'ERC721: approval to owner'); - - self._token_approvals.write(token_id, to); - self.emit(Approval { owner, approved: to, token_id }); - } - - fn _set_approval_for_all( - ref self: ContractState, - owner: ContractAddress, - operator: ContractAddress, - approved: bool - ) { - assert(owner != operator, 'ERC721: self approval'); - self._operator_approvals.write((owner, operator), approved); - self.emit(ApprovalForAll { owner, operator, approved }); - } - - fn _mint(ref self: ContractState, to: ContractAddress, token_id: u256) { - assert(!to.is_zero(), 'ERC721: invalid receiver'); - assert(!self._exists(token_id), 'ERC721: token already minted'); - - self._balances.write(to, self._balances.read(to) + 1); - self._owners.write(token_id, to); - - self.emit(Transfer { from: Zeroable::zero(), to, token_id }); - } - - fn _transfer( - ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 - ) { - assert(!to.is_zero(), 'ERC721: invalid receiver'); - let owner = self._owner_of(token_id); - assert(from == owner, 'ERC721: wrong sender'); - - // Implicit clear approvals, no need to emit an event - self._token_approvals.write(token_id, Zeroable::zero()); - - 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.emit(Transfer { from, to, token_id }); - } - - fn _burn(ref self: ContractState, token_id: u256) { - let owner = self._owner_of(token_id); - - // Implicit clear approvals, no need to emit an event - self._token_approvals.write(token_id, Zeroable::zero()); - - self._balances.write(owner, self._balances.read(owner) - 1); - self._owners.write(token_id, Zeroable::zero()); - - self.emit(Transfer { from: owner, to: Zeroable::zero(), token_id }); - } - - fn _safe_mint( - ref self: ContractState, to: ContractAddress, token_id: u256, data: Span - ) { - self._mint(to, token_id); - assert( - _check_on_erc721_received(Zeroable::zero(), to, token_id, data), - 'ERC721: safe mint failed' - ); - } - - fn _safe_transfer( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ) { - self._transfer(from, to, token_id); - assert( - _check_on_erc721_received(from, to, token_id, data), 'ERC721: safe transfer failed' - ); - } - - fn assert_only_owner(self: @ContractState) { - let owner: ContractAddress = self._owner.read(); - let caller: ContractAddress = get_caller_address(); - assert(!caller.is_zero(), 'Caller is the zero address'); - assert(caller == owner, 'Caller is not the owner'); - } - - fn _transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { - let previous_owner: ContractAddress = self._owner.read(); - self._owner.write(new_owner); - self - .emit( - OwnershipTransferred { previous_owner: previous_owner, new_owner: new_owner } - ); - } - fn _token_uri(self: @ContractState, token_id: u256) -> Array:: { - assert(self._exists(token_id), 'ERC721: invalid token ID'); - - let mut content = ArrayTrait::::new(); - - // Name & Description - content.append('data:application/json;utf8,'); - content.append('{"name":"Golden Token",'); - content.append('"description":"One free game, '); - content.append('every day, forever"'); - - // Image - content.append(',"image":"'); - content.append('data:image/svg+xml;utf8,"}'); - - content - } - } - - - #[external(v0)] - impl OwnableImpl of ownable::interface::IOwnable { - fn owner(self: @ContractState) -> ContractAddress { - self._owner.read() - } - - fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { - assert(!new_owner.is_zero(), 'New owner is the zero address'); - self.assert_only_owner(); - self._transfer_ownership(new_owner); - } - - fn renounce_ownership(ref self: ContractState) { - self.assert_only_owner(); - self._transfer_ownership(Zeroable::zero()); - } - } - - #[internal] - fn _check_on_erc721_received( - from: ContractAddress, to: ContractAddress, token_id: u256, data: Span - ) -> bool { - if (DualCaseSRC5 { contract_address: to } - .supports_interface(interface::IERC721_RECEIVER_ID)) { - DualCaseERC721Receiver { contract_address: to } - .on_erc721_received( - get_caller_address(), from, token_id, data - ) == interface::IERC721_RECEIVER_ID - } else { - DualCaseSRC5 { contract_address: to }.supports_interface(account::interface::ISRC6_ID) - } + self.open.write(true); + } + } + + fn _token_uri(self: @ContractState, token_id: u256) -> Array:: { + assert(self.erc721._exists(token_id), 'ERC721: invalid token ID'); + + let mut content = ArrayTrait::::new(); + + // Name & Description + content.append('data:application/json;utf8,'); + content.append('{"name":"Golden Token",'); + content.append('"description":"One free game, '); + content.append('every day, forever"'); + + // Image + content.append(',"image":"'); + content.append('data:image/svg+xml;utf8,"}'); + + content } }