Skip to content

Commit bcabbf6

Browse files
Merge pull request #210 from caxtonacollins/admin-init
feat: implemented contract initialization with admin and configuration
2 parents 184a44c + 129345e commit bcabbf6

6 files changed

Lines changed: 277 additions & 12 deletions

File tree

contracts/predictify-hybrid/src/admin.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ impl AdminInitializer {
201201
/// control over the contract. Consider using a multi-signature wallet
202202
/// or governance contract for production deployments.
203203
pub fn initialize(env: &Env, admin: &Address) -> Result<(), Error> {
204+
// Check for re-initialization attempt (critical security check)
205+
AdminValidator::validate_contract_not_initialized(env)?;
206+
204207
// Validate admin address
205208
AdminValidator::validate_admin_address(env, admin)?;
206209

@@ -2342,7 +2345,7 @@ impl AdminValidator {
23422345
let admin_exists = env.storage().persistent().has(&Symbol::new(env, "Admin"));
23432346

23442347
if admin_exists {
2345-
return Err(Error::InvalidState);
2348+
return Err(Error::AlreadyInitialized);
23462349
}
23472350

23482351
Ok(())

contracts/predictify-hybrid/src/events.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,28 @@ pub struct AdminInitializedEvent {
704704
pub timestamp: u64,
705705
}
706706

707+
#[contracttype]
708+
#[derive(Clone, Debug, Eq, PartialEq)]
709+
pub struct ContractInitializedEvent {
710+
/// Admin address
711+
pub admin: Address,
712+
/// Platform fee percentage
713+
pub platform_fee_percentage: i128,
714+
/// Initialization timestamp
715+
pub timestamp: u64,
716+
}
717+
718+
#[contracttype]
719+
#[derive(Clone, Debug, Eq, PartialEq)]
720+
pub struct PlatformFeeSetEvent {
721+
/// Fee percentage
722+
pub fee_percentage: i128,
723+
/// Set by admin
724+
pub set_by: Address,
725+
/// Set timestamp
726+
pub timestamp: u64,
727+
}
728+
707729
/// Dispute timeout set event
708730
#[contracttype]
709731
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -1342,6 +1364,28 @@ impl EventEmitter {
13421364
Self::store_event(env, &symbol_short!("adm_init"), &event);
13431365
}
13441366

1367+
/// Emit contract initialized event (full initialization with platform fee)
1368+
pub fn emit_contract_initialized(env: &Env, admin: &Address, platform_fee_percentage: i128) {
1369+
let event = ContractInitializedEvent {
1370+
admin: admin.clone(),
1371+
platform_fee_percentage,
1372+
timestamp: env.ledger().timestamp(),
1373+
};
1374+
1375+
Self::store_event(env, &symbol_short!("init"), &event);
1376+
}
1377+
1378+
/// Emit platform fee set event
1379+
pub fn emit_platform_fee_set(env: &Env, fee_percentage: i128, set_by: &Address) {
1380+
let event = PlatformFeeSetEvent {
1381+
fee_percentage,
1382+
set_by: set_by.clone(),
1383+
timestamp: env.ledger().timestamp(),
1384+
};
1385+
1386+
Self::store_event(env, &symbol_short!("fee_set"), &event);
1387+
}
1388+
13451389
/// Emit config initialized event
13461390
pub fn emit_config_initialized(env: &Env, admin: &Address, environment: &Environment) {
13471391
let event = ConfigInitializedEvent {

contracts/predictify-hybrid/src/integration_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ impl IntegrationTestSuite {
4646
// Initialize contract
4747
let contract_id = env.register(PredictifyHybrid, ());
4848
let client = PredictifyHybridClient::new(&env, &contract_id);
49-
client.initialize(&admin);
49+
client.initialize(&admin, &None);
5050

5151
// Set token for staking
5252
env.as_contract(&contract_id, || {

contracts/predictify-hybrid/src/lib.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub use types::*;
6767

6868
use crate::config::{
6969
ConfigChanges, ConfigManager, ConfigUpdateRecord, ContractConfig, MarketLimits,
70+
DEFAULT_PLATFORM_FEE_PERCENTAGE, MAX_PLATFORM_FEE_PERCENTAGE, MIN_PLATFORM_FEE_PERCENTAGE,
7071
};
7172
use crate::events::EventEmitter;
7273
use crate::graceful_degradation::{OracleBackup, OracleHealth};
@@ -85,22 +86,25 @@ const PERCENTAGE_DENOMINATOR: i128 = 100;
8586
#[contractimpl]
8687
impl PredictifyHybrid {
8788
// Recovery methods appended later in file after existing functions to maintain readability.
88-
/// Initializes the Predictify Hybrid smart contract with an administrator.
89+
/// Initializes the Predictify Hybrid smart contract with administrator and platform configuration.
8990
///
9091
/// This function must be called once after contract deployment to set up the initial
91-
/// administrative configuration. It establishes the contract admin who will have
92-
/// privileges to create markets and perform administrative functions.
92+
/// administrative configuration and platform fee structure. It establishes the contract admin who
93+
/// will have privileges to create markets and perform administrative functions, and configures
94+
/// the platform fee percentage for market operations.
9395
///
9496
/// # Parameters
9597
///
9698
/// * `env` - The Soroban environment for blockchain operations
9799
/// * `admin` - The address that will be granted administrative privileges
100+
/// * `platform_fee_percentage` - Optional platform fee percentage (0-10%). If `None`, defaults to 2%
98101
///
99102
/// # Panics
100103
///
101104
/// This function will panic if:
102-
/// - The contract has already been initialized
105+
/// - The contract has already been initialized (Error code 504: AlreadyInitialized)
103106
/// - The admin address is invalid
107+
/// - The platform fee percentage is negative or exceeds 10%
104108
/// - Storage operations fail
105109
///
106110
/// # Example
@@ -111,19 +115,58 @@ impl PredictifyHybrid {
111115
/// # let env = Env::default();
112116
/// # let admin_address = Address::generate(&env);
113117
///
114-
/// // Initialize the contract with an admin
115-
/// PredictifyHybrid::initialize(env.clone(), admin_address);
118+
/// // Initialize with default 2% platform fee
119+
/// PredictifyHybrid::initialize(env.clone(), admin_address.clone(), None);
120+
///
121+
/// // Or initialize with custom 5% platform fee
122+
/// PredictifyHybrid::initialize(env.clone(), admin_address, Some(5));
116123
/// ```
117124
///
125+
/// # Platform Fee
126+
///
127+
/// The platform fee is a percentage (0-10%) taken from winning payouts to support
128+
/// platform operations. Fee is applied during payout calculation:
129+
/// - Default: 2% (200 basis points)
130+
/// - Minimum: 0% (no fee)
131+
/// - Maximum: 10% (1000 basis points)
132+
///
118133
/// # Security
119134
///
120135
/// The admin address should be carefully chosen as it will have significant
121136
/// control over the contract's operation, including market creation and resolution.
122-
pub fn initialize(env: Env, admin: Address) {
137+
/// Consider using a multi-signature wallet or governance contract for production.
138+
///
139+
/// # Re-initialization Prevention
140+
///
141+
/// This function can only be called once. Any subsequent calls will panic with
142+
/// `Error::AlreadyInitialized` to prevent admin takeover attacks.
143+
pub fn initialize(env: Env, admin: Address, platform_fee_percentage: Option<i128>) {
144+
// Determine platform fee (default 2% if not specified)
145+
let fee_percentage = platform_fee_percentage.unwrap_or(DEFAULT_PLATFORM_FEE_PERCENTAGE);
146+
147+
// Validate fee percentage bounds (0-10%)
148+
if fee_percentage < MIN_PLATFORM_FEE_PERCENTAGE
149+
|| fee_percentage > MAX_PLATFORM_FEE_PERCENTAGE
150+
{
151+
panic_with_error!(env, Error::InvalidFeeConfig);
152+
}
153+
154+
// Initialize admin (includes re-initialization check)
123155
match AdminInitializer::initialize(&env, &admin) {
124-
Ok(_) => (), // Success
156+
Ok(_) => (),
125157
Err(e) => panic_with_error!(env, e),
126158
}
159+
160+
// Store platform fee configuration in persistent storage
161+
env.storage()
162+
.persistent()
163+
.set(&Symbol::new(&env, "platform_fee"), &fee_percentage);
164+
165+
// Emit contract initialized event
166+
EventEmitter::emit_contract_initialized(&env, &admin, fee_percentage);
167+
168+
// Emit platform fee set event
169+
EventEmitter::emit_platform_fee_set(&env, fee_percentage, &admin);
127170
}
128171

129172
/// Creates a new prediction market with specified parameters and oracle configuration.

contracts/predictify-hybrid/src/property_based_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ impl PropertyBasedTestSuite {
4242
let admin = Address::generate(&env);
4343
let contract_id = env.register(PredictifyHybrid, ());
4444
let client = PredictifyHybridClient::new(&env, &contract_id);
45-
client.initialize(&admin);
45+
client.initialize(&admin, &None);
4646

4747
// Generate multiple test users for comprehensive testing
4848
let users = (0..10).map(|_| Address::generate(&env)).collect();

contracts/predictify-hybrid/src/test.rs

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl PredictifyTest {
7070
// Initialize contract
7171
let contract_id = env.register(PredictifyHybrid, ());
7272
let client = PredictifyHybridClient::new(&env, &contract_id);
73-
client.initialize(&admin);
73+
client.initialize(&admin, &None);
7474

7575
// Set token for staking
7676
env.as_contract(&contract_id, || {
@@ -740,3 +740,178 @@ fn test_error_recovery_scenarios() {
740740
assert!(validation_result);
741741
});
742742
}
743+
744+
// ===== INITIALIZATION TESTS =====
745+
746+
#[test]
747+
fn test_initialize_with_default_fee() {
748+
let env = Env::default();
749+
env.mock_all_auths();
750+
751+
let admin = Address::generate(&env);
752+
let contract_id = env.register(PredictifyHybrid, ());
753+
let client = PredictifyHybridClient::new(&env, &contract_id);
754+
755+
// Initialize with None (default 2% fee)
756+
client.initialize(&admin, &None);
757+
758+
// Verify admin is set
759+
let stored_admin: Address = env.as_contract(&contract_id, || {
760+
env.storage()
761+
.persistent()
762+
.get(&Symbol::new(&env, "Admin"))
763+
.unwrap()
764+
});
765+
assert_eq!(stored_admin, admin);
766+
767+
// Verify platform fee is default 2%
768+
let stored_fee: i128 = env.as_contract(&contract_id, || {
769+
env.storage()
770+
.persistent()
771+
.get(&Symbol::new(&env, "platform_fee"))
772+
.unwrap()
773+
});
774+
assert_eq!(stored_fee, 2);
775+
}
776+
777+
#[test]
778+
fn test_initialize_with_custom_fee() {
779+
let env = Env::default();
780+
env.mock_all_auths();
781+
782+
let admin = Address::generate(&env);
783+
let contract_id = env.register(PredictifyHybrid, ());
784+
let client = PredictifyHybridClient::new(&env, &contract_id);
785+
786+
// Initialize with custom 5% fee
787+
client.initialize(&admin, &Some(5));
788+
789+
// Verify platform fee is 5%
790+
let stored_fee: i128 = env.as_contract(&contract_id, || {
791+
env.storage()
792+
.persistent()
793+
.get(&Symbol::new(&env, "platform_fee"))
794+
.unwrap()
795+
});
796+
assert_eq!(stored_fee, 5);
797+
}
798+
799+
#[test]
800+
#[should_panic(expected = "Error(Contract, #504)")] // AlreadyInitialized = 504
801+
fn test_reinitialize_prevention() {
802+
let env = Env::default();
803+
env.mock_all_auths();
804+
805+
let admin = Address::generate(&env);
806+
let contract_id = env.register(PredictifyHybrid, ());
807+
let client = PredictifyHybridClient::new(&env, &contract_id);
808+
809+
// First initialization - should succeed
810+
client.initialize(&admin, &None);
811+
812+
// Second initialization - should panic with AlreadyInitialized
813+
client.initialize(&admin, &Some(3));
814+
}
815+
816+
#[test]
817+
#[should_panic(expected = "Error(Contract, #402)")] // InvalidFeeConfig = 402
818+
fn test_initialize_invalid_fee_negative() {
819+
let env = Env::default();
820+
env.mock_all_auths();
821+
822+
let admin = Address::generate(&env);
823+
let contract_id = env.register(PredictifyHybrid, ());
824+
let client = PredictifyHybridClient::new(&env, &contract_id);
825+
826+
// Initialize with negative fee - should panic
827+
client.initialize(&admin, &Some(-1));
828+
}
829+
830+
#[test]
831+
#[should_panic(expected = "Error(Contract, #402)")] // InvalidFeeConfig = 402
832+
fn test_initialize_invalid_fee_too_high() {
833+
let env = Env::default();
834+
env.mock_all_auths();
835+
836+
let admin = Address::generate(&env);
837+
let contract_id = env.register(PredictifyHybrid, ());
838+
let client = PredictifyHybridClient::new(&env, &contract_id);
839+
840+
// Initialize with fee exceeding max 10% - should panic
841+
client.initialize(&admin, &Some(11));
842+
}
843+
844+
#[test]
845+
fn test_initialize_valid_fee_bounds() {
846+
// Test minimum fee (0%)
847+
{
848+
let env = Env::default();
849+
env.mock_all_auths();
850+
let admin = Address::generate(&env);
851+
let contract_id = env.register(PredictifyHybrid, ());
852+
let client = PredictifyHybridClient::new(&env, &contract_id);
853+
854+
client.initialize(&admin, &Some(0));
855+
856+
let stored_fee: i128 = env.as_contract(&contract_id, || {
857+
env.storage()
858+
.persistent()
859+
.get(&Symbol::new(&env, "platform_fee"))
860+
.unwrap()
861+
});
862+
assert_eq!(stored_fee, 0);
863+
}
864+
865+
// Test maximum fee (10%)
866+
{
867+
let env = Env::default();
868+
env.mock_all_auths();
869+
let admin = Address::generate(&env);
870+
let contract_id = env.register(PredictifyHybrid, ());
871+
let client = PredictifyHybridClient::new(&env, &contract_id);
872+
873+
client.initialize(&admin, &Some(10));
874+
875+
let stored_fee: i128 = env.as_contract(&contract_id, || {
876+
env.storage()
877+
.persistent()
878+
.get(&Symbol::new(&env, "platform_fee"))
879+
.unwrap()
880+
});
881+
assert_eq!(stored_fee, 10);
882+
}
883+
}
884+
885+
#[test]
886+
fn test_initialize_storage_verification() {
887+
let env = Env::default();
888+
env.mock_all_auths();
889+
890+
let admin = Address::generate(&env);
891+
let contract_id = env.register(PredictifyHybrid, ());
892+
let client = PredictifyHybridClient::new(&env, &contract_id);
893+
894+
client.initialize(&admin, &Some(3));
895+
896+
// Verify admin address is in persistent storage
897+
env.as_contract(&contract_id, || {
898+
let has_admin = env.storage().persistent().has(&Symbol::new(&env, "Admin"));
899+
assert!(has_admin);
900+
});
901+
902+
// Verify platform fee is in persistent storage
903+
env.as_contract(&contract_id, || {
904+
let has_fee = env
905+
.storage()
906+
.persistent()
907+
.has(&Symbol::new(&env, "platform_fee"));
908+
assert!(has_fee);
909+
});
910+
911+
// Verify initialization flag (admin existence serves as initialization flag)
912+
env.as_contract(&contract_id, || {
913+
let admin_result: Option<Address> =
914+
env.storage().persistent().get(&Symbol::new(&env, "Admin"));
915+
assert!(admin_result.is_some());
916+
});
917+
}

0 commit comments

Comments
 (0)