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

(draft pr) implement basic erc721 structure for certificates #14

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 92 additions & 1 deletion backend-sc/src/components/certificate/certificate.cairo
Original file line number Diff line number Diff line change
@@ -1,2 +1,93 @@
#[starknet::component]
mod NFTComponent {}
mod NFTComponent {
// Starknet imports
use starknet::{ContractAddress, get_caller_address};

// Internal imports
use carbon_locker::components::certificate::interface::INFTComponent;
use carbon_locker::components::certificate::interface::LOCKER_ROLE;
use carbon_locker::components::locker::interface::Lock;

// Roles
use openzeppelin::access::accesscontrol::interface::IAccessControl;
use openzeppelin::access::accesscontrol::AccessControlComponent;
use openzeppelin_access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait;

// SRC5
use openzeppelin::introspection::src5::SRC5Component;
use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait};

// ERC721
use openzeppelin::token::erc721::{
ERC721Component, ERC721HooksEmptyImpl, ERC721Component::InternalTrait as ERC721InternalTrait
};

mod Errors {
const INVALID_ROLE: felt252 = 'Only Locker is allowed';
}

#[storage]
struct Storage {//metadatas: Map<u256, Lock>
}

#[embeddable_as(NFTComponentImpl)]
impl NFTComponent<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC721: ERC721Component::HasComponent<TContractState>,
+SRC5Component::HasComponent<TContractState>,
+IAccessControl<TContractState>,
impl AccessControl: AccessControlComponent::HasComponent<TContractState>,
> of INFTComponent<ComponentState<TContractState>> {
fn mint(
ref self: ComponentState<TContractState>,
to: ContractAddress,
token_id: u256,
lock_data: Lock
) {
self.assert_only_locker(LOCKER_ROLE);
let mut erc721_component = get_dep_component_mut!(ref self, ERC721);
erc721_component.mint(to, token_id);
}

fn burn(ref self: ComponentState<TContractState>, token_id: u256) {
self.assert_only_locker(LOCKER_ROLE);
let mut erc721_component = get_dep_component_mut!(ref self, ERC721);
erc721_component.burn(token_id);
}
}

#[generate_trait]
impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC721: ERC721Component::HasComponent<TContractState>,
+SRC5Component::HasComponent<TContractState>,
+IAccessControl<TContractState>,
impl AccessControl: AccessControlComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
fn initializer(
ref self: ComponentState<TContractState>,
locker_address: ContractAddress,
token_name: ByteArray,
token_symbol: ByteArray,
token_base_uri: ByteArray,
) {
let mut access_control = get_dep_component_mut!(ref self, AccessControl);
access_control.initializer();
access_control._grant_role(LOCKER_ROLE, locker_address);

let mut erc721_component = get_dep_component_mut!(ref self, ERC721);
erc721_component.initializer(token_name, token_symbol, token_base_uri);
}

// Only the Locker address is allowed to mint and burn a token
fn assert_only_locker(self: @ComponentState<TContractState>, role: felt252) {
let caller = get_caller_address();
let has_role = self.get_contract().has_role(role, caller);
assert(has_role, Errors::INVALID_ROLE);
}
}
}
9 changes: 8 additions & 1 deletion backend-sc/src/components/certificate/interface.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
use starknet::ContractAddress;
/// Implement NFT Certicate. Only Locker can mint, burn and update metadata.

const LOCKER_ROLE: felt252 = selector!("Locker");

use carbon_locker::components::locker::interface::Lock;

#[starknet::interface]
trait INFTComponent<TContractState> {
fn mint(ref self: TContractState, to: ContractAddress, token_id: u256, lock_data: Lock);
fn burn(ref self: TContractState, token_id: u256);
}
4 changes: 4 additions & 0 deletions backend-sc/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ mod components {
mod interface;
mod locker_handler;
}
mod certificate {
mod interface;
mod certificate;
}
}
4 changes: 4 additions & 0 deletions backend-sc/tests/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
mod tests_locker;
mod tests_utils;
mod tests_certificate;
mod mocks {
mod erc721;
}
65 changes: 65 additions & 0 deletions backend-sc/tests/mocks/erc721.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use starknet::ContractAddress;

#[starknet::contract]
mod MockERC721 {
use starknet::ContractAddress;

// SRC5
use openzeppelin_introspection::src5::SRC5Component;
// ERC721
use openzeppelin_token::erc721::{ERC721Component, ERC721HooksEmptyImpl};
// Access Control - RBA
use openzeppelin::access::accesscontrol::AccessControlComponent;
// Certificate NFT Component
use carbon_locker::components::certificate::certificate::NFTComponent;

component!(path: ERC721Component, storage: erc721, event: ERC721Event);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);
component!(path: NFTComponent, storage: nft_component, event: NFTComponentEvent);

// ERC721 Mixin
#[abi(embed_v0)]
impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
// Access Control
#[abi(embed_v0)]
impl AccessControlImpl =
AccessControlComponent::AccessControlImpl<ContractState>;
impl AccessControlInternalImpl = AccessControlComponent::InternalImpl<ContractState>;
// NFT Certificate
#[abi(embed_v0)]
impl NFTComponentImpl = NFTComponent::NFTComponentImpl<ContractState>;
impl NFTComponentInternalImpl = NFTComponent::InternalImpl<ContractState>;

