Skip to content
Merged
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
78 changes: 78 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "budget_contract"
version = "0.1.0"
dependencies = [
"openzeppelin_access",
"openzeppelin_introspection",
"openzeppelin_token",
"openzeppelin_upgrades",
"openzeppelin_utils",
"snforge_std",
]

[[package]]
name = "openzeppelin_access"
version = "0.20.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:7734901a0ca7a7065e69416fea615dd1dc586c8dc9e76c032f25ee62e8b2a06c"
dependencies = [
"openzeppelin_introspection",
]

[[package]]
name = "openzeppelin_account"
version = "0.20.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:1aa3a71e2f40f66f98d96aa9bf9f361f53db0fd20fa83ef7df04426a3c3a926a"
dependencies = [
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_introspection"
version = "0.20.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:13e04a2190684e6804229a77a6c56de7d033db8b9ef519e5e8dee400a70d8a3d"

[[package]]
name = "openzeppelin_token"
version = "0.20.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:4452f449dc6c1ea97cf69d1d9182749abd40e85bd826cd79652c06a627eafd91"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_upgrades"
version = "0.20.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:15fdd63f6b50a0fda7b3f8f434120aaf7637bcdfe6fd8d275ad57343d5ede5e1"

[[package]]
name = "openzeppelin_utils"
version = "0.20.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:44f32d242af1e43982decc49c563e613a9b67ade552f5c3d5cde504e92f74607"

[[package]]
name = "snforge_scarb_plugin"
version = "0.44.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:ec8c7637b33392a53153c1e5b87a4617ddcb1981951b233ea043cad5136697e2"

[[package]]
name = "snforge_std"
version = "0.44.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:d4affedfb90715b1ac417b915c0a63377ae6dd69432040e5d933130d65114702"
dependencies = [
"snforge_scarb_plugin",
]
18 changes: 18 additions & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "budget_contract"
version = "0.1.0"
edition = "2024_07"

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
starknet = "2.9.2"
openzeppelin_access = "0.20.0"
openzeppelin_introspection = "0.20.0"
openzeppelin_token = "0.20.0"
openzeppelin_upgrades = "0.20.0"

[dev-dependencies]
assert_macros = "2.9.2"
openzeppelin_utils = "0.20.0"
snforge_std = "0.44.0"
1 change: 1 addition & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod project_manager;
144 changes: 144 additions & 0 deletions src/project_manager.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use starknet::{ContractAddress, get_caller_address};

#[starknet::interface]
pub trait IProjectManager<TContractState> {
fn authorize_organization(ref self: TContractState, org: ContractAddress);
fn revoke_organization(ref self: TContractState, org: ContractAddress);
fn create_project(
ref self: TContractState, project_owner: ContractAddress, total_budget: u256,
) -> u64;
fn get_project(self: @TContractState, id: u64) -> Project;
fn get_project_count(self: @TContractState) -> u64;
fn has_project_creator_role(self: @TContractState, org: ContractAddress) -> bool;
}

#[derive(Drop, Copy, starknet::Store, Serde)]
pub struct Project {
pub id: u64,
pub org: ContractAddress,
pub owner: ContractAddress,
pub total_budget: u256,
pub remaining_budget: u256,
}

#[starknet::contract]
pub mod ProjectManager {
use core::num::traits::Zero;
use openzeppelin_access::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE};
use openzeppelin_introspection::src5::SRC5Component;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use super::{ContractAddress, IProjectManager, Project, get_caller_address};

const PROJECT_CREATOR_ROLE: felt252 = selector!("PROJECT_CREATOR_ROLE");

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

#[abi(embed_v0)]
impl AccessControlMixinImpl =
AccessControlComponent::AccessControlMixinImpl<ContractState>;
impl AccessControlInternalImpl = AccessControlComponent::InternalImpl<ContractState>;

// Removed erroneous SRC5MixinImpl implementation as the trait or impl was not found.
impl SRC5InternalImpl = SRC5Component::InternalImpl<ContractState>;

#[storage]
struct Storage {
#[substorage(v0)]
accesscontrol: AccessControlComponent::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage,
project_count: u64,
projects: Map<u64, Project>,
}

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

#[derive(Drop, starknet::Event)]
pub struct ProjectAllocated {
pub project_id: u64,
pub org: ContractAddress,
pub project_owner: ContractAddress,
pub total_budget: u256,
}

#[constructor]
fn constructor(ref self: ContractState) {
let caller = get_caller_address();
self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, caller);
self.accesscontrol._grant_role(PROJECT_CREATOR_ROLE, caller);
self.project_count.write(0);
}

#[abi(embed_v0)]
pub impl ProjectManagerImpl of IProjectManager<ContractState> {
fn authorize_organization(ref self: ContractState, org: ContractAddress) {
let _caller = get_caller_address();
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
self.accesscontrol.grant_role(PROJECT_CREATOR_ROLE, org);
}

fn revoke_organization(ref self: ContractState, org: ContractAddress) {
let _caller = get_caller_address();
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
self.accesscontrol.revoke_role(PROJECT_CREATOR_ROLE, org);
}

fn create_project(
ref self: ContractState, project_owner: ContractAddress, total_budget: u256,
) -> u64 {
let caller = get_caller_address();
self.accesscontrol.assert_only_role(PROJECT_CREATOR_ROLE);
assert(!project_owner.is_zero(), 'Owner cannot be zero');

let current_id = self.project_count.read();
let new_id = current_id;

let project = Project {
id: new_id,
org: caller,
owner: project_owner,
total_budget,
remaining_budget: total_budget,
};

self.projects.write(new_id, project);
self.project_count.write(new_id + 1);

self
.emit(
Event::ProjectAllocated(
ProjectAllocated {
project_id: new_id, org: caller, project_owner, total_budget,
},
),
);

new_id
}

fn get_project(self: @ContractState, id: u64) -> Project {
assert(id < self.project_count.read(), 'Invalid project ID');
self.projects.read(id)
}

fn get_project_count(self: @ContractState) -> u64 {
self.project_count.read()
}

fn has_project_creator_role(self: @ContractState, org: ContractAddress) -> bool {
self.accesscontrol.has_role(PROJECT_CREATOR_ROLE, org)
}
}
}
Loading
Loading