Skip to content

Commit 09a5a5d

Browse files
authored
feat: Role-Based Access Control (#294)
1 parent 8de80da commit 09a5a5d

File tree

6 files changed

+1404
-42
lines changed

6 files changed

+1404
-42
lines changed

contract/contracts/predifi-contract/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ crate-type = ["cdylib", "lib"]
1010
[dependencies]
1111
soroban-sdk = { workspace = true }
1212
predifi-errors = { workspace = true }
13+
access-control = { path = "../access-control" }
1314

1415
[dev-dependencies]
1516
soroban-sdk = { workspace = true, features = ["testutils"] }

contract/contracts/predifi-contract/src/lib.rs

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![no_std]
2+
use access_control::Role;
23
use predifi_errors::PrediFiError;
34
use soroban_sdk::{contract, contractevent, contractimpl, contracttype, token, Address, Env};
45

@@ -70,6 +71,7 @@ pub enum DataKey {
7071
FeeBps, // Fee in basis points (1/100 of a percent)
7172
Treasury, // Protocol treasury address
7273
CollectedFees(u64), // PoolId -> Collected fee amount
74+
AccessControlAddress, // Access control contract address
7375
}
7476

7577
#[contracttype]
@@ -84,6 +86,49 @@ pub struct PredifiContract;
8486

8587
#[contractimpl]
8688
impl PredifiContract {
89+
/// Get the access control contract address
90+
///
91+
/// # Returns
92+
/// The address of the access control contract
93+
///
94+
/// # Errors
95+
/// * `NotInitialized` - If access control contract is not set
96+
fn get_access_control_address(env: &Env) -> Result<Address, PrediFiError> {
97+
env.storage()
98+
.instance()
99+
.get(&DataKey::AccessControlAddress)
100+
.ok_or(PrediFiError::NotInitialized)
101+
}
102+
103+
/// Get the access control client
104+
///
105+
/// # Returns
106+
/// The AccessControlClient instance
107+
fn get_access_control_client(env: &Env) -> access_control::AccessControlClient<'_> {
108+
let access_control_addr = Self::get_access_control_address(env).unwrap();
109+
access_control::AccessControlClient::new(env, &access_control_addr)
110+
}
111+
112+
/// Set the access control contract address (only callable once)
113+
///
114+
/// # Arguments
115+
/// * `access_control_address` - The address of the access control contract
116+
///
117+
/// # Errors
118+
/// * `AlreadyInitialized` - If access control contract is already set
119+
pub fn set_access_control(
120+
env: Env,
121+
access_control_address: Address,
122+
) -> Result<(), PrediFiError> {
123+
if env.storage().instance().has(&DataKey::AccessControlAddress) {
124+
return Err(PrediFiError::AlreadyInitialized);
125+
}
126+
env.storage()
127+
.instance()
128+
.set(&DataKey::AccessControlAddress, &access_control_address);
129+
Ok(())
130+
}
131+
87132
/// Initialize the contract.
88133
///
89134
/// Sets up the initial pool ID counter, fee basis points, and treasury address.
@@ -107,14 +152,24 @@ impl PredifiContract {
107152
/// Set the protocol fee in basis points.
108153
///
109154
/// # Arguments
155+
/// * `caller` - The address calling the function
110156
/// * `fee_bps` - Fee in basis points (e.g., 100 = 1%)
111-
pub fn set_fee_bps(env: Env, fee_bps: u32) {
112-
// TODO: Add access control to restrict who can call this
157+
///
158+
/// # Errors
159+
/// * `InsufficientPermissions` - If caller doesn't have Admin role
160+
pub fn set_fee_bps(env: Env, caller: Address, fee_bps: u32) -> Result<(), PrediFiError> {
161+
// Check if caller has Admin role
162+
let access_control_client = Self::get_access_control_client(&env);
163+
if !access_control_client.has_role(&caller, &Role::Admin) {
164+
return Err(PrediFiError::InsufficientPermissions);
165+
}
166+
113167
env.storage().instance().set(&DataKey::FeeBps, &fee_bps);
114168
SetFeeBpsEvent {
115169
new_fee_bps: fee_bps,
116170
}
117171
.publish(&env);
172+
Ok(())
118173
}
119174

120175
/// Get the current protocol fee in basis points.
@@ -128,14 +183,24 @@ impl PredifiContract {
128183
/// Set the treasury address.
129184
///
130185
/// # Arguments
186+
/// * `caller` - The address calling the function
131187
/// * `treasury` - New treasury address
132-
pub fn set_treasury(env: Env, treasury: Address) {
133-
// TODO: Add access control to restrict who can call this
188+
///
189+
/// # Errors
190+
/// * `InsufficientPermissions` - If caller doesn't have Admin role
191+
pub fn set_treasury(env: Env, caller: Address, treasury: Address) -> Result<(), PrediFiError> {
192+
// Check if caller has Admin role
193+
let access_control_client = Self::get_access_control_client(&env);
194+
if !access_control_client.has_role(&caller, &Role::Admin) {
195+
return Err(PrediFiError::InsufficientPermissions);
196+
}
197+
134198
env.storage().instance().set(&DataKey::Treasury, &treasury);
135199
SetTreasuryEvent {
136200
new_treasury: treasury.clone(),
137201
}
138202
.publish(&env);
203+
Ok(())
139204
}
140205

141206
/// Get the treasury address.
@@ -221,6 +286,7 @@ impl PredifiContract {
221286
/// Resolve a prediction pool with the final outcome.
222287
///
223288
/// # Arguments
289+
/// * `caller` - The address calling the function
224290
/// * `pool_id` - ID of the pool to resolve
225291
/// * `outcome` - The winning outcome number
226292
///
@@ -229,7 +295,19 @@ impl PredifiContract {
229295
/// * `PoolAlreadyResolved` - If the pool has already been resolved
230296
/// * `PoolNotExpired` - If the pool end time hasn't been reached
231297
/// * `ResolutionWindowExpired` - If the resolution window has passed
232-
pub fn resolve_pool(env: Env, pool_id: u64, outcome: u32) -> Result<(), PrediFiError> {
298+
/// * `InsufficientPermissions` - If caller doesn't have Oracle role
299+
pub fn resolve_pool(
300+
env: Env,
301+
caller: Address,
302+
pool_id: u64,
303+
outcome: u32,
304+
) -> Result<(), PrediFiError> {
305+
// Check if caller has Oracle role
306+
let access_control_client = Self::get_access_control_client(&env);
307+
if !access_control_client.has_role(&caller, &Role::Oracle) {
308+
return Err(PrediFiError::InsufficientPermissions);
309+
}
310+
233311
let mut pool: Pool = env
234312
.storage()
235313
.instance()
@@ -278,6 +356,7 @@ impl PredifiContract {
278356
/// Place a prediction on a pool.
279357
///
280358
/// # Arguments
359+
/// * `caller` - The address calling the function (should match user)
281360
/// * `user` - Address of the user placing the prediction
282361
/// * `pool_id` - ID of the pool
283362
/// * `amount` - Amount to stake (must be positive)
@@ -289,13 +368,21 @@ impl PredifiContract {
289368
/// * `PredictionTooLate` - If pool has already ended
290369
/// * `PoolAlreadyResolved` - If pool is already resolved
291370
/// * `PredictionAlreadyExists` - If user already has a prediction on this pool
371+
/// * `InsufficientPermissions` - If caller doesn't have User role or doesn't match user
292372
pub fn place_prediction(
293373
env: Env,
374+
caller: Address,
294375
user: Address,
295376
pool_id: u64,
296377
amount: i128,
297378
outcome: u32,
298379
) -> Result<(), PrediFiError> {
380+
// Check if caller has User role and matches the user address
381+
let access_control_client = Self::get_access_control_client(&env);
382+
if !access_control_client.has_role(&caller, &Role::User) || caller != user {
383+
return Err(PrediFiError::InsufficientPermissions);
384+
}
385+
299386
user.require_auth();
300387

301388
// Validate amount

0 commit comments

Comments
 (0)