diff --git a/Cargo.lock b/Cargo.lock index e0346d9..3e05c15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,7 +808,7 @@ dependencies = [ [[package]] name = "reflector-dao-contract" -version = "0.3.0" +version = "1.0.0" dependencies = [ "soroban-sdk", ] @@ -831,9 +831,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -962,9 +962,9 @@ checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "soroban-builtin-sdk-macros" -version = "21.2.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44877373b3dc6c662377cb1600e3a62706d75e484b6064f9cd22e467c676b159" +checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" dependencies = [ "itertools", "proc-macro2", @@ -974,9 +974,9 @@ dependencies = [ [[package]] name = "soroban-env-common" -version = "21.2.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590add16843a61b01844e19e89bccaaee6aa21dc76809017b0662c17dc139ee9" +checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" dependencies = [ "arbitrary", "crate-git-revision", @@ -993,9 +993,9 @@ dependencies = [ [[package]] name = "soroban-env-guest" -version = "21.2.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ec8dc43acdd6c7e7b371acf44fc1a7dac24934ae3b2f05fafd618818548176" +checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" dependencies = [ "soroban-env-common", "static_assertions", @@ -1003,9 +1003,9 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "21.2.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e25aaffe0c62eb65e0e349f725b4b8b13ad0764d78a15aab5bbccb5c4797726" +checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" dependencies = [ "backtrace", "curve25519-dalek", @@ -1036,9 +1036,9 @@ dependencies = [ [[package]] name = "soroban-env-macros" -version = "21.2.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e16b761459fdf3c4b62b24df3941498d14e5246e6fadfb4774ed8114d243aa4" +checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" dependencies = [ "itertools", "proc-macro2", @@ -1051,9 +1051,9 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" -version = "21.4.0" +version = "21.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaebb7961fc6d8f47e00d404d9240f51aba85df9d67a4f556ef1c6057b5327a8" +checksum = "43793d5deb5fc27c3e14e036e24cb3afcf7d1e2a172d9166e37f3d174b928749" dependencies = [ "serde", "serde_json", @@ -1065,15 +1065,16 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "21.4.0" +version = "21.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60cd55eb88cbe1d9e7fe3ab1845c7c10c26b27e2d226e973150e5b55580aa359" +checksum = "25c539fecb2862ce0c1f49880134660a855e2d35889692e01d1e8d8a1e53f98e" dependencies = [ "arbitrary", "bytes-lit", "ctor", "ed25519-dalek", "rand", + "rustc_version", "serde", "serde_json", "soroban-env-guest", @@ -1085,9 +1086,9 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "21.4.0" +version = "21.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1916985d1871aa340d7eec834c387f74f80231c846801193c3252266a60a41e" +checksum = "a9ad528a770ec7adb524635d855b424ae2fd4fef04fb702bb0ab466a4c354d78" dependencies = [ "crate-git-revision", "darling", @@ -1105,9 +1106,9 @@ dependencies = [ [[package]] name = "soroban-spec" -version = "21.4.0" +version = "21.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439faff6a155975a9951f27aaa04ae8ef3fd8fe9413d202e2ea2ff094a593449" +checksum = "5b262c82d840552f71ee9254f2e928622fd803bd4df4815e65f73f73efc2fa9c" dependencies = [ "base64 0.13.1", "stellar-xdr", @@ -1117,9 +1118,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "21.4.0" +version = "21.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b2a571055f1ed15427ccb8d34ce4208b4135666eade124cfbecfc010fa2ea3" +checksum = "85a061820c2dd0bd3ece9411e0dd3aeb6ed9ca2b7d64270eda9e798c3b6dec5f" dependencies = [ "prettyplease", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index ac1f9b2..4846c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflector-dao-contract" -version = "0.3.0" +version = "1.0.0" edition = "2021" [lib] @@ -17,10 +17,10 @@ codegen-units = 1 lto = true [dependencies] -soroban-sdk = "21.4.0" +soroban-sdk = "21.7.6" [dev-dependencies] -soroban-sdk = { version = "21.4.0", features = ["testutils"] } +soroban-sdk = { version = "21.7.6", features = ["testutils"] } [features] testutils = ["soroban-sdk/testutils"] diff --git a/src/extensions/env_extensions.rs b/src/extensions/env_extensions.rs index 4e6a014..906dd52 100644 --- a/src/extensions/env_extensions.rs +++ b/src/extensions/env_extensions.rs @@ -24,7 +24,7 @@ pub trait EnvExtensions { fn set_last_ballot_id(&self, last_ballot_id: u64); - fn set_last_unlock(&self, last_uplock: u64); + fn set_last_unlock(&self, last_unlock: u64); fn get_last_unlock(&self) -> u64; diff --git a/src/lib.rs b/src/lib.rs index b95a711..e329a2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] use extensions::env_extensions::EnvExtensions; -use soroban_sdk::{contract, contractimpl, token::TokenClient, Address, Env, Map, Vec}; +use soroban_sdk::{contract, contractimpl, symbol_short, token::TokenClient, Address, Env, Map, Symbol, Vec}; use types::{ ballot::Ballot, ballot_category::BallotCategory, ballot_init_params::BallotInitParams, ballot_status::BallotStatus, contract_config::ContractConfig, error::Error, @@ -9,10 +9,13 @@ use types::{ mod extensions; mod types; -// 0.24% weekly distribution +//10000 is 100% +const PERCENTAGE_FACTOR: i128 = 10000; + +// 0.24% weekly distribution, 10000 is 100% const OPERATORS_SHARE: i128 = 24; -// 0.06% weekly distribution +// 0.06% weekly distribution, 10000 is 100% const DEVELOPERS_SHARE: i128 = 6; // 1 week @@ -24,6 +27,8 @@ const BALLOT_DURATION: u32 = 604800 * 2; // 2 months const BALLOT_RENTAL_PERIOD: u32 = 17280 * 30 * 2; +const REFLECTOR: Symbol = symbol_short!("reflector"); + #[contract] pub struct DAOContract; @@ -56,23 +61,23 @@ impl DAOContract { e.set_admin(&config.admin); e.set_token(&config.token); e.set_last_unlock(config.start_date); - //set deposit params - set_deposit(&e, config.deposit_params); // transfer tokens to the DAO contract token(&e).transfer(&config.admin, &e.current_contract_address(), &config.amount); // set initial DAO balance - update_dao_balance(&e, &config.amount.into()); + update_dao_balance(&e, config.amount.into()); + //set deposit params + set_deposit(&e, config.deposit_params); } /// Sets the deposit amount for each ballot category /// Requires admin permissions - /// + /// /// # Arguments - /// + /// /// * `deposit_params` - Map of deposit amounts for each ballot category - /// + /// /// # Panics - /// + /// /// Panics if the caller doesn't match admin address /// Panics if the deposit amount is invalid /// Panics if the deposit amount is not set for all categories @@ -103,29 +108,44 @@ impl DAOContract { if now - last_unlock < UNLOCK_PERIOD as u64 { e.panic_with_error(Error::UnlockUnavailable); } + // check if the operators list is empty or not unique + if operators.is_empty() || + operators.iter().any(|x| operators.iter().filter(|y| x == *y).count() > 1) { + e.panic_with_error(Error::InvalidOperators); + } // fetch the remaining DAO balance let dao_balance = e.get_dao_balance(); // actual unlocked amount can be different from the calculated percentage due to rounding errors let mut total_unlocked = 0i128; // calculate unlocked amount that goes to operators - let operators_unlocked = calc_percentage(dao_balance, OPERATORS_SHARE); + let operators_unlocked = calc_share(&e, dao_balance, OPERATORS_SHARE); // the amount a single operator would get - let unlock_per_operator = &(operators_unlocked / operators.len() as i128); + let unlock_per_operator = operators_unlocked / operators.len() as i128; // update available balances for every operator for operator in operators.iter() { // increase outstanding available balance update_available_balance(&e, &operator, unlock_per_operator); - total_unlocked += unlock_per_operator; + total_unlocked = sum(&e, total_unlocked, unlock_per_operator); } // get developer unlocked amount - let developer_unlocked = &calc_percentage(dao_balance, DEVELOPERS_SHARE); + let developer_unlocked = calc_share(&e, dao_balance, DEVELOPERS_SHARE); // increase outstanding developer available balance update_available_balance(&e, &developer, developer_unlocked); - total_unlocked += developer_unlocked; + total_unlocked = sum(&e, total_unlocked, developer_unlocked); // add week to last unlock date e.set_last_unlock(last_unlock + UNLOCK_PERIOD as u64); // update dao balance e.set_dao_balance(dao_balance - total_unlocked); + + // publish unlock event + e.events().publish( + ( + REFLECTOR, + symbol_short!("dao"), + symbol_short!("unlocked") + ), + () + ); } /// Fetches the DAO tokens amount available for claiming @@ -173,7 +193,7 @@ impl DAOContract { token(&e).transfer(&e.current_contract_address(), &to, &amount); // update available balance - update_available_balance(&e, &claimant, &(-amount)); + update_available_balance(&e, &claimant, -amount); } /// Create a new ballot @@ -215,13 +235,24 @@ impl DAOContract { // transfer deposit to DAO fund token(&e).transfer(&ballot.initiator, &e.current_contract_address(), &deposit); // update internal DAO balance - update_dao_balance(&e, &deposit); + update_dao_balance(&e, deposit); // save ballot e.set_ballot(ballot_id, &ballot); // extend ballot TTL e.extend_ballot_ttl(ballot_id, e.ledger().sequence() + BALLOT_RENTAL_PERIOD); // update ID counter e.set_last_ballot_id(ballot_id); + + // publish ballot event + e.events().publish( + ( + REFLECTOR, + symbol_short!("dao"), + symbol_short!("ballot") + ), + ballot + ); + // return created ballot ID ballot_id } @@ -263,24 +294,35 @@ impl DAOContract { // calculate the refund amount based on the ballot status let refunded = match ballot.status { // if the proposal has been rejected by the DAO, the initiator receives 75% refund - BallotStatus::Rejected => (ballot.deposit * 75) / 100, + BallotStatus::Rejected => get_value_percentage(&e, ballot.deposit, 75), // if the DAO members haven't voted in a timely manner, the initiator receives extra 25% of the deposit BallotStatus::Draft => { // draft ballots can be retracted only after the voting period is over if e.ledger().timestamp() - ballot.created < BALLOT_DURATION as u64 { e.panic_with_error(Error::RefundUnavailable); } - (ballot.deposit * 125) / 100 + get_value_percentage(&e, ballot.deposit, 125) } _ => e.panic_with_error(Error::RefundUnavailable), }; // refund tokens to the initiator address token(&e).transfer(&e.current_contract_address(), &ballot.initiator, &refunded); // update remaining DAO balance - update_dao_balance(&e, &(-refunded)); + update_dao_balance(&e, -refunded); // update ballot status ballot.status = BallotStatus::Retracted; e.set_ballot(ballot_id, &ballot); + + // publish retracted event + e.events().publish( + ( + REFLECTOR, + symbol_short!("dao"), + symbol_short!("retracted") + ), + ballot_id + ); + } /// Set ballot decision based on the operators voting (decision requires the majority of signatures) @@ -313,17 +355,27 @@ impl DAOContract { }; // calculate the amount of DAO tokens to burn let burn_amount = match new_status { - BallotStatus::Rejected => (ballot.deposit * 25) / 100, + BallotStatus::Rejected => get_value_percentage(&e, ballot.deposit, 25), BallotStatus::Accepted => ballot.deposit, _ => e.panic_with_error(Error::BallotClosed), }; // burn tokens from the deposit according to the decision token(&e).burn(&e.current_contract_address(), &burn_amount); // update current DAO balance - update_dao_balance(&e, &(-burn_amount)); + update_dao_balance(&e, -burn_amount); // update ballot status ballot.status = new_status; e.set_ballot(ballot_id, &ballot); + + // publish voted event + e.events().publish( + ( + REFLECTOR, + symbol_short!("dao"), + symbol_short!("voted") + ), + (ballot_id, accepted) + ); } } @@ -335,6 +387,16 @@ fn set_deposit(e: &Env, deposit_params: Map) { } e.set_deposit(category, amount); } + + // publish updated event + e.events().publish( + ( + REFLECTOR, + symbol_short!("dao"), + symbol_short!("configed") + ), + deposit_params, + ); } // fetch ballot from the persistent storage @@ -354,20 +416,52 @@ fn token(e: &Env) -> TokenClient { } // calculate percentage from a given amount -fn calc_percentage(value: i128, percentage: i128) -> i128 { - (value * percentage) / 10000 +fn calc_share(e: &Env, value: i128, percentage: i128) -> i128 { + div(e, mul(e, value, percentage), PERCENTAGE_FACTOR) } // update the balance available for claiming for a particular account -fn update_available_balance(e: &Env, address: &Address, amount: &i128) { +fn update_available_balance(e: &Env, address: &Address, amount: i128) { let balance = e.get_available_balance(address); - e.set_available_balance(address, balance + amount); + e.set_available_balance(address, sum(&e, balance, amount)); } // update the remaining DAO balance -fn update_dao_balance(e: &Env, amount: &i128) { +fn update_dao_balance(e: &Env, amount: i128) { let dao_balance = e.get_dao_balance(); - e.set_dao_balance(dao_balance + amount); + e.set_dao_balance(sum(&e, dao_balance, amount)); +} + +// calculate the percentage of a given value with overflow check +fn get_value_percentage(e: &Env, value: i128, percentage: i128) -> i128 { + div(e, mul(e, value, percentage),100) +} + +// addition with overflow check +fn sum(e: &Env, a: i128, b: i128) -> i128 { + let sum = a.checked_add(b); + if sum.is_none() { + e.panic_with_error(Error::Overflow); + } + sum.unwrap() +} + +// division with overflow check +fn div(e: &Env, a: i128, b: i128) -> i128 { + let result = a.checked_div(b); + if result.is_none() { + e.panic_with_error(Error::Overflow); + } + result.unwrap() +} + +// multiplication with overflow check +fn mul(e: &Env, a: i128, b: i128) -> i128 { + let result = a.checked_mul(b); + if result.is_none() { + e.panic_with_error(Error::Overflow); + } + result.unwrap() } mod test; diff --git a/src/test.rs b/src/test.rs index a01c593..22025b7 100644 --- a/src/test.rs +++ b/src/test.rs @@ -15,7 +15,7 @@ fn init_contract_with_admin<'a>() -> (Env, DAOContractClient<'a>, ContractConfig let contract_id = env.register_contract(None, DAOContract); let client: DAOContractClient<'a> = DAOContractClient::new(&env, &contract_id); - let token = env.register_stellar_asset_contract(admin.clone()); + let token = env.register_stellar_asset_contract_v2(admin.clone()).address(); env.mock_all_auths(); diff --git a/src/types/error.rs b/src/types/error.rs index 230ed64..d06af58 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -14,6 +14,10 @@ pub enum Error { InvalidAmount = 3, /// Invalid ballot create parameters InvalidBallotParams = 4, + /// Overflow occurred during the operation + Overflow = 5, + /// Operators param is invalid + InvalidOperators = 6, /// Last unlock process has been executed less than a week ago UnlockUnavailable = 10, /// Proposal has been created less than two weeks ago and refund is not available yet, or the ballot has been closed