Skip to content

Commit 95a8fe3

Browse files
author
aradix16
committed
add RBAC for mint and burn functions and test coverage
1 parent 156bb32 commit 95a8fe3

8 files changed

Lines changed: 349 additions & 17 deletions

File tree

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,93 @@
11
#[starknet::component]
2-
mod NFTComponent {}
2+
mod NFTComponent {
3+
// Starknet imports
4+
use starknet::{ContractAddress, get_caller_address};
5+
6+
// Internal imports
7+
use carbon_locker::components::certificate::interface::INFTComponent;
8+
use carbon_locker::components::certificate::interface::LOCKER_ROLE;
9+
use carbon_locker::components::locker::interface::Lock;
10+
11+
// Roles
12+
use openzeppelin::access::accesscontrol::interface::IAccessControl;
13+
use openzeppelin::access::accesscontrol::AccessControlComponent;
14+
use openzeppelin_access::accesscontrol::AccessControlComponent::InternalTrait as AccessControlInternalTrait;
15+
16+
// SRC5
17+
use openzeppelin::introspection::src5::SRC5Component;
18+
use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait};
19+
20+
// ERC721
21+
use openzeppelin::token::erc721::{
22+
ERC721Component, ERC721HooksEmptyImpl, ERC721Component::InternalTrait as ERC721InternalTrait
23+
};
24+
25+
mod Errors {
26+
const INVALID_ROLE: felt252 = 'Only Locker is allowed';
27+
}
28+
29+
#[storage]
30+
struct Storage {//metadatas: Map<u256, Lock>
31+
}
32+
33+
#[embeddable_as(NFTComponentImpl)]
34+
impl NFTComponent<
35+
TContractState,
36+
+HasComponent<TContractState>,
37+
+Drop<TContractState>,
38+
impl ERC721: ERC721Component::HasComponent<TContractState>,
39+
+SRC5Component::HasComponent<TContractState>,
40+
+IAccessControl<TContractState>,
41+
impl AccessControl: AccessControlComponent::HasComponent<TContractState>,
42+
> of INFTComponent<ComponentState<TContractState>> {
43+
fn mint(
44+
ref self: ComponentState<TContractState>,
45+
to: ContractAddress,
46+
token_id: u256,
47+
lock_data: Lock
48+
) {
49+
self.assert_only_locker(LOCKER_ROLE);
50+
let mut erc721_component = get_dep_component_mut!(ref self, ERC721);
51+
erc721_component.mint(to, token_id);
52+
}
53+
54+
fn burn(ref self: ComponentState<TContractState>, token_id: u256) {
55+
self.assert_only_locker(LOCKER_ROLE);
56+
let mut erc721_component = get_dep_component_mut!(ref self, ERC721);
57+
erc721_component.burn(token_id);
58+
}
59+
}
60+
61+
#[generate_trait]
62+
impl InternalImpl<
63+
TContractState,
64+
+HasComponent<TContractState>,
65+
+Drop<TContractState>,
66+
impl ERC721: ERC721Component::HasComponent<TContractState>,
67+
+SRC5Component::HasComponent<TContractState>,
68+
+IAccessControl<TContractState>,
69+
impl AccessControl: AccessControlComponent::HasComponent<TContractState>,
70+
> of InternalTrait<TContractState> {
71+
fn initializer(
72+
ref self: ComponentState<TContractState>,
73+
locker_address: ContractAddress,
74+
token_name: ByteArray,
75+
token_symbol: ByteArray,
76+
token_base_uri: ByteArray,
77+
) {
78+
let mut access_control = get_dep_component_mut!(ref self, AccessControl);
79+
access_control.initializer();
80+
access_control._grant_role(LOCKER_ROLE, locker_address);
81+
82+
let mut erc721_component = get_dep_component_mut!(ref self, ERC721);
83+
erc721_component.initializer(token_name, token_symbol, token_base_uri);
84+
}
85+
86+
// Only the Locker address is allowed to mint and burn a token
87+
fn assert_only_locker(self: @ComponentState<TContractState>, role: felt252) {
88+
let caller = get_caller_address();
89+
let has_role = self.get_contract().has_role(role, caller);
90+
assert(has_role, Errors::INVALID_ROLE);
91+
}
92+
}
93+
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
use starknet::ContractAddress;
2-
/// Implement NFT Certicate. Only Locker can mint, burn and update metadata.
32

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

5+
use carbon_locker::components::locker::interface::Lock;
6+
7+
#[starknet::interface]
8+
trait INFTComponent<TContractState> {
9+
fn mint(ref self: TContractState, to: ContractAddress, token_id: u256, lock_data: Lock);
10+
fn burn(ref self: TContractState, token_id: u256);
11+
}

backend-sc/src/lib.cairo

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ mod components {
77
mod interface;
88
mod locker_handler;
99
}
10+
mod certificate {
11+
mod interface;
12+
mod certificate;
13+
}
1014
}

backend-sc/tests/lib.cairo

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
mod tests_locker;
22
mod tests_utils;
3+
mod tests_certificate;
4+
mod mocks {
5+
mod erc721;
6+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use starknet::ContractAddress;
2+
3+
#[starknet::contract]
4+
mod MockERC721 {
5+
use starknet::ContractAddress;
6+
7+
// SRC5
8+
use openzeppelin_introspection::src5::SRC5Component;
9+
// ERC721
10+
use openzeppelin_token::erc721::{ERC721Component, ERC721HooksEmptyImpl};
11+
// Access Control - RBA
12+
use openzeppelin::access::accesscontrol::AccessControlComponent;
13+
// Certificate NFT Component
14+
use carbon_locker::components::certificate::certificate::NFTComponent;
15+
16+
component!(path: ERC721Component, storage: erc721, event: ERC721Event);
17+
component!(path: SRC5Component, storage: src5, event: SRC5Event);
18+
component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);
19+
component!(path: NFTComponent, storage: nft_component, event: NFTComponentEvent);
20+
21+
// ERC721 Mixin
22+
#[abi(embed_v0)]
23+
impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
24+
impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
25+
// Access Control
26+
#[abi(embed_v0)]
27+
impl AccessControlImpl =
28+
AccessControlComponent::AccessControlImpl<ContractState>;
29+
impl AccessControlInternalImpl = AccessControlComponent::InternalImpl<ContractState>;
30+
// NFT Certificate
31+
#[abi(embed_v0)]
32+
impl NFTComponentImpl = NFTComponent::NFTComponentImpl<ContractState>;
33+
impl NFTComponentInternalImpl = NFTComponent::InternalImpl<ContractState>;
34+
35+
#[storage]
36+
struct Storage {
37+
#[substorage(v0)]
38+
erc721: ERC721Component::Storage,
39+
#[substorage(v0)]
40+
src5: SRC5Component::Storage,
41+
#[substorage(v0)]
42+
accesscontrol: AccessControlComponent::Storage,
43+
#[substorage(v0)]
44+
nft_component: NFTComponent::Storage,
45+
}
46+
47+
#[event]
48+
#[derive(Drop, starknet::Event)]
49+
enum Event {
50+
#[flat]
51+
ERC721Event: ERC721Component::Event,
52+
#[flat]
53+
SRC5Event: SRC5Component::Event,
54+
#[flat]
55+
AccessControlEvent: AccessControlComponent::Event,
56+
#[flat]
57+
NFTComponentEvent: NFTComponent::Event,
58+
}
59+
60+
#[constructor]
61+
fn constructor(ref self: ContractState, locker_address: ContractAddress) {
62+
self.nft_component.initializer(locker_address, "Certificate", "CERT", "data:application/json,");
63+
}
64+
}
65+
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Starknet deps
2+
3+
use starknet::{ContractAddress, contract_address_const, get_caller_address, get_block_timestamp};
4+
5+
// External deps
6+
7+
use snforge_std as snf;
8+
use snforge_std::{
9+
ContractClassTrait, EventSpy, start_cheat_caller_address, stop_cheat_caller_address, spy_events,
10+
start_cheat_block_timestamp_global,
11+
cheatcodes::events::{EventSpyAssertionsTrait, EventSpyTrait, EventsFilterTrait}
12+
};
13+
14+
// ERC721 Components
15+
16+
use openzeppelin::token::erc721::interface::{
17+
IERC721Dispatcher, IERC721DispatcherTrait,
18+
IERC721MetadataDispatcher, IERC721MetadataDispatcherTrait
19+
};
20+
21+
// Internal interfaces
22+
use carbon_locker::components::certificate::interface::{
23+
INFTComponentDispatcher, INFTComponentDispatcherTrait
24+
};
25+
use carbon_locker::components::locker::interface::Lock;
26+
27+
// Contracts
28+
use super::mocks::erc721::MockERC721;
29+
30+
// Utils for testing purposes
31+
use super::tests_utils::{deploy_all};
32+
33+
fn generate_lock_data() -> Lock {
34+
Lock {
35+
id: 1_u256,
36+
user: contract_address_const::<'USER'>(),
37+
token_id: 1_u256,
38+
amount: 1000_u256,
39+
start_time: get_block_timestamp(),
40+
end_time: get_block_timestamp() + 1000_u64,
41+
offsettable: false,
42+
is_offsetted: false
43+
}
44+
}
45+
46+
/// Test the intializer function
47+
#[test]
48+
fn test_certificate_initializer() {
49+
let (_, _, _, _, _, certificate_address) = deploy_all();
50+
let erc721_metadata = IERC721MetadataDispatcher { contract_address: certificate_address };
51+
52+
let name = erc721_metadata.name();
53+
assert(name == "Certificate", 'Token name mismatch');
54+
55+
let symbol = erc721_metadata.symbol();
56+
assert(symbol == "CERT", 'Token symbol mismatch');
57+
}
58+
59+
#[test]
60+
fn test_mint() {
61+
let (_, locker_address, _, _, _, certificate_address) = deploy_all();
62+
let certificate = INFTComponentDispatcher { contract_address: certificate_address };
63+
64+
// Call with locker permissions
65+
start_cheat_caller_address(certificate_address, locker_address);
66+
67+
// Mint the token
68+
let user_address: ContractAddress = contract_address_const::<'USER'>();
69+
let token_id: u256 = 1;
70+
let lock_data = generate_lock_data();
71+
certificate.mint(user_address, token_id, lock_data);
72+
73+
// Check the user_address balance
74+
let erc721 = IERC721Dispatcher { contract_address: certificate_address };
75+
let balance = erc721.balance_of(user_address);
76+
assert(balance == 1, 'Balance should be 1');
77+
}
78+
79+
#[test]
80+
#[should_panic(expected: 'Only Locker is allowed')]
81+
fn test_unauthorized_caller_mint() {
82+
let (_, _, _, _, _, certificate_address) = deploy_all();
83+
let certificate = INFTComponentDispatcher { contract_address: certificate_address };
84+
85+
// Call with user_address permissions
86+
let user_address: ContractAddress = contract_address_const::<'USER'>();
87+
start_cheat_caller_address(certificate_address, user_address);
88+
89+
// Mint the token
90+
let token_id: u256 = 1;
91+
let lock_data = generate_lock_data();
92+
certificate.mint(user_address, token_id, lock_data);
93+
stop_cheat_caller_address(certificate_address);
94+
}
95+
96+
#[test]
97+
fn test_burn() {
98+
let (_, locker_address, _, _, _, certificate_address) = deploy_all();
99+
let certificate = INFTComponentDispatcher { contract_address: certificate_address };
100+
101+
// Call with locker permissions
102+
start_cheat_caller_address(certificate_address, locker_address);
103+
104+
// Mint the token
105+
let user_address: ContractAddress = contract_address_const::<'USER'>();
106+
let token_id: u256 = 1;
107+
let lock_data = generate_lock_data();
108+
certificate.mint(user_address, token_id, lock_data);
109+
110+
// Burn the token
111+
certificate.burn(token_id);
112+
113+
// Check the user_address balance
114+
let erc721 = IERC721Dispatcher { contract_address: certificate_address };
115+
let balance = erc721.balance_of(user_address);
116+
assert(balance == 0, 'Balance should be 0');
117+
}
118+
119+
#[test]
120+
#[should_panic(expected: 'Only Locker is allowed')]
121+
fn test_unauthorized_burn() {
122+
let (_, locker_address, _, _, _, certificate_address) = deploy_all();
123+
let certificate = INFTComponentDispatcher { contract_address: certificate_address };
124+
125+
// Call with locker permissions
126+
start_cheat_caller_address(certificate_address, locker_address);
127+
128+
// Mint the token
129+
let user_address: ContractAddress = contract_address_const::<'USER'>();
130+
let token_id: u256 = 1;
131+
let lock_data = generate_lock_data();
132+
certificate.mint(user_address, token_id, lock_data);
133+
134+
// Call with user_address permission
135+
start_cheat_caller_address(certificate_address, user_address);
136+
137+
// Burn the token
138+
certificate.burn(token_id);
139+
}

0 commit comments

Comments
 (0)