Skip to content
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

Safe upgrades: Add healthcheck function and call it when upgrading a contract #116 -Malachi PR #122

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
102 changes: 102 additions & 0 deletions src/health.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use array::ArrayTrait;
use debug::PrintTrait;
use konoha::contract::IGovernanceDispatcher;
use konoha::proposals::IProposalsDispatcher;
use konoha::upgrades::IUpgradesDispatcher;
use openzeppelin::token::erc20::interface::IERC20Dispatcher;
use starknet::ContractAddress;

#[starknet::interface]
trait IHealthCheck<TContractState> {
fn check_if_healthy(self: @TContractState, gov_address: ContractAddress) -> bool;
fn check_correct_contract_type(
self: @TContractState, proposed_contract_type: felt252, previous_contract_type: u64
) -> bool;
}

#[starknet::contract]
mod HealthCheck {
use array::ArrayTrait;
use debug::PrintTrait;
use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait};
use konoha::proposals::{IProposalsDispatcher, IProposalsDispatcherTrait};
use konoha::upgrades::{IUpgradesDispatcher, IUpgradesDispatcherTrait};
use openzeppelin::token::erc20::interface::IERC20;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::ContractAddress;
use super::IHealthCheck;

#[storage]
struct Storage {}

#[embeddable_as(HealthImpl)]
impl Health<TContractState> of super::IHealthCheck<TContractState> {
fn check_if_healthy(self: @TContractState, gov_address: ContractAddress) -> bool {
let proposals_dispatcher = IProposalsDispatcher { contract_address: gov_address };
let upgrades_dispatcher = IUpgradesDispatcher { contract_address: gov_address };

// Check if there are no proposals
let current_prop_id = proposals_dispatcher.get_latest_proposal_id();
if current_prop_id == 0 {
return true;
}
// Retrieve current proposal details
let current_prop_details = proposals_dispatcher.get_proposal_details(current_prop_id);

// Check if the latest upgrade type matches the proposal's upgrade type
let (_, last_upgrade_type) = upgrades_dispatcher.get_latest_upgrade();

if last_upgrade_type.into() != current_prop_details.to_upgrade{
return false;
}

// Ensure that the type of the new proposal matches the required contract type
if !self.check_correct_contract_type(
current_prop_details.to_upgrade, last_upgrade_type.into()
) {
return false;
}

// Check the governance state
let gov_token_addr = IGovernanceDispatcher { contract_address: gov_address }
.get_governance_token_address();
let total_eligible_votes_u256: u256 = IERC20Dispatcher {
contract_address: gov_token_addr
}
.total_supply();
if total_eligible_votes_u256.high != 0 {
return false;
}
let total_eligible_votes: u128 = total_eligible_votes_u256.low;
if total_eligible_votes == 0 {
return false;
}

true
}

fn check_correct_contract_type(
self: @TContractState, proposed_contract_type: felt252, previous_contract_type: u64
) -> bool {
// Check if the proposed contract type is compatible with the previous contract type
if proposed_contract_type == 1 && previous_contract_type == 1 {
// Generic contract upgrade is allowed
true
} else if proposed_contract_type == 3 && previous_contract_type == 3 {
// Airdrop upgrade is allowed
true
} else if proposed_contract_type == 5 && previous_contract_type == 5 {
// Custom proposal is allowed
true
} else if proposed_contract_type == 6 && previous_contract_type == 6 {
// Arbitrary proposal is allowed
true
} else if (proposed_contract_type == 0 && previous_contract_type == 0)
|| (proposed_contract_type == 2 && previous_contract_type == 2) {
false
} else {
false
}
}
}
}
1 change: 1 addition & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod airdrop;
mod constants;
mod contract;
mod discussion;
mod health;
mod merkle_tree;
mod proposals;
mod staking;
Expand Down
6 changes: 6 additions & 0 deletions src/proposals.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use starknet::ContractAddress;
trait IProposals<TContractState> {
fn vote(ref self: TContractState, prop_id: felt252, opinion: felt252);
fn get_proposal_details(self: @TContractState, prop_id: felt252) -> PropDetails;
fn get_latest_proposal_id(self: @TContractState) -> felt252;

fn get_vote_counts(self: @TContractState, prop_id: felt252) -> (u128, u128);
fn submit_proposal(
ref self: TContractState, payload: felt252, to_upgrade: ContractType
Expand Down Expand Up @@ -300,6 +302,10 @@ mod proposals {
self.proposal_details.read(prop_id)
}

fn get_latest_proposal_id(self: @ComponentState<TContractState>) -> felt252 {
self.get_free_prop_id_timestamp() - 1
}

fn get_live_proposals(self: @ComponentState<TContractState>) -> Array<felt252> {
let max: u32 = self.get_free_prop_id_timestamp().try_into().unwrap();
let mut i: u32 = 0;
Expand Down
129 changes: 111 additions & 18 deletions src/testing/setup.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use core::traits::Into;
use core::traits::TryInto;
use debug::PrintTrait;
use konoha::constants;


use konoha::contract::IGovernanceDispatcher;
use konoha::contract::IGovernanceDispatcherTrait;
use konoha::proposals::IProposalsDispatcher;
use konoha::proposals::IProposalsDispatcherTrait;
use konoha::staking::{IStakingDispatcher, IStakingDispatcherTrait};

use konoha::types::ContractType;
use konoha::upgrades::IUpgradesDispatcher;
use konoha::upgrades::IUpgradesDispatcherTrait;
use openzeppelin::token::erc20::interface::IERC20;
Expand All @@ -19,14 +20,17 @@ use snforge_std::{
};
use starknet::ContractAddress;
use starknet::get_block_timestamp;
//use super::staking_tests::set_floating_token_address;


const GOV_TOKEN_INITIAL_SUPPLY: felt252 = 1000000000000000000;
const GOV_TOKEN_INITIAL_SUPPLY: u256 = 1000000000000000000;

const first_address: felt252 = 0x1;
const second_address: felt252 = 0x2;
const admin_addr: felt252 = 0x3;

const governance_address: felt252 = 0x99999;

// DEPRECATED, use deploy_governance_and_both_tokens instead
fn deploy_governance(token_address: ContractAddress) -> IGovernanceDispatcher {
let gov_contract = declare("Governance").expect('unable to declare governance');
let mut args: Array<felt252> = ArrayTrait::new();
Expand All @@ -35,23 +39,49 @@ fn deploy_governance(token_address: ContractAddress) -> IGovernanceDispatcher {
IGovernanceDispatcher { contract_address: address }
}

// return governance, voting token, floating token.
// by default, all floating tokens are minted to admin address.
fn deploy_governance_and_both_tokens() -> (
IGovernanceDispatcher, IERC20Dispatcher, IERC20Dispatcher
) {
let gov_contract = declare("Governance").expect('unable to declare governance');
let floating_token_class = declare("FloatingToken").expect('unable to declare FloatingToken');
let voting_token_class = declare("VotingToken").expect('unable to declare VotingToken');
let mut args: Array<felt252> = ArrayTrait::new();
args.append(voting_token_class.class_hash.into());
args.append(floating_token_class.class_hash.into());
args.append(admin_addr);
gov_contract
.deploy_at(@args, governance_address.try_into().unwrap())
.expect('unable to deploy governance');
let gov_dispatcher = IGovernanceDispatcher {
contract_address: governance_address.try_into().unwrap()
};
let staking = IStakingDispatcher { contract_address: governance_address.try_into().unwrap() };

let voting_token_dispatcher = IERC20Dispatcher {
contract_address: gov_dispatcher.get_governance_token_address()
};

let floating_token_dispatcher = IERC20Dispatcher {
contract_address: staking.get_floating_token_address()
};

(gov_dispatcher, voting_token_dispatcher, floating_token_dispatcher)
}

// DEPRECATED, use deploy_governance_and_both_tokens instead
fn deploy_and_distribute_gov_tokens(recipient: ContractAddress) -> IERC20Dispatcher {
let mut calldata = ArrayTrait::new();
calldata.append(GOV_TOKEN_INITIAL_SUPPLY);
calldata.append(GOV_TOKEN_INITIAL_SUPPLY.low.into());
calldata.append(GOV_TOKEN_INITIAL_SUPPLY.high.into());
calldata.append(recipient.into());

let gov_token_contract = declare("FloatingToken").expect('unable to declare FloatingToken');
let (token_addr, _) = gov_token_contract
.deploy(@calldata)
.expect('unable to deploy FloatingToken');
let token: IERC20Dispatcher = IERC20Dispatcher { contract_address: token_addr };

start_prank(CheatTarget::One(token_addr), admin_addr.try_into().unwrap());

token.transfer(first_address.try_into().unwrap(), 100000);
token.transfer(second_address.try_into().unwrap(), 100000);
token
IERC20Dispatcher { contract_address: token_addr }
}


Expand Down Expand Up @@ -79,10 +109,73 @@ fn test_vote_upgrade_root(new_merkle_root: felt252) {
assert(check_if_healthy(gov_contract_addr), 'new gov not healthy');
}

fn check_if_healthy(gov_contract_addr: ContractAddress) -> bool {
// TODO
let dispatcher = IProposalsDispatcher { contract_address: gov_contract_addr };
dispatcher.get_proposal_status(0);
let prop_details = dispatcher.get_proposal_details(0);
(prop_details.payload + prop_details.to_upgrade) != 0
//health check completed for checking governance type.
//TODO: version history

fn check_if_healthy(gov_address: ContractAddress) -> bool {
let proposals_dispatcher = IProposalsDispatcher { contract_address: gov_address };
let upgrades_dispatcher = IUpgradesDispatcher { contract_address: gov_address };

// Check if there are no proposals
let current_prop_id = proposals_dispatcher.get_latest_proposal_id();
if current_prop_id == 0 {
return true;
}
// Retrieve current proposal details
let current_prop_details = proposals_dispatcher.get_proposal_details(current_prop_id);

// Check if the latest upgrade type matches the proposal's upgrade type
let (_, last_upgrade_type) = upgrades_dispatcher.get_latest_upgrade();

if last_upgrade_type.into() != current_prop_details.to_upgrade{
return false;
}

// Ensure that the type of the new proposal matches the required contract type
if !check_correct_contract_type(
current_prop_details.to_upgrade, last_upgrade_type.into()
) {
return false;
}

// Check the governance state
let gov_token_addr = IGovernanceDispatcher { contract_address: gov_address }
.get_governance_token_address();
let total_eligible_votes_u256: u256 = IERC20Dispatcher {
contract_address: gov_token_addr
}
.total_supply();
if total_eligible_votes_u256.high != 0 {
return false;
}
let total_eligible_votes: u128 = total_eligible_votes_u256.low;
if total_eligible_votes == 0 {
return false;
}

true
}

fn check_correct_contract_type(
proposed_contract_type: felt252, previous_contract_type: u64
) -> bool {
// Check if the proposed contract type is compatible with the previous contract type
if proposed_contract_type == 1 && previous_contract_type == 1 {
// Generic contract upgrade is allowed
true
} else if proposed_contract_type == 3 && previous_contract_type == 3 {
// Airdrop upgrade is allowed
true
} else if proposed_contract_type == 5 && previous_contract_type == 5 {
// Custom proposal is allowed
true
} else if proposed_contract_type == 6 && previous_contract_type == 6 {
// Arbitrary proposal is allowed
true
} else if (proposed_contract_type == 0 && previous_contract_type == 0)
|| (proposed_contract_type == 2 && previous_contract_type == 2) {
false
} else {
false
}
}
20 changes: 18 additions & 2 deletions src/upgrades.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use starknet::ContractAddress;
#[starknet::interface]
trait IUpgrades<TContractState> {
fn apply_passed_proposal(ref self: TContractState, prop_id: felt252);
fn get_latest_upgrade(self: @TContractState) -> (u64, u64);
}

#[starknet::component]
Expand All @@ -12,11 +14,18 @@ mod upgrades {
use konoha::contract::Governance::ContractState;
use konoha::contract::Governance;

use konoha::contract::IGovernanceDispatcher;

use konoha::health::{IHealthCheckDispatcher, IHealthCheckDispatcherTrait};
use konoha::proposals::IProposalsDispatcher;

use konoha::proposals::proposals as proposals_component;
use konoha::proposals::proposals::ProposalsImpl;
use konoha::traits::IERC20Dispatcher;
use konoha::traits::get_governance_token_address_self;

use konoha::types::{CustomProposalConfig, PropDetails};
use konoha::upgrades::IUpgradesDispatcher;
use option::OptionTrait;
use starknet::ClassHash;
use starknet::ContractAddress;
Expand All @@ -31,9 +40,11 @@ mod upgrades {
#[storage]
struct Storage {
proposal_applied: LegacyMap::<felt252, bool>,
amm_address: ContractAddress
amm_address: ContractAddress,
latest_upgrade: (u64, u64), // (prop_id, upgrade_type)
}


#[derive(starknet::Event, Drop)]
#[event]
enum Event {
Expand All @@ -54,6 +65,9 @@ mod upgrades {
impl Proposals: proposals_component::HasComponent<TContractState>,
impl Airdrop: airdrop_component::HasComponent<TContractState>
> of super::IUpgrades<ComponentState<TContractState>> {
fn get_latest_upgrade(self: @ComponentState<TContractState>) -> (u64, u64) {
self.latest_upgrade.read()
}
fn apply_passed_proposal(ref self: ComponentState<TContractState>, prop_id: felt252) {
let proposals_comp = get_dep_component!(@self, Proposals);
let status = proposals_comp
Expand Down Expand Up @@ -122,7 +136,7 @@ mod upgrades {
calldata.span()
);
res.expect('contract call failed');
}
}
},
6 => {
// arbitrary proposal
Expand All @@ -136,6 +150,8 @@ mod upgrades {
_ => { panic_with_felt252('invalid to_upgrade') }
};
self.proposal_applied.write(prop_id, true); // Mark the proposal as applied
let upgrade_type: u64 = contract_type.try_into().unwrap();
self.latest_upgrade.write((prop_id.try_into().unwrap(), upgrade_type));
self
.emit(
Upgraded {
Expand Down
18 changes: 10 additions & 8 deletions tests/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod airdrop_tests;
mod basic;
mod proposals_tests;
//mod airdrop_tests;
//mod basic;
//mod proposals_tests;
mod setup;
mod staking_tests;
mod test_storage_pack;
mod test_streaming;
mod test_treasury;
mod upgrades_tests;
mod vesting;
//mod test_storage_pack;
//mod test_streaming;
//mod test_treasury;
//mod upgrades_tests;
//mod vesting;
mod test_health;

Loading