#[storage]
struct Storage {
#[substorage(v0)]
erc721: ERC721Component::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage,
#[substorage(v0)]
accesscontrol: AccessControlComponent::Storage,
#[substorage(v0)]
nft_component: NFTComponent::Storage,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC721Event: ERC721Component::Event,
#[flat]
SRC5Event: SRC5Component::Event,
#[flat]
AccessControlEvent: AccessControlComponent::Event,
#[flat]
NFTComponentEvent: NFTComponent::Event,
}

#[constructor]
fn constructor(ref self: ContractState, locker_address: ContractAddress) {
self.nft_component.initializer(locker_address, "Certificate", "CERT", "data:application/json,");
}
}

139 changes: 139 additions & 0 deletions backend-sc/tests/tests_certificate.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Starknet deps

use starknet::{ContractAddress, contract_address_const, get_caller_address, get_block_timestamp};

// External deps

use snforge_std as snf;
use snforge_std::{
ContractClassTrait, EventSpy, start_cheat_caller_address, stop_cheat_caller_address, spy_events,
start_cheat_block_timestamp_global,
cheatcodes::events::{EventSpyAssertionsTrait, EventSpyTrait, EventsFilterTrait}
};

// ERC721 Components

use openzeppelin::token::erc721::interface::{
IERC721Dispatcher, IERC721DispatcherTrait,
IERC721MetadataDispatcher, IERC721MetadataDispatcherTrait
};

// Internal interfaces
use carbon_locker::components::certificate::interface::{
INFTComponentDispatcher, INFTComponentDispatcherTrait
};
use carbon_locker::components::locker::interface::Lock;

// Contracts
use super::mocks::erc721::MockERC721;

// Utils for testing purposes
use super::tests_utils::{deploy_all};

fn generate_lock_data() -> Lock {
Lock {
id: 1_u256,
user: contract_address_const::<'USER'>(),
token_id: 1_u256,
amount: 1000_u256,
start_time: get_block_timestamp(),
end_time: get_block_timestamp() + 1000_u64,
offsettable: false,
is_offsetted: false
}
}

/// Test the intializer function
#[test]
fn test_certificate_initializer() {
let (_, _, _, _, _, certificate_address) = deploy_all();
let erc721_metadata = IERC721MetadataDispatcher { contract_address: certificate_address };

let name = erc721_metadata.name();
assert(name == "Certificate", 'Token name mismatch');

let symbol = erc721_metadata.symbol();
assert(symbol == "CERT", 'Token symbol mismatch');
}

#[test]
fn test_mint() {
let (_, locker_address, _, _, _, certificate_address) = deploy_all();
let certificate = INFTComponentDispatcher { contract_address: certificate_address };

// Call with locker permissions
start_cheat_caller_address(certificate_address, locker_address);

// Mint the token
let user_address: ContractAddress = contract_address_const::<'USER'>();
let token_id: u256 = 1;
let lock_data = generate_lock_data();
certificate.mint(user_address, token_id, lock_data);

// Check the user_address balance
let erc721 = IERC721Dispatcher { contract_address: certificate_address };
let balance = erc721.balance_of(user_address);
assert(balance == 1, 'Balance should be 1');
}

#[test]
#[should_panic(expected: 'Only Locker is allowed')]
fn test_unauthorized_caller_mint() {
let (_, _, _, _, _, certificate_address) = deploy_all();
let certificate = INFTComponentDispatcher { contract_address: certificate_address };

// Call with user_address permissions
let user_address: ContractAddress = contract_address_const::<'USER'>();
start_cheat_caller_address(certificate_address, user_address);

// Mint the token
let token_id: u256 = 1;
let lock_data = generate_lock_data();
certificate.mint(user_address, token_id, lock_data);
stop_cheat_caller_address(certificate_address);
}

#[test]
fn test_burn() {
let (_, locker_address, _, _, _, certificate_address) = deploy_all();
let certificate = INFTComponentDispatcher { contract_address: certificate_address };

// Call with locker permissions
start_cheat_caller_address(certificate_address, locker_address);

// Mint the token
let user_address: ContractAddress = contract_address_const::<'USER'>();
let token_id: u256 = 1;
let lock_data = generate_lock_data();
certificate.mint(user_address, token_id, lock_data);

// Burn the token
certificate.burn(token_id);

// Check the user_address balance
let erc721 = IERC721Dispatcher { contract_address: certificate_address };
let balance = erc721.balance_of(user_address);
assert(balance == 0, 'Balance should be 0');
}

#[test]
#[should_panic(expected: 'Only Locker is allowed')]
fn test_unauthorized_burn() {
let (_, locker_address, _, _, _, certificate_address) = deploy_all();
let certificate = INFTComponentDispatcher { contract_address: certificate_address };

// Call with locker permissions
start_cheat_caller_address(certificate_address, locker_address);

// Mint the token
let user_address: ContractAddress = contract_address_const::<'USER'>();
let token_id: u256 = 1;
let lock_data = generate_lock_data();
certificate.mint(user_address, token_id, lock_data);

// Call with user_address permission
start_cheat_caller_address(certificate_address, user_address);

// Burn the token
certificate.burn(token_id);
}
Loading