-
Notifications
You must be signed in to change notification settings - Fork 88
feat(validator): Implement validator reputation system #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
0d7d737
66ffedb
20c37c7
a062bbf
27f863d
62db194
7510517
25db2f2
b94f4b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ pub mod Predifi { | |
| use core::poseidon::PoseidonTrait; | ||
| // oz imports | ||
| use openzeppelin::access::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE}; | ||
|
|
||
| use openzeppelin::introspection::src5::SRC5Component; | ||
| use openzeppelin::security::{PausableComponent, ReentrancyGuardComponent}; | ||
| use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; | ||
|
|
@@ -25,7 +26,7 @@ pub mod Predifi { | |
| FeesCollected, PoolAutomaticallySettled, PoolCancelled, PoolEmergencyFrozen, | ||
| PoolEmergencyResolved, PoolEmergencyUnfrozen, PoolResolved, PoolStateTransition, | ||
| PoolSuspended, StakeRefunded, UserStaked, ValidatorAdded, ValidatorRemoved, | ||
| ValidatorResultSubmitted, ValidatorsAssigned, | ||
| ValidatorResultSubmitted, ValidatorsAssigned,ValidatorSlashed, ValidatorPerformanceUpdated,FeesDistributed | ||
| }; | ||
| use crate::base::security::{Security, SecurityTrait}; | ||
|
|
||
|
|
@@ -152,6 +153,11 @@ pub mod Predifi { | |
| upgradeable: UpgradeableComponent::Storage, | ||
| #[substorage(v0)] | ||
| reentrancy_guard: ReentrancyGuardComponent::Storage, | ||
| //validator performance tracking | ||
| validator_reputation: Map<ContractAddress, u256>, | ||
| validator_success_count: Map<ContractAddress,u256>, | ||
| validator_fail_count:Map<ContractAddress,u256>, | ||
| validator_slashed:Map<ContractAddress,u256>, | ||
| } | ||
|
|
||
| /// @notice Events emitted by the Predifi contract. | ||
|
|
@@ -215,8 +221,12 @@ pub mod Predifi { | |
| UpgradeableEvent: UpgradeableComponent::Event, | ||
| #[flat] | ||
| ReentrancyGuardEvent: ReentrancyGuardComponent::Event, | ||
| ValidatorSlashed:ValidatorSlashed, | ||
| ValidatorPerformanceUpdated:ValidatorPerformanceUpdated, | ||
| FeesDistributed: FeesDistributed, | ||
| } | ||
|
|
||
|
|
||
|
|
||
| #[derive(Drop, Hash)] | ||
| struct HashingProperties { | ||
| username: felt252, | ||
|
|
@@ -228,7 +238,113 @@ pub mod Predifi { | |
| id: felt252, | ||
| login: HashingProperties, | ||
| } | ||
| /// @notice Update validator performance and adjust reputation. | ||
| /// @param validator Address of the validator. | ||
| /// @param success True if validation was correct, false otherwise. | ||
| fn update_performance(ref self:ContractState,validator:ContractAddress,success:bool){ | ||
|
||
| if(success){ | ||
| let prev: u256=self.validator_success_count.read(validator); | ||
| self.validator_success_count.write(validator,prev+1); | ||
|
|
||
| let rep: u256=self.validator_reputation.read(validator)+1; | ||
| self.validator_reputation.write(validator,rep); | ||
|
|
||
| self.emit(Event::ValidatorPerformanceUpdated( | ||
| ValidatorPerformanceUpdated{validator,success,reputation_after:rep} | ||
| )); | ||
| } else{ | ||
| let prev: u256=self.validator_fail_count.read(validator); | ||
| self.validator_fail_count.write(validator,prev+1); | ||
|
|
||
| let rep: u256=self.validator_reputation.read(validator); | ||
| let new_rep: u256=if rep>0 {rep-1} else { 0 }; | ||
| self.validator_reputation.write(validator,new_rep); | ||
|
|
||
| self.emit(Event::ValidatorPerformanceUpdated( | ||
| ValidatorPerformanceUpdated{validator,success,reputation_after:new_rep} | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| /// @notice Slash a validator by halving reputation and treasury. | ||
| /// @param validator Validator address to slash. | ||
| fn slash_validator(ref self:Storage,validator:ContractAddress){ | ||
|
||
| let reputation: u256=self.validator_reputation.read(validator); | ||
| let treasury: u256=self.validator_treasuries.read(validator); | ||
|
|
||
| let new_rep: u256=reputation/2; | ||
| let new_treasury: u256=treasury/2; | ||
|
|
||
| self.validator_reputation.write(validator,new_rep); | ||
| self.validator_treasuries.write(validator,new_treasury); | ||
|
|
||
| self.emit(Event::ValidatorSlashed( | ||
| ValidatorSlashed{ | ||
| validator, | ||
| amount:treasury-new_treasury, | ||
| reputation_after:new_rep, | ||
| } | ||
| )); | ||
|
|
||
| } | ||
|
|
||
| /// @notice Distribute fees among to validators of a pool who chose the correct option. | ||
| /// @param pool_id ID fo the pool to distribute fees for. | ||
| fn distribute_validator_fees(ref self:Storage,pool_id:u256){ | ||
| //Ensure pool exists | ||
| let pool_data: PoolDetails=self.pools.read(pool_id); | ||
|
|
||
| //Ensure pool has ended | ||
| assert(pool_data.has_ended==true,1002); | ||
|
|
||
| //Ensure pool has been resolved | ||
| assert(pool_data.is_resolved==true,1003); | ||
|
|
||
| let correct_option=pool_data.correct_option; | ||
| let total_fees: u256=pool_data.fee_pool; | ||
|
|
||
| //Filter validators that chose the correct option | ||
| let mut eligible_validators=ArrayTrait::new(); | ||
| let mut total_reputation: u256=0; | ||
|
|
||
| for validator in pool_data.validators{ | ||
| // Use your pool_validation_results storage | ||
| let choice:bool=self.pool_validation_results.read((pool_id,*validator)); | ||
| if choice==correct_option{ | ||
| let rep: u256=self.validator_reputation.read(*validator); | ||
| eligible_validators.append(*validator); | ||
| total_reputation+=rep; | ||
| } | ||
| } | ||
| //Distribute proportionally by reputation | ||
| for validator in eligible_validators{ | ||
| let rep: u256=self.validator_reputation.read(*validator); | ||
| let share: u256 =total_fees*rep/total_reputation; | ||
|
|
||
| let balance: u256=self.validator_treasuries.read(*validator); | ||
| self.validator_treasuries.write(*validator,balance+share); | ||
| } | ||
| self.emit(Events::FeesDistributed( | ||
| FeesDistributed{pool_id,total_distributed:total_fees} | ||
| )); | ||
| } | ||
|
|
||
| /// @notice Get validator reputation | ||
| fn get_validator_reputation(self:@ContractState, validator:ContractAddress)->u256{ | ||
| self.validator_reputation.read(validator) | ||
| } | ||
| /// @notice Get validator success count | ||
| fn get_validator_success(self:@ContractState, validator:ContractAddress)->u256{ | ||
| self.validator_success_count.read(validator) | ||
| } | ||
| /// @notice Get validator fail count | ||
| fn get_validator_slashed(self:@ContractState, validator:ContractAddress)->u256{ | ||
| self.validator_slashed.read(validator) | ||
| } | ||
| /// @notice Get validator treasury | ||
| fn get_validator_treasury(self:@ContractState, validator:ContractAddress)->u256{ | ||
| self.validator_treasuries.read(validator) | ||
| } | ||
| /// @notice Initializes the Predifi contract. | ||
| /// @param self The contract state. | ||
| /// @param token_addr The address of the STRK token contract. | ||
|
|
||
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| #[cfg(test)] | ||
| mod tests{ | ||
| use super::super::Predifi; | ||
| use starknet::ContractAddress; | ||
| use starknet::testing::*; | ||
| fn addr(x:u126)->ContractAdress{ | ||
| ContractAddress::from_u128(x) | ||
| } | ||
| #[test] | ||
| fn test_update_performance_success_and_fail(){ | ||
| let(contract_address,mut state)=deploy_contract("src/predifi.cairo"); | ||
|
|
||
| let v= addr(1); | ||
|
|
||
| //success path | ||
| Predifi::update_performance(&mut state, v, true); | ||
| let rep1=Predifi::get_validator_reputation(&state,v); | ||
| assert(rep1==1,'Reputation should increase'); | ||
| assert(success_count==1,'Success count should increment'); | ||
|
|
||
| //fail path | ||
| Predifi::update_performance(&mut state, v, false); | ||
| let rep2=Predifi::get_validator_reputation(&state,v); | ||
| let fail_count=Predifi::get_validator_fail(&state,v); | ||
| assert(rep2==1,'Reputation should decrease'); | ||
| assert(fail_count==1,'Fail count should increment'); | ||
| } | ||
| #[test] | ||
| fn test_slash_validator_reduces_rep_and_treasury(){ | ||
| let (contract_address,mut state)=deploy_contract("src/predifi.cairo"); | ||
| let v= addr(2); | ||
| //give validator some rep and treasury | ||
| state.validator_reputation.write(v,100); | ||
| state.validator_treasuries.write(v,100); | ||
|
|
||
| //slash 50 | ||
| Predifi::slash_validator(&mut state,v,50); | ||
|
|
||
| let rep=Predifi::get_validator_reputation(&state,v); | ||
| let treasury=Predifi::get_validator_treasury(&state,v); | ||
| let slashed=Predifi::get_validator_slashed(&state,v); | ||
|
|
||
| assert(rep==50,'Reputation should replace by 50'); | ||
| assert(treasury==50,'Treasury should replace by 50'); | ||
| assert(slashed==50,'Slashed amount recorded'); | ||
| #[test] | ||
| fn test_distribute_validator_fees_propotional(){ | ||
| let(contract_address,mut state)=deploy_contract("src/predifi.cairo"); | ||
| let v1=addr(10); | ||
| let v2=addr(20); | ||
| let v3=addr(30); | ||
|
|
||
| state.validators.push(v1); | ||
| state.validators.push(v2); | ||
| state.validators.push(v3); | ||
|
|
||
| state.validator_reputation.write(v1,10); | ||
| state.validator_reputation.write(v2,20); | ||
| state.validator_reputation.write(v3,30); | ||
|
|
||
| state.validator_fee.write(1,60); | ||
|
|
||
| Predifi::distribute_validator_fees(&mut state,1); | ||
|
|
||
| let t1=Predifi::get_validator_treasury(&state,v1); | ||
| let t2=Predifi::get_validator_treasury(&state,v2); | ||
| let t3=Predifi::get_validator_treasury(&state,v3); | ||
|
|
||
| assert(t1==10,'v1 should get 10'); | ||
| assert(t2==10,'v2 should get 20'); | ||
| assert(t3==10,'v3 should get 30'); | ||
|
|
||
|
|
||
|
|
||
| } | ||
|
|
||
|
|
||
|
|
||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -971,3 +971,65 @@ fn test_remove_validator_unauthorized() { | |
| // Unauthorized caller attempt to remove the validator role | ||
| IPredifiValidator::remove_validator(ref state, validator); | ||
| } | ||
| #[test] | ||
| fn test_slash_validator_halves_rep_and_treasury(){ | ||
| let (_contract_address, mut state)= deploy_contract("src/predifi.cairo"); | ||
|
||
| let v=addr(2); | ||
|
|
||
| //Send reputation and treasury | ||
| state.reputations.write(v,100); | ||
| state.treasuries.write(v,200); | ||
|
|
||
| // Slash validator | ||
| Predifi::slash_validator(ref state,v); | ||
|
|
||
| let rep=Predifi::get_validator_reputation(@state,v); | ||
| let treasury=Predifi::get_validator_treasury(@state,v); | ||
|
|
||
| assert(rep==50,'Reputation should be halved'); | ||
| assert(treasury==100,'Treasury should be halved'); | ||
| } | ||
| #[test] | ||
| fn test_distribute_validator_fees_only_correct_validators(){ | ||
| let (_contract_address, mut state)=deploy_contract("src/predifi.cairo"); | ||
| let v1=addr(10); | ||
| let v2=addr(20); | ||
| let v3=addr(30); | ||
|
|
||
| let pool_id=1; | ||
|
|
||
| //Setup pool | ||
| let pool=PoolData{ | ||
| has_ended:true, | ||
| is_resolved: true, | ||
| correct_option:1, | ||
| fee_pool:60, | ||
| validators:array![v1,v2,v3], | ||
| }; | ||
| state.pools.write(pool_id,Some(pool)); | ||
|
|
||
| //Set reputations | ||
| state.reputations.write(v1,10); | ||
| state.reputataion.write(v2,20); | ||
| state.reputation.write(v3,30); | ||
|
|
||
| //Mark choices: v1 and v2 correct, v3 Wrong | ||
| state.validator_choices.write((pool_id,v1),1); | ||
| state.validator_choices.write((pool_id,v2),1); | ||
| state.validator_choices.write((pool_id,v3),0); | ||
|
|
||
| Predifi::distribute_validator_fees(ref state, pool_id); | ||
|
|
||
| let t1=Predifi::get_validator_treasury(@state,v1); | ||
| let t2=Predifi::get_validator_treasury(@state,v2); | ||
| let t3=Predifi::get_validator_treasury(@state,v3); | ||
|
|
||
| //Eligible rep=30(10+20) | ||
| //v1 share=(10/30)*60=20 | ||
| //v2 share=(20/30)*60=20 | ||
| //v3 gets 0 | ||
| assert(t1==20,'v1 should get 20'); | ||
| assert(t2==40,'v2 should get 40'); | ||
| assert(t3==0,'v3 should get 0'); | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great all events are here now, easier to modify later and structured