sync: mirror deployed github repo (chains + sanitized bridge), keep GitLab CI

Make this monorepo's tracked tree identical to the deployed github repo
(github.com/pezkuwichain/pezkuwi-DKS): all RC/AH/People runtimes and framework
carry the mainnet fixes (incl. U128CurrencyToVote everywhere, so the staking
election stall cannot recur), and tools/usdt-bridge matches the current,
public-safe custody tooling (real config/deploy units gitignored, secret-file
locations and server IPs stripped, sanitized *.example templates committed).

Only exception to the mirror: GitLab-native CI is preserved (.gitlab-ci.yml and
.gitlab/), to be rebuilt to match the github CI pipeline in a follow-up.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-07-24 11:42:50 -07:00
1 parent 82543bdfab
commit d285d59108
736 files changed
+15082 -16359

No files matched your search

@@ -77,7 +77,7 @@
"name": "Pezkuwichain Asset Hub",
"properties": {
"tokenDecimals": 12,
"tokenSymbol": "TYR"
"tokenSymbol": "HEZ"
},
"protocolId": null,
"telemetryEndpoints": null
@@ -82,7 +82,7 @@
"properties": {
"ss58Format": 42,
"tokenDecimals": 12,
"tokenSymbol": "TYR"
"tokenSymbol": "HEZ"
},
"protocolId": null,
"telemetryEndpoints": null
@@ -69,7 +69,7 @@
"properties": {
"ss58Format": 42,
"tokenDecimals": 12,
"tokenSymbol": "TYR"
"tokenSymbol": "HEZ"
},
"protocolId": null,
"telemetryEndpoints": null
@@ -75,7 +75,7 @@
"properties": {
"ss58Format": 42,
"tokenDecimals": 12,
"tokenSymbol": "TYR"
"tokenSymbol": "HEZ"
},
"protocolId": null,
"telemetryEndpoints": null
@@ -28,11 +28,11 @@ use pezkuwi_primitives::{AssignmentId, ValidatorId};
use emulated_integration_tests_common::{
accounts, build_genesis_storage, get_host_config, validators,
};
use pezkuwichain_runtime_constants::currency::UNITS as TYR;
use pezkuwichain_runtime_constants::currency::UNITS as HEZ;
use teyrchains_common::Balance;
pub const ED: Balance = pezkuwichain_runtime_constants::currency::EXISTENTIAL_DEPOSIT;
const ENDOWMENT: u128 = 1_000_000 * TYR;
const ENDOWMENT: u128 = 1_000_000 * HEZ;
fn session_keys(
babe: BabeId,
@@ -21,10 +21,10 @@ use pezframe_support::{
use pezkuwi_runtime_common::impls::VersionedLocatableAsset;
use pezkuwichain_runtime_constants::currency::GRAND;
// Fund Treasury account on Asset Hub from Treasury account on Relay Chain with TYRs.
// Fund Treasury account on Asset Hub from Treasury account on Relay Chain with HEZ.
#[test]
fn spend_roc_on_asset_hub() {
// initial treasury balance on Asset Hub in TYRs.
// initial treasury balance on Asset Hub in HEZ.
let treasury_balance = 9_000 * GRAND;
// the balance spend on Asset Hub.
let treasury_spend_balance = 1_000 * GRAND;
@@ -42,7 +42,7 @@ fn spend_roc_on_asset_hub() {
type Balances = <Pezkuwichain as PezkuwichainPallet>::Balances;
type Treasury = <Pezkuwichain as PezkuwichainPallet>::Treasury;
// Fund Treasury account on Asset Hub with TYRs.
// Fund Treasury account on Asset Hub with HEZ.
let root = <Pezkuwichain as Chain>::RuntimeOrigin::root();
let treasury_account = Treasury::account_id();
@@ -208,14 +208,14 @@ fn transfer_foreign_assets_from_asset_hub_to_para() {
let native_asset_location = RelayLocation::get();
let receiver = PenpalAReceiver::get();
let assets_owner = PenpalAssetOwner::get();
// Foreign asset used: bridged TYR
// Foreign asset used: bridged HEZ
let foreign_amount_to_send = ASSET_HUB_ZAGROS_ED * 10_000_000;
let roc_at_zagros_teyrchains = Location::new(
2,
[Junction::GlobalConsensus(NetworkId::ByGenesis(PEZKUWICHAIN_GENESIS_HASH))],
);
// Configure destination chain to trust AH as reserve of TYR
// Configure destination chain to trust AH as reserve of HEZ
PenpalA::execute_with(|| {
assert_ok!(<PenpalA as Chain>::System::set_storage(
<PenpalA as Chain>::RuntimeOrigin::root(),
@@ -339,14 +339,14 @@ fn transfer_foreign_assets_from_para_to_asset_hub() {
let native_asset_location = RelayLocation::get();
let assets_owner = PenpalAssetOwner::get();
// Foreign asset used: bridged TYR
// Foreign asset used: bridged HEZ
let foreign_amount_to_send = ASSET_HUB_ZAGROS_ED * 10_000_000;
let roc_at_zagros_teyrchains = Location::new(
2,
[Junction::GlobalConsensus(NetworkId::ByGenesis(PEZKUWICHAIN_GENESIS_HASH))],
);
// Configure destination chain to trust AH as reserve of TYR
// Configure destination chain to trust AH as reserve of HEZ
PenpalA::execute_with(|| {
assert_ok!(<PenpalA as Chain>::System::set_storage(
<PenpalA as Chain>::RuntimeOrigin::root(),
@@ -499,7 +499,7 @@ fn transfer_foreign_assets_from_para_to_para_through_asset_hub() {
let sov_of_receiver_on_ah = AssetHubZagros::sovereign_account_id_of(receiver_as_seen_by_ah);
let roc_to_send = ASSET_HUB_ZAGROS_ED * 10_000_000;
// Configure source and destination chains to trust AH as reserve of TYR
// Configure source and destination chains to trust AH as reserve of HEZ
PenpalA::execute_with(|| {
assert_ok!(<PenpalA as Chain>::System::set_storage(
<PenpalA as Chain>::RuntimeOrigin::root(),
@@ -519,7 +519,7 @@ fn transfer_foreign_assets_from_para_to_para_through_asset_hub() {
));
});
// Register TYR as foreign asset and transfer it around the Zagros ecosystem
// Register HEZ as foreign asset and transfer it around the Zagros ecosystem
let roc_at_zagros_teyrchains = Location::new(
2,
[Junction::GlobalConsensus(NetworkId::ByGenesis(PEZKUWICHAIN_GENESIS_HASH))],
@@ -109,7 +109,7 @@ fn send_assets_from_penpal_pezkuwichain_through_pezkuwichain_ah_to_zagros_ah(
}
#[test]
/// Test transfer of TYR from AssetHub Pezkuwichain to AssetHub Zagros.
/// Test transfer of HEZ from AssetHub Pezkuwichain to AssetHub Zagros.
fn send_roc_from_asset_hub_pezkuwichain_to_asset_hub_zagros() {
let amount = ASSET_HUB_PEZKUWICHAIN_ED * 1_000_000;
let sender = AssetHubPezkuwichainSender::get();
@@ -131,7 +131,7 @@ fn send_roc_from_asset_hub_pezkuwichain_to_asset_hub_zagros() {
let receiver_rocs_before =
foreign_balance_on_ah_zagros(bridged_roc_at_asset_hub_zagros.clone(), &receiver);
// send TYRs, use them for fees
// send HEZ, use them for fees
send_assets_over_bridge(|| {
let destination = asset_hub_zagros_location();
let assets: Assets = (roc_at_asset_hub_pezkuwichain.clone(), amount).into();
@@ -152,7 +152,7 @@ fn send_roc_from_asset_hub_pezkuwichain_to_asset_hub_zagros() {
assert_expected_events!(
AssetHubZagros,
vec![
// issue TYRs on AHW
// issue HEZ on AHW
RuntimeEvent::ForeignAssets(pezpallet_assets::Event::Issued { asset_id, owner, .. }) => {
asset_id: *asset_id == roc,
owner: owner == &receiver,
@@ -171,11 +171,11 @@ fn send_roc_from_asset_hub_pezkuwichain_to_asset_hub_zagros() {
let rocs_in_reserve_on_ahr_after =
<AssetHubPezkuwichain as Chain>::account_data_of(sov_ahw_on_ahr.clone()).free;
// Sender's TYR balance is reduced
// Sender's HEZ balance is reduced
assert!(sender_rocs_before > sender_rocs_after);
// Receiver's TYR balance is increased
// Receiver's HEZ balance is increased
assert!(receiver_rocs_after > receiver_rocs_before);
// Reserve TYR balance is increased by sent amount
// Reserve HEZ balance is increased by sent amount
assert_eq!(rocs_in_reserve_on_ahr_after, rocs_in_reserve_on_ahr_before + amount);
}
@@ -183,7 +183,7 @@ fn send_roc_from_asset_hub_pezkuwichain_to_asset_hub_zagros() {
/// Send bridged assets "back" from AssetHub Pezkuwichain to AssetHub Zagros.
///
/// This mix of assets should cover the whole range:
/// - bridged native assets: TYR,
/// - bridged native assets: HEZ,
/// - bridged trust-based assets: USDT (exists only on Zagros, Pezkuwichain gets it from Zagros over
/// bridge),
/// - bridged foreign asset / double-bridged asset (other bridge / Snowfork): wETH (bridged from
@@ -412,7 +412,7 @@ fn send_rocs_from_penpal_pezkuwichain_through_asset_hub_pezkuwichain_to_asset_hu
let receiver_rocs_before =
foreign_balance_on_ah_zagros(roc_at_asset_hub_zagros.clone(), &receiver);
// Send TYRs over bridge
// Send HEZ over bridge
{
let destination = asset_hub_zagros_location();
let assets: Assets = (roc_at_pezkuwichain_teyrchains.clone(), amount).into();
@@ -440,7 +440,7 @@ fn send_rocs_from_penpal_pezkuwichain_through_asset_hub_pezkuwichain_to_asset_hu
assert_expected_events!(
AssetHubZagros,
vec![
// issue TYRs on AHW
// issue HEZ on AHW
RuntimeEvent::ForeignAssets(pezpallet_assets::Event::Issued { asset_id, owner, .. }) => {
asset_id: *asset_id == roc,
owner: owner == &receiver,
@@ -477,7 +477,7 @@ fn send_back_wnds_from_penpal_pezkuwichain_through_asset_hub_pezkuwichain_to_ass
let sender = PenpalASender::get();
let receiver = AssetHubZagrosReceiver::get();
// set up TYRs for transfer
// set up HEZ for transfer
let (roc_at_pezkuwichain_teyrchains, _) =
set_up_rocs_for_penpal_pezkuwichain_through_ahr_to_ahw(&sender, amount);
@@ -528,7 +528,7 @@ fn send_back_wnds_from_penpal_pezkuwichain_through_asset_hub_pezkuwichain_to_ass
});
let receiver_wnds_before = <AssetHubZagros as Chain>::account_data_of(receiver.clone()).free;
// send ZGRs over the bridge, TYRs only used to pay fees on local AH, pay with ZGR on remote AH
// send ZGRs over the bridge, HEZ only used to pay fees on local AH, pay with ZGR on remote AH
{
let final_destination = asset_hub_zagros_location();
let intermediary_hop = PenpalA::sibling_location_of(AssetHubPezkuwichain::para_id());
@@ -584,7 +584,7 @@ fn send_back_wnds_from_penpal_pezkuwichain_through_asset_hub_pezkuwichain_to_ass
assert_expected_events!(
AssetHubZagros,
vec![
// issue TYRs on AHW
// issue HEZ on AHW
RuntimeEvent::Balances(pezpallet_balances::Event::Issued { .. }) => {},
// message processed successfully
RuntimeEvent::MessageQueue(
@@ -53,7 +53,7 @@ pub(crate) fn bridge_hub_zagros_location() -> Location {
)
}
// TYR and wTYR
// HEZ and wTYR
pub(crate) fn roc_at_ah_pezkuwichain() -> Location {
Parent.into()
}
@@ -288,7 +288,7 @@ fn send_wnds_usdt_and_weth_from_asset_hub_zagros_to_asset_hub_pezkuwichain() {
}
#[test]
/// Send bridged TYRs "back" from AssetHub Zagros to AssetHub Pezkuwichain.
/// Send bridged HEZ "back" from AssetHub Zagros to AssetHub Pezkuwichain.
fn send_back_rocs_from_asset_hub_zagros_to_asset_hub_pezkuwichain() {
let prefund_amount = 10_000_000_000_000u128;
let amount_to_send = ASSET_HUB_PEZKUWICHAIN_ED * 1_000;
@@ -304,7 +304,7 @@ fn send_back_rocs_from_asset_hub_zagros_to_asset_hub_pezkuwichain() {
prefund_accounts,
);
// fund the AHW's SA on AHR with the TYR tokens held in reserve
// fund the AHW's SA on AHR with the HEZ tokens held in reserve
let sov_ahw_on_ahr =
AssetHubPezkuwichain::sovereign_account_of_teyrchain_on_other_global_consensus(
ByGenesis(ZAGROS_GENESIS_HASH),
@@ -322,7 +322,7 @@ fn send_back_rocs_from_asset_hub_zagros_to_asset_hub_pezkuwichain() {
let receiver_rocs_before =
<AssetHubPezkuwichain as Chain>::account_data_of(receiver.clone()).free;
// send back TYRs, use them for fees
// send back HEZ, use them for fees
send_assets_over_bridge(|| {
let destination = asset_hub_pezkuwichain_location();
let assets: Assets = (bridged_roc_at_asset_hub_zagros.clone(), amount_to_send).into();
@@ -336,14 +336,14 @@ fn send_back_rocs_from_asset_hub_zagros_to_asset_hub_pezkuwichain() {
assert_expected_events!(
AssetHubPezkuwichain,
vec![
// TYR is withdrawn from AHW's SA on AHR
// HEZ is withdrawn from AHW's SA on AHR
RuntimeEvent::Balances(
pezpallet_balances::Event::Burned { who, amount }
) => {
who: *who == sov_ahw_on_ahr,
amount: *amount == amount_to_send,
},
// TYRs deposited to beneficiary
// HEZ deposited to beneficiary
RuntimeEvent::Balances(pezpallet_balances::Event::Minted { who, .. }) => {
who: *who == receiver,
},
@@ -754,7 +754,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
let (wnd_at_zagros_teyrchains, _) =
set_up_wnds_for_penpal_zagros_through_ahw_to_ahr(&sender, amount);
// set up TYRs for transfer
// set up HEZ for transfer
let penpal_location = AssetHubZagros::sibling_location_of(PenpalB::para_id());
let sov_penpal_on_ahw = AssetHubZagros::sovereign_account_id_of(penpal_location);
let reserves = vec![(asset_hub_pezkuwichain_location(), false).into()];
@@ -768,7 +768,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
ASSET_MIN_BALANCE,
vec![(sender.clone(), amount * 2)],
);
// Configure source Penpal chain to trust local AH as reserve of bridged TYR
// Configure source Penpal chain to trust local AH as reserve of bridged HEZ
PenpalB::execute_with(|| {
assert_ok!(<PenpalB as Chain>::System::set_storage(
<PenpalB as Chain>::RuntimeOrigin::root(),
@@ -779,7 +779,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
));
});
// fund the AHW's SA on AHR with the TYR tokens held in reserve
// fund the AHW's SA on AHR with the HEZ tokens held in reserve
let sov_ahw_on_ahr =
AssetHubPezkuwichain::sovereign_account_of_teyrchain_on_other_global_consensus(
ByGenesis(ZAGROS_GENESIS_HASH),
@@ -795,7 +795,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
let receiver_rocs_before =
<AssetHubPezkuwichain as Chain>::account_data_of(receiver.clone()).free;
// send TYRs over the bridge, ZGRs only used to pay fees on local AH, pay with TYR on remote AH
// send HEZ over the bridge, ZGRs only used to pay fees on local AH, pay with HEZ on remote AH
{
let final_destination = asset_hub_pezkuwichain_location();
let intermediary_hop = PenpalB::sibling_location_of(AssetHubZagros::para_id());
@@ -803,10 +803,10 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
// what happens at final destination
let beneficiary = AccountId32Junction { network: None, id: receiver.clone().into() }.into();
// use TYR as fees on the final destination (AHW)
// use HEZ as fees on the final destination (AHW)
let remote_fees: Asset = (roc_at_zagros_teyrchains.clone(), amount).into();
let remote_fees = remote_fees.reanchored(&final_destination, &context).unwrap();
// buy execution using TYRs, then deposit all remaining TYRs
// buy execution using HEZ, then deposit all remaining HEZ
let xcm_on_final_dest = Xcm::<()>(vec![
BuyExecution { fees: remote_fees, weight_limit: WeightLimit::Unlimited },
DepositAsset { assets: Wild(AllCounted(1)), beneficiary },
@@ -816,13 +816,13 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
// reanchor final dest (Asset Hub Pezkuwichain) to the view of hop (Asset Hub Zagros)
let mut final_destination = final_destination.clone();
final_destination.reanchor(&intermediary_hop, &context).unwrap();
// reanchor TYRs to the view of hop (Asset Hub Zagros)
// reanchor HEZ to the view of hop (Asset Hub Zagros)
let asset: Asset = (roc_at_zagros_teyrchains.clone(), amount).into();
let asset = asset.reanchored(&intermediary_hop, &context).unwrap();
// on Asset Hub Zagros, forward a request to withdraw TYRs from reserve on Asset Hub
// on Asset Hub Zagros, forward a request to withdraw HEZ from reserve on Asset Hub
// Pezkuwichain
let xcm_on_hop = Xcm::<()>(vec![InitiateReserveWithdraw {
assets: Definite(asset.into()), // TYRs
assets: Definite(asset.into()), // HEZ
reserve: final_destination, // AHR
xcm: xcm_on_final_dest, // XCM to execute on AHR
}]);
@@ -883,7 +883,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
let sender = PenpalBSender::get();
let receiver = PenpalAReceiver::get();
// set up TYRs for transfer
// set up HEZ for transfer
let penpal_location = AssetHubZagros::sibling_location_of(PenpalB::para_id());
let sov_penpal_on_ahw = AssetHubZagros::sovereign_account_id_of(penpal_location);
let reserves = vec![(asset_hub_pezkuwichain_location(), false).into()];
@@ -903,7 +903,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
sender.clone(),
amount,
);
// Create and fund bridged TYRs on Zagros Penpal
// Create and fund bridged HEZ on Zagros Penpal
PenpalB::force_create_foreign_asset(
roc_at_zagros_teyrchains.clone(),
asset_owner.clone(),
@@ -911,7 +911,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
ASSET_MIN_BALANCE,
vec![(sender.clone(), amount * 2)],
);
// Configure source Penpal chain to trust local AH as reserve of bridged TYR
// Configure source Penpal chain to trust local AH as reserve of bridged HEZ
PenpalB::execute_with(|| {
assert_ok!(<PenpalB as Chain>::System::set_storage(
<PenpalB as Chain>::RuntimeOrigin::root(),
@@ -922,7 +922,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
));
});
// fund the AHW's SA on AHR with the TYR tokens held in reserve
// fund the AHW's SA on AHR with the HEZ tokens held in reserve
let sov_ahw_on_ahr =
AssetHubPezkuwichain::sovereign_account_of_teyrchain_on_other_global_consensus(
ByGenesis(ZAGROS_GENESIS_HASH),
@@ -940,7 +940,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
<Assets as Inspect<_>>::balance(roc_at_pezkuwichain_teyrchains.clone(), &receiver)
});
// send TYRs over the bridge, all fees paid with TYR along the way
// send HEZ over the bridge, all fees paid with HEZ along the way
{
let local_asset_hub = PenpalB::sibling_location_of(AssetHubZagros::para_id());
let beneficiary: Location =
@@ -1026,7 +1026,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
assert_expected_events!(
AssetHubPezkuwichain,
vec![
// burn TYRs from AHW's SA on AHR
// burn HEZ from AHW's SA on AHR
RuntimeEvent::Balances(
pezpallet_balances::Event::Burned { who, .. }
) => {
@@ -1073,7 +1073,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
let receiver = PezkuwichainReceiver::get();
let mut topic_id_tracker = TopicIdTracker::new();
// set up TYRs for transfer
// set up HEZ for transfer
let penpal_location = AssetHubZagros::sibling_location_of(PenpalB::para_id());
let sov_penpal_on_ahw = AssetHubZagros::sovereign_account_id_of(penpal_location);
let reserves = vec![(asset_hub_pezkuwichain_location(), false).into()];
@@ -1093,7 +1093,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
sender.clone(),
amount,
);
// Create and fund bridged TYRs on Zagros Penpal
// Create and fund bridged HEZ on Zagros Penpal
PenpalB::force_create_foreign_asset(
roc_at_zagros_teyrchains.clone(),
asset_owner.clone(),
@@ -1101,7 +1101,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
ASSET_MIN_BALANCE,
vec![(sender.clone(), amount * 2)],
);
// Configure source Penpal chain to trust local AH as reserve of bridged TYR
// Configure source Penpal chain to trust local AH as reserve of bridged HEZ
PenpalB::execute_with(|| {
assert_ok!(<PenpalB as Chain>::System::set_storage(
<PenpalB as Chain>::RuntimeOrigin::root(),
@@ -1112,7 +1112,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
));
});
// fund the AHW's SA on AHR with the TYR tokens held in reserve
// fund the AHW's SA on AHR with the HEZ tokens held in reserve
let sov_ahw_on_ahr =
AssetHubPezkuwichain::sovereign_account_of_teyrchain_on_other_global_consensus(
ByGenesis(ZAGROS_GENESIS_HASH),
@@ -1133,7 +1133,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
});
let receiver_rocs_before = <Pezkuwichain as Chain>::account_data_of(receiver.clone()).free;
// send TYRs over the bridge, all fees paid with TYR along the way
// send HEZ over the bridge, all fees paid with HEZ along the way
{
let local_asset_hub = PenpalB::sibling_location_of(AssetHubZagros::para_id());
let beneficiary: Location =
@@ -1227,7 +1227,7 @@ fn send_back_rocs_from_penpal_zagros_through_asset_hub_zagros_to_asset_hub_pezku
assert_expected_events!(
AssetHubPezkuwichain,
vec![
// burn TYRs from AHW's SA on AHR
// burn HEZ from AHW's SA on AHR
RuntimeEvent::Balances(
pezpallet_balances::Event::Burned { who, .. }
) => {
@@ -72,7 +72,7 @@ pub(crate) fn bridged_wnd_at_ah_pezkuwichain() -> Location {
Location::new(2, [GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH))])
}
// TYR and wTYR
// HEZ and wTYR
pub(crate) fn bridged_roc_at_ah_zagros() -> Location {
Location::new(2, [GlobalConsensus(ByGenesis(PEZKUWICHAIN_GENESIS_HASH))])
}
@@ -434,7 +434,7 @@ pub fn erc20_token_location(token_id: H160) -> Location {
)
}
// TYR and wTYR
// HEZ and wTYR
pub(crate) fn roc_at_ah_pezkuwichain() -> Location {
Parent.into()
}
@@ -507,7 +507,7 @@ pub fn register_roc_on_bh() {
type RuntimeEvent = <BridgeHubZagros as Chain>::RuntimeEvent;
type RuntimeOrigin = <BridgeHubZagros as Chain>::RuntimeOrigin;
// Register TYR on BH
// Register HEZ on BH
assert_ok!(<BridgeHubZagros as BridgeHubZagrosPallet>::EthereumSystem::register_token(
RuntimeOrigin::root(),
Box::new(VersionedLocation::from(bridged_roc_at_ah_zagros())),
@@ -472,7 +472,7 @@ fn send_roc_from_ethereum_to_pezkuwichain() {
));
});
// fund the AHW's SA on AHR with the TYR tokens held in reserve
// fund the AHW's SA on AHR with the HEZ tokens held in reserve
let sov_ahw_on_ahr =
AssetHubPezkuwichain::sovereign_account_of_teyrchain_on_other_global_consensus(
ByGenesis(ZAGROS_GENESIS_HASH),
@@ -581,14 +581,14 @@ fn send_roc_from_ethereum_to_pezkuwichain() {
assert_expected_events!(
AssetHubPezkuwichain,
vec![
// TYR is withdrawn from AHW's SA on AHR
// HEZ is withdrawn from AHW's SA on AHR
RuntimeEvent::Balances(
pezpallet_balances::Event::Burned { who, amount }
) => {
who: *who == sov_ahw_on_ahr,
amount: *amount == TOKEN_AMOUNT,
},
// TYRs deposited to beneficiary
// HEZ deposited to beneficiary
RuntimeEvent::Balances(pezpallet_balances::Event::Minted { who, .. }) => {
who: *who == AssetHubPezkuwichainReceiver::get(),
},
@@ -185,7 +185,7 @@ fn send_roc_from_asset_hub_pezkuwichain_to_ethereum() {
AssetHubPezkuwichain::force_xcm_version(asset_hub_zagros_global_location(), XCM_VERSION);
BridgeHubPezkuwichain::force_xcm_version(bridge_hub_zagros_location(), XCM_VERSION);
// send TYRs, use them for fees
// send HEZ, use them for fees
let local_fee_asset: Asset = (roc_at_asset_hub_pezkuwichain.clone(), roc_fee_amount).into();
let remote_fee_on_zagros: Asset =
(roc_at_asset_hub_pezkuwichain.clone(), roc_fee_amount).into();
@@ -187,7 +187,14 @@ pub mod pezpallet {
pub type IdentityHashes<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, H256>;
/// Reverse mapping: identity hash -> account ID (uniqueness enforcement)
/// Ensures no two accounts can register with the same identity hash
/// Ensures no two accounts can register with the same identity hash.
///
/// IMPORTANT: This map is populated at `apply_for_citizenship` time (not just
/// at `confirm_citizenship` time) so that the hash is *reserved* for the whole
/// lifetime of an in-flight application. This prevents two different accounts
/// from concurrently applying with the same identity_hash (Sybil resistance) -
/// without this early reservation, two pending applications could both later
/// confirm and silently overwrite each other's reverse mapping.
#[pezpallet::storage]
#[pezpallet::getter(fn identity_hash_owner)]
pub type IdentityHashToAccount<T: Config> = StorageMap<_, Blake2_128Concat, H256, T::AccountId>;
@@ -353,6 +360,12 @@ pub mod pezpallet {
let deposit = T::KycApplicationDeposit::get();
T::Currency::reserve(&applicant, deposit)?;
// Reserve the identity hash immediately (not just at confirm time).
// This closes the Sybil window where two different accounts could
// both apply with the same identity_hash before either confirms -
// see the doc comment on IdentityHashToAccount for details.
IdentityHashToAccount::<T>::insert(identity_hash, &applicant);
// Store application (only hash, no personal data)
let application =
CitizenshipApplication { identity_hash, referrer: actual_referrer.clone() };
@@ -398,6 +411,16 @@ pub mod pezpallet {
// Only the referrer can approve
ensure!(application.referrer == caller, Error::<T>::NotTheReferrer);
// Re-check the referrer's *current* KYC status. The referrer may have
// been an Approved citizen when the application was created, but could
// have been revoked by governance since then (e.g. for running a
// referral-selling scheme). A revoked citizen must not retain the
// ability to vouch new citizens into the system.
ensure!(
KycStatuses::<T>::get(&caller) == KycLevel::Approved,
Error::<T>::ReferrerNotCitizen
);
// Update status to ReferrerApproved
KycStatuses::<T>::insert(&applicant, KycLevel::ReferrerApproved);
@@ -438,7 +461,10 @@ pub mod pezpallet {
// Store identity hash permanently (for proof of citizenship)
IdentityHashes::<T>::insert(&applicant, application.identity_hash);
// Store reverse mapping for uniqueness enforcement
// Reverse mapping was already reserved for this applicant at apply time
// (see apply_for_citizenship); re-affirm it here for good measure. This
// is a no-op in the normal path since no other account can hold the
// same identity_hash (the apply-time reservation guarantees that).
IdentityHashToAccount::<T>::insert(application.identity_hash, &applicant);
// Store referrer permanently (for direct responsibility tracking)
@@ -532,8 +558,11 @@ pub mod pezpallet {
Error::<T>::CannotCancelInCurrentState
);
// Remove application
Applications::<T>::remove(&applicant);
// Remove application and release the reserved identity hash so it can
// be used again (by this account or another) in a future application.
let application =
Applications::<T>::take(&applicant).ok_or(Error::<T>::ApplicationNotFound)?;
IdentityHashToAccount::<T>::remove(application.identity_hash);
// Reset status
KycStatuses::<T>::insert(&applicant, KycLevel::NotStarted);
@@ -8,7 +8,7 @@ use pezframe_support::{assert_noop, assert_ok, traits::Currency};
use pezsp_core::H256;
use pezsp_runtime::DispatchError;
// Kolay erişim için paletimize bir takma ad veriyoruz.
// We give our pallet an alias for easy access.
type IdentityKycPallet = crate::Pezpallet<Test>;
// ============================================================================
@@ -142,21 +142,21 @@ where
)
}
}
// --- Dış Dünya İçin Arayüzler (Traits) ---
// --- Interfaces for the Outside World (Traits) ---
/// Bir hesabın KYC durumunu sorgulamak için arayüz.
/// Interface for querying the KYC status of an account.
pub trait KycStatus<AccountId> {
fn get_kyc_status(who: &AccountId) -> KycLevel;
}
/// Bir hesabın kimlik bilgilerini sorgulamak için arayüz.
/// Interface for querying the identity information of an account.
pub trait IdentityInfoProvider<AccountId, MaxStringLength: Get<u32>> {
fn get_identity_info(who: &AccountId) -> Option<IdentityInfo<MaxStringLength>>;
}
/// KYC onaylandığında tetiklenecek eylemleri tanımlayan arayüz.
/// Bu trait identity-kyc palletinde tanımlanır ve diğer palletler (örn. referral)
/// tarafından implement edilir, böylece circular dependency oluşmaz.
/// Interface defining the actions to be triggered when KYC is approved.
/// This trait is defined in the identity-kyc pallet and is implemented by other
/// pallets (e.g. referral), so that no circular dependency is created.
///
/// UPDATED (Gemini suggestion): Now includes referrer parameter to avoid
/// data loss when identity-kyc and referral have separate storage.
@@ -172,9 +172,9 @@ impl<AccountId> OnKycApproved<AccountId> for () {
fn on_kyc_approved(_who: &AccountId, _referrer: &AccountId) {}
}
/// Vatandaşlık NFT'si mintlemek için arayüz.
/// Bu trait identity-kyc palletinde tanımlanır ve tiki pezpallet tarafından
/// implement edilir, böylece circular dependency oluşmaz.
/// Interface for minting a citizenship NFT.
/// This trait is defined in the identity-kyc pallet and is implemented by the tiki
/// pezpallet, so that no circular dependency is created.
pub trait CitizenNftProvider<AccountId> {
fn mint_citizen_nft(who: &AccountId) -> pezsp_runtime::DispatchResult;
@@ -179,6 +179,36 @@ pub mod pezpallet {
#[pezpallet::storage]
pub type CleanupCursor<T: Config> = StorageValue<_, (u32, BoundedVec<u8, ConstU32<256>>)>;
/// FIFO queue of era indices awaiting cleanup, oldest first.
///
/// `on_initialize` enqueues the newly-expired era on every rotation instead of
/// clobbering `CleanupCursor` directly. `on_idle` only pulls the next era off this
/// queue once the current `CleanupCursor` entry (if any) has fully finished, so a
/// slow/congested era's cleanup is never silently dropped by a later rotation.
///
/// Bounded to `MaxPendingCleanupEras` eras of backlog. If cleanup falls behind by
/// more than that many eras (i.e. `on_idle` gets essentially no weight for that many
/// consecutive era lengths), the oldest still-pending era is dropped to make room for
/// the newest one -- an explicit, generous, and documented bound rather than the
/// previous single-slot behaviour where any one stalled era was silently orphaned.
#[pezpallet::storage]
pub type PendingCleanupEras<T: Config> =
StorageValue<_, BoundedVec<u32, ConstU32<64>>, ValueQuery>;
/// Per-account encryption-key registration counter for the current era.
/// Reset (via era-prefix cleanup) when the era rotates. Used for rate limiting
/// `register_encryption_key`, mirroring `SendCount` for `send_message`.
#[pezpallet::storage]
pub type KeyRegistrationCount<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
u32, // era_index
Blake2_128Concat,
T::AccountId, // who
u32, // count
ValueQuery,
>;
// ============= EVENTS =============
#[pezpallet::event]
@@ -227,7 +257,8 @@ pub mod pezpallet {
#[pezpallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
/// Check for era rotation at the start of each block.
/// Cost: 2 reads (CurrentEra + EraStartBlock), 0-2 writes on rotation.
/// Cost: 2 reads (CurrentEra + EraStartBlock), plus on rotation an extra
/// read+write of PendingCleanupEras to enqueue the newly-expired era.
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
let era_length = T::EraLength::get();
let era_start = EraStartBlock::<T>::get();
@@ -241,14 +272,24 @@ pub mod pezpallet {
Self::deposit_event(Event::EraRotated { old_era, new_era });
// Schedule cleanup: store the expired era index for on_idle
// Schedule cleanup: enqueue the expired era index for on_idle instead of
// overwriting CleanupCursor directly, so an in-progress cleanup (or a
// backlog of not-yet-started ones) is never dropped by this rotation.
if old_era > 0 {
// Clean era before the one that just ended (2 eras of grace)
let cleanup_era = old_era.saturating_sub(1);
CleanupCursor::<T>::put((cleanup_era, BoundedVec::default()));
let mut pending = PendingCleanupEras::<T>::get();
if pending.try_push(cleanup_era).is_err() {
// Backlog is at capacity (MaxPendingCleanupEras eras of unresolved
// cleanup). Drop the oldest pending era to make room for the one
// that just expired, rather than silently dropping the newest.
pending.remove(0);
let _ = pending.try_push(cleanup_era);
}
PendingCleanupEras::<T>::put(pending);
}
T::DbWeight::get().reads_writes(2, 3)
T::DbWeight::get().reads_writes(3, 4)
} else {
T::DbWeight::get().reads(2)
}
@@ -265,6 +306,20 @@ pub mod pezpallet {
return meter.consumed();
}
// If no cleanup is currently in progress, pull the next oldest era off the
// pending queue so a backlog left behind by stalled/congested blocks is
// eventually worked through instead of being silently forgotten.
if CleanupCursor::<T>::get().is_none() {
let _ = meter.try_consume(T::DbWeight::get().reads(1));
let mut pending = PendingCleanupEras::<T>::get();
if !pending.is_empty() {
let next_era = pending.remove(0);
PendingCleanupEras::<T>::put(pending);
CleanupCursor::<T>::put((next_era, BoundedVec::default()));
let _ = meter.try_consume(T::DbWeight::get().writes(2));
}
}
if let Some((cleanup_era, cursor)) = CleanupCursor::<T>::get() {
// Consume weight for reading the cursor
let _ = meter.try_consume(T::DbWeight::get().reads(1));
@@ -278,11 +333,15 @@ pub mod pezpallet {
let writes_weight = T::DbWeight::get().writes(result.unique as u64);
let _ = meter.try_consume(writes_weight);
// Also clean SendCount for this era
// Also clean SendCount and KeyRegistrationCount for this era
let send_result = SendCount::<T>::clear_prefix(cleanup_era, 50, None);
let send_writes = T::DbWeight::get().writes(send_result.unique as u64);
let _ = meter.try_consume(send_writes);
let key_reg_result = KeyRegistrationCount::<T>::clear_prefix(cleanup_era, 50, None);
let key_reg_writes = T::DbWeight::get().writes(key_reg_result.unique as u64);
let _ = meter.try_consume(key_reg_writes);
match result.maybe_cursor {
Some(new_cursor) => {
// More items remain, save cursor for next block
@@ -340,8 +399,16 @@ pub mod pezpallet {
Error::<T>::InsufficientTrustScore
);
// Rate limit: this call is fee-free for citizens, so it needs the same
// per-era counter protection as `send_message` to stay consistent with the
// pallet's spam-resistance design. Reuses `MaxMessagesPerEra` as the cap.
let current_era = CurrentEra::<T>::get();
let reg_count = KeyRegistrationCount::<T>::get(current_era, &who);
ensure!(reg_count < T::MaxMessagesPerEra::get(), Error::<T>::RateLimitExceeded);
// Store the encryption key
EncryptionKeys::<T>::insert(&who, public_key);
KeyRegistrationCount::<T>::insert(current_era, &who, reg_count.saturating_add(1));
Self::deposit_event(Event::EncryptionKeyRegistered { who });
Ok(())
@@ -192,6 +192,13 @@ pub mod pezpallet {
#[pezpallet::getter(fn next_course_id)]
pub type NextCourseId<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Number of students currently enrolled in a given course.
/// Enforces the `MaxStudentsPerCourse` bound in `enroll()`.
#[pezpallet::storage]
#[pezpallet::getter(fn course_enrollment_count)]
pub type CourseEnrollmentCount<T: Config> =
StorageMap<_, Blake2_128Concat, u32, u32, ValueQuery>;
#[pezpallet::storage]
#[pezpallet::getter(fn enrollments)]
pub type Enrollments<T: Config> =
@@ -232,6 +239,8 @@ pub mod pezpallet {
CourseIdOverflow,
/// Points exceed the maximum allowed per course
PointsExceedMax,
/// Course has reached its maximum number of enrolled students
TooManyStudents,
}
#[pezpallet::call]
@@ -278,6 +287,10 @@ pub mod pezpallet {
Error::<T>::AlreadyEnrolled
);
// Enforce the per-course enrollment cap.
let enrolled_count = CourseEnrollmentCount::<T>::get(course_id);
ensure!(enrolled_count < T::MaxStudentsPerCourse::get(), Error::<T>::TooManyStudents);
let enrollment = Enrollment {
student: student.clone(),
course_id,
@@ -290,6 +303,7 @@ pub mod pezpallet {
StudentCourses::<T>::try_mutate(&student, |courses| {
courses.try_push(course_id).map_err(|_| Error::<T>::TooManyCourses)
})?;
CourseEnrollmentCount::<T>::insert(course_id, enrolled_count.saturating_add(1));
Self::deposit_event(Event::StudentEnrolled { student, course_id });
Ok(())
@@ -15,13 +15,13 @@ use pezsp_runtime::{
BuildStorage,
};
// Temel tipleri tanımlıyoruz.
// Define the basic types.
pub type AccountId = u64;
pub type Balance = u128;
pub type BlockNumber = u64;
pub type Block = pezframe_system::mocking::MockBlock<Test>;
// Test runtime'ımızı kuruyoruz.
// Set up our test runtime.
construct_runtime!(
pub enum Test
{
@@ -32,7 +32,7 @@ construct_runtime!(
}
);
// pezframe_system için implementasyon.
// Implementation for pezframe_system.
impl pezframe_system::Config for Test {
type BaseCallFilter = Everything;
type BlockWeights = ();
@@ -66,7 +66,7 @@ impl pezframe_system::Config for Test {
type PostTransactions = ();
}
// pezpallet_balances için implementasyon.
// Implementation for pezpallet_balances.
impl pezpallet_balances::Config for Test {
type Balance = Balance;
type DustRemoval = ();
@@ -93,23 +93,23 @@ parameter_types! {
pub const MaxPointsPerCourse: u32 = 1000; // Max points per course completion
}
// --- KESİN ÇÖZÜM BURADA BAŞLIYOR ---
// --- THE DEFINITIVE SOLUTION STARTS HERE ---
// AdminOrigin'i test etmek için kendi özel yetki sağlayıcımızı oluşturuyoruz.
// We create our own custom authority provider to test AdminOrigin.
use pezframe_system::EnsureSignedBy;
// Bu struct, derleyicinin talep ettiği `SortedMembers` trait'ini manuel olarak uygular.
// Bu, harici ve versiyona bağımlı araçlara olan ihtiyacı ortadan kaldırır.
// This struct manually implements the `SortedMembers` trait that the compiler requires.
// This eliminates the need for external, version-dependent tools.
pub struct TestAdminProvider;
impl SortedMembers<AccountId> for TestAdminProvider {
fn sorted_members() -> Vec<AccountId> {
// Test için admin olarak sadece 0 ID'li hesabı yetkili kılıyoruz.
// For tests, we authorize only the account with ID 0 as admin.
vec![0]
}
}
impl pezpallet_perwerde::Config for Test {
// AdminOrigin'i, kendi yazdığımız ve sadece 0'ı admin kabul eden sağlayıcıya bağlıyoruz.
// We bind AdminOrigin to our own provider, which accepts only 0 as admin.
type AdminOrigin = EnsureSignedBy<TestAdminProvider, AccountId>;
type WeightInfo = ();
type MaxCourseNameLength = MaxCourseNameLength;
@@ -121,7 +121,7 @@ impl pezpallet_perwerde::Config for Test {
type TrustScoreUpdater = ();
}
// Council Paletinin Mock Kurulumu (construct_runtime'da gerekli olduğu için kalıyor)
// Mock setup for the Council pallet (kept because it is required in construct_runtime)
use pezpallet_collective::Instance1;
parameter_types! {
pub const CouncilMotionDuration: BlockNumber = 5 * 60; // 5 minutes
@@ -147,8 +147,8 @@ impl pezpallet_collective::Config<Instance1> for Test {
pub fn new_test_ext() -> pezsp_io::TestExternalities {
let t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
// `pezpallet-collective`'in genesis'ini de kurmamıza gerek kalmadı çünkü artık testimiz ona
// bağlı değil.
// We no longer need to set up the genesis of `pezpallet-collective` because our test is no
// longer dependent on it.
let mut ext = pezsp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
@@ -17,10 +17,10 @@ fn create_bounded_vec<L: Get<u32>>(s: &[u8]) -> BoundedVec<u8, L> {
#[test]
fn create_course_works() {
new_test_ext().execute_with(|| {
// Admin olarak mock.rs'te TestAdminProvider içinde tanımladığımız hesabı kullanıyoruz.
// As admin, we use the account defined in TestAdminProvider in mock.rs.
let admin_account_id = 0;
// Eylem: Yetkili admin ile kurs oluştur.
// Action: create a course with the authorized admin.
assert_ok!(PerwerdePallet::create_course(
RuntimeOrigin::signed(admin_account_id),
create_bounded_vec(b"Blockchain 101"),
@@ -28,7 +28,7 @@ fn create_course_works() {
create_bounded_vec(b"http://example.com")
));
// Doğrulama
// Verification
assert!(crate::Courses::<Test>::contains_key(0));
let course = crate::Courses::<Test>::get(0).unwrap();
assert_eq!(course.owner, admin_account_id);
@@ -41,7 +41,7 @@ fn create_course_works() {
#[test]
fn create_course_fails_for_non_admin() {
new_test_ext().execute_with(|| {
// Admin (0) dışındaki bir hesap (2) kurs oluşturamaz.
// An account (2) other than the admin (0) cannot create a course.
let non_admin = 2;
assert_noop!(
PerwerdePallet::create_course(
@@ -164,7 +164,7 @@ pub mod pezpallet {
/// Authority check for root origin
type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// NFT Collection ID ve Item ID types - must match pezpallet_nfts::Config
/// NFT Collection ID and Item ID types - must match pezpallet_nfts::Config
type CollectionId: Member + Parameter + MaxEncodedLen + Copy + From<u32> + Into<u32>;
type ItemId: Member + Parameter + MaxEncodedLen + Copy + From<u32> + Into<u32>;
}
@@ -481,7 +481,7 @@ pub mod pezpallet {
ensure!(total_reward_pool > Zero::zero(), Error::<T>::InsufficientIncentivePot);
// Parliamentary rewards distribute et (10%)
// Distribute parliamentary rewards (10%)
Self::distribute_parliamentary_rewards(current_epoch, total_reward_pool)?;
// Remaining 90% for trust score rewards
@@ -507,7 +507,7 @@ pub mod pezpallet {
Zero::zero()
};
// Talep son tarihini belirle (1 hafta sonra)
// Set the claim deadline (1 week later)
let claim_deadline = current_block.saturating_add(CLAIM_PERIOD_BLOCKS.into());
// Save reward pool information
@@ -170,7 +170,7 @@ fn finalize_epoch_calculates_rewards_correctly() {
System::block_number() + crate::CLAIM_PERIOD_BLOCKS as u64
);
// FIX: Event'te trust_score_pool (90%) bekle
// FIX: Expect trust_score_pool (90%) in the event
System::assert_has_event(
Event::EpochRewardPoolCalculated {
epoch_index: 0,
@@ -188,7 +188,7 @@ fn finalize_epoch_calculates_rewards_correctly() {
}
.into(),
);
// FIX: Finalize sonrası ClaimPeriod
// FIX: ClaimPeriod after finalize
assert_eq!(PezRewards::epoch_status(0), EpochState::ClaimPeriod);
assert_eq!(PezRewards::epoch_status(1), EpochState::Open);
});
@@ -233,7 +233,7 @@ fn finalize_epoch_no_participants() {
#[test]
fn finalize_epoch_zero_trust_score_participant() {
new_test_ext().execute_with(|| {
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(dave()))); // Skor 0
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(dave()))); // Score 0
// FIX: Zero scores are now being recorded
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &dave()), Some(0));
@@ -425,7 +425,7 @@ fn claim_reward_fails_for_wrong_epoch() {
Error::<Test>::ClaimPeriodExpired
);
// Epoch 999 yok -> ClaimPeriodExpired
// Epoch 999 does not exist -> ClaimPeriodExpired
assert_noop!(
PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 999),
Error::<Test>::ClaimPeriodExpired
@@ -440,8 +440,8 @@ fn claim_reward_fails_for_wrong_epoch() {
#[test]
fn close_epoch_works_after_claim_period() {
new_test_ext().execute_with(|| {
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // Claim etmeyecek
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(bob()))); // Claim edecek
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // Will not claim
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(bob()))); // Will claim
let incentive_pot = PezRewards::incentive_pot_account_id();
let _pot_balance_before_finalize = pez_balance(&incentive_pot);
@@ -453,7 +453,7 @@ fn close_epoch_works_after_claim_period() {
let _alice_reward = reward_pool.reward_per_trust_point * 100;
let bob_reward = reward_pool.reward_per_trust_point * 50;
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(bob()), 0)); // Bob claim etti
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(bob()), 0)); // Bob claimed
let clawback_recipient = ClawbackRecipient::get();
let balance_before = pez_balance(&clawback_recipient);
@@ -702,13 +702,13 @@ fn multiple_epochs_work_correctly() {
fund_incentive_pot(1_000_000_000_000_000);
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100 (Epoch 1 için)
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100 (for Epoch 1)
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root())); // Epoch 1'i finalize et
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root())); // Finalize Epoch 1
let reward_pool_1 = PezRewards::get_epoch_reward_pool(1).unwrap(); // Epoch 1 havuzu
let reward_pool_1 = PezRewards::get_epoch_reward_pool(1).unwrap(); // Epoch 1 pool
let reward1_1 = reward_pool_1.reward_per_trust_point * 100;
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 1)); // Epoch 1'den claim et
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 1)); // Claim from Epoch 1
// Check balances
let alice_balance = pez_balance(&alice());
@@ -10,7 +10,7 @@ use crate::Pezpallet as PezTreasury;
use pezframe_benchmarking::v2::*;
use pezframe_support::traits::{
fungibles::{Inspect, Mutate},
Get, // HATA GİDERİLDİ: .get() fonksiyonu için bu trait eklendi
Get, // BUG FIXED: this trait was added for the .get() function
};
use pezframe_system::RawOrigin;
use pezsp_runtime::traits::{Saturating, Zero};
@@ -60,7 +60,7 @@ mod benchmarks {
crate::HalvingInfo::<T>::kill();
crate::NextReleaseMonth::<T>::kill();
crate::GenesisDistributionDone::<T>::kill();
// Deprecated `remove_all` yerine `clear` kullanılıyor.
// Using `clear` instead of the deprecated `remove_all`.
let _ = crate::MonthlyReleases::<T>::clear(u32::MAX, None);
// First do genesis distribution to properly fund the treasury
@@ -4,7 +4,7 @@
// SPDX-License-Identifier: Apache-2.0
// pezkuwi/pallets/pez-treasury/src/mock.rs
// VERSION 3: AccountId tipi H256 yapıldı (u64 yerine)
// VERSION 3: AccountId type changed to H256 (instead of u64)
use crate::{self as pezpallet_pez_treasury, weights};
use pezframe_support::{
@@ -44,7 +44,7 @@ impl pezframe_system::Config for Test {
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = H256; // V3: u64 -> H256 değişti
type AccountId = H256; // V3: changed u64 -> H256
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
@@ -123,7 +123,7 @@ impl pezpallet_assets::Config for Test {
type BenchmarkHelper = ();
}
// CRITICAL: Bu üç PalletId FARKLI olmak ZORUNDA
// CRITICAL: These three PalletIds MUST be DIFFERENT
parameter_types! {
pub const PezTreasuryPalletId: PalletId = PalletId(*b"py/pztrs");
pub const PezIncentivePotId: PalletId = PalletId(*b"py/pzinc");
@@ -131,7 +131,7 @@ parameter_types! {
pub const PezAssetId: u32 = 1;
}
// V3: Test accounts - H256 formatında
// V3: Test accounts - in H256 format
use pezsp_runtime::traits::AccountIdConversion;
pub fn alice() -> H256 {
@@ -227,7 +227,7 @@ pub fn assert_pez_balance(account: H256, expected: u128) {
);
}
// V3: Helper fonksiyonlar - H256 dönüyor
// V3: Helper functions - return H256
#[allow(dead_code)]
pub fn treasury_account() -> H256 {
PezTreasuryPalletId::get().into_account_truncating()
@@ -239,7 +239,7 @@ fn release_monthly_funds_splits_correctly() {
// 75% to incentive, 25% to government
assert_eq!(incentive_balance, monthly_amount * 75 / 100);
// lib.rs'deki mantıkla aynı olmalı (saturating_sub)
// Must match the logic in lib.rs (saturating_sub)
let incentive_amount_calculated = monthly_amount * 75 / 100;
assert_eq!(government_balance, monthly_amount - incentive_amount_calculated);
@@ -410,8 +410,8 @@ fn release_exactly_at_boundary_block_fails() {
assert_ok!(PezTreasury::do_genesis_distribution());
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
// Tam 432_000. blok (start_block=1 olduğu için) 431_999 blok geçti demektir.
// Bu, 1 tam ay (432_000 blok) değildir.
// Exactly block 432_000 (since start_block=1) means 431_999 blocks have passed.
// This is not a full month (432_000 blocks).
run_to_block(432_000);
assert_noop!(
PezTreasury::release_monthly_funds(RuntimeOrigin::root()),
@@ -555,7 +555,7 @@ fn pot_accounts_are_different() {
println!("Government: {government:?}");
println!("================================\n");
// Tüm üçü farklı olmalı
// All three must be different
assert_ne!(treasury, incentive, "Treasury and Incentive must be different");
assert_ne!(treasury, government, "Treasury and Government must be different");
assert_ne!(incentive, government, "Incentive and Government must be different");
@@ -674,7 +674,7 @@ fn full_halving_cycle_test() {
if month < 47 {
cumulative_released += initial_monthly;
} else {
// 48. sürümde (index 47) halving tetiklenir ve yarı tutar kullanılır
// On the 48th release (index 47) halving is triggered and the halved amount is used
cumulative_released += initial_monthly / 2;
}
}
@@ -690,7 +690,7 @@ fn full_halving_cycle_test() {
if month < 95 {
cumulative_released += initial_monthly / 2;
} else {
// 96. sürümde (index 95) ikinci halving tetiklenir
// On the 96th release (index 95) the second halving is triggered
cumulative_released += initial_monthly / 4;
}
}
@@ -944,7 +944,7 @@ fn first_period_total_is_half_of_treasury() {
let expected_first_period = treasury_allocation / 2;
let diff = expected_first_period.saturating_sub(first_period_total);
// Kalanların toplamı 48'den az olmalı (her ay en fazla 1 birim kalan)
// The sum of the remainders must be less than 48 (at most 1 unit of remainder per month)
assert!(diff < 48, "Rounding error too large: {diff}");
});
}
@@ -966,7 +966,7 @@ fn geometric_series_sum_validates() {
// After infinite halvings, total distributed = treasury_allocation
// first_period_total * 2 = treasury_allocation
let diff = treasury_allocation.saturating_sub(first_period_total * 2);
// Kalanların toplamı (2 ile çarpılmış) 96'dan az olmalı
// The sum of the remainders (multiplied by 2) must be less than 96
assert!(diff < 96, "Rounding error too large: {diff}");
});
}
@@ -261,9 +261,18 @@ mod benchmarks {
// Advance blocks past presale end
pezframe_system::Pezpallet::<T>::set_block_number(2000u32.into());
// Finalize presale (requires root)
// Finalize presale (requires root) -> Successful (not yet Finalized).
let _ = Presale::<T>::finalize_presale(RawOrigin::Root.into(), presale_id);
// Distribute to the sole contributor -> Finalized, and records the
// immediate-release portion in VestingClaimed that claim_vested builds on.
let _ = Presale::<T>::batch_distribute(
RawOrigin::Signed(caller.clone()).into(),
presale_id,
0,
1,
);
// Advance past cliff period
pezframe_system::Pezpallet::<T>::set_block_number(3000u32.into());
@@ -345,10 +354,11 @@ mod benchmarks {
#[extrinsic_call]
finalize_presale(RawOrigin::Root, presale_id);
// Verify presale was finalized
// finalize_presale only ever transitions to Successful (soft cap reached;
// Finalized is reached later via batch_distribute) or Failed (soft cap missed).
let presale = crate::Presales::<T>::get(presale_id).unwrap();
assert!(
presale.status == PresaleStatus::Finalized || presale.status == PresaleStatus::Failed
presale.status == PresaleStatus::Successful || presale.status == PresaleStatus::Failed
);
}
@@ -430,5 +440,49 @@ mod benchmarks {
}
}
/// Benchmark worst case: owner withdraws raised payment-asset funds from a
/// Finalized presale (reached only after `finalize_presale` + a
/// `batch_distribute` covering every contributor).
#[benchmark]
fn withdraw_funds() {
let caller: T::AccountId = whitelisted_caller();
// Get next presale ID before creating
let presale_id = NextPresaleId::<T>::get();
let presale_treasury = Presale::<T>::presale_account_id(presale_id);
// Setup assets
let (payment_asset, reward_asset) = setup_benchmark_assets::<T>(&caller, &presale_treasury);
// Create presale (will get the presale_id we calculated)
let _ = create_test_presale::<T>(&caller, payment_asset, reward_asset, false, false);
// Contribute enough to clear the soft cap.
let amount: u128 = 1_000_000u128;
let _ =
Presale::<T>::contribute(RawOrigin::Signed(caller.clone()).into(), presale_id, amount);
// Advance blocks past presale end, then finalize (-> Successful).
pezframe_system::Pezpallet::<T>::set_block_number(2000u32.into());
let _ = Presale::<T>::finalize_presale(RawOrigin::Root.into(), presale_id);
// Distribute to every contributor in one batch (-> Finalized), leaving
// the raised payment-asset balance sitting in the treasury sub-account
// for withdraw_funds to move out.
let _ = Presale::<T>::batch_distribute(
RawOrigin::Signed(caller.clone()).into(),
presale_id,
0,
1,
);
let presale = crate::Presales::<T>::get(presale_id).unwrap();
assert_eq!(presale.status, PresaleStatus::Finalized);
#[extrinsic_call]
withdraw_funds(RawOrigin::Signed(caller), presale_id);
// Verify the withdrawal was recorded (guards double-withdrawal).
assert!(crate::FundsWithdrawn::<T>::get(presale_id));
}
impl_benchmark_test_suite!(Presale, crate::mock::new_test_ext(), crate::mock::Test);
}
@@ -56,7 +56,7 @@ pub mod pezpallet {
BoundedVec, PalletId,
};
use pezframe_system::pezpallet_prelude::*;
use pezsp_runtime::traits::{AtLeast32BitUnsigned, Saturating};
use pezsp_runtime::traits::{AtLeast32BitUnsigned, CheckedAdd, Saturating};
pub type PresaleId = u32;
@@ -435,6 +435,13 @@ pub mod pezpallet {
#[pezpallet::getter(fn successful_presales)]
pub type SuccessfulPresales<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Whether the raised payment-asset funds have been withdrawn by the owner for a
/// Successful/Finalized presale. Guards against double-withdrawal.
#[pezpallet::storage]
#[pezpallet::getter(fn funds_withdrawn)]
pub type FundsWithdrawn<T: Config> =
StorageMap<_, Blake2_128Concat, PresaleId, bool, ValueQuery>;
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
@@ -479,6 +486,8 @@ pub mod pezpallet {
distributed_count: u32,
total_distributed: u128,
},
/// Raised funds withdrawn by presale owner [presale_id, owner, amount]
FundsWithdrawn { presale_id: PresaleId, owner: T::AccountId, amount: u128 },
}
#[pezpallet::error]
@@ -510,6 +519,14 @@ pub mod pezpallet {
PresaleNotSuccessful,
SoftCapNotReached,
InvalidSoftCap,
/// `duration`, `grace_period_blocks`, `cliff_blocks`, or `vesting_duration_blocks`
/// is large enough that computing the presale's end/vesting block numbers would
/// overflow the runtime's BlockNumber type.
InvalidDuration,
/// Withdrawal is only allowed once, on a Successful or Finalized presale.
WithdrawalNotAllowed,
/// The raised funds for this presale have already been withdrawn.
FundsAlreadyWithdrawn,
}
#[pezpallet::call]
@@ -547,6 +564,22 @@ pub mod pezpallet {
let presale_id = NextPresaleId::<T>::get();
let start_block = <pezframe_system::Pezpallet<T>>::block_number();
// Reject durations/vesting parameters that would make downstream block-number
// arithmetic (start_block + duration [+ cliff_blocks [+ vesting_duration_blocks]])
// overflow BlockNumberFor<T>. Without this, a permissionless caller (any signed
// account, since CreatePresaleOrigin = EnsureSigned) could craft a presale that
// traps every later contribute()/refund()/finalize_presale()/claim_vested() call.
let end_block =
start_block.checked_add(&params.duration).ok_or(Error::<T>::InvalidDuration)?;
if let Some(ref vesting) = params.vesting {
let vesting_start = end_block
.checked_add(&vesting.cliff_blocks)
.ok_or(Error::<T>::InvalidDuration)?;
vesting_start
.checked_add(&vesting.vesting_duration_blocks)
.ok_or(Error::<T>::InvalidDuration)?;
}
// Start with empty bonus tiers - can be added later
let bounded_bonus_tiers = BoundedVec::<BonusTier, T::MaxBonusTiers>::default();
@@ -600,7 +633,10 @@ pub mod pezpallet {
ensure!(amount > 0, Error::<T>::ZeroContribution);
let current_block = <pezframe_system::Pezpallet<T>>::block_number();
let end_block = presale.start_block + presale.duration;
let end_block = presale
.start_block
.checked_add(&presale.duration)
.ok_or(Error::<T>::ArithmeticOverflow)?;
ensure!(current_block < end_block, Error::<T>::PresaleEnded);
// Check whitelist
@@ -651,33 +687,42 @@ pub mod pezpallet {
// Distribute platform fee
Self::distribute_platform_fee(presale.payment_asset, &who, platform_fee)?;
// Track contribution with timestamp preservation
let contribution = if let Some(existing) = existing_contribution {
// Update existing contribution - preserve original timestamp
ContributionInfo {
// Ensure `who` occupies a slot in the Contributors list. A fully-refunded
// contributor is removed from this list by `refund()` (to free the slot for
// `MaxContributors`), so we must re-add them here rather than assuming presence
// is implied by having a (possibly stale/refunded) ContributionInfo entry.
Contributors::<T>::try_mutate(presale_id, |contributors| -> DispatchResult {
if !contributors.contains(&who) {
contributors
.try_push(who.clone())
.map_err(|_| Error::<T>::TooManyContributors)?;
}
Ok(())
})?;
// Track contribution with timestamp preservation.
// Only accumulate onto an existing record if it still has a non-zero amount
// (i.e. it was never refunded, or a prior partial state). A fully-refunded
// contribution (amount == 0, refunded == true, set by `refund()`) starts fresh:
// this is what prevents a refund-then-recontribute cycle from inflating the
// recorded `amount` far beyond what is actually held in the treasury.
let contribution = match existing_contribution {
Some(existing) if existing.amount > 0 => ContributionInfo {
// Update existing contribution - preserve original timestamp
amount: existing.amount.saturating_add(amount),
contributed_at: existing.contributed_at, // ✅ Keep original timestamp
refunded: false,
refunded_at: None,
refund_fee_paid: 0,
}
} else {
// New contribution - add to contributors list
Contributors::<T>::try_mutate(presale_id, |contributors| -> DispatchResult {
contributors
.try_push(who.clone())
.map_err(|_| Error::<T>::TooManyContributors)?;
Ok(())
})?;
// Create new contribution with current timestamp
ContributionInfo {
},
_ => ContributionInfo {
// New contribution, or re-contribution after a full refund
amount,
contributed_at: current_block, // ✅ Set timestamp for first contribution only
contributed_at: current_block, // ✅ Fresh timestamp for grace-period purposes
refunded: false,
refunded_at: None,
refund_fee_paid: 0,
}
},
};
Contributions::<T>::insert(presale_id, &who, contribution);
@@ -705,7 +750,10 @@ pub mod pezpallet {
ensure!(presale.status == PresaleStatus::Active, Error::<T>::PresaleNotActive);
let current_block = <pezframe_system::Pezpallet<T>>::block_number();
let end_block = presale.start_block + presale.duration;
let end_block = presale
.start_block
.checked_add(&presale.duration)
.ok_or(Error::<T>::ArithmeticOverflow)?;
ensure!(current_block >= end_block, Error::<T>::PresaleNotEnded);
let total_raised = TotalRaised::<T>::get(presale_id);
@@ -754,7 +802,10 @@ pub mod pezpallet {
ensure!(presale.status == PresaleStatus::Active, Error::<T>::RefundNotAllowed);
let current_block = <pezframe_system::Pezpallet<T>>::block_number();
let end_block = presale.start_block + presale.duration;
let end_block = presale
.start_block
.checked_add(&presale.duration)
.ok_or(Error::<T>::ArithmeticOverflow)?;
ensure!(current_block < end_block, Error::<T>::RefundNotAllowed);
let mut contribution_info =
@@ -801,14 +852,30 @@ pub mod pezpallet {
Self::distribute_platform_fee(presale.payment_asset, &treasury, fee)?;
}
// Update contribution info (mark as refunded instead of removing)
// Decrement TotalRaised using the amount that was actually on record before we
// clear it below.
let refunded_amount = contribution_info.amount;
TotalRaised::<T>::mutate(presale_id, |r| *r = r.saturating_sub(refunded_amount));
// Update contribution info: mark as refunded AND zero the recorded amount.
// Zeroing `amount` is essential — otherwise a refund-then-recontribute cycle
// would let `contribute()`'s `existing.amount.saturating_add(amount)` keep
// accumulating a stale amount that is no longer backed by real treasury funds,
// inflating this wallet's share of reward-token distribution at other
// contributors' expense.
contribution_info.refunded = true;
contribution_info.refunded_at = Some(current_block);
contribution_info.refund_fee_paid = fee;
contribution_info.amount = 0;
Contributions::<T>::insert(presale_id, &who, contribution_info);
TotalRaised::<T>::mutate(presale_id, |r| {
*r = r.saturating_sub(contribution_info.amount)
// Free up this wallet's slot in the bounded Contributors list now that it has
// no active contribution. Without this, a Sybil attacker could contribute the
// minimum amount from many accounts and immediately refund, permanently
// consuming MaxContributors capacity at near-zero cost (only the refund fee is
// lost) and locking legitimate contributors out with `TooManyContributors`.
Contributors::<T>::mutate(presale_id, |contributors| {
contributors.retain(|acc| acc != &who);
});
Self::deposit_event(Event::Refunded { presale_id, who, amount: refund_amount, fee });
@@ -834,8 +901,13 @@ pub mod pezpallet {
ensure!(!contribution_info.refunded, Error::<T>::NoContribution);
let current_block = <pezframe_system::Pezpallet<T>>::block_number();
let end_block = presale.start_block + presale.duration;
let vesting_start = end_block + vesting.cliff_blocks;
let end_block = presale
.start_block
.checked_add(&presale.duration)
.ok_or(Error::<T>::ArithmeticOverflow)?;
let vesting_start = end_block
.checked_add(&vesting.cliff_blocks)
.ok_or(Error::<T>::ArithmeticOverflow)?;
ensure!(current_block >= vesting_start, Error::<T>::NothingToClaim);
@@ -853,7 +925,9 @@ pub mod pezpallet {
// Calculate vested amount
let already_claimed = VestingClaimed::<T>::get(presale_id, &who);
let vesting_end = vesting_start + vesting.vesting_duration_blocks;
let vesting_end = vesting_start
.checked_add(&vesting.vesting_duration_blocks)
.ok_or(Error::<T>::ArithmeticOverflow)?;
let claimable = if current_block >= vesting_end {
// All vested
@@ -983,6 +1057,15 @@ pub mod pezpallet {
let contributor = &contributors[i as usize];
if let Some(contribution_info) = Contributions::<T>::get(presale_id, contributor) {
// Skip contributors who already received their reward-token payout via
// batch_distribute() before this presale was cancelled (a Successful
// presale can sit in that status across many batch_distribute() calls).
// Without this check they would double-dip: keep the distributed reward
// tokens AND receive their payment-asset contribution back.
if VestingClaimed::<T>::contains_key(presale_id, contributor) {
continue;
}
if !contribution_info.refunded && contribution_info.amount > 0 {
// Calculate net amount in treasury (original - platform fee already deducted at contribution)
let platform_fee_at_contribution = contribution_info
@@ -1239,6 +1322,59 @@ pub mod pezpallet {
Ok(())
}
/// Withdraw the raised payment-asset funds from a Finalized presale's treasury
/// sub-account to the presale owner. Callable once per presale by the owner only.
#[pezpallet::call_index(10)]
#[pezpallet::weight(T::PresaleWeightInfo::withdraw_funds())]
pub fn withdraw_funds(origin: OriginFor<T>, presale_id: PresaleId) -> DispatchResult {
let who = ensure_signed(origin)?;
let presale = Presales::<T>::get(presale_id).ok_or(Error::<T>::PresaleNotFound)?;
ensure!(who == presale.owner, Error::<T>::NotPresaleOwner);
// Only allow withdrawal once the presale is Finalized (i.e. batch_distribute has
// paid out every contributor's reward tokens). Deliberately NOT allowed while
// still Successful: cancel_presale() can cancel a Successful presale, in which
// case refund_cancelled_presale() must be able to refund contributors out of the
// same treasury sub-account. Once Finalized, cancel_presale() can no longer
// target this presale (it only matches Active/Pending/Paused/Successful), so
// withdrawing here can never race with a contributor refund claim.
ensure!(presale.status == PresaleStatus::Finalized, Error::<T>::WithdrawalNotAllowed);
ensure!(!FundsWithdrawn::<T>::get(presale_id), Error::<T>::FundsAlreadyWithdrawn);
let treasury = Self::presale_account_id(presale_id);
let balance = T::Assets::reducible_balance(
presale.payment_asset,
&treasury,
Preservation::Expendable,
Fortitude::Polite,
);
let balance_u128: u128 = balance.into();
ensure!(balance_u128 > 0, Error::<T>::InsufficientBalance);
T::Assets::transfer(
presale.payment_asset,
&treasury,
&who,
balance,
Preservation::Expendable,
)?;
// Guard against double-withdrawal regardless of whether more funds later land
// in the treasury sub-account (e.g. dust from later batch operations).
FundsWithdrawn::<T>::insert(presale_id, true);
Self::deposit_event(Event::FundsWithdrawn {
presale_id,
owner: who,
amount: balance_u128,
});
Ok(())
}
}
impl<T: Config> Pezpallet<T> {
@@ -1,6 +1,6 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,23 +18,23 @@
//! Autogenerated weights for `pezpallet_presale`
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.1
//! DATE: 2026-07-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
//! HOSTNAME: `vmi3386610`, CPU: `AMD EPYC Processor (with IBPB)`
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
// Executed Command:
// ./target/release/frame-omni-bencher
// target/release/pezframe-omni-bencher
// v1
// benchmark
// pezpallet
// --runtime
// target/release/wbuild/asset-hub-pezkuwichain-runtime/asset_hub_pezkuwichain_runtime.compact.compressed.wasm
// --pallets
// --pallet
// pezpallet_presale
// -e
// all
// --extrinsic
// *
// --steps
// 50
// --repeat
@@ -64,6 +64,7 @@ pub trait WeightInfo {
fn refund_cancelled_presale() -> Weight;
fn finalize_presale(n: u32, ) -> Weight;
fn batch_refund_failed_presale(n: u32, ) -> Weight;
fn withdraw_funds() -> Weight;
}
/// Weights for `pezpallet_presale` using the Bizinikiwi node and recommended hardware.
@@ -77,8 +78,8 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
// Proof Size summary in bytes:
// Measured: `147`
// Estimated: `1489`
// Minimum execution time: 9_638_000 picoseconds.
Weight::from_parts(10_003_000, 1489)
// Minimum execution time: 35_647_000 picoseconds.
Weight::from_parts(40_765_000, 1489)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
@@ -88,8 +89,8 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
// Proof Size summary in bytes:
// Measured: `356`
// Estimated: `3717`
// Minimum execution time: 12_202_000 picoseconds.
Weight::from_parts(12_492_000, 3717)
// Minimum execution time: 35_606_000 picoseconds.
Weight::from_parts(47_778_000, 3717)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
@@ -101,8 +102,8 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
// Proof Size summary in bytes:
// Measured: `356`
// Estimated: `3717`
// Minimum execution time: 12_973_000 picoseconds.
Weight::from_parts(14_077_000, 3717)
// Minimum execution time: 27_901_000 picoseconds.
Weight::from_parts(39_263_000, 3717)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
@@ -128,10 +129,10 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Proof: `Presale::TotalPlatformFees` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn contribute() -> Weight {
// Proof Size summary in bytes:
// Measured: `1169`
// Measured: `1203`
// Estimated: `323487`
// Minimum execution time: 160_665_000 picoseconds.
Weight::from_parts(165_629_000, 323487)
// Minimum execution time: 464_453_000 picoseconds.
Weight::from_parts(495_981_000, 323487)
.saturating_add(T::DbWeight::get().reads(13_u64))
.saturating_add(T::DbWeight::get().writes(11_u64))
}
@@ -151,14 +152,16 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
/// Storage: `Presale::TotalRaised` (r:1 w:1)
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributors` (r:1 w:1)
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
fn refund() -> Weight {
// Proof Size summary in bytes:
// Measured: `1531`
// Estimated: `11426`
// Minimum execution time: 169_420_000 picoseconds.
Weight::from_parts(173_774_000, 11426)
.saturating_add(T::DbWeight::get().reads(11_u64))
.saturating_add(T::DbWeight::get().writes(10_u64))
// Measured: `1608`
// Estimated: `323487`
// Minimum execution time: 335_754_000 picoseconds.
Weight::from_parts(511_931_000, 323487)
.saturating_add(T::DbWeight::get().reads(12_u64))
.saturating_add(T::DbWeight::get().writes(11_u64))
}
/// Storage: `Presale::Presales` (r:1 w:0)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
@@ -176,10 +179,10 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
fn claim_vested() -> Weight {
// Proof Size summary in bytes:
// Measured: `1392`
// Measured: `1426`
// Estimated: `6208`
// Minimum execution time: 72_526_000 picoseconds.
Weight::from_parts(74_529_000, 6208)
// Minimum execution time: 173_863_000 picoseconds.
Weight::from_parts(221_982_000, 6208)
.saturating_add(T::DbWeight::get().reads(8_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
}
@@ -189,6 +192,8 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributions` (r:1 w:1)
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
/// Storage: `Presale::VestingClaimed` (r:1 w:0)
/// Proof: `Presale::VestingClaimed` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
/// Storage: `Assets::Asset` (r:1 w:1)
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Assets::Account` (r:2 w:2)
@@ -197,45 +202,31 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
fn refund_cancelled_presale() -> Weight {
// Proof Size summary in bytes:
// Measured: `1370`
// Measured: `1428`
// Estimated: `323487`
// Minimum execution time: 66_454_000 picoseconds.
Weight::from_parts(69_615_000, 323487)
.saturating_add(T::DbWeight::get().reads(7_u64))
// Minimum execution time: 162_231_000 picoseconds.
Weight::from_parts(227_132_000, 323487)
.saturating_add(T::DbWeight::get().reads(8_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
}
/// Storage: `Presale::Presales` (r:1 w:1)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
/// Storage: `Presale::TotalRaised` (r:1 w:0)
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributors` (r:1 w:0)
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributions` (r:100 w:0)
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
/// Storage: `Assets::Asset` (r:1 w:1)
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Assets::Account` (r:101 w:101)
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:101 w:101)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
/// Storage: `Presale::SuccessfulPresales` (r:1 w:1)
/// Proof: `Presale::SuccessfulPresales` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// The range of component `n` is `[1, 100]`.
fn finalize_presale(n: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `525`
// Estimated: `3717 + n * (3235 ±0)`
// Minimum execution time: 17_663_000 picoseconds.
Weight::from_parts(18_454_000, 3717)
// Standard Error: 463_975
.saturating_add(Weight::from_parts(2_728_899, 0).saturating_mul(n.into()))
// Estimated: `3717 + n * (15 ±0)`
// Minimum execution time: 45_775_000 picoseconds.
Weight::from_parts(79_721_748, 3717)
// Standard Error: 16_126
.saturating_add(Weight::from_parts(317_025, 0).saturating_mul(n.into()))
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
.saturating_add(Weight::from_parts(0, 3235).saturating_mul(n.into()))
.saturating_add(Weight::from_parts(0, 15).saturating_mul(n.into()))
}
/// Storage: `Presale::Presales` (r:1 w:0)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
@@ -252,18 +243,41 @@ impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// The range of component `n` is `[1, 100]`.
fn batch_refund_failed_presale(n: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `1244 + n * (198 ±0)`
// Measured: `1278 + n * (198 ±0)`
// Estimated: `323487 + n * (2609 ±0)`
// Minimum execution time: 70_612_000 picoseconds.
Weight::from_parts(17_304_947, 323487)
// Standard Error: 88_572
.saturating_add(Weight::from_parts(42_218_217, 0).saturating_mul(n.into()))
// Minimum execution time: 149_508_000 picoseconds.
Weight::from_parts(190_635_000, 323487)
// Standard Error: 335_699
.saturating_add(Weight::from_parts(127_279_841, 0).saturating_mul(n.into()))
.saturating_add(T::DbWeight::get().reads(5_u64))
.saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into())))
.saturating_add(T::DbWeight::get().writes(2_u64))
.saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into())))
.saturating_add(Weight::from_parts(0, 2609).saturating_mul(n.into()))
}
/// Storage: `Presale::Presales` (r:1 w:0)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
/// Storage: `Presale::FundsWithdrawn` (r:1 w:1)
/// Proof: `Presale::FundsWithdrawn` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
/// Storage: `Assets::Asset` (r:1 w:1)
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Assets::Account` (r:2 w:2)
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
fn withdraw_funds() -> Weight {
// Proof Size summary in bytes:
// Measured: `1366`
// Estimated: `6208`
// Minimum execution time: 159_568_000 picoseconds.
Weight::from_parts(253_502_000, 6208)
.saturating_add(T::DbWeight::get().reads(8_u64))
.saturating_add(T::DbWeight::get().writes(7_u64))
}
}
// For backwards compatibility and tests.
@@ -276,8 +290,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `147`
// Estimated: `1489`
// Minimum execution time: 9_638_000 picoseconds.
Weight::from_parts(10_003_000, 1489)
// Minimum execution time: 35_647_000 picoseconds.
Weight::from_parts(40_765_000, 1489)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
@@ -287,8 +301,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `356`
// Estimated: `3717`
// Minimum execution time: 12_202_000 picoseconds.
Weight::from_parts(12_492_000, 3717)
// Minimum execution time: 35_606_000 picoseconds.
Weight::from_parts(47_778_000, 3717)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
@@ -300,8 +314,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `356`
// Estimated: `3717`
// Minimum execution time: 12_973_000 picoseconds.
Weight::from_parts(14_077_000, 3717)
// Minimum execution time: 27_901_000 picoseconds.
Weight::from_parts(39_263_000, 3717)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
@@ -327,10 +341,10 @@ impl WeightInfo for () {
/// Proof: `Presale::TotalPlatformFees` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn contribute() -> Weight {
// Proof Size summary in bytes:
// Measured: `1169`
// Measured: `1203`
// Estimated: `323487`
// Minimum execution time: 160_665_000 picoseconds.
Weight::from_parts(165_629_000, 323487)
// Minimum execution time: 464_453_000 picoseconds.
Weight::from_parts(495_981_000, 323487)
.saturating_add(RocksDbWeight::get().reads(13_u64))
.saturating_add(RocksDbWeight::get().writes(11_u64))
}
@@ -350,14 +364,16 @@ impl WeightInfo for () {
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
/// Storage: `Presale::TotalRaised` (r:1 w:1)
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributors` (r:1 w:1)
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
fn refund() -> Weight {
// Proof Size summary in bytes:
// Measured: `1531`
// Estimated: `11426`
// Minimum execution time: 169_420_000 picoseconds.
Weight::from_parts(173_774_000, 11426)
.saturating_add(RocksDbWeight::get().reads(11_u64))
.saturating_add(RocksDbWeight::get().writes(10_u64))
// Measured: `1608`
// Estimated: `323487`
// Minimum execution time: 335_754_000 picoseconds.
Weight::from_parts(511_931_000, 323487)
.saturating_add(RocksDbWeight::get().reads(12_u64))
.saturating_add(RocksDbWeight::get().writes(11_u64))
}
/// Storage: `Presale::Presales` (r:1 w:0)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
@@ -375,10 +391,10 @@ impl WeightInfo for () {
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
fn claim_vested() -> Weight {
// Proof Size summary in bytes:
// Measured: `1392`
// Measured: `1426`
// Estimated: `6208`
// Minimum execution time: 72_526_000 picoseconds.
Weight::from_parts(74_529_000, 6208)
// Minimum execution time: 173_863_000 picoseconds.
Weight::from_parts(221_982_000, 6208)
.saturating_add(RocksDbWeight::get().reads(8_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
@@ -388,6 +404,8 @@ impl WeightInfo for () {
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributions` (r:1 w:1)
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
/// Storage: `Presale::VestingClaimed` (r:1 w:0)
/// Proof: `Presale::VestingClaimed` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
/// Storage: `Assets::Asset` (r:1 w:1)
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Assets::Account` (r:2 w:2)
@@ -396,45 +414,31 @@ impl WeightInfo for () {
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
fn refund_cancelled_presale() -> Weight {
// Proof Size summary in bytes:
// Measured: `1370`
// Measured: `1428`
// Estimated: `323487`
// Minimum execution time: 66_454_000 picoseconds.
Weight::from_parts(69_615_000, 323487)
.saturating_add(RocksDbWeight::get().reads(7_u64))
// Minimum execution time: 162_231_000 picoseconds.
Weight::from_parts(227_132_000, 323487)
.saturating_add(RocksDbWeight::get().reads(8_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
/// Storage: `Presale::Presales` (r:1 w:1)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
/// Storage: `Presale::TotalRaised` (r:1 w:0)
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributors` (r:1 w:0)
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
/// Storage: `Presale::Contributions` (r:100 w:0)
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
/// Storage: `Assets::Asset` (r:1 w:1)
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Assets::Account` (r:101 w:101)
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:101 w:101)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
/// Storage: `Presale::SuccessfulPresales` (r:1 w:1)
/// Proof: `Presale::SuccessfulPresales` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// The range of component `n` is `[1, 100]`.
fn finalize_presale(n: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `525`
// Estimated: `3717 + n * (3235 ±0)`
// Minimum execution time: 17_663_000 picoseconds.
Weight::from_parts(18_454_000, 3717)
// Standard Error: 463_975
.saturating_add(Weight::from_parts(2_728_899, 0).saturating_mul(n.into()))
// Estimated: `3717 + n * (15 ±0)`
// Minimum execution time: 45_775_000 picoseconds.
Weight::from_parts(79_721_748, 3717)
// Standard Error: 16_126
.saturating_add(Weight::from_parts(317_025, 0).saturating_mul(n.into()))
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
.saturating_add(Weight::from_parts(0, 3235).saturating_mul(n.into()))
.saturating_add(Weight::from_parts(0, 15).saturating_mul(n.into()))
}
/// Storage: `Presale::Presales` (r:1 w:0)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
@@ -451,16 +455,39 @@ impl WeightInfo for () {
/// The range of component `n` is `[1, 100]`.
fn batch_refund_failed_presale(n: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `1244 + n * (198 ±0)`
// Measured: `1278 + n * (198 ±0)`
// Estimated: `323487 + n * (2609 ±0)`
// Minimum execution time: 70_612_000 picoseconds.
Weight::from_parts(17_304_947, 323487)
// Standard Error: 88_572
.saturating_add(Weight::from_parts(42_218_217, 0).saturating_mul(n.into()))
// Minimum execution time: 149_508_000 picoseconds.
Weight::from_parts(190_635_000, 323487)
// Standard Error: 335_699
.saturating_add(Weight::from_parts(127_279_841, 0).saturating_mul(n.into()))
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(n.into())))
.saturating_add(RocksDbWeight::get().writes(2_u64))
.saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(n.into())))
.saturating_add(Weight::from_parts(0, 2609).saturating_mul(n.into()))
}
/// Storage: `Presale::Presales` (r:1 w:0)
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
/// Storage: `Presale::FundsWithdrawn` (r:1 w:1)
/// Proof: `Presale::FundsWithdrawn` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
/// Storage: `Assets::Asset` (r:1 w:1)
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Assets::Account` (r:2 w:2)
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
fn withdraw_funds() -> Weight {
// Proof Size summary in bytes:
// Measured: `1366`
// Estimated: `6208`
// Minimum execution time: 159_568_000 picoseconds.
Weight::from_parts(253_502_000, 6208)
.saturating_add(RocksDbWeight::get().reads(8_u64))
.saturating_add(RocksDbWeight::get().writes(7_u64))
}
}
@@ -376,7 +376,7 @@ pub mod pezpallet {
fn get_referral_score(who: &T::AccountId) -> RawScore {
let stats = ReferrerStatsStorage::<T>::get(who);
// Step 1: "Haksız olanı geri alma" - Remove revoked referrals from count
// Step 1: "Reverse the unfair ones" - Remove revoked referrals from count
// This is NOT a penalty, it's correcting the record to reflect reality
let good_referrals = stats.total_referrals.saturating_sub(stats.revoked_referrals);
@@ -395,7 +395,7 @@ pub mod pezpallet {
_ => 500,
};
// Step 3: "Cezalandırma" - Apply stored penalty from PenaltyPerRevocation
// Step 3: "Punishment" - Apply stored penalty from PenaltyPerRevocation
// Uses the pre-calculated penalty_score accumulated in on_citizenship_revoked()
// This is the actual punishment: "you should have been more careful"
base_score.saturating_sub(stats.penalty_score)
@@ -6,11 +6,25 @@
//! Benchmarking setup for pezpallet-staking-score
use crate::{
CachedStakingDetails, Call, Config, Pezpallet, StakingDetails, StakingSource,
StakingStartBlock, UNITS,
BalanceOf, CachedStakingDetails, Call, Config, NoterBonds, NoterCheck, PendingStakingDetails,
PendingSubmission, Pezpallet, StakingDetails, StakingSource, StakingStartBlock, UNITS,
};
use pezframe_benchmarking::v2::*;
use pezframe_support::traits::{Currency, EnsureOrigin, Get};
use pezframe_system::RawOrigin;
use pezsp_runtime::traits::Saturating;
/// Fund `who` with double the noter bond and register them as an active
/// (bonded) noter — `T::NoterChecker::make_noter` handles whatever a real
/// runtime's checker needs (e.g. minting the tiki pallet's Noter role NFT),
/// so this pallet's benchmarks stay decoupled from that pallet's internals.
fn fund_and_register_noter<T: Config>(who: &T::AccountId) {
let bond: BalanceOf<T> = T::NoterBondAmount::get();
let _ = T::Currency::deposit_creating(who, bond + bond);
T::NoterChecker::make_noter(who);
Pezpallet::<T>::register_as_noter(RawOrigin::Signed(who.clone()).into())
.expect("benchmark setup: register_as_noter must succeed");
}
#[benchmarks]
mod benchmarks {
@@ -68,4 +82,122 @@ mod benchmarks {
assert!(CachedStakingDetails::<T>::get(&target, StakingSource::RelayChain).is_some());
}
#[benchmark]
fn register_as_noter() {
let caller: T::AccountId = whitelisted_caller();
let bond: BalanceOf<T> = T::NoterBondAmount::get();
let _ = T::Currency::deposit_creating(&caller, bond + bond);
T::NoterChecker::make_noter(&caller);
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert!(NoterBonds::<T>::get(&caller).is_some());
}
#[benchmark]
fn unregister_as_noter() {
let caller: T::AccountId = whitelisted_caller();
fund_and_register_noter::<T>(&caller);
// No submission on record — the dispute-window gate is a no-op here,
// which is the cheapest path but still exercises the full call.
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert!(NoterBonds::<T>::get(&caller).is_none());
}
/// Benchmark worst case: DisputeOrigin freezing a pending submission that
/// does exist (the existence check + removal, not just the origin check).
#[benchmark]
fn dispute_staking_details() {
let noter: T::AccountId = whitelisted_caller();
fund_and_register_noter::<T>(&noter);
let target: T::AccountId = account("target", 0, 0);
PendingStakingDetails::<T>::insert(
&target,
StakingSource::RelayChain,
PendingSubmission {
details: StakingDetails {
staked_amount: (500u128 * UNITS).into(),
nominations_count: 3,
unlocking_chunks_count: 0,
},
submitted_by: noter,
submitted_at: 0u32.into(),
},
);
let dispute_origin = T::DisputeOrigin::try_successful_origin()
.expect("DisputeOrigin must have a benchmark origin");
#[extrinsic_call]
_(dispute_origin, target.clone(), StakingSource::RelayChain);
assert!(PendingStakingDetails::<T>::get(&target, StakingSource::RelayChain).is_none());
}
/// Benchmark worst case: same cost profile as the Root path of
/// `receive_staking_details` (a full `apply_staking_update`), plus the
/// pending-entry lookup/removal.
#[benchmark]
fn finalize_staking_details() {
let noter: T::AccountId = whitelisted_caller();
fund_and_register_noter::<T>(&noter);
let target: T::AccountId = account("target", 0, 0);
CachedStakingDetails::<T>::insert(
&target,
StakingSource::AssetHub,
StakingDetails {
staked_amount: (200u128 * UNITS).into(),
nominations_count: 1,
unlocking_chunks_count: 0,
},
);
PendingStakingDetails::<T>::insert(
&target,
StakingSource::RelayChain,
PendingSubmission {
details: StakingDetails {
staked_amount: (500u128 * UNITS).into(),
nominations_count: 3,
unlocking_chunks_count: 0,
},
submitted_by: noter,
submitted_at: 0u32.into(),
},
);
// Advance past the runtime's actual configured DisputeWindow so the
// submission above has matured — submitted_at alone (block 0) proves
// nothing if the current block hasn't moved past it too.
pezframe_system::Pezpallet::<T>::set_block_number(
T::DisputeWindow::get().saturating_add(1u32.into()),
);
let caller: T::AccountId = account("finalizer", 0, 0);
#[extrinsic_call]
_(RawOrigin::Signed(caller), target.clone(), StakingSource::RelayChain);
assert!(PendingStakingDetails::<T>::get(&target, StakingSource::RelayChain).is_none());
assert!(CachedStakingDetails::<T>::get(&target, StakingSource::RelayChain).is_some());
}
#[benchmark]
fn slash_noter() {
let noter: T::AccountId = whitelisted_caller();
fund_and_register_noter::<T>(&noter);
let slash_origin = T::SlashOrigin::try_successful_origin()
.expect("SlashOrigin must have a benchmark origin");
#[extrinsic_call]
_(slash_origin, noter.clone());
assert!(NoterBonds::<T>::get(&noter).is_none());
}
}
@@ -15,7 +15,25 @@
//!
//! The sudo account delegates `receive_staking_details` authority to accounts that
//! hold the `Noter` tiki (role NFT). A bot collects staking data from Relay Chain
//! and Asset Hub, then a noter signs and submits the data to People Chain.
//! and Asset Hub, then a noter signs and submits the data to People Chain. The
//! Noter tiki is not a single hardcoded account — any number of independent
//! accounts can hold it, the same way a state can authorize any number of notaries.
//!
//! ## Bonded Registration and Dispute Window
//!
//! Holding the Noter tiki alone is not enough to submit data: an account must
//! also call `register_as_noter()` and post a bond (`NoterBondAmount`), mirroring
//! a real notary's insurance/liability bond. A noter-submitted update does not
//! take effect immediately — it sits in `PendingStakingDetails` for
//! `DisputeWindow` blocks (like a deed's recording/contestability period) during
//! which `T::DisputeOrigin` (any single Council member) can call
//! `dispute_staking_details()` to freeze it pending governance review. If
//! governance (`T::SlashOrigin`) confirms the submission was fraudulent, it can
//! call `slash_noter()` to burn the noter's bond to `T::SlashDestination`.
//!
//! Root (XCM Transact) submissions are exempt from the bond and the dispute
//! window: that origin is chain-authenticated (backed by consensus, not a single
//! private key), so it carries strictly more assurance than any bonded noter.
//!
//! ## Dual-Chain Staking
//!
@@ -47,12 +65,29 @@ pub mod weights;
pub mod pezpallet {
use super::weights::WeightInfo;
use core::ops::Div;
use pezframe_support::pezpallet_prelude::*;
use pezframe_support::{
pezpallet_prelude::*,
traits::{Currency, ExistenceRequirement, ReservableCurrency},
};
use pezframe_system::pezpallet_prelude::*;
use pezsp_runtime::traits::{Saturating, Zero};
// --- Constants ---
//
// Block-time assumption: 10 blocks/minute (6s blocks), matching this
// chain's actual configured slot duration (`testnet_teyrchains_constants::
// pezkuwichain::consensus::MILLISECS_PER_BLOCK = 6_000` — the module
// `people-pezkuwichain` actually imports its `HOURS`/`DAYS`/`SlotDuration`
// from; `teyrchains_common::MILLISECS_PER_BLOCK = 12_000` is a
// same-named but unrelated, unused-for-timing constant in a different
// crate and does not govern this runtime's real block cadence). An
// earlier revision of this file assumed the 12s figure and halved every
// duration-tier threshold below (confirmed against live mainnet state via
// try-runtime and empirical block-time measurement before it shipped). If
// this chain's block time ever changes, these two constants need
// updating to match — they are not derived automatically.
pub const MONTH_IN_BLOCKS: u32 = 30 * 24 * 60 * 10;
pub const HOUR_IN_BLOCKS: u32 = 60 * 10;
pub const UNITS: u128 = 1_000_000_000_000;
/// The chain from which staking data originates.
@@ -82,6 +117,15 @@ pub mod pezpallet {
/// Noter-authorized accounts can submit staking details on behalf of users.
pub trait NoterCheck<AccountId> {
fn is_noter(who: &AccountId) -> bool;
/// Grant noter authority to `who`, for benchmarking only. Lets a
/// runtime's real checker (e.g. one backed by the tiki pallet's role
/// NFTs) make its own benchmark setup self-contained, instead of this
/// pallet's generic benchmarks needing to know how an external role
/// system works. Default no-op; runtimes whose `is_noter` depends on
/// external state should override it.
#[cfg(feature = "runtime-benchmarks")]
fn make_noter(_who: &AccountId) {}
}
/// Default implementation: nobody is noter (safe default for tests).
@@ -118,8 +162,42 @@ pub mod pezpallet {
/// Checker for noter authority. Accounts with the Noter tiki can submit
/// staking details without requiring root origin.
type NoterChecker: NoterCheck<Self::AccountId>;
/// Reservable currency used for the noter registration bond.
type Currency: ReservableCurrency<Self::AccountId>;
/// Bond a Noter-tiki holder must reserve via `register_as_noter()` before
/// their submissions are accepted. Slashable by `SlashOrigin` if governance
/// confirms a submission was fraudulent.
#[pezpallet::constant]
type NoterBondAmount: Get<BalanceOf<Self>>;
/// How long a noter-submitted (signed-origin) staking update sits as a
/// candidate before it takes effect, giving `DisputeOrigin` a window to
/// freeze it. Root/XCM-Transact submissions are exempt.
#[pezpallet::constant]
type DisputeWindow: Get<BlockNumberFor<Self>>;
/// Origin allowed to dispute (freeze) a pending noter submission before it
/// matures. Deliberately lightweight — any single authorized member.
/// `Success = AccountId` so the disputing member's identity can be
/// recorded in `Event::StakingDetailsDisputed`.
type DisputeOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
/// Origin allowed to slash a noter's bond, after confirming a disputed
/// submission was fraudulent. Deliberately a stronger, collective bar than
/// `DisputeOrigin`.
type SlashOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// Where a slashed noter's bond goes.
type SlashDestination: Get<Self::AccountId>;
}
/// Balance type of `T::Currency` (the noter bond currency) — distinct from
/// `T::Balance` above, which represents staking amounts, not reservable funds.
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
// --- Storage ---
#[pezpallet::storage]
@@ -141,17 +219,73 @@ pub mod pezpallet {
OptionQuery,
>;
/// Registered (bonded) noters, and the exact amount each reserved. An
/// account must appear here — in addition to holding the Noter tiki — before
/// `receive_staking_details` accepts their submissions.
#[pezpallet::storage]
#[pezpallet::getter(fn noter_bond)]
pub type NoterBonds<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, OptionQuery>;
/// Block a noter last submitted data at. Gates `unregister_as_noter` until
/// any submission they made has cleared its dispute window, so a noter can't
/// submit fraudulent data and immediately withdraw their bond ahead of review.
#[pezpallet::storage]
pub type NoterLastSubmission<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, BlockNumberFor<T>, OptionQuery>;
/// A noter-submitted staking update awaiting its dispute window. Does not
/// affect `get_staking_score` until finalized into `CachedStakingDetails` —
/// either opportunistically (before the same account's next submission),
/// explicitly via `finalize_staking_details`, or discarded via
/// `dispute_staking_details`. Root/XCM-Transact submissions skip this
/// entirely and write `CachedStakingDetails` directly.
#[pezpallet::storage]
#[pezpallet::getter(fn pending_staking_details)]
pub type PendingStakingDetails<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Blake2_128Concat,
StakingSource,
PendingSubmission<T::Balance, T::AccountId, BlockNumberFor<T>>,
OptionQuery,
>;
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A user started time-based scoring.
ScoreTrackingStarted { who: T::AccountId, start_block: BlockNumberFor<T> },
/// Staking details received from a chain via XCM.
/// Staking details took effect — either a Root/XCM-Transact submission
/// (immediate) or a noter submission that has cleared its dispute window.
StakingDetailsReceived {
who: T::AccountId,
source: StakingSource,
staked_amount: T::Balance,
},
/// A noter-signed staking update was recorded as a pending candidate and
/// will take effect at `matures_at` unless disputed before then.
StakingDetailsPending {
who: T::AccountId,
source: StakingSource,
staked_amount: T::Balance,
submitted_by: T::AccountId,
matures_at: BlockNumberFor<T>,
},
/// A pending noter submission was frozen before it could take effect.
StakingDetailsDisputed {
who: T::AccountId,
source: StakingSource,
disputed_by: T::AccountId,
},
/// An account registered as an active (bonded) noter.
NoterRegistered { who: T::AccountId, bond: BalanceOf<T> },
/// An account unregistered as noter and reclaimed its bond.
NoterUnregistered { who: T::AccountId, bond_returned: BalanceOf<T> },
/// A noter's bond was slashed by governance after a confirmed fraudulent
/// submission.
NoterSlashed { who: T::AccountId, amount: BalanceOf<T> },
}
#[pezpallet::error]
@@ -160,8 +294,19 @@ pub mod pezpallet {
NoStakeFound,
/// Score tracking has already been started for this account.
TrackingAlreadyStarted,
/// Caller does not have noter authority.
/// Caller does not have noter authority (missing the Noter tiki).
NotAuthorized,
/// Caller holds the Noter tiki but has not posted the registration bond.
NotRegisteredNoter,
/// Caller has already registered (and bonded) as a noter.
AlreadyRegisteredNoter,
/// Cannot unregister while a submission is still within its dispute
/// window — wait for it to mature or be disputed first.
PendingSubmissionExists,
/// No pending submission exists for this (account, source) pair.
NoPendingSubmission,
/// The pending submission's dispute window has not yet elapsed.
NotYetMatured,
}
#[pezpallet::call]
@@ -196,13 +341,11 @@ pub mod pezpallet {
/// Receive staking details for an account.
///
/// Accepts root origin (XCM Transact) or a signed origin from an account
/// that holds the Noter tiki. This allows a noter-authorized bot to submit
/// staking data collected from Relay Chain and Asset Hub.
///
/// If `staked_amount` is zero, the cached entry for the given source is
/// removed. If no stake remains from any source, `StakingStartBlock` is
/// also cleaned up, effectively resetting the user's staking score to zero.
/// Root origin (XCM Transact) is chain-authenticated and takes effect
/// immediately, exactly as before. A signed origin from a *registered*
/// Noter-tiki holder instead records the update as a pending candidate
/// (see `PendingStakingDetails`) that only takes effect after
/// `DisputeWindow` blocks — see the module docs for why.
#[pezpallet::call_index(1)]
#[pezpallet::weight(T::WeightInfo::receive_staking_details())]
pub fn receive_staking_details(
@@ -213,31 +356,174 @@ pub mod pezpallet {
nominations_count: u32,
unlocking_chunks_count: u32,
) -> DispatchResult {
// Root (XCM Transact) OR noter-authorized signed origin.
if ensure_root(origin.clone()).is_err() {
let caller = ensure_signed(origin)?;
ensure!(T::NoterChecker::is_noter(&caller), Error::<T>::NotAuthorized);
if ensure_root(origin.clone()).is_ok() {
Self::apply_staking_update(
&who,
source,
staked_amount,
nominations_count,
unlocking_chunks_count,
);
return Ok(());
}
if staked_amount.is_zero() {
// Zero stake: remove the cached entry for this source.
CachedStakingDetails::<T>::remove(&who, source);
let caller = ensure_signed(origin)?;
ensure!(T::NoterChecker::is_noter(&caller), Error::<T>::NotAuthorized);
ensure!(NoterBonds::<T>::contains_key(&caller), Error::<T>::NotRegisteredNoter);
// Check if any stake remains from other sources.
let remaining = Self::total_cached_stake(&who);
if remaining.is_zero() {
// No stake from any source — clean up tracking.
StakingStartBlock::<T>::remove(&who);
}
} else {
let details =
StakingDetails { staked_amount, nominations_count, unlocking_chunks_count };
CachedStakingDetails::<T>::insert(&who, source, details);
// Opportunistically finalize a previously-matured pending entry for
// this (who, source) before recording the new candidate. Without this,
// a steady stream of legitimate noter updates would keep resetting the
// dispute window and the account's effective stake would never
// progress past whatever Root last committed (often zero).
Self::maybe_finalize(&who, source);
let current_block = pezframe_system::Pezpallet::<T>::block_number();
NoterLastSubmission::<T>::insert(&caller, current_block);
let details =
StakingDetails { staked_amount, nominations_count, unlocking_chunks_count };
PendingStakingDetails::<T>::insert(
&who,
source,
PendingSubmission {
details,
submitted_by: caller.clone(),
submitted_at: current_block,
},
);
Self::deposit_event(Event::StakingDetailsPending {
who,
source,
staked_amount,
submitted_by: caller,
matures_at: current_block.saturating_add(T::DisputeWindow::get()),
});
Ok(())
}
/// Register as an active noter: reserve `NoterBondAmount` and become
/// eligible to submit staking data. Requires already holding the Noter
/// tiki — this only adds the bond on top of that role, it does not grant
/// the role itself (that remains a separate governance decision).
#[pezpallet::call_index(2)]
#[pezpallet::weight(T::WeightInfo::register_as_noter())]
pub fn register_as_noter(origin: OriginFor<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(T::NoterChecker::is_noter(&who), Error::<T>::NotAuthorized);
ensure!(!NoterBonds::<T>::contains_key(&who), Error::<T>::AlreadyRegisteredNoter);
let bond = T::NoterBondAmount::get();
T::Currency::reserve(&who, bond)?;
NoterBonds::<T>::insert(&who, bond);
Self::deposit_event(Event::NoterRegistered { who, bond });
Ok(())
}
/// Unregister as noter and reclaim the bond. Blocked while a submission
/// this account made is still within its dispute window, so a noter can't
/// submit fraudulent data and withdraw ahead of review.
#[pezpallet::call_index(3)]
#[pezpallet::weight(T::WeightInfo::unregister_as_noter())]
pub fn unregister_as_noter(origin: OriginFor<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
let bond = NoterBonds::<T>::get(&who).ok_or(Error::<T>::NotRegisteredNoter)?;
if let Some(last_submission) = NoterLastSubmission::<T>::get(&who) {
let current_block = pezframe_system::Pezpallet::<T>::block_number();
ensure!(
current_block >= last_submission.saturating_add(T::DisputeWindow::get()),
Error::<T>::PendingSubmissionExists
);
}
T::OnStakingUpdate::on_staking_data_changed(&who);
T::Currency::unreserve(&who, bond);
NoterBonds::<T>::remove(&who);
NoterLastSubmission::<T>::remove(&who);
Self::deposit_event(Event::StakingDetailsReceived { who, source, staked_amount });
Self::deposit_event(Event::NoterUnregistered { who, bond_returned: bond });
Ok(())
}
/// Freeze a pending noter submission before it matures. Discards the
/// candidate entirely — it does not become effective, and does not by
/// itself slash the noter's bond (that is a separate governance decision
/// via `slash_noter`, once the dispute has actually been reviewed).
#[pezpallet::call_index(4)]
#[pezpallet::weight(T::WeightInfo::dispute_staking_details())]
pub fn dispute_staking_details(
origin: OriginFor<T>,
who: T::AccountId,
source: StakingSource,
) -> DispatchResult {
let disputed_by = T::DisputeOrigin::ensure_origin(origin)?;
ensure!(
PendingStakingDetails::<T>::contains_key(&who, source),
Error::<T>::NoPendingSubmission
);
PendingStakingDetails::<T>::remove(&who, source);
Self::deposit_event(Event::StakingDetailsDisputed { who, source, disputed_by });
Ok(())
}
/// Commit a pending noter submission once its dispute window has elapsed.
/// Permissionless (any signed, fee-paying account may call it) — like
/// recording a notarized deed once its contestability period has passed,
/// this is a mechanical step anyone can trigger, not a privileged action.
#[pezpallet::call_index(5)]
#[pezpallet::weight(T::WeightInfo::finalize_staking_details())]
pub fn finalize_staking_details(
origin: OriginFor<T>,
who: T::AccountId,
source: StakingSource,
) -> DispatchResult {
ensure_signed(origin)?;
let pending = PendingStakingDetails::<T>::get(&who, source)
.ok_or(Error::<T>::NoPendingSubmission)?;
let current_block = pezframe_system::Pezpallet::<T>::block_number();
ensure!(
current_block >= pending.submitted_at.saturating_add(T::DisputeWindow::get()),
Error::<T>::NotYetMatured
);
PendingStakingDetails::<T>::remove(&who, source);
Self::apply_staking_update(
&who,
source,
pending.details.staked_amount,
pending.details.nominations_count,
pending.details.unlocking_chunks_count,
);
Ok(())
}
/// Slash a noter's bond after governance confirms a disputed submission
/// was fraudulent, and remove their registration. This is a deliberate,
/// reviewed governance action (`SlashOrigin`) — never automatic — the
/// same way a real notary's insurance bond is only drawn on after a
/// finding of fault, not on a bare accusation.
#[pezpallet::call_index(6)]
#[pezpallet::weight(T::WeightInfo::slash_noter())]
pub fn slash_noter(origin: OriginFor<T>, noter: T::AccountId) -> DispatchResult {
T::SlashOrigin::ensure_origin(origin)?;
let bond = NoterBonds::<T>::take(&noter).ok_or(Error::<T>::NotRegisteredNoter)?;
NoterLastSubmission::<T>::remove(&noter);
T::Currency::unreserve(&noter, bond);
T::Currency::transfer(
&noter,
&T::SlashDestination::get(),
bond,
ExistenceRequirement::AllowDeath,
)?;
Self::deposit_event(Event::NoterSlashed { who: noter, amount: bond });
Ok(())
}
}
@@ -266,6 +552,16 @@ pub mod pezpallet {
pub unlocking_chunks_count: u32,
}
/// A noter-submitted staking update awaiting its dispute window.
#[derive(
Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, TypeInfo, Debug, MaxEncodedLen,
)]
pub struct PendingSubmission<Balance, AccountId, BlockNumber> {
pub details: StakingDetails<Balance>,
pub submitted_by: AccountId,
pub submitted_at: BlockNumber,
}
// --- Traits ---
/// Interface for querying staking scores. Used by trust pallet.
@@ -295,6 +591,77 @@ pub mod pezpallet {
}
total
}
/// Actually apply a staking update to `CachedStakingDetails`. This is the
/// single place data becomes *effective* — called directly for Root/XCM
/// submissions, and via `maybe_finalize` once a noter submission's
/// dispute window has elapsed. The anti flash-stake duration-reset guard
/// lives here so it fires when data becomes effective, not at submission
/// time (which, for noter submissions, is not yet trustworthy).
fn apply_staking_update(
who: &T::AccountId,
source: StakingSource,
staked_amount: T::Balance,
nominations_count: u32,
unlocking_chunks_count: u32,
) {
let previous_total = Self::total_cached_stake(who);
if staked_amount.is_zero() {
CachedStakingDetails::<T>::remove(who, source);
let remaining = Self::total_cached_stake(who);
if remaining.is_zero() {
StakingStartBlock::<T>::remove(who);
}
} else {
let details =
StakingDetails { staked_amount, nominations_count, unlocking_chunks_count };
CachedStakingDetails::<T>::insert(who, source, details);
let new_total = Self::total_cached_stake(who);
if !previous_total.is_zero()
&& new_total > previous_total
&& StakingStartBlock::<T>::contains_key(who)
{
let current_block = pezframe_system::Pezpallet::<T>::block_number();
StakingStartBlock::<T>::insert(who, current_block);
}
}
T::OnStakingUpdate::on_staking_data_changed(who);
Self::deposit_event(Event::StakingDetailsReceived {
who: who.clone(),
source,
staked_amount,
});
}
/// If a pending noter submission for `(who, source)` exists and has
/// cleared its dispute window, commit it via `apply_staking_update` and
/// clear the pending slot. Returns `true` if it finalized something.
/// No-op (returns `false`) if there's nothing pending, or it hasn't
/// matured yet — callers that need to distinguish those two cases (like
/// `finalize_staking_details`) check the storage directly instead.
fn maybe_finalize(who: &T::AccountId, source: StakingSource) -> bool {
let Some(pending) = PendingStakingDetails::<T>::get(who, source) else {
return false;
};
let current_block = pezframe_system::Pezpallet::<T>::block_number();
if current_block < pending.submitted_at.saturating_add(T::DisputeWindow::get()) {
return false;
}
PendingStakingDetails::<T>::remove(who, source);
Self::apply_staking_update(
who,
source,
pending.details.staked_amount,
pending.details.nominations_count,
pending.details.unlocking_chunks_count,
);
true
}
}
// --- StakingScoreProvider Implementation ---
@@ -8,9 +8,11 @@
use crate as pezpallet_staking_score;
use pezframe_support::{
construct_runtime, derive_impl, parameter_types, traits::ConstU32,
construct_runtime, derive_impl, parameter_types,
traits::{ConstU32, SortedMembers},
weights::constants::RocksDbWeight,
};
use pezframe_system::EnsureSignedBy;
use pezsp_runtime::BuildStorage;
use crate::UNITS;
@@ -21,10 +23,45 @@ pub type AccountId = u64;
pub type Balance = u128;
pub type BlockNumber = u64;
// --- Test-only well-known accounts ---
/// A second, independent registered noter (distinct from account 99, the
/// pre-existing "the" noter in most tests) — used by dispute-window tests
/// that need two noters to exist simultaneously, mirroring how the Noter
/// tiki is designed to support any number of accounts holding it.
pub const NOTER_2: AccountId = 98;
/// A Council-equivalent member for `DisputeOrigin` in tests.
pub const DISPUTE_MEMBER: AccountId = 40;
/// A stronger governance-equivalent member for `SlashOrigin` in tests.
pub const SLASH_MEMBER: AccountId = 41;
/// Where slashed noter bonds land in tests (stands in for `RelayTreasuryAccount`).
pub const TREASURY: AccountId = 999;
/// A short, test-friendly dispute window — real enough to prove the
/// mechanism (tests advance blocks past it) without needing huge block jumps
/// in every test. The runtime uses `HOUR_IN_BLOCKS` (600 blocks); this mock
/// deliberately does not reuse that constant so tests stay fast.
pub const DISPUTE_WINDOW: BlockNumber = 10;
// --- Constants ---
parameter_types! {
pub const BlockHashCount: BlockNumber = 250;
pub const ExistentialDeposit: Balance = 1;
pub const NoterBondAmount: Balance = 1_000 * UNITS;
pub const DisputeWindowBlocks: BlockNumber = DISPUTE_WINDOW;
pub const SlashDestinationAccount: AccountId = TREASURY;
}
pub struct DisputeMemberProvider;
impl SortedMembers<AccountId> for DisputeMemberProvider {
fn sorted_members() -> Vec<AccountId> {
vec![DISPUTE_MEMBER]
}
}
pub struct SlashMemberProvider;
impl SortedMembers<AccountId> for SlashMemberProvider {
fn sorted_members() -> Vec<AccountId> {
vec![SLASH_MEMBER]
}
}
// --- Runtime ---
@@ -53,11 +90,11 @@ impl pezpallet_balances::Config for Test {
}
/// Mock noter checker for tests.
/// Account 99 is noter, everyone else is not.
/// Accounts 99 and 98 (`NOTER_2`) are noters, everyone else is not.
pub struct MockNoterChecker;
impl crate::NoterCheck<AccountId> for MockNoterChecker {
fn is_noter(who: &AccountId) -> bool {
*who == 99
*who == 99 || *who == NOTER_2
}
}
@@ -66,6 +103,12 @@ impl crate::Config for Test {
type WeightInfo = ();
type OnStakingUpdate = ();
type NoterChecker = MockNoterChecker;
type Currency = Balances;
type NoterBondAmount = NoterBondAmount;
type DisputeWindow = DisputeWindowBlocks;
type DisputeOrigin = EnsureSignedBy<DisputeMemberProvider, AccountId>;
type SlashOrigin = EnsureSignedBy<SlashMemberProvider, AccountId>;
type SlashDestination = SlashDestinationAccount;
}
// --- ExtBuilder ---
@@ -89,7 +132,8 @@ impl ExtBuilder {
(10, 1_000_000 * UNITS),
(20, 100_000 * UNITS),
(30, 100_000 * UNITS), // Charlie
(99, 100_000 * UNITS), // NOTER
(99, 100_000 * UNITS), // NOTER — enough to post NoterBondAmount (1,000 UNITS)
(NOTER_2, 100_000 * UNITS),
],
..Default::default()
}
@@ -103,3 +147,49 @@ impl ExtBuilder {
self.build().execute_with(test);
}
}
// --- Test helpers: bonded-noter registration + dispute-window resolution ---
/// Register `noter` (idempotent — no-op if already registered) as a bonded,
/// active noter. Every noter-signed `receive_staking_details` call in tests
/// now requires this first, matching the real pallet's `register_as_noter`
/// gate.
pub fn ensure_registered(noter: AccountId) {
if crate::NoterBonds::<Test>::get(noter).is_none() {
assert!(
StakingScore::register_as_noter(RuntimeOrigin::signed(noter)).is_ok(),
"test setup: register_as_noter failed for {noter}"
);
}
}
/// Submit staking details as `noter`, then advance the block number past
/// `DisputeWindow` and finalize — i.e. simulate a noter submission that goes
/// unchallenged and takes effect. This is the noter-path equivalent of the
/// old (pre dispute-window) immediate-commit behavior most tests want.
pub fn submit_and_finalize(
noter: AccountId,
who: AccountId,
source: crate::StakingSource,
staked_amount: Balance,
nominations_count: u32,
unlocking_chunks_count: u32,
) {
ensure_registered(noter);
assert!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(noter),
who,
source,
staked_amount,
nominations_count,
unlocking_chunks_count,
)
.is_ok());
let matured_at = System::block_number() + DISPUTE_WINDOW;
System::set_block_number(matured_at);
assert!(
StakingScore::finalize_staking_details(RuntimeOrigin::signed(noter), who, source).is_ok(),
"test setup: finalize_staking_details failed"
);
}
@@ -8,8 +8,8 @@
//! mirroring the real People Chain architecture.
use crate::{
mock::*, CachedStakingDetails, Error, Event, StakingScoreProvider, StakingSource,
StakingStartBlock, MONTH_IN_BLOCKS, UNITS,
mock::*, CachedStakingDetails, Error, Event, NoterBonds, PendingStakingDetails,
StakingScoreProvider, StakingSource, StakingStartBlock, MONTH_IN_BLOCKS, UNITS,
};
use pezframe_support::{assert_noop, assert_ok};
@@ -565,6 +565,17 @@ const NOTER: AccountId = 99; // MockNoterChecker recognizes 99 as noter
#[test]
fn noter_can_submit_staking_details() {
ExtBuilder.build_and_execute(|| {
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 200 * UNITS, 0, 0);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 30);
});
}
#[test]
fn noter_signed_submission_is_pending_not_immediate() {
ExtBuilder.build_and_execute(|| {
System::set_block_number(1);
ensure_registered(NOTER);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
@@ -574,7 +585,15 @@ fn noter_can_submit_staking_details() {
0
));
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 30);
// Not yet effective: sits in PendingStakingDetails, not CachedStakingDetails.
assert!(CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_none());
assert!(PendingStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_some());
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 0);
let events = System::events();
assert!(events.iter().any(|event| {
matches!(event.event, RuntimeEvent::StakingScore(Event::StakingDetailsPending { .. }))
}));
});
}
@@ -602,25 +621,11 @@ fn root_can_still_submit_staking_details() {
fn zero_stake_removes_cached_entry() {
ExtBuilder.build_and_execute(|| {
// Setup: noter submits 200 HEZ for relay chain
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 200 * UNITS, 0, 0);
assert!(CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_some());
// Zero stake removes the entry
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 0u128, 0, 0);
assert!(CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_none());
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 0);
});
@@ -634,24 +639,10 @@ fn zero_stake_cleans_up_tracking_when_no_stake_remains() {
assert!(StakingStartBlock::<Test>::get(USER_STASH).is_some());
// Noter submits stake
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 200 * UNITS, 0, 0);
// Zero out the only source → StakingStartBlock should be cleaned up
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 0u128, 0, 0);
assert!(StakingStartBlock::<Test>::get(USER_STASH).is_none());
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 0);
});
@@ -664,34 +655,13 @@ fn zero_stake_one_source_keeps_tracking_if_other_source_has_stake() {
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
// Noter submits stake for both sources
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
100 * UNITS,
0,
0
));
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::AssetHub,
150 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 100 * UNITS, 0, 0);
submit_and_finalize(NOTER, USER_STASH, StakingSource::AssetHub, 150 * UNITS, 0, 0);
// Total 250 HEZ → tier 30
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 30);
// Zero out relay chain only
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 0u128, 0, 0);
// Tracking preserved (AssetHub still has stake)
assert!(StakingStartBlock::<Test>::get(USER_STASH).is_some());
@@ -715,19 +685,12 @@ fn full_workflow_ali_scenario() {
assert_eq!(StakingScore::get_staking_score(&ali).0, 0); // No data yet
// 2. Bot + noter submit Ali's 200 HEZ relay stake
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
ali,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
submit_and_finalize(NOTER, ali, StakingSource::RelayChain, 200 * UNITS, 0, 0);
// 200 HEZ → tier 30, duration < 1 month → x1.0 → 30
assert_eq!(StakingScore::get_staking_score(&ali).0, 30);
// 3. After 28 days → still < 1 month → x1.0 → 30
System::set_block_number(1000 + 28 * 24 * 60 * 10);
System::set_block_number(1000 + 28 * 24 * 60 * 5);
assert_eq!(StakingScore::get_staking_score(&ali).0, 30);
});
}
@@ -739,25 +702,11 @@ fn full_workflow_bob_unbond_scenario() {
// Bob opts in and has stake
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(bob)));
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
bob,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
submit_and_finalize(NOTER, bob, StakingSource::RelayChain, 200 * UNITS, 0, 0);
assert_eq!(StakingScore::get_staking_score(&bob).0, 30);
// Bob unbonds → noter reports zero
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
bob,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, bob, StakingSource::RelayChain, 0u128, 0, 0);
// Score = 0, tracking cleaned up
assert_eq!(StakingScore::get_staking_score(&bob).0, 0);
@@ -775,56 +724,32 @@ fn full_workflow_charlie_dual_chain_partial_unbond() {
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(charlie)));
// Noter submits both sources: 100 relay + 150 asset hub = 250 HEZ
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
charlie,
StakingSource::RelayChain,
100 * UNITS,
0,
0
));
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
charlie,
StakingSource::AssetHub,
150 * UNITS,
0,
0
));
submit_and_finalize(NOTER, charlie, StakingSource::RelayChain, 100 * UNITS, 0, 0);
// The anti flash-stake guard restarts StakingStartBlock on this second,
// stake-increasing submission — read it back instead of assuming 500.
submit_and_finalize(NOTER, charlie, StakingSource::AssetHub, 150 * UNITS, 0, 0);
// 250 HEZ → tier 30
assert_eq!(StakingScore::get_staking_score(&charlie).0, 30);
let effective_start = StakingStartBlock::<Test>::get(charlie).unwrap();
// Charlie unbonds from Relay Chain
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
charlie,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, charlie, StakingSource::RelayChain, 0u128, 0, 0);
// Relay entry removed, Asset Hub remains
assert!(CachedStakingDetails::<Test>::get(charlie, StakingSource::RelayChain).is_none());
assert!(CachedStakingDetails::<Test>::get(charlie, StakingSource::AssetHub).is_some());
// Tracking preserved
assert!(StakingStartBlock::<Test>::get(charlie).is_some());
// Tracking preserved, unaffected by the zero-stake unbond above
// (the guard only fires on stake *increases*, not removals).
assert_eq!(StakingStartBlock::<Test>::get(charlie), Some(effective_start));
// 150 HEZ → tier 30 (still in 101-250 range)
assert_eq!(StakingScore::get_staking_score(&charlie).0, 30);
// After 3 months: 30 * 1.4 = 42
System::set_block_number(500 + (3 * MONTH_IN_BLOCKS) as u64);
// After 3 months from the effective start: 30 * 1.4 = 42
System::set_block_number(effective_start + (3 * MONTH_IN_BLOCKS) as u64);
assert_eq!(StakingScore::get_staking_score(&charlie).0, 42);
// Charlie unbonds from Asset Hub too → fully zeroed
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
charlie,
StakingSource::AssetHub,
0u128,
0,
0
));
submit_and_finalize(NOTER, charlie, StakingSource::AssetHub, 0u128, 0, 0);
assert!(StakingStartBlock::<Test>::get(charlie).is_none());
assert_eq!(StakingScore::get_staking_score(&charlie).0, 0);
});
@@ -842,22 +767,16 @@ fn duration_counts_from_optin_not_from_data_arrival() {
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
assert_eq!(StakingScore::get_staking_score(&USER_STASH), (0, 0u64));
// Block 50_000: Bot + noter submit data (much later)
// Block 50_000: Bot + noter submit data (much later). It finalizes
// DISPUTE_WINDOW blocks after submission (block 50_010), not at 50_000.
System::set_block_number(50_000);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 200 * UNITS, 0, 0);
// Duration is from block 100, NOT from block 50_000.
// 50_000 - 100 = 49_900 blocks. MONTH_IN_BLOCKS = 432_000
// 49_900 < 432_000 → x1.0 → 30
// Duration is from block 100, NOT from block 50_000 (or 50_010).
// 50_010 - 100 = 49_910 blocks. MONTH_IN_BLOCKS = 432_000
// 49_910 < 432_000 → x1.0 → 30
let (score, duration) = StakingScore::get_staking_score(&USER_STASH);
assert_eq!(duration, 49_900);
assert_eq!(duration, 49_910);
assert_eq!(score, 30);
// After reaching 1 month from opt-in: 100 + 432_000 = 432_100
@@ -878,25 +797,11 @@ fn re_optin_after_full_unbond() {
// Phase 1: opt-in + stake + score
System::set_block_number(100);
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 200 * UNITS, 0, 0);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 30);
// Phase 2: full unbond → cleanup
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 0u128, 0, 0);
assert!(StakingStartBlock::<Test>::get(USER_STASH).is_none());
// Phase 3: re-opt-in at block 1000 (fresh start)
@@ -905,14 +810,7 @@ fn re_optin_after_full_unbond() {
assert_eq!(StakingStartBlock::<Test>::get(USER_STASH), Some(1000));
// Phase 4: new stake data
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
500 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 500 * UNITS, 0, 0);
// 500 HEZ → tier 40, duration from block 1000
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 40);
@@ -939,30 +837,9 @@ fn noter_batch_multiple_users() {
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(user3)));
// Noter submits batch: different amounts for each user
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
user1,
StakingSource::RelayChain,
50 * UNITS, // tier 20
0,
0
));
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
user2,
StakingSource::RelayChain,
200 * UNITS, // tier 30
0,
0
));
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
user3,
StakingSource::AssetHub,
800 * UNITS, // tier 50
0,
0
));
submit_and_finalize(NOTER, user1, StakingSource::RelayChain, 50 * UNITS, 0, 0); // tier 20
submit_and_finalize(NOTER, user2, StakingSource::RelayChain, 200 * UNITS, 0, 0); // tier 30
submit_and_finalize(NOTER, user3, StakingSource::AssetHub, 800 * UNITS, 0, 0); // tier 50
assert_eq!(StakingScore::get_staking_score(&user1).0, 20);
assert_eq!(StakingScore::get_staking_score(&user2).0, 30);
@@ -980,14 +857,7 @@ fn data_without_optin_still_cached_but_no_duration() {
System::set_block_number(100);
// Noter submits data for user who hasn't opted in
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
300 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 300 * UNITS, 0, 0);
// Data is cached → score is base tier only (no duration)
assert!(CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_some());
@@ -1138,24 +1008,10 @@ fn zero_stake_emits_event_with_zero_amount() {
System::set_block_number(1);
// Setup
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 200 * UNITS, 0, 0);
// Zero-stake
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 0u128, 0, 0);
let events = System::events();
// Last event should be StakingDetailsReceived with amount 0
@@ -1184,14 +1040,7 @@ fn zero_stake_emits_event_with_zero_amount() {
fn noter_overwrites_previous_submission() {
ExtBuilder.build_and_execute(|| {
// First submission: 100 HEZ
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
100 * UNITS,
5,
2
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 100 * UNITS, 5, 2);
let details =
CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).unwrap();
assert_eq!(details.staked_amount, 100 * UNITS);
@@ -1199,14 +1048,7 @@ fn noter_overwrites_previous_submission() {
assert_eq!(details.unlocking_chunks_count, 2);
// Second submission: updated values
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
300 * UNITS,
10,
1
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 300 * UNITS, 10, 1);
let details =
CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).unwrap();
assert_eq!(details.staked_amount, 300 * UNITS);
@@ -1223,14 +1065,7 @@ fn noter_overwrites_previous_submission() {
fn zero_stake_for_nonexistent_source_is_noop() {
ExtBuilder.build_and_execute(|| {
// Zero-stake for a source that was never set — should be a no-op
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
0u128,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 0u128, 0, 0);
// No ghost entries
assert!(CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_none());
@@ -1251,30 +1086,21 @@ fn max_score_scenario() {
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
// Dual-chain max: 500 relay + 500 asset hub = 1000 HEZ → tier 50
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
500 * UNITS,
0,
0
));
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::AssetHub,
500 * UNITS,
0,
0
));
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 500 * UNITS, 0, 0);
// The anti flash-stake guard (pre-existing, see receive_staking_details'
// docs) restarts StakingStartBlock on this second, stake-increasing
// submission — so "start" for duration purposes is wherever this
// finalizes, not block 1. Read it back instead of assuming block 1.
submit_and_finalize(NOTER, USER_STASH, StakingSource::AssetHub, 500 * UNITS, 0, 0);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 50);
let effective_start = StakingStartBlock::<Test>::get(USER_STASH).unwrap();
// 12+ months → 50 * 2.0 = 100 (capped)
System::set_block_number(1 + (12 * MONTH_IN_BLOCKS) as u64);
System::set_block_number(effective_start + (12 * MONTH_IN_BLOCKS) as u64);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 100);
// 24 months → still 100 (cap)
System::set_block_number(1 + (24 * MONTH_IN_BLOCKS) as u64);
System::set_block_number(effective_start + (24 * MONTH_IN_BLOCKS) as u64);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 100);
});
}
@@ -1322,3 +1148,407 @@ fn duration_exact_month_boundaries() {
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 40);
});
}
// ============================================================================
// Bonded Noter Registration
// ============================================================================
#[test]
fn unregistered_noter_cannot_submit() {
ExtBuilder.build_and_execute(|| {
// NOTER holds the tiki (MockNoterChecker recognizes 99) but has not
// called register_as_noter — submissions must be rejected.
assert_noop!(
StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
100 * UNITS,
0,
0
),
Error::<Test>::NotRegisteredNoter
);
});
}
#[test]
fn register_as_noter_reserves_bond() {
ExtBuilder.build_and_execute(|| {
System::set_block_number(1);
let free_before = Balances::free_balance(NOTER);
assert_ok!(StakingScore::register_as_noter(RuntimeOrigin::signed(NOTER)));
assert_eq!(NoterBonds::<Test>::get(NOTER), Some(NoterBondAmount::get()));
assert_eq!(Balances::reserved_balance(NOTER), NoterBondAmount::get());
assert_eq!(Balances::free_balance(NOTER), free_before - NoterBondAmount::get());
let events = System::events();
assert!(events.iter().any(|event| {
matches!(event.event, RuntimeEvent::StakingScore(Event::NoterRegistered { .. }))
}));
});
}
#[test]
fn register_as_noter_rejects_non_tiki_holder() {
ExtBuilder.build_and_execute(|| {
// USER_STASH does not hold the Noter tiki per MockNoterChecker.
assert_noop!(
StakingScore::register_as_noter(RuntimeOrigin::signed(USER_STASH)),
Error::<Test>::NotAuthorized
);
});
}
#[test]
fn register_as_noter_rejects_double_registration() {
ExtBuilder.build_and_execute(|| {
assert_ok!(StakingScore::register_as_noter(RuntimeOrigin::signed(NOTER)));
assert_noop!(
StakingScore::register_as_noter(RuntimeOrigin::signed(NOTER)),
Error::<Test>::AlreadyRegisteredNoter
);
});
}
#[test]
fn unregister_as_noter_returns_bond_when_no_pending_activity() {
ExtBuilder.build_and_execute(|| {
let free_before = Balances::free_balance(NOTER);
assert_ok!(StakingScore::register_as_noter(RuntimeOrigin::signed(NOTER)));
assert_ok!(StakingScore::unregister_as_noter(RuntimeOrigin::signed(NOTER)));
assert!(NoterBonds::<Test>::get(NOTER).is_none());
assert_eq!(Balances::reserved_balance(NOTER), 0);
assert_eq!(Balances::free_balance(NOTER), free_before);
});
}
#[test]
fn unregister_as_noter_blocked_within_dispute_window() {
ExtBuilder.build_and_execute(|| {
System::set_block_number(100);
ensure_registered(NOTER);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
100 * UNITS,
0,
0
));
// Still inside the window: blocked.
assert_noop!(
StakingScore::unregister_as_noter(RuntimeOrigin::signed(NOTER)),
Error::<Test>::PendingSubmissionExists
);
// After the window elapses: allowed.
System::set_block_number(100 + DISPUTE_WINDOW);
assert_ok!(StakingScore::unregister_as_noter(RuntimeOrigin::signed(NOTER)));
});
}
#[test]
fn unregister_as_noter_rejects_never_registered() {
ExtBuilder.build_and_execute(|| {
assert_noop!(
StakingScore::unregister_as_noter(RuntimeOrigin::signed(USER_STASH)),
Error::<Test>::NotRegisteredNoter
);
});
}
// ============================================================================
// Dispute Window
// ============================================================================
#[test]
fn finalize_before_window_elapses_fails() {
ExtBuilder.build_and_execute(|| {
System::set_block_number(100);
ensure_registered(NOTER);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
100 * UNITS,
0,
0
));
// One block before maturity.
System::set_block_number(100 + DISPUTE_WINDOW - 1);
assert_noop!(
StakingScore::finalize_staking_details(
RuntimeOrigin::signed(USER_STASH),
USER_STASH,
StakingSource::RelayChain
),
Error::<Test>::NotYetMatured
);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 0);
});
}
#[test]
fn finalize_is_permissionless() {
ExtBuilder.build_and_execute(|| {
System::set_block_number(100);
ensure_registered(NOTER);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
100 * UNITS,
0,
0
));
System::set_block_number(100 + DISPUTE_WINDOW);
// Finalized by an unrelated bystander account, not the noter or the
// affected user — anyone may trigger it once matured.
let bystander = 77;
assert_ok!(StakingScore::finalize_staking_details(
RuntimeOrigin::signed(bystander),
USER_STASH,
StakingSource::RelayChain
));
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 20);
});
}
#[test]
fn finalize_with_nothing_pending_fails() {
ExtBuilder.build_and_execute(|| {
assert_noop!(
StakingScore::finalize_staking_details(
RuntimeOrigin::signed(USER_STASH),
USER_STASH,
StakingSource::RelayChain
),
Error::<Test>::NoPendingSubmission
);
});
}
#[test]
fn dispute_discards_pending_submission_before_it_takes_effect() {
ExtBuilder.build_and_execute(|| {
System::set_block_number(100);
ensure_registered(NOTER);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
500 * UNITS,
0,
0
));
assert_ok!(StakingScore::dispute_staking_details(
RuntimeOrigin::signed(DISPUTE_MEMBER),
USER_STASH,
StakingSource::RelayChain
));
// Even well past the window, a disputed submission never takes effect.
System::set_block_number(100 + DISPUTE_WINDOW * 10);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 0);
assert_noop!(
StakingScore::finalize_staking_details(
RuntimeOrigin::signed(USER_STASH),
USER_STASH,
StakingSource::RelayChain
),
Error::<Test>::NoPendingSubmission
);
let events = System::events();
assert!(events.iter().any(|event| {
matches!(event.event, RuntimeEvent::StakingScore(Event::StakingDetailsDisputed { .. }))
}));
});
}
#[test]
fn dispute_rejects_non_dispute_origin() {
ExtBuilder.build_and_execute(|| {
ensure_registered(NOTER);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
500 * UNITS,
0,
0
));
// A random signed account (not DISPUTE_MEMBER, not Root) cannot dispute.
assert_noop!(
StakingScore::dispute_staking_details(
RuntimeOrigin::signed(USER_STASH),
USER_STASH,
StakingSource::RelayChain
),
pezsp_runtime::DispatchError::BadOrigin
);
});
}
#[test]
fn dispute_with_nothing_pending_fails() {
ExtBuilder.build_and_execute(|| {
assert_noop!(
StakingScore::dispute_staking_details(
RuntimeOrigin::signed(DISPUTE_MEMBER),
USER_STASH,
StakingSource::RelayChain
),
Error::<Test>::NoPendingSubmission
);
});
}
#[test]
fn repeated_legitimate_submissions_do_not_regress_score_during_window() {
// A noter sending a second update before anyone finalized the first
// shouldn't drop the account's effective score back to 0 for the second
// update's own window — `receive_staking_details` opportunistically
// finalizes an already-matured prior pending entry before recording the
// new one, so the first update's result is never lost.
ExtBuilder.build_and_execute(|| {
System::set_block_number(100);
ensure_registered(NOTER);
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
100 * UNITS, // tier 20
0,
0
));
// First submission's window has matured, but nobody called
// finalize_staking_details yet.
System::set_block_number(100 + DISPUTE_WINDOW);
// A second submission arrives before anyone finalized the first.
// Its own `receive_staking_details` call must opportunistically
// commit the first (matured) value before recording this candidate.
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::signed(NOTER),
USER_STASH,
StakingSource::RelayChain,
300 * UNITS, // tier 40
0,
0
));
// The first submission's value is now visible via CachedStakingDetails
// — not 0, even though the second submission's own window hasn't
// elapsed yet.
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 20);
// Second submission's window elapses; finalize it explicitly.
System::set_block_number(100 + DISPUTE_WINDOW + DISPUTE_WINDOW);
assert_ok!(StakingScore::finalize_staking_details(
RuntimeOrigin::signed(USER_STASH),
USER_STASH,
StakingSource::RelayChain
));
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 40);
});
}
// ============================================================================
// Root Path Unaffected (immediate, no bond, no window)
// ============================================================================
#[test]
fn root_submission_is_immediate_and_needs_no_bond() {
ExtBuilder.build_and_execute(|| {
// USER_STASH is not a registered noter at all — Root doesn't care.
assert_ok!(StakingScore::receive_staking_details(
RuntimeOrigin::root(),
USER_STASH,
StakingSource::RelayChain,
200 * UNITS,
0,
0
));
assert!(PendingStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_none());
assert!(CachedStakingDetails::<Test>::get(USER_STASH, StakingSource::RelayChain).is_some());
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 30);
});
}
// ============================================================================
// Slashing
// ============================================================================
#[test]
fn slash_noter_burns_bond_to_treasury() {
ExtBuilder.build_and_execute(|| {
System::set_block_number(1);
assert_ok!(StakingScore::register_as_noter(RuntimeOrigin::signed(NOTER)));
let treasury_before = Balances::free_balance(TREASURY);
let bond = NoterBondAmount::get();
assert_ok!(StakingScore::slash_noter(RuntimeOrigin::signed(SLASH_MEMBER), NOTER));
assert!(NoterBonds::<Test>::get(NOTER).is_none());
assert_eq!(Balances::reserved_balance(NOTER), 0);
assert_eq!(Balances::free_balance(TREASURY), treasury_before + bond);
let events = System::events();
assert!(events.iter().any(|event| {
matches!(event.event, RuntimeEvent::StakingScore(Event::NoterSlashed { .. }))
}));
});
}
#[test]
fn slash_noter_rejects_non_slash_origin() {
ExtBuilder.build_and_execute(|| {
assert_ok!(StakingScore::register_as_noter(RuntimeOrigin::signed(NOTER)));
// DISPUTE_MEMBER can freeze a submission but cannot slash — that's a
// deliberately stronger bar (SlashOrigin), not the lightweight
// DisputeOrigin.
assert_noop!(
StakingScore::slash_noter(RuntimeOrigin::signed(DISPUTE_MEMBER), NOTER),
pezsp_runtime::DispatchError::BadOrigin
);
});
}
#[test]
fn slash_noter_rejects_unregistered_account() {
ExtBuilder.build_and_execute(|| {
assert_noop!(
StakingScore::slash_noter(RuntimeOrigin::signed(SLASH_MEMBER), USER_STASH),
Error::<Test>::NotRegisteredNoter
);
});
}
#[test]
fn a_second_independent_noter_can_register_and_submit() {
// The system is designed to support any number of independently
// registered noters, not a single hardcoded account.
ExtBuilder.build_and_execute(|| {
submit_and_finalize(NOTER, USER_STASH, StakingSource::RelayChain, 100 * UNITS, 0, 0);
submit_and_finalize(NOTER_2, 20, StakingSource::RelayChain, 200 * UNITS, 0, 0);
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 20);
assert_eq!(StakingScore::get_staking_score(&20).0, 30);
assert!(NoterBonds::<Test>::get(NOTER).is_some());
assert!(NoterBonds::<Test>::get(NOTER_2).is_some());
});
}
@@ -1,6 +1,6 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,14 +15,34 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Manually estimated weights for `pezpallet_staking_score`
//! Autogenerated weights for `pezpallet_staking_score`
//!
//! These weights are conservative overestimates pending proper benchmark runs.
//! They account for the `OnStakingUpdate` callback cost (trust pallet update).
//!
//! DATE: 2026-02-16
//! UPDATED: 2026-02-16 (noter delegation + zero-stake cleanup)
//! TODO: Run proper benchmarks to replace these estimates.
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.1
//! DATE: 2026-07-03, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `vmi3386610`, CPU: `AMD EPYC Processor (with IBPB)`
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
// Executed Command:
// target/release/pezframe-omni-bencher
// v1
// benchmark
// pezpallet
// --runtime
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
// --pallet
// pezpallet_staking_score
// --extrinsic
// *
// --steps
// 50
// --repeat
// 20
// --output
// pezcumulus/teyrchains/pezpallets/staking-score/src/weights.rs
// --template
// bizinikiwi/.maintain/frame-weight-template.hbs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
@@ -37,77 +57,264 @@ use core::marker::PhantomData;
pub trait WeightInfo {
fn start_score_tracking() -> Weight;
fn receive_staking_details() -> Weight;
fn register_as_noter() -> Weight;
fn unregister_as_noter() -> Weight;
fn dispute_staking_details() -> Weight;
fn finalize_staking_details() -> Weight;
fn slash_noter() -> Weight;
}
/// Weights for `pezpallet_staking_score` using the Bizinikiwi node and recommended hardware.
pub struct BizinikiwiWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// `start_score_tracking` — user opt-in, no stake check needed.
///
/// Pallet operations:
/// StakingScore::StakingStartBlock (r:1 w:1) — check + insert
///
/// OnStakingUpdate callback (trust pallet):
/// StakingScore::CachedStakingDetails (r:2 w:0) — iter_prefix for score calc
/// StakingScore::StakingStartBlock (r:1 w:0) — duration lookup in score calc
/// Trust::TrustScores (r:1 w:1) — update trust score
/// Trust::TotalActiveTrustScore (r:1 w:1) — update aggregate
/// IdentityKyc::KycStatuses (r:1 w:0) — citizenship gate
///
/// Total: r:7 w:3
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:1)
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
/// Storage: `Trust::TrustScores` (r:1 w:1)
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::CachedStakingDetails` (r:2 w:0)
/// Proof: `StakingScore::CachedStakingDetails` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`)
/// Storage: `Referral::ReferrerStatsStorage` (r:1 w:0)
/// Proof: `Referral::ReferrerStatsStorage` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`)
/// Storage: `Perwerde::StudentCourses` (r:1 w:0)
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn start_score_tracking() -> Weight {
// ~30us execution + 7 reads + 3 writes
// Proof size: StakingStartBlock(52) + CachedStakingDetails(77*2) +
// TrustScores(48) + TotalActiveTrustScore(16) + KycStatuses(34) = ~7500
Weight::from_parts(30_000_000, 7_500)
.saturating_add(T::DbWeight::get().reads(7_u64))
// Proof Size summary in bytes:
// Measured: `475`
// Estimated: `6118`
// Minimum execution time: 103_333_000 picoseconds.
Weight::from_parts(129_582_000, 6118)
.saturating_add(T::DbWeight::get().reads(8_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// `receive_staking_details` — worst case: noter signed + zero-stake cleanup.
///
/// Origin check (noter path):
/// Tiki::UserTikis (r:1 w:0) — noter authority check via has_tiki()
///
/// Zero-stake cleanup path (worst case):
/// StakingScore::CachedStakingDetails (r:1 w:1) — remove entry for source
/// StakingScore::CachedStakingDetails (r:2 w:0) — iter_prefix remaining check
/// StakingScore::StakingStartBlock (r:1 w:1) — remove if no remaining stake
///
/// OnStakingUpdate callback (trust pallet):
/// StakingScore::CachedStakingDetails (r:2 w:0) — iter for score calc (overlaps)
/// StakingScore::StakingStartBlock (r:1 w:0) — duration lookup (overlaps)
/// Trust::TrustScores (r:1 w:1) — update trust score
/// Trust::TotalActiveTrustScore (r:1 w:1) — update aggregate
/// IdentityKyc::KycStatuses (r:1 w:0) — citizenship gate
///
/// Note: Some reads overlap (CachedStakingDetails iter + StakingStartBlock read
/// are done both in cleanup and in the trust callback score calculation).
/// Counting unique reads conservatively:
///
/// Total: r:10 w:4
/// Storage: `StakingScore::CachedStakingDetails` (r:3 w:1)
/// Proof: `StakingScore::CachedStakingDetails` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
/// Storage: `Trust::TrustScores` (r:1 w:1)
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Referral::ReferrerStatsStorage` (r:1 w:0)
/// Proof: `Referral::ReferrerStatsStorage` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`)
/// Storage: `Perwerde::StudentCourses` (r:1 w:0)
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn receive_staking_details() -> Weight {
// ~40us execution + 10 reads + 4 writes
// Proof size: Tiki::UserTikis(200) + CachedStakingDetails(77*2) +
// StakingStartBlock(52) + TrustScores(48) +
// TotalActiveTrustScore(16) + KycStatuses(34) = ~9500
Weight::from_parts(40_000_000, 9_500)
// Proof Size summary in bytes:
// Measured: `475`
// Estimated: `8682`
// Minimum execution time: 127_528_000 picoseconds.
Weight::from_parts(201_235_000, 8682)
.saturating_add(T::DbWeight::get().reads(9_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::NoterBonds` (r:1 w:1)
/// Proof: `StakingScore::NoterBonds` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
fn register_as_noter() -> Weight {
// Proof Size summary in bytes:
// Measured: `322`
// Estimated: `3534`
// Minimum execution time: 85_700_000 picoseconds.
Weight::from_parts(132_988_000, 3534)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: `StakingScore::NoterBonds` (r:1 w:1)
/// Proof: `StakingScore::NoterBonds` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::NoterLastSubmission` (r:1 w:1)
/// Proof: `StakingScore::NoterLastSubmission` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
fn unregister_as_noter() -> Weight {
// Proof Size summary in bytes:
// Measured: `166`
// Estimated: `3529`
// Minimum execution time: 69_740_000 picoseconds.
Weight::from_parts(72_445_000, 3529)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: `StakingScore::PendingStakingDetails` (r:1 w:1)
/// Proof: `StakingScore::PendingStakingDetails` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`)
fn dispute_staking_details() -> Weight {
// Proof Size summary in bytes:
// Measured: `232`
// Estimated: `3590`
// Minimum execution time: 30_306_000 picoseconds.
Weight::from_parts(41_478_000, 3590)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: `StakingScore::PendingStakingDetails` (r:1 w:1)
/// Proof: `StakingScore::PendingStakingDetails` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::CachedStakingDetails` (r:3 w:1)
/// Proof: `StakingScore::CachedStakingDetails` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
/// Storage: `Trust::TrustScores` (r:1 w:1)
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Referral::ReferrerStatsStorage` (r:1 w:0)
/// Proof: `Referral::ReferrerStatsStorage` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`)
/// Storage: `Perwerde::StudentCourses` (r:1 w:0)
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn finalize_staking_details() -> Weight {
// Proof Size summary in bytes:
// Measured: `674`
// Estimated: `8682`
// Minimum execution time: 172_351_000 picoseconds.
Weight::from_parts(202_728_000, 8682)
.saturating_add(T::DbWeight::get().reads(10_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
}
/// Storage: `StakingScore::NoterBonds` (r:1 w:1)
/// Proof: `StakingScore::NoterBonds` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::NoterLastSubmission` (r:0 w:1)
/// Proof: `StakingScore::NoterLastSubmission` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
fn slash_noter() -> Weight {
// Proof Size summary in bytes:
// Measured: `166`
// Estimated: `3593`
// Minimum execution time: 151_553_000 picoseconds.
Weight::from_parts(158_345_000, 3593)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
}
// For backwards compatibility and tests.
impl WeightInfo for () {
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:1)
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
/// Storage: `Trust::TrustScores` (r:1 w:1)
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::CachedStakingDetails` (r:2 w:0)
/// Proof: `StakingScore::CachedStakingDetails` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`)
/// Storage: `Referral::ReferrerStatsStorage` (r:1 w:0)
/// Proof: `Referral::ReferrerStatsStorage` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`)
/// Storage: `Perwerde::StudentCourses` (r:1 w:0)
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn start_score_tracking() -> Weight {
Weight::from_parts(30_000_000, 7_500)
.saturating_add(RocksDbWeight::get().reads(7_u64))
// Proof Size summary in bytes:
// Measured: `475`
// Estimated: `6118`
// Minimum execution time: 103_333_000 picoseconds.
Weight::from_parts(129_582_000, 6118)
.saturating_add(RocksDbWeight::get().reads(8_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: `StakingScore::CachedStakingDetails` (r:3 w:1)
/// Proof: `StakingScore::CachedStakingDetails` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
/// Storage: `Trust::TrustScores` (r:1 w:1)
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Referral::ReferrerStatsStorage` (r:1 w:0)
/// Proof: `Referral::ReferrerStatsStorage` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`)
/// Storage: `Perwerde::StudentCourses` (r:1 w:0)
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn receive_staking_details() -> Weight {
Weight::from_parts(40_000_000, 9_500)
// Proof Size summary in bytes:
// Measured: `475`
// Estimated: `8682`
// Minimum execution time: 127_528_000 picoseconds.
Weight::from_parts(201_235_000, 8682)
.saturating_add(RocksDbWeight::get().reads(9_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::NoterBonds` (r:1 w:1)
/// Proof: `StakingScore::NoterBonds` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
fn register_as_noter() -> Weight {
// Proof Size summary in bytes:
// Measured: `322`
// Estimated: `3534`
// Minimum execution time: 85_700_000 picoseconds.
Weight::from_parts(132_988_000, 3534)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: `StakingScore::NoterBonds` (r:1 w:1)
/// Proof: `StakingScore::NoterBonds` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::NoterLastSubmission` (r:1 w:1)
/// Proof: `StakingScore::NoterLastSubmission` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
fn unregister_as_noter() -> Weight {
// Proof Size summary in bytes:
// Measured: `166`
// Estimated: `3529`
// Minimum execution time: 69_740_000 picoseconds.
Weight::from_parts(72_445_000, 3529)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: `StakingScore::PendingStakingDetails` (r:1 w:1)
/// Proof: `StakingScore::PendingStakingDetails` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`)
fn dispute_staking_details() -> Weight {
// Proof Size summary in bytes:
// Measured: `232`
// Estimated: `3590`
// Minimum execution time: 30_306_000 picoseconds.
Weight::from_parts(41_478_000, 3590)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: `StakingScore::PendingStakingDetails` (r:1 w:1)
/// Proof: `StakingScore::PendingStakingDetails` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::CachedStakingDetails` (r:3 w:1)
/// Proof: `StakingScore::CachedStakingDetails` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
/// Storage: `Trust::TrustScores` (r:1 w:1)
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Referral::ReferrerStatsStorage` (r:1 w:0)
/// Proof: `Referral::ReferrerStatsStorage` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`)
/// Storage: `Perwerde::StudentCourses` (r:1 w:0)
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
/// Storage: `Tiki::UserTikis` (r:1 w:0)
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
fn finalize_staking_details() -> Weight {
// Proof Size summary in bytes:
// Measured: `674`
// Estimated: `8682`
// Minimum execution time: 172_351_000 picoseconds.
Weight::from_parts(202_728_000, 8682)
.saturating_add(RocksDbWeight::get().reads(10_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
/// Storage: `StakingScore::NoterBonds` (r:1 w:1)
/// Proof: `StakingScore::NoterBonds` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `StakingScore::NoterLastSubmission` (r:0 w:1)
/// Proof: `StakingScore::NoterLastSubmission` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
fn slash_noter() -> Weight {
// Proof Size summary in bytes:
// Measured: `166`
// Estimated: `3593`
// Minimum execution time: 151_553_000 picoseconds.
Weight::from_parts(158_345_000, 3593)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
}
@@ -10,13 +10,13 @@ use super::*;
use crate::Pezpallet as Tiki;
use pezframe_benchmarking::v2::*;
use pezframe_system::RawOrigin;
// Gerekli trait'leri import ediyoruz
// Import the required traits
use pezframe_support::traits::{Currency, Get};
use pezpallet_balances::Pezpallet as Balances;
use pezsp_runtime::traits::StaticLookup;
extern crate alloc;
// Gerekli trait kısıtlamalarını ana benchmarks bloğuna ekliyoruz.
// Add the required trait bounds to the main benchmarks block.
#[benchmarks(
where
T::CollectionId: Copy + Default + PartialOrd,
@@ -25,13 +25,13 @@ extern crate alloc;
mod benchmarks {
use super::*;
// Bu yardımcı fonksiyon, runtime'da tanımlanan Tiki koleksiyonunu oluşturur.
// This helper function creates the Tiki collection defined in the runtime.
fn ensure_collection_exists<T: Config + pezpallet_balances::Config>()
where
T::CollectionId: Default + PartialOrd,
{
let collection_id = T::TikiCollectionId::get();
// Koleksiyon sahibi olarak fonlanmış `whitelisted_caller`'ı kullanıyoruz.
// Use the funded `whitelisted_caller` as the collection owner.
let caller: T::AccountId = whitelisted_caller();
// Fund the caller account with sufficient balance for NFT deposits
@@ -39,7 +39,7 @@ mod benchmarks {
let funding = Balances::<T>::minimum_balance() * 1_000_000_000u32.into();
Balances::<T>::make_free_balance_be(&caller, funding);
// `while` döngüsü, 'Step' trait'ine olan ihtiyacı ortadan kaldırır.
// The `while` loop removes the need for the 'Step' trait.
while pezpallet_nfts::NextCollectionId::<T>::get().unwrap_or_default() <= collection_id {
let _ = pezpallet_nfts::Pezpallet::<T>::force_create(
RawOrigin::Root.into(),
@@ -75,9 +75,9 @@ mod benchmarks {
#[benchmark]
fn grant_tiki() -> Result<(), BenchmarkError> {
// NFT'yi alacak 'dest' hesabı olarak fonlanmış `whitelisted_caller`'ı kullanıyoruz.
// Use the funded `whitelisted_caller` as the 'dest' account that will receive the NFT.
let dest: T::AccountId = whitelisted_caller();
// Appointed role kullan (Serok yerine Wezir)
// Use an appointed role (Wezir instead of Serok)
let tiki = crate::Tiki::Wezir;
// Ensure the dest account has a citizen NFT before granting a tiki
@@ -93,7 +93,7 @@ mod benchmarks {
#[benchmark]
fn revoke_tiki() -> Result<(), BenchmarkError> {
// NFT'yi alacak 'dest' hesabı olarak fonlanmış `whitelisted_caller`'ı kullanıyoruz.
// Use the funded `whitelisted_caller` as the 'dest' account that will receive the NFT.
let dest: T::AccountId = whitelisted_caller();
let tiki = crate::Tiki::Wezir; // Use appointed role
@@ -119,13 +119,13 @@ mod benchmarks {
// Ensure collection exists first
ensure_collection_exists::<T>();
// Henüz vatandaş olmamalı
// Should not be a citizen yet
assert!(Tiki::<T>::citizen_nft(&dest).is_none());
#[extrinsic_call]
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()));
// Vatandaş olduğundan emin ol
// Ensure they are now a citizen
assert!(Tiki::<T>::citizen_nft(&dest).is_some());
assert!(Tiki::<T>::is_citizen(&dest));
@@ -135,15 +135,15 @@ mod benchmarks {
#[benchmark]
fn grant_earned_role() -> Result<(), BenchmarkError> {
let dest: T::AccountId = whitelisted_caller();
let tiki = crate::Tiki::Axa; // Earned bir rol
let tiki = crate::Tiki::Axa; // An earned role
// Ön koşul: Vatandaş olmalı
// Precondition: must be a citizen
ensure_citizen_nft::<T>(dest.clone())?;
#[extrinsic_call]
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()), tiki);
// Rolün verildiğini doğrula
// Verify the role was granted
assert!(Tiki::<T>::has_tiki(&dest, &tiki));
Ok(())
@@ -152,15 +152,15 @@ mod benchmarks {
#[benchmark]
fn grant_elected_role() -> Result<(), BenchmarkError> {
let dest: T::AccountId = whitelisted_caller();
let tiki = crate::Tiki::Parlementer; // Elected bir rol
let tiki = crate::Tiki::Parlementer; // An elected role
// Ön koşul: Vatandaş olmalı
// Precondition: must be a citizen
ensure_citizen_nft::<T>(dest.clone())?;
#[extrinsic_call]
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()), tiki);
// Rolün verildiğini doğrula
// Verify the role was granted
assert!(Tiki::<T>::has_tiki(&dest, &tiki));
Ok(())
+118 -25
View File
@@ -97,6 +97,10 @@
//! impl pezpallet_tiki::Config for Runtime {
//! type RuntimeEvent = RuntimeEvent;
//! type AdminOrigin = EnsureRoot<AccountId>;
//! // Elected/Earned roles use their own origin, kept distinct from ordinary
//! // admin appointment so a real election/exam pipeline can gate them later.
//! type ElectedRoleOrigin = EnsureRoot<AccountId>;
//! type EarnedRoleOrigin = EnsureRoot<AccountId>;
//! type WeightInfo = pezpallet_tiki::weights::BizinikiwiWeight<Runtime>;
//! type TikiCollectionId = ConstU32<1>; // Tiki collection ID
//! type MaxTikisPerUser = ConstU32<20>; // Max 20 roles per user
@@ -154,6 +158,20 @@ pub mod pezpallet {
+ pezpallet_identity_kyc::Config
{
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// Origin required to grant a role through the election system
/// (`grant_elected_role`). Deliberately kept distinct from `AdminOrigin`: an
/// Elected office (Serok, SerokiMeclise, Parlementer, ...) is meant to carry
/// evidence of a genuine election, not merely ordinary admin/committee say-so.
/// Runtimes should wire this to whatever origin their election pipeline
/// escalates to (e.g. `EnsureRoot`), rather than reusing `AdminOrigin`.
type ElectedRoleOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// Origin required to grant a role through the exam/earned system
/// (`grant_earned_role`). Deliberately kept distinct from `AdminOrigin` for the
/// same reason as `ElectedRoleOrigin`.
type EarnedRoleOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type WeightInfo: weights::WeightInfo;
/// Collection ID holding Tiki (Role) NFTs.
@@ -265,6 +283,21 @@ pub mod pezpallet {
WezireTend,
WezireAva,
WezireCand,
// Newly added functional / professional roles. Appended at the end to preserve
// the SCALE encoding (discriminant order) of existing on-chain values. The trust
// bonuses assigned below are provisional and should be ratified by governance.
Bernamenivîs, // Software developer / engineer (builds the chain itself)
Wergêr, // Translator (a six-language nation needs this)
Aborînas, // Economist
Hesabdar, // Accountant
Rojnamevan, // Journalist
PisporêBazarkirinê, // Marketing specialist
Statîstîknas, // Statistician
Piştrastkar, // KYC verifier
Hilbijartinkar, // Election officer
Îcrakar, // Executor / enforcement officer
Karguzar, // Human-resources officer
Plansaz, // Budget planner
}
impl From<Tiki> for u32 {
@@ -301,6 +334,23 @@ pub mod pezpallet {
#[pezpallet::getter(fn next_item_id)]
pub type NextItemId<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Records, per (account, tiki), which `RoleAssignmentType` path was used the last
/// time that role was granted to that account (Automatic/Appointed/Elected/Earned).
/// This gives an on-chain, queryable audit trail distinguishing a role that came
/// through `grant_elected_role`/`grant_earned_role` from one granted directly via
/// `grant_tiki`, even though today all of these entry points share similar origins.
#[pezpallet::storage]
#[pezpallet::getter(fn role_assignment_type_of)]
pub type RoleAssignmentTypeOf<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Blake2_128Concat,
Tiki,
RoleAssignmentType,
OptionQuery,
>;
#[pezpallet::error]
pub enum Error<T> {
/// Role already belongs to someone else
@@ -422,7 +472,7 @@ pub mod pezpallet {
#[pezpallet::call]
impl<T: Config> Pezpallet<T> {
/// Admin tarafından belirli bir kullanıcıya Tiki (rol) verme
/// Grant a Tiki (role) to a specific user by an admin
#[pezpallet::call_index(0)]
#[pezpallet::weight(<T as crate::pezpallet::Config>::WeightInfo::grant_tiki())]
pub fn grant_tiki(
@@ -440,10 +490,11 @@ pub mod pezpallet {
);
Self::internal_grant_role(&dest_account, tiki)?;
RoleAssignmentTypeOf::<T>::insert(&dest_account, tiki, RoleAssignmentType::Appointed);
Ok(())
}
/// Admin tarafından belirli bir kullanıcıdan Tiki (rol) alma
/// Remove a Tiki (role) from a specific user by an admin
#[pezpallet::call_index(1)]
#[pezpallet::weight(<T as crate::pezpallet::Config>::WeightInfo::revoke_tiki())]
pub fn revoke_tiki(
@@ -480,7 +531,9 @@ pub mod pezpallet {
dest: <T::Lookup as StaticLookup>::Source,
tiki: Tiki,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?; // pezpallet-voting will call with Root origin
// Distinct from AdminOrigin: an Elected role must come through the
// election-specific origin (see `ElectedRoleOrigin` doc comment).
T::ElectedRoleOrigin::ensure_origin(origin)?;
let dest_account = T::Lookup::lookup(dest)?;
// Check if the role can be granted through election
@@ -490,6 +543,7 @@ pub mod pezpallet {
);
Self::internal_grant_role(&dest_account, tiki)?;
RoleAssignmentTypeOf::<T>::insert(&dest_account, tiki, RoleAssignmentType::Elected);
Ok(())
}
@@ -501,7 +555,9 @@ pub mod pezpallet {
dest: <T::Lookup as StaticLookup>::Source,
tiki: Tiki,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?; // For now admin, later exam pezpallet
// Distinct from AdminOrigin: an Earned role must come through the
// exam-specific origin (see `EarnedRoleOrigin` doc comment).
T::EarnedRoleOrigin::ensure_origin(origin)?;
let dest_account = T::Lookup::lookup(dest)?;
// Check if the role can be earned
@@ -511,6 +567,7 @@ pub mod pezpallet {
);
Self::internal_grant_role(&dest_account, tiki)?;
RoleAssignmentTypeOf::<T>::insert(&dest_account, tiki, RoleAssignmentType::Earned);
Ok(())
}
@@ -543,7 +600,7 @@ pub mod pezpallet {
from: T::AccountId,
to: T::AccountId,
) -> DispatchResult {
// Tiki NFT koleksiyonu ise transfer'e izin verme
// If it is the Tiki NFT collection, do not allow the transfer
if collection_id == T::TikiCollectionId::get() {
Self::deposit_event(Event::TransferBlocked { collection_id, item_id, from, to });
return Err(DispatchError::Other("Citizen NFTs are non-transferable"));
@@ -582,6 +639,7 @@ pub mod pezpallet {
UserTikis::<T>::mutate(user, |tikis| {
let _ = tikis.try_push(Tiki::Welati);
});
RoleAssignmentTypeOf::<T>::insert(user, Tiki::Welati, RoleAssignmentType::Automatic);
// Set NFT metadata
Self::update_nft_metadata(user)?;
@@ -647,6 +705,9 @@ pub mod pezpallet {
TikiHolder::<T>::remove(tiki);
}
// Clear the recorded assignment-type provenance for this (account, tiki) pair
RoleAssignmentTypeOf::<T>::remove(target_account, tiki);
// Update NFT metadata
Self::update_nft_metadata(target_account)?;
@@ -702,7 +763,24 @@ pub mod pezpallet {
/// Checks if a specific role is unique (can belong to only one person)
pub fn is_unique_role(tiki: &Tiki) -> bool {
matches!(tiki, Tiki::Serok | Tiki::SerokiMeclise | Tiki::Xezinedar | Tiki::Balyoz)
matches!(
tiki,
Tiki::Serok
| Tiki::SerokiMeclise
| Tiki::Xezinedar
| Tiki::Balyoz
// Government roles: single-portfolio cabinet offices, each of which
// must have at most one holder at a time (see get_bonus_for_tiki,
// which grants each of these a fixed per-office trust bonus).
| Tiki::SerokWeziran
| Tiki::WezireDarayiye
| Tiki::WezireParez
| Tiki::WezireDad
| Tiki::WezireBelaw
| Tiki::WezireTend
| Tiki::WezireAva
| Tiki::WezireCand
)
}
/// Returns the assignment type of a specific role
@@ -744,12 +822,12 @@ pub mod pezpallet {
}
}
/// KYC sonrası otomatik Welati rolü verme
/// Automatically grant the Welati role after KYC
pub fn auto_grant_citizenship(account: &T::AccountId) -> DispatchResult {
// KYC kontrolü
// KYC check
let kyc_status = pezpallet_identity_kyc::Pezpallet::<T>::kyc_status_of(account);
if kyc_status == pezpallet_identity_kyc::types::KycLevel::Approved {
// Vatandaşlık NFT'si yoksa bas
// Mint the citizenship NFT if it does not exist
if Self::citizen_nft(account).is_none() {
Self::mint_citizen_nft_for_user(account)?;
}
@@ -757,31 +835,31 @@ pub mod pezpallet {
Ok(())
}
/// Kullanıcının belirli bir Tiki'ye sahip olup olmadığını kontrol eder
/// Checks whether the user holds a specific Tiki
pub fn has_tiki(who: &T::AccountId, tiki: &Tiki) -> bool {
Self::user_tikis(who).contains(tiki)
}
/// Kullanıcının vatandaş olup olmadığını kontrol eder
/// Checks whether the user is a citizen
pub fn is_citizen(who: &T::AccountId) -> bool {
Self::citizen_nft(who).is_some()
}
}
}
/// Diğer paletlerin, bu paletten Tiki puanlarını sorgulaması için kullanılacak trait
/// Trait used by other pallets to query Tiki scores from this pallet
pub trait TikiScoreProvider<AccountId> {
fn get_tiki_score(who: &AccountId) -> u32;
}
/// Diğer paletlerin, Tiki sahipliğini sorgulaması için kullanılacak trait
/// Trait used by other pallets to query Tiki ownership
pub trait TikiProvider<AccountId> {
fn has_tiki(who: &AccountId, tiki: &Tiki) -> bool;
fn get_user_tikis(who: &AccountId) -> Vec<Tiki>;
fn is_citizen(who: &AccountId) -> bool;
}
/// Trait implementasyonları
/// Trait implementations
impl<T: Config> TikiScoreProvider<T::AccountId> for Pezpallet<T> {
fn get_tiki_score(who: &T::AccountId) -> u32 {
let tikis = Self::user_tikis(who);
@@ -806,12 +884,12 @@ impl<T: Config> TikiProvider<T::AccountId> for Pezpallet<T> {
}
}
// Puanlama mantığını ayrı bir impl bloğunda tutarak kodu daha düzenli hale getiriyoruz.
// Keeping the scoring logic in a separate impl block to keep the code more organized.
impl<T: Config> Pezpallet<T> {
/// Belirli bir Tiki'nin Trust Puanı'na olan katkısını döndürür.
/// Returns the contribution of a specific Tiki to the Trust Score.
pub fn get_bonus_for_tiki(tiki: &Tiki) -> u32 {
match tiki {
// Anayasa v5.0'da Belirlenen Özel Puanlar
// Special scores defined in Anayasa v5.0
Tiki::Axa => 250,
Tiki::RêveberêProjeyê => 250,
Tiki::ModeratorêCivakê => 200,
@@ -819,13 +897,13 @@ impl<T: Config> Pezpallet<T> {
Tiki::Mela => 50,
Tiki::Feqî => 50,
// Hiyerarşik Devlet Puanları
// Yargı
// Hierarchical State Scores
// Judiciary
Tiki::EndameDiwane => 175,
Tiki::Dadger => 150,
Tiki::Dozger => 120,
Tiki::Hiquqnas => 75,
// Yürütme
// Executive
Tiki::Serok => 200,
Tiki::Wezir => 100,
Tiki::SerokWeziran => 125,
@@ -837,18 +915,18 @@ impl<T: Config> Pezpallet<T> {
Tiki::WezireAva => 100,
Tiki::WezireCand => 100,
// Yasama
// Legislature
Tiki::SerokiMeclise => 150,
Tiki::Parlementer => 100,
// Atanmış Üst Düzey Memurlar
// Appointed Senior Officials
Tiki::Xezinedar => 100,
Tiki::PisporêEwlehiyaSîber => 100,
Tiki::Mufetîs => 90,
Tiki::Balyoz => 80,
Tiki::Berdevk => 70,
// Diğer Memurlar ve Uzmanlar
// Other Officials and Experts
Tiki::Mamoste => 70,
Tiki::OperatorêTorê => 60,
Tiki::Noter => 50,
@@ -866,7 +944,21 @@ impl<T: Config> Pezpallet<T> {
Tiki::Bazargan => 60,
Tiki::Pêseng => 80,
// Temel Vatandaşlık
// Newly added functional / professional roles (provisional bonuses — ratify via governance)
Tiki::Bernamenivîs => 80,
Tiki::Aborînas => 75,
Tiki::Plansaz => 70,
Tiki::Piştrastkar => 60,
Tiki::Hilbijartinkar => 60,
Tiki::Îcrakar => 60,
Tiki::Wergêr => 50,
Tiki::Hesabdar => 50,
Tiki::Rojnamevan => 50,
Tiki::Statîstîknas => 50,
Tiki::PisporêBazarkirinê => 40,
Tiki::Karguzar => 40,
// Basic Citizenship
Tiki::Welati => 10,
}
}
@@ -895,12 +987,13 @@ impl<T: Config> pezpallet_identity_kyc::types::CitizenNftProvider<T::AccountId>
item_id,
)?;
// Clear unique role mappings before removing roles
// Clear unique role mappings and assignment-type provenance before removing roles
let user_tikis = UserTikis::<T>::get(who);
for tiki in user_tikis.iter() {
if Self::is_unique_role(tiki) {
TikiHolder::<T>::remove(tiki);
}
RoleAssignmentTypeOf::<T>::remove(who, tiki);
}
// Remove all roles and citizen NFT mapping
@@ -64,7 +64,7 @@ type Block = pezframe_system::mocking::MockBlock<Test>;
pub type AccountId = u64;
pub type Balance = u128;
// Runtime'ı oluştur - Identity ve IdentityKyc pezpallet'lerini de ekle
// Build the runtime - also add the Identity and IdentityKyc pezpallets
construct_runtime!(
pub enum Test
{
@@ -106,7 +106,7 @@ impl pezframe_system::Config for Test {
type MultiBlockMigrator = ();
type PreInherents = ();
type PostInherents = ();
type PostTransactions = (); // Eksik olan trait
type PostTransactions = (); // The missing trait
type ExtensionsWeightInfo = ();
}
@@ -127,7 +127,7 @@ impl pezpallet_balances::Config for Test {
type DoneSlashHandler = ();
}
// pezpallet_identity::Config implementasyonu
// pezpallet_identity::Config implementation
parameter_types! {
pub const BasicDeposit: Balance = 1000;
pub const ByteDeposit: Balance = 10;
@@ -270,6 +270,8 @@ parameter_types! {
impl crate::Config for Test {
type AdminOrigin = pezframe_system::EnsureRoot<AccountId>;
type ElectedRoleOrigin = pezframe_system::EnsureRoot<AccountId>;
type EarnedRoleOrigin = pezframe_system::EnsureRoot<AccountId>;
type WeightInfo = ();
type TikiCollectionId = TikiCollectionId;
type MaxTikisPerUser = MaxTikisPerUser;
@@ -291,7 +293,7 @@ pub fn new_test_ext() -> pezsp_io::TestExternalities {
ext.execute_with(|| {
System::set_block_number(1);
// Tiki koleksiyonunu oluştur - mint permissions ile
// Create the Tiki collection - with mint permissions
assert_ok!(Nfts::force_create(
RuntimeOrigin::root(),
1, // owner
+128 -128
View File
@@ -11,29 +11,29 @@ use pezsp_runtime::DispatchError;
type TikiPallet = crate::Pezpallet<Test>;
// === Temel NFT ve Rol Testleri ===
// === Basic NFT and Role Tests ===
#[test]
fn force_mint_citizen_nft_works() {
new_test_ext().execute_with(|| {
let user_account = 2;
// Başlangıçta vatandaşlık NFT'si olmamalı
// Initially there should be no citizenship NFT
assert_eq!(TikiPallet::citizen_nft(user_account), None);
assert!(TikiPallet::user_tikis(user_account).is_empty());
assert!(!TikiPallet::is_citizen(&user_account));
// Vatandaşlık NFT'si bas
// Mint citizenship NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
// NFT'nin basıldığını ve Welati rolünün eklendiğini kontrol et
// Verify the NFT was minted and the Welati role was added
assert!(TikiPallet::citizen_nft(user_account).is_some());
assert!(TikiPallet::is_citizen(&user_account));
let user_tikis = TikiPallet::user_tikis(user_account);
assert!(user_tikis.contains(&TikiEnum::Welati));
assert!(TikiPallet::has_tiki(&user_account, &TikiEnum::Welati));
// Event'in doğru atıldığını kontrol et
// Verify the event was emitted correctly
System::assert_has_event(
Event::CitizenNftMinted {
who: user_account,
@@ -50,19 +50,19 @@ fn grant_appointed_role_works() {
let user_account = 2;
let tiki_to_grant = TikiEnum::Wezir; // Appointed role
// Önce vatandaşlık NFT'si bas
// First mint the citizenship NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
// Tiki ver
// Grant the Tiki
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user_account, tiki_to_grant));
// Kullanıcının rollerini kontrol et
// Verify the user's roles
let user_tikis = TikiPallet::user_tikis(user_account);
assert!(user_tikis.contains(&TikiEnum::Welati)); // Otomatik eklenen
assert!(user_tikis.contains(&tiki_to_grant)); // Manuel eklenen
assert!(user_tikis.contains(&TikiEnum::Welati)); // Automatically added
assert!(user_tikis.contains(&tiki_to_grant)); // Manually added
assert!(TikiPallet::has_tiki(&user_account, &tiki_to_grant));
// Event'in doğru atıldığını kontrol et
// Verify the event was emitted correctly
System::assert_has_event(
Event::TikiGranted { who: user_account, tiki: tiki_to_grant }.into(),
);
@@ -75,10 +75,10 @@ fn cannot_grant_elected_role_through_admin() {
let user_account = 2;
let elected_role = TikiEnum::Parlementer; // Elected role
// Vatandaşlık NFT'si bas
// Mint citizenship NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
// Seçilen rolü admin ile vermeye çalış - başarısız olmalı
// Attempt to grant the elected role via admin - should fail
assert_noop!(
TikiPallet::grant_tiki(RuntimeOrigin::root(), user_account, elected_role),
Error::<Test>::InvalidRoleAssignmentMethod
@@ -86,18 +86,18 @@ fn cannot_grant_elected_role_through_admin() {
});
}
// === KYC ve Identity Testleri ===
// === KYC and Identity Tests ===
#[test]
fn apply_for_citizenship_works_with_kyc() {
new_test_ext().execute_with(|| {
let user_account = 2;
// Basit KYC test - Identity setup'ını skip edelim, sadece force mint test edelim
// Direkt force mint ile test edelim (KYC bypass)
// Simple KYC test - skip the Identity setup, just test force mint
// Test directly via force mint (KYC bypass)
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
// NFT'nin basıldığını kontrol et
// Verify the NFT was minted
assert!(TikiPallet::citizen_nft(user_account).is_some());
assert!(TikiPallet::user_tikis(user_account).contains(&TikiEnum::Welati));
assert!(TikiPallet::is_citizen(&user_account));
@@ -109,7 +109,7 @@ fn apply_for_citizenship_fails_without_kyc() {
new_test_ext().execute_with(|| {
let user_account = 2;
// KYC olmadan vatandaşlık başvurusu yap
// Apply for citizenship without KYC
assert_noop!(
TikiPallet::apply_for_citizenship(RuntimeOrigin::signed(user_account)),
Error::<Test>::KycNotCompleted
@@ -122,16 +122,16 @@ fn auto_grant_citizenship_simplified() {
new_test_ext().execute_with(|| {
let user = 2;
// Identity setup complex olduğu için, sadece fonksiyonun çalıştığını test edelim
// KYC olmadan çağrıldığında hata vermemeli (sadece hiçbir şey yapmamalı)
// Since the Identity setup is complex, just test that the function runs
// When called without KYC it should not error (it should simply do nothing)
assert_ok!(TikiPallet::auto_grant_citizenship(&user));
// KYC olmadığı için NFT basılmamalı
// No NFT should be minted because there is no KYC
assert!(TikiPallet::citizen_nft(user).is_none());
});
}
// === Role Assignment Types Testleri ===
// === Role Assignment Types Tests ===
#[test]
fn role_assignment_types_work_correctly() {
@@ -186,13 +186,13 @@ fn grant_earned_role_works() {
let user_account = 2;
let earned_role = TikiEnum::Axa; // Earned role
// Vatandaşlık NFT'si bas
// Mint citizenship NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
// Earned rolü ver
// Grant the earned role
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), user_account, earned_role));
// Rolün eklendiğini kontrol et
// Verify the role was added
assert!(TikiPallet::user_tikis(user_account).contains(&earned_role));
assert!(TikiPallet::has_tiki(&user_account, &earned_role));
});
@@ -204,23 +204,23 @@ fn grant_elected_role_works() {
let user_account = 2;
let elected_role = TikiEnum::Parlementer; // Elected role
// Vatandaşlık NFT'si bas
// Mint citizenship NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
// Elected rolü ver (pezpallet-voting tarafından çağrılacak)
// Grant the elected role (will be called by pezpallet-voting)
assert_ok!(TikiPallet::grant_elected_role(
RuntimeOrigin::root(),
user_account,
elected_role
));
// Rolün eklendiğini kontrol et
// Verify the role was added
assert!(TikiPallet::user_tikis(user_account).contains(&elected_role));
assert!(TikiPallet::has_tiki(&user_account, &elected_role));
});
}
// === Unique Roles Testleri ===
// === Unique Roles Tests ===
#[test]
fn unique_roles_work_correctly() {
@@ -229,20 +229,20 @@ fn unique_roles_work_correctly() {
let user2 = 3;
let unique_role = TikiEnum::Serok; // Unique role
// Her iki kullanıcı için NFT bas
// Mint NFT for both users
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user1));
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user2));
// İlk kullanıcıya unique rolü ver (elected role olarak)
// Grant the unique role to the first user (as an elected role)
assert_ok!(TikiPallet::grant_elected_role(RuntimeOrigin::root(), user1, unique_role));
// İkinci kullanıcıya aynı rolü vermeye çalış
// Attempt to grant the same role to the second user
assert_noop!(
TikiPallet::grant_elected_role(RuntimeOrigin::root(), user2, unique_role),
Error::<Test>::RoleAlreadyTaken
);
// TikiHolder'da doğru şekilde kaydedildiğini kontrol et
// Verify it was recorded correctly in TikiHolder
assert_eq!(TikiPallet::tiki_holder(unique_role), Some(user1));
});
}
@@ -270,23 +270,23 @@ fn revoke_tiki_works() {
let user_account = 2;
let tiki_to_revoke = TikiEnum::Wezir;
// NFT bas ve role ver
// Mint NFT and grant the role
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user_account, tiki_to_revoke));
// Rolün eklendiğini kontrol et
// Verify the role was added
assert!(TikiPallet::user_tikis(user_account).contains(&tiki_to_revoke));
// Rolü kaldır
// Revoke the role
assert_ok!(TikiPallet::revoke_tiki(RuntimeOrigin::root(), user_account, tiki_to_revoke));
// Rolün kaldırıldığını kontrol et
// Verify the role was removed
assert!(!TikiPallet::user_tikis(user_account).contains(&tiki_to_revoke));
assert!(!TikiPallet::has_tiki(&user_account, &tiki_to_revoke));
// Welati rolünün hala durduğunu kontrol et
// Verify the Welati role is still present
assert!(TikiPallet::user_tikis(user_account).contains(&TikiEnum::Welati));
// Event kontrol et
// Verify the event
System::assert_has_event(
Event::TikiRevoked { who: user_account, tiki: tiki_to_revoke }.into(),
);
@@ -298,10 +298,10 @@ fn cannot_revoke_hemwelati_role() {
new_test_ext().execute_with(|| {
let user_account = 2;
// NFT bas
// Mint NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user_account));
// Welati rolünü kaldırmaya çalış
// Attempt to remove the Welati role
assert_noop!(
TikiPallet::revoke_tiki(RuntimeOrigin::root(), user_account, TikiEnum::Welati),
Error::<Test>::RoleNotAssigned
@@ -315,43 +315,43 @@ fn revoke_unique_role_clears_holder() {
let user = 2;
let unique_role = TikiEnum::Serok; // Unique role
// NFT bas ve unique rolü ver
// Mint NFT and grant the unique role
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
assert_ok!(TikiPallet::grant_elected_role(RuntimeOrigin::root(), user, unique_role));
// TikiHolder'da kayıtlı olduğunu kontrol et
// Verify it is recorded in TikiHolder
assert_eq!(TikiPallet::tiki_holder(unique_role), Some(user));
// Rolü kaldır
// Revoke the role
assert_ok!(TikiPallet::revoke_tiki(RuntimeOrigin::root(), user, unique_role));
// TikiHolder'dan temizlendiğini kontrol et
// Verify it was cleared from TikiHolder
assert_eq!(TikiPallet::tiki_holder(unique_role), None);
assert!(!TikiPallet::user_tikis(user).contains(&unique_role));
});
}
// === Scoring System Testleri ===
// === Scoring System Tests ===
#[test]
fn tiki_scoring_works_correctly() {
new_test_ext().execute_with(|| {
let user = 2;
// NFT bas (Welati otomatik eklenir - 10 puan)
// Mint NFT (Welati is added automatically - 10 points)
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
assert_eq!(TikiPallet::get_tiki_score(&user), 10);
// Yüksek puanlı rol ekle
assert_ok!(TikiPallet::grant_elected_role(RuntimeOrigin::root(), user, TikiEnum::Serok)); // 200 puan
// Add a high-scoring role
assert_ok!(TikiPallet::grant_elected_role(RuntimeOrigin::root(), user, TikiEnum::Serok)); // 200 points
// Toplam puanı kontrol et (10 + 200 = 210)
// Verify the total score (10 + 200 = 210)
assert_eq!(TikiPallet::get_tiki_score(&user), 210);
// Başka bir rol ekle
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), user, TikiEnum::Axa)); // 250 puan
// Add another role
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), user, TikiEnum::Axa)); // 250 points
// Toplam puan (10 + 200 + 250 = 460)
// Total score (10 + 200 + 250 = 460)
assert_eq!(TikiPallet::get_tiki_score(&user), 460);
});
}
@@ -359,7 +359,7 @@ fn tiki_scoring_works_correctly() {
#[test]
fn scoring_system_comprehensive() {
new_test_ext().execute_with(|| {
// Test individual scores - Anayasa v5.0'a göre
// Test individual scores - according to Anayasa v5.0
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Axa), 250);
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::RêveberêProjeyê), 250);
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Serok), 200);
@@ -373,7 +373,7 @@ fn scoring_system_comprehensive() {
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Parlementer), 100);
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Xezinedar), 100);
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::PisporêEwlehiyaSîber), 100);
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Bazargan), 60); // Yeni eklenen
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Bazargan), 60); // Newly added
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Mela), 50);
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Feqî), 50);
assert_eq!(TikiPallet::get_bonus_for_tiki(&TikiEnum::Welati), 10);
@@ -387,25 +387,25 @@ fn scoring_updates_after_role_changes() {
new_test_ext().execute_with(|| {
let user = 2;
// NFT bas
// Mint NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// İki rol ekle
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir)); // 100 puan
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger)); // 150 puan
// Add two roles
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir)); // 100 points
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger)); // 150 points
// Toplam: 10 + 100 + 150 = 260
// Total: 10 + 100 + 150 = 260
assert_eq!(TikiPallet::get_tiki_score(&user), 260);
// Bir rolü kaldır
// Revoke one role
assert_ok!(TikiPallet::revoke_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir));
// Puan güncellenmeli: 10 + 150 = 160
// The score should be updated: 10 + 150 = 160
assert_eq!(TikiPallet::get_tiki_score(&user), 160);
});
}
// === Multiple Users ve Isolation Testleri ===
// === Multiple Users and Isolation Tests ===
#[test]
fn multiple_users_work_independently() {
@@ -413,40 +413,40 @@ fn multiple_users_work_independently() {
let user1 = 2;
let user2 = 3;
// Her iki kullanıcı için NFT bas
// Mint NFT for both users
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user1));
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user2));
// Farklı roller ver
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), user1, TikiEnum::Axa)); // 250 puan
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user2, TikiEnum::Wezir)); // 100 puan
// Grant different roles
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), user1, TikiEnum::Axa)); // 250 points
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user2, TikiEnum::Wezir)); // 100 points
// Puanları kontrol et
// Verify the scores
assert_eq!(TikiPallet::get_tiki_score(&user1), 260); // 10 + 250
assert_eq!(TikiPallet::get_tiki_score(&user2), 110); // 10 + 100
// Rollerin doğru dağıldığını kontrol et
// Verify the roles are distributed correctly
assert!(TikiPallet::user_tikis(user1).contains(&TikiEnum::Axa));
assert!(!TikiPallet::user_tikis(user1).contains(&TikiEnum::Wezir));
assert!(TikiPallet::user_tikis(user2).contains(&TikiEnum::Wezir));
assert!(!TikiPallet::user_tikis(user2).contains(&TikiEnum::Axa));
// TikiProvider trait testleri
// TikiProvider trait tests
assert!(TikiPallet::has_tiki(&user1, &TikiEnum::Axa));
assert!(!TikiPallet::has_tiki(&user1, &TikiEnum::Wezir));
assert_eq!(TikiPallet::get_user_tikis(&user1).len(), 2); // Welati + Axa
});
}
// === Edge Cases ve Error Handling ===
// === Edge Cases and Error Handling ===
#[test]
fn cannot_grant_role_without_citizen_nft() {
new_test_ext().execute_with(|| {
let user_account = 2;
// NFT olmadan rol vermeye çalış
// Attempt to grant a role without an NFT
assert_noop!(
TikiPallet::grant_tiki(RuntimeOrigin::root(), user_account, TikiEnum::Wezir),
Error::<Test>::CitizenNftNotFound
@@ -464,7 +464,7 @@ fn nft_id_increments_correctly() {
assert_eq!(TikiPallet::citizen_nft(*user), Some(i as u32));
}
// Next ID'nin doğru arttığını kontrol et
// Verify the next ID increments correctly
assert_eq!(TikiPallet::next_item_id(), users.len() as u32);
});
}
@@ -475,11 +475,11 @@ fn duplicate_roles_not_allowed() {
let user = 2;
let role = TikiEnum::Mamoste;
// NFT bas ve rol ver
// Mint NFT and grant the role
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), user, role));
// Aynı rolü tekrar vermeye çalış
// Attempt to grant the same role again
assert_noop!(
TikiPallet::grant_earned_role(RuntimeOrigin::root(), user, role),
Error::<Test>::UserAlreadyHasRole
@@ -492,10 +492,10 @@ fn citizen_nft_already_exists_error() {
new_test_ext().execute_with(|| {
let user = 2;
// İlk NFT'yi bas
// Mint the first NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Aynı kullanıcıya tekrar NFT basmaya çalış
// Attempt to mint an NFT again for the same user
assert_noop!(
TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user),
Error::<Test>::CitizenNftAlreadyExists
@@ -509,10 +509,10 @@ fn cannot_revoke_role_user_does_not_have() {
let user = 2;
let role = TikiEnum::Wezir;
// NFT bas ama rol verme
// Mint NFT but do not grant a role
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Sahip olmadığı rolü kaldırmaya çalış
// Attempt to revoke a role the user does not have
assert_noop!(
TikiPallet::revoke_tiki(RuntimeOrigin::root(), user, role),
Error::<Test>::RoleNotAssigned
@@ -530,10 +530,10 @@ fn nft_transfer_protection_works() {
let collection_id = 0; // TikiCollectionId
let item_id = 0;
// NFT bas
// Mint NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user1));
// Transfer korumasını test et
// Test the transfer protection
assert_noop!(
TikiPallet::check_transfer_permission(
RuntimeOrigin::signed(user1),
@@ -552,10 +552,10 @@ fn non_tiki_nft_transfer_allowed() {
new_test_ext().execute_with(|| {
let user1 = 2;
let user2 = 3;
let other_collection_id = 1; // Farklı koleksiyon
let other_collection_id = 1; // Different collection
let item_id = 0;
// Diğer koleksiyonlar için transfer izni olmalı
// Transfers should be permitted for other collections
assert_ok!(TikiPallet::check_transfer_permission(
RuntimeOrigin::signed(user1),
other_collection_id,
@@ -573,11 +573,11 @@ fn tiki_provider_trait_works() {
new_test_ext().execute_with(|| {
let user = 2;
// NFT bas
// Mint NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir));
// TikiProvider trait fonksiyonlarını test et
// Test the TikiProvider trait functions
assert!(TikiPallet::is_citizen(&user));
assert!(TikiPallet::has_tiki(&user, &TikiEnum::Welati));
assert!(TikiPallet::has_tiki(&user, &TikiEnum::Wezir));
@@ -595,10 +595,10 @@ fn complex_multi_role_scenario() {
new_test_ext().execute_with(|| {
let user = 2;
// NFT bas
// Mint NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Çeşitli tipte roller ekle
// Add roles of various types
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir)); // Appointed
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), user, TikiEnum::Mamoste)); // Earned
assert_ok!(TikiPallet::grant_elected_role(
@@ -607,17 +607,17 @@ fn complex_multi_role_scenario() {
TikiEnum::Parlementer
)); // Elected
// Tüm rollerin eklendiğini kontrol et
// Verify all roles were added
let user_tikis = TikiPallet::user_tikis(user);
assert!(user_tikis.contains(&TikiEnum::Welati)); // 10 puan
assert!(user_tikis.contains(&TikiEnum::Wezir)); // 100 puan
assert!(user_tikis.contains(&TikiEnum::Mamoste)); // 70 puan
assert!(user_tikis.contains(&TikiEnum::Parlementer)); // 100 puan
assert!(user_tikis.contains(&TikiEnum::Welati)); // 10 points
assert!(user_tikis.contains(&TikiEnum::Wezir)); // 100 points
assert!(user_tikis.contains(&TikiEnum::Mamoste)); // 70 points
assert!(user_tikis.contains(&TikiEnum::Parlementer)); // 100 points
// Toplam puanı kontrol et (10 + 100 + 70 + 100 = 280)
// Verify the total score (10 + 100 + 70 + 100 = 280)
assert_eq!(TikiPallet::get_tiki_score(&user), 280);
// Bir rolü kaldır ve puanın güncellendiğini kontrol et
// Revoke one role and verify the score is updated
assert_ok!(TikiPallet::revoke_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir));
assert_eq!(TikiPallet::get_tiki_score(&user), 180); // 280 - 100 = 180
});
@@ -646,7 +646,7 @@ fn role_assignment_type_logic_comprehensive() {
RoleAssignmentType::Elected
);
// Earned roles (Sosyal roller + bazı uzman roller)
// Earned roles (social roles + some expert roles)
assert_eq!(
TikiPallet::get_role_assignment_type(&TikiEnum::Axa),
RoleAssignmentType::Earned
@@ -668,7 +668,7 @@ fn role_assignment_type_logic_comprehensive() {
RoleAssignmentType::Earned
);
// Appointed roles (Memur rolleri - default)
// Appointed roles (officer roles - default)
assert_eq!(
TikiPallet::get_role_assignment_type(&TikiEnum::Wezir),
RoleAssignmentType::Appointed
@@ -688,19 +688,19 @@ fn role_assignment_type_logic_comprehensive() {
});
}
// === Performance ve Stress Tests ===
// === Performance and Stress Tests ===
#[test]
fn stress_test_multiple_users_roles() {
new_test_ext().execute_with(|| {
let users = vec![2, 3, 4, 5];
// Tüm kullanıcılar için NFT bas
// Mint NFT for all users
for user in &users {
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), *user));
}
// Her kullanıcıya farklı rol kombinasyonları ver
// Grant different role combinations to each user
// User 2: High-level elected roles
assert_ok!(TikiPallet::grant_elected_role(RuntimeOrigin::root(), 2, TikiEnum::Serok)); // Unique
@@ -726,17 +726,17 @@ fn stress_test_multiple_users_roles() {
assert_ok!(TikiPallet::grant_earned_role(RuntimeOrigin::root(), 5, TikiEnum::Axa));
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), 5, TikiEnum::Dadger));
// Puanları kontrol et
// Verify the scores
assert_eq!(TikiPallet::get_tiki_score(&2), 310); // 10 + 200 + 100
assert_eq!(TikiPallet::get_tiki_score(&3), 180); // 10 + 70 + 100
assert_eq!(TikiPallet::get_tiki_score(&4), 260); // 10 + 100 + 150
assert_eq!(TikiPallet::get_tiki_score(&5), 410); // 10 + 250 + 150
// Unique rollerin doğru atandığını kontrol et
// Verify the unique roles are assigned correctly
assert_eq!(TikiPallet::tiki_holder(TikiEnum::Serok), Some(2));
assert_eq!(TikiPallet::tiki_holder(TikiEnum::SerokiMeclise), Some(4));
// Toplam vatandaş sayısını kontrol et
// Verify the total citizen count
let mut citizen_count = 0;
for user in &users {
if TikiPallet::is_citizen(user) {
@@ -752,10 +752,10 @@ fn maximum_roles_per_user_limit() {
new_test_ext().execute_with(|| {
let user = 2;
// NFT bas
// Mint NFT
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Test amaçlı sadece birkaç rol ekle (metadata uzunluk limitini aşmamak için)
// For testing purposes add only a few roles (to avoid exceeding the metadata length limit)
let roles_to_add = vec![
TikiEnum::Wezir,
TikiEnum::Dadger,
@@ -765,19 +765,19 @@ fn maximum_roles_per_user_limit() {
TikiEnum::Berdevk,
];
// Rolleri ekle
// Add the roles
for role in roles_to_add {
if TikiPallet::can_grant_role_type(&role, &RoleAssignmentType::Appointed) {
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, role));
}
}
// Kullanıcının pek çok role sahip olduğunu kontrol et
// Verify the user holds many roles
let final_tikis = TikiPallet::user_tikis(user);
assert!(final_tikis.len() >= 5); // En az 5 rol olmalı (Welati + 4+ diğer)
assert!(final_tikis.len() <= 100); // Max limit'i aşmamalı
assert!(final_tikis.len() >= 5); // Should be at least 5 roles (Welati + 4 or more others)
assert!(final_tikis.len() <= 100); // Should not exceed the max limit
// Toplam puanın makul olduğunu kontrol et
// Verify the total score is reasonable
assert!(TikiPallet::get_tiki_score(&user) > 200);
});
}
@@ -791,20 +791,20 @@ fn apply_for_citizenship_twice_same_user() {
new_test_ext().execute_with(|| {
let user = 5;
// İlk başvuru - use force_mint to bypass KYC
// First application - use force_mint to bypass KYC
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
let first_score = TikiPallet::get_tiki_score(&user);
assert_eq!(first_score, 10);
// İkinci kez mint etmeye çalış (başarısız olmalı - zaten NFT var)
// Attempt to mint a second time (should fail - the NFT already exists)
assert_noop!(
TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user),
Error::<Test>::CitizenNftAlreadyExists
);
let second_score = TikiPallet::get_tiki_score(&user);
assert_eq!(second_score, 10); // Skor değişmemeli
assert_eq!(second_score, 10); // The score should not change
});
}
@@ -815,7 +815,7 @@ fn apply_for_citizenship_adds_hemwelati() {
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Welati rolü var
// The Welati role is present
let tikis = TikiPallet::user_tikis(user);
assert!(tikis.contains(&TikiEnum::Welati));
});
@@ -828,7 +828,7 @@ fn apply_for_citizenship_initial_score() {
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Welati puanı 10
// Welati score is 10
let score = TikiPallet::get_tiki_score(&user);
assert_eq!(score, 10);
});
@@ -843,7 +843,7 @@ fn apply_for_citizenship_multiple_users_independent() {
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), *user));
}
// Hepsi 10 puana sahip olmalı
// All should have a score of 10
for user in &users {
assert_eq!(TikiPallet::get_tiki_score(user), 10);
}
@@ -859,21 +859,21 @@ fn revoke_tiki_reduces_score() {
new_test_ext().execute_with(|| {
let user = 13;
// NFT bas ve rol ekle
// Mint NFT and add a role
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger));
let initial_score = TikiPallet::get_tiki_score(&user);
assert!(initial_score > 10);
// Rolü geri al
// Revoke the role
assert_ok!(TikiPallet::revoke_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger));
// Skor düştü
// The score has decreased
let final_score = TikiPallet::get_tiki_score(&user);
assert!(final_score < initial_score);
// Rol listesinde yok
// It is not in the role list
let tikis = TikiPallet::user_tikis(user);
assert!(!tikis.contains(&TikiEnum::Dadger));
});
@@ -902,7 +902,7 @@ fn revoke_tiki_nonexistent_role() {
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Kullanıcı bu role sahip değil
// The user does not have this role
assert_noop!(
TikiPallet::revoke_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir),
Error::<Test>::RoleNotAssigned
@@ -931,16 +931,16 @@ fn get_tiki_score_role_accumulation() {
assert_ok!(TikiPallet::force_mint_citizen_nft(RuntimeOrigin::root(), user));
// Başlangıç: Welati = 10
// Initial: Welati = 10
let score1 = TikiPallet::get_tiki_score(&user);
assert_eq!(score1, 10);
// Dadger ekle (+150)
// Add Dadger (+150)
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger));
let score2 = TikiPallet::get_tiki_score(&user);
assert_eq!(score2, 160); // 10 + 150
// Wezir ekle (+100)
// Add Wezir (+100)
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir));
let score3 = TikiPallet::get_tiki_score(&user);
assert_eq!(score3, 260); // 10 + 150 + 100
@@ -959,7 +959,7 @@ fn get_tiki_score_revoke_decreases() {
let score_before = TikiPallet::get_tiki_score(&user);
assert_eq!(score_before, 280); // 10 + 150 + 120
// Bir rolü geri al
// Revoke one role
assert_ok!(TikiPallet::revoke_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger));
let score_after = TikiPallet::get_tiki_score(&user);
@@ -981,10 +981,10 @@ fn user_tikis_updated_after_grant() {
let tikis_before = TikiPallet::user_tikis(user);
assert_eq!(tikis_before.len(), 1); // Only Welati
// Rol ekle
// Add a role
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger));
// UserTikis güncellendi
// UserTikis was updated
let tikis_after = TikiPallet::user_tikis(user);
assert_eq!(tikis_after.len(), 2);
assert!(tikis_after.contains(&TikiEnum::Dadger));
@@ -1000,7 +1000,7 @@ fn user_tikis_consistent_with_score() {
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Dadger));
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user, TikiEnum::Wezir));
// UserTikis sayısı ile score tutarlı olmalı
// The UserTikis count should be consistent with the score
let user_tikis = TikiPallet::user_tikis(user);
let score = TikiPallet::get_tiki_score(&user);
@@ -1021,7 +1021,7 @@ fn multiple_users_independent_roles() {
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user1, TikiEnum::Dadger));
assert_ok!(TikiPallet::grant_tiki(RuntimeOrigin::root(), user2, TikiEnum::Wezir));
// Roller bağımsız
// The roles are independent
let tikis1 = TikiPallet::user_tikis(user1);
let tikis2 = TikiPallet::user_tikis(user2);
@@ -32,13 +32,18 @@
//! ## Score Calculation
//!
//! ```text
//! trust_score = (staking_score + referral_score + perwerde_score + tiki_score) * multiplier
//! weighted_sum = 100 * staking_score
//! + 300 * (referral_score + perwerde_score + tiki_score)
//! trust_score = staking_score * weighted_sum / base
//! ```
//!
//! Where:
//! - Each component score is normalized and weighted
//! - The multiplier is configurable via `ScoreMultiplierBase`
//! - Citizenship status is required (KYC approved)
//! - `base` is configurable via `ScoreMultiplierBase`.
//! - Staking is both a gate and a multiplier: a citizen with zero staking_score
//! has a trust_score of zero, and staking scales the entire weighted sum.
//! - Within weighted_sum, referral/perwerde/tiki each carry weight 300 versus
//! staking's 100 (3x per unit), rewarding contribution over capital alone.
//! - Citizenship status is required (KYC approved).
//!
//! ## Update Mechanisms
//!
@@ -361,19 +366,19 @@ pub mod pezpallet {
Ok(())
}
/// Periyodik güncellemeyi başlatan function
/// Function that starts the periodic update
#[pezpallet::call_index(2)]
#[pezpallet::weight(<T as Config>::WeightInfo::periodic_trust_score_update())]
pub fn periodic_trust_score_update(origin: OriginFor<T>) -> DispatchResult {
ensure_root(origin)?;
// Eğer önceki update devam ediyorsa bekle
// If a previous update is still in progress, wait
ensure!(!BatchUpdateInProgress::<T>::get(), Error::<T>::UpdateInProgress);
// Yeni periyodik güncellemeyi başlat
// Start the new periodic update
Self::update_all_trust_scores(OriginFor::<T>::root())?;
// Bir sonraki periyodik güncellemeyi schedule et
// Schedule the next periodic update
let current_block = pezframe_system::Pezpallet::<T>::block_number();
let next_update_block = current_block + T::UpdateInterval::get();
@@ -118,7 +118,7 @@ impl pezpallet_identity_kyc::Config for Test {
parameter_types! {
pub const ScoreMultiplierBase: u128 = 1000;
pub const TrustUpdateInterval: u64 = 100; // Test için kısa interval
pub const TrustUpdateInterval: u64 = 100; // Short interval for tests
pub const MaxBatchSizeValue: u32 = 100; // Max users per batch
}
@@ -153,7 +153,7 @@ impl pezpallet_trust::TikiScoreProvider<u64> for MockTikiScoreProvider {
pub struct MockCitizenshipStatusProvider;
impl pezpallet_trust::CitizenshipStatusProvider<u64> for MockCitizenshipStatusProvider {
fn is_citizen(who: &u64) -> bool {
// Test için: 1-100 arası hesaplar vatandaş, 999 değil
// For tests: accounts 1-100 are citizens, 999 is not
*who >= 1 && *who <= 100 && *who != 999
}
}
@@ -107,13 +107,13 @@ fn force_recalculate_trust_score_requires_root() {
#[test]
fn update_all_trust_scores_works() {
new_test_ext().execute_with(|| {
// Event'leri yakalamak için block number set et
// Set block number to capture events
System::set_block_number(1);
assert_ok!(TrustPallet::update_all_trust_scores(RuntimeOrigin::root()));
// Mock implementation boş account listesi kullandığı için
// AllTrustScoresUpdated event'i yayınlanır (count: 0 ile)
// Because the mock implementation uses an empty account list,
// the AllTrustScoresUpdated event is emitted (with count: 0)
let events = System::events();
assert!(events.iter().any(|event| {
matches!(
@@ -134,18 +134,18 @@ fn update_all_trust_scores_requires_root() {
#[test]
fn periodic_trust_score_update_works() {
new_test_ext().execute_with(|| {
// Event'leri yakalamak için block number set et
// Set block number to capture events
System::set_block_number(1);
assert_ok!(TrustPallet::periodic_trust_score_update(RuntimeOrigin::root()));
// Periyodik güncelleme event'inin yayınlandığını kontrol et
// Verify the periodic update event was emitted
let events = System::events();
assert!(events.iter().any(|event| {
matches!(event.event, RuntimeEvent::TrustPallet(Event::PeriodicUpdateScheduled { .. }))
}));
// Ayrıca AllTrustScoresUpdated event'i de yayınlanmalı
// The AllTrustScoresUpdated event should also be emitted
assert!(events.iter().any(|event| {
matches!(event.event, RuntimeEvent::TrustPallet(Event::AllTrustScoresUpdated { .. }))
}));
@@ -155,10 +155,10 @@ fn periodic_trust_score_update_works() {
#[test]
fn periodic_update_fails_when_batch_in_progress() {
new_test_ext().execute_with(|| {
// Batch update'i başlat
// Start the batch update
crate::BatchUpdateInProgress::<Test>::put(true);
// Periyodik update'in başarısız olmasını bekle
// Expect the periodic update to fail
assert_noop!(
TrustPallet::periodic_trust_score_update(RuntimeOrigin::root()),
Error::<Test>::UpdateInProgress
@@ -211,18 +211,18 @@ fn trust_score_updater_trait_works() {
#[test]
fn batch_update_storage_works() {
new_test_ext().execute_with(|| {
// Başlangıçta batch update aktif değil
// Initially the batch update is not active
assert!(!crate::BatchUpdateInProgress::<Test>::get());
assert!(crate::LastProcessedAccount::<Test>::get().is_none());
// Batch update'i simüle et
// Simulate the batch update
crate::BatchUpdateInProgress::<Test>::put(true);
crate::LastProcessedAccount::<Test>::put(42u64);
assert!(crate::BatchUpdateInProgress::<Test>::get());
assert_eq!(crate::LastProcessedAccount::<Test>::get(), Some(42u64));
// Temizle
// Clean up
crate::BatchUpdateInProgress::<Test>::put(false);
crate::LastProcessedAccount::<Test>::kill();
@@ -238,7 +238,7 @@ fn periodic_update_scheduling_works() {
assert_ok!(TrustPallet::periodic_trust_score_update(RuntimeOrigin::root()));
// Event'te next_block'un doğru hesaplandığını kontrol et
// Verify that next_block is calculated correctly in the event
let events = System::events();
let scheduled_event = events.iter().find(|event| {
matches!(event.event, RuntimeEvent::TrustPallet(Event::PeriodicUpdateScheduled { .. }))
@@ -910,9 +910,14 @@ pub mod pezpallet {
#[pezpallet::call_index(10)]
#[pezpallet::weight(<T as pezpallet::Config>::WeightInfo::nominate_official())]
#[pezpallet::feeless_if(|origin: &OriginFor<T>, _nominee: &T::AccountId, _role: &OfficialRole, _justification: &BoundedVec<u8, ConstU32<1000>>| -> bool {
// Governance members are exempt from fees when performing official duties
// Fee exemption must mirror the in-body authorization check exactly
// (Serok or a Minister) -- NOT the broader is_governance_member set,
// otherwise unauthorized governance-adjacent accounts (Parliament/Diwan
// members) could spam this call for free and always fail authorization.
match ensure_signed(origin.clone()) {
Ok(who) => Pezpallet::<T>::is_governance_member(&who),
Ok(who) => {
Pezpallet::<T>::is_serok(&who) || Pezpallet::<T>::is_minister(&who)
},
Err(_) => false,
}
})]
@@ -1012,6 +1017,15 @@ pub mod pezpallet {
Error::<T>::AppointmentAlreadyProcessed
);
// Re-validate that the role is still unfilled. Two competing
// AppointmentProcess entries (different nominees, same role) can both
// reach WaitingPresidentialApproval; without this check, approving a
// second stale process would silently overwrite an already-appointed
// official with no removal event and no error.
if let Some(current_holder) = AppointedOfficials::<T>::get(process.position) {
ensure!(current_holder == process.nominee, Error::<T>::RoleAlreadyFilled);
}
// Get nomination
let mut nomination = PendingNominations::<T>::get(process.position, &process.nominee)
.ok_or(Error::<T>::NominationNotFound)?;
@@ -1045,10 +1059,13 @@ pub mod pezpallet {
#[pezpallet::call_index(20)]
#[pezpallet::weight(<T as pezpallet::Config>::WeightInfo::submit_proposal())]
#[pezpallet::feeless_if(|origin: &OriginFor<T>, _title: &BoundedVec<u8, ConstU32<100>>, _description: &BoundedVec<u8, ConstU32<1000>>, _decision_type: &CollectiveDecisionType, _priority: &ProposalPriority, _call: &Option<Box<<T as pezframe_system::Config>::RuntimeCall>>| -> bool {
// Governance members are exempt from fees when submitting proposals
#[pezpallet::feeless_if(|origin: &OriginFor<T>, _title: &BoundedVec<u8, ConstU32<100>>, _description: &BoundedVec<u8, ConstU32<1000>>, decision_type: &CollectiveDecisionType, _priority: &ProposalPriority, _call: &Option<Box<<T as pezframe_system::Config>::RuntimeCall>>| -> bool {
// Fee exemption must mirror can_propose() exactly (Serok, or Parliament
// member/Serok for everything else) -- NOT the broader is_governance_member
// set, otherwise Diwan members/Ministers could spam this call for free and
// always fail authorization.
match ensure_signed(origin.clone()) {
Ok(who) => Pezpallet::<T>::is_governance_member(&who),
Ok(who) => Pezpallet::<T>::can_propose(&who, decision_type).unwrap_or(false),
Err(_) => false,
}
})]
@@ -1106,10 +1123,16 @@ pub mod pezpallet {
#[pezpallet::call_index(21)]
#[pezpallet::weight(<T as pezpallet::Config>::WeightInfo::vote_on_proposal())]
#[pezpallet::feeless_if(|origin: &OriginFor<T>, _proposal_id: &u32, _vote: &VoteChoice, _rationale: &Option<BoundedVec<u8, ConstU32<500>>>| -> bool {
// Governance members are exempt from fees when voting
#[pezpallet::feeless_if(|origin: &OriginFor<T>, proposal_id: &u32, _vote: &VoteChoice, _rationale: &Option<BoundedVec<u8, ConstU32<500>>>| -> bool {
// Fee exemption must mirror the in-body per-decision-type authorization
// check exactly -- NOT the broader is_governance_member set, otherwise
// accounts unauthorized for this specific proposal's decision type
// (e.g. Ministers, who are never authorized to vote) could spam this
// call for free and always fail authorization.
match ensure_signed(origin.clone()) {
Ok(who) => Pezpallet::<T>::is_governance_member(&who),
Ok(who) => ActiveProposals::<T>::get(proposal_id)
.map(|proposal| Pezpallet::<T>::is_authorized_to_vote(&proposal.decision_type, &who))
.unwrap_or(false),
Err(_) => false,
}
})]
@@ -1132,36 +1155,12 @@ pub mod pezpallet {
let proposal =
ActiveProposals::<T>::get(proposal_id).ok_or(Error::<T>::ProposalNotFound)?;
// Enforce access control based on decision type
match proposal.decision_type {
CollectiveDecisionType::ParliamentSimpleMajority
| CollectiveDecisionType::ParliamentSuperMajority
| CollectiveDecisionType::ParliamentAbsoluteMajority
| CollectiveDecisionType::VetoOverride => {
// Parliament members only
let members = ParliamentMembers::<T>::get();
let is_member = members.iter().any(|m| m.account == voter);
ensure!(is_member, Error::<T>::NotAuthorizedToVote);
},
CollectiveDecisionType::ConstitutionalReview
| CollectiveDecisionType::ConstitutionalUnanimous => {
// Diwan members only
ensure!(Self::is_diwan_member(&voter), Error::<T>::NotAuthorizedToVote,);
},
CollectiveDecisionType::ExecutiveDecision => {
// Serok (President) only
let serok = CurrentOfficials::<T>::get(GovernmentPosition::Serok);
ensure!(serok.as_ref() == Some(&voter), Error::<T>::NotAuthorizedToVote,);
},
CollectiveDecisionType::HybridDecision => {
// Parliament members OR Serok
let members = ParliamentMembers::<T>::get();
let is_parliament = members.iter().any(|m| m.account == voter);
let serok = CurrentOfficials::<T>::get(GovernmentPosition::Serok);
let is_serok = serok.as_ref() == Some(&voter);
ensure!(is_parliament || is_serok, Error::<T>::NotAuthorizedToVote,);
},
}
// Enforce access control based on decision type (shared with feeless_if
// via is_authorized_to_vote so the two checks cannot drift apart)
ensure!(
Self::is_authorized_to_vote(&proposal.decision_type, &voter),
Error::<T>::NotAuthorizedToVote
);
// Record the vote
let vote_info = CollectiveVote {
@@ -1328,6 +1327,16 @@ pub mod pezpallet {
election_id: u32,
election: &ElectionInfo<T>,
) -> Result<ElectionOutcome<T::AccountId>, Error<T>> {
// Enforce the minimum turnout quorum recorded on the election at initiation
// time. Without this check, elections could be finalized (and officeholders
// seated) with arbitrarily low citizen participation.
let total_citizen_count = Self::get_total_citizen_count();
if total_citizen_count > 0 {
let turnout_percentage = ((election.total_votes as u64).saturating_mul(100)
/ total_citizen_count as u64) as u8;
ensure!(turnout_percentage >= election.minimum_turnout, Error::<T>::QuorumNotMet);
}
let mut candidates_with_votes: Vec<(T::AccountId, u32)> = election
.candidates
.iter()
@@ -1348,7 +1357,9 @@ pub mod pezpallet {
candidates_with_votes.iter().map(|(_, v)| *v).sum::<u32>().max(1);
let (top_winner, top_vote_count) = candidates_with_votes[0].clone();
if (top_vote_count * 100) / total_valid_votes >= 50 {
if ((top_vote_count as u64).saturating_mul(100)) / (total_valid_votes as u64)
>= 50
{
let winners_vec: BoundedVec<_, _> = vec![top_winner]
.try_into()
.map_err(|_| Error::<T>::CalculationOverflow)?;
@@ -1410,39 +1421,76 @@ pub mod pezpallet {
}
},
ElectionType::Parliamentary => {
let current_block = pezframe_system::Pezpallet::<T>::block_number();
let term_end = current_block
+ BlockNumberFor::<T>::from(4u32 * 365u32 * 24u32 * 60u32 * 10u32);
// Guard against wiping the sitting Parliament when an election produces
// no winners (e.g. zero candidates registered). Leave the previous
// Parliament untouched in that case, mirroring the Presidential and
// SpeakerElection arms below.
if !winners.is_empty() {
let current_block = pezframe_system::Pezpallet::<T>::block_number();
let term_end = current_block
+ BlockNumberFor::<T>::from(4u32 * 365u32 * 24u32 * 60u32 * 10u32);
let parliament_members: Result<BoundedVec<_, _>, _> = winners
.iter()
.map(|winner| ParliamentMember {
account: winner.clone(),
elected_at: current_block,
term_ends_at: term_end,
votes_participated: 0,
total_votes_eligible: 0,
participation_rate: 100,
committees: Default::default(),
})
.collect::<Vec<_>>()
.try_into();
let parliament_members: Result<BoundedVec<_, _>, _> = winners
.iter()
.map(|winner| ParliamentMember {
account: winner.clone(),
elected_at: current_block,
term_ends_at: term_end,
votes_participated: 0,
total_votes_eligible: 0,
participation_rate: 100,
committees: Default::default(),
})
.collect::<Vec<_>>()
.try_into();
ParliamentMembers::<T>::put(
parliament_members.map_err(|_| Error::<T>::ParliamentFull)?,
);
ParliamentMembers::<T>::put(
parliament_members.map_err(|_| Error::<T>::ParliamentFull)?,
);
Self::deposit_event(Event::ParliamentUpdated {
new_members: winners.to_vec(),
term_start: current_block,
});
Self::deposit_event(Event::ParliamentUpdated {
new_members: winners.to_vec(),
term_start: current_block,
});
}
},
ElectionType::SpeakerElection => {
if let Some(winner) = winners.first() {
CurrentOfficials::<T>::insert(GovernmentPosition::MeclisBaskanı, winner);
}
},
_ => {},
ElectionType::ConstitutionalCourt => {
// Same non-destructive guard as Parliamentary: an empty winners list
// leaves the sitting Diwan untouched instead of wiping it.
if !winners.is_empty() {
let current_block = pezframe_system::Pezpallet::<T>::block_number();
let term_end = current_block
+ BlockNumberFor::<T>::from(9u32 * 365u32 * 24u32 * 60u32 * 10u32);
let diwan_members: Result<BoundedVec<_, _>, _> = winners
.iter()
.map(|winner| DiwanMember {
account: winner.clone(),
appointed_at: current_block,
term_ends_at: term_end,
appointed_by: AppointmentAuthority::Parliament,
specialization: ConstitutionalSpecialization::FundamentalRights,
decisions_made: 0,
})
.collect::<Vec<_>>()
.try_into();
DiwanMembers::<T>::put(diwan_members.map_err(|_| Error::<T>::DiwanFull)?);
for winner in winners {
Self::deposit_event(Event::DiwanMemberAppointed {
member: winner.clone(),
appointed_by: AppointmentAuthority::Parliament,
specialization: ConstitutionalSpecialization::FundamentalRights,
});
}
}
},
}
Ok(())
}
@@ -1469,6 +1517,27 @@ pub mod pezpallet {
}
}
/// Check voting authority for a given decision type. Shared by
/// `vote_on_proposal`'s in-body enforcement and its `feeless_if` fee
/// exemption gate so the two checks can never drift apart.
pub fn is_authorized_to_vote(
decision_type: &CollectiveDecisionType,
voter: &T::AccountId,
) -> bool {
match decision_type {
CollectiveDecisionType::ParliamentSimpleMajority
| CollectiveDecisionType::ParliamentSuperMajority
| CollectiveDecisionType::ParliamentAbsoluteMajority
| CollectiveDecisionType::VetoOverride => Self::is_parliament_member(voter),
CollectiveDecisionType::ConstitutionalReview
| CollectiveDecisionType::ConstitutionalUnanimous => Self::is_diwan_member(voter),
CollectiveDecisionType::ExecutiveDecision => Self::is_serok(voter),
CollectiveDecisionType::HybridDecision => {
Self::is_parliament_member(voter) || Self::is_serok(voter)
},
}
}
/// Calculate voting threshold
fn get_voting_threshold(decision_type: &CollectiveDecisionType) -> u32 {
match decision_type {
@@ -103,7 +103,7 @@ impl pezpallet_timestamp::Config for Test {
type WeightInfo = ();
}
// Mock Randomness - SADECE BİR KEZ TANIMLA
// Mock Randomness - DEFINE ONLY ONCE
pub struct MockRandomness;
impl Randomness<H256, u64> for MockRandomness {
fn random(_subject: &[u8]) -> (H256, u64) {
@@ -288,11 +288,23 @@ impl pezpallet_identity_kyc::Config for Test {
}
// Staking Score Configuration
parameter_types! {
pub const StakingScoreNoterBondAmount: Balance = 1_000;
pub const StakingScoreDisputeWindow: u64 = 10;
pub const StakingScoreSlashDestination: AccountId = 999;
}
impl pezpallet_staking_score::Config for Test {
type Balance = Balance;
type OnStakingUpdate = ();
type WeightInfo = ();
type NoterChecker = ();
type Currency = Balances;
type NoterBondAmount = StakingScoreNoterBondAmount;
type DisputeWindow = StakingScoreDisputeWindow;
type DisputeOrigin = pezframe_system::EnsureSigned<AccountId>;
type SlashOrigin = pezframe_system::EnsureRoot<AccountId>;
type SlashDestination = StakingScoreSlashDestination;
}
// Referral Configuration
@@ -316,6 +328,8 @@ parameter_types! {
impl pezpallet_tiki::Config for Test {
type AdminOrigin = pezframe_system::EnsureRoot<AccountId>;
type ElectedRoleOrigin = pezframe_system::EnsureRoot<AccountId>;
type EarnedRoleOrigin = pezframe_system::EnsureRoot<AccountId>;
type WeightInfo = ();
type MaxTikisPerUser = MaxTikisPerUser;
type Tiki = pezpallet_tiki::Tiki;
@@ -323,56 +337,56 @@ impl pezpallet_tiki::Config for Test {
type TrustScoreUpdater = ();
}
// Mock implementations for required traits - YÜKSEK SKORLAR VER
// Mock implementations for required traits - PROVIDE HIGH SCORES
pub struct MockStakingScoreProvider;
impl pezpallet_staking_score::StakingScoreProvider<AccountId, u64> for MockStakingScoreProvider {
fn get_staking_score(_account: &AccountId) -> (u32, u64) {
(1000, 0) // Yüksek skor
(1000, 0) // High score
}
}
pub struct MockReferralScoreProvider;
impl pezpallet_trust::ReferralScoreProvider<AccountId> for MockReferralScoreProvider {
fn get_referral_score(_account: &AccountId) -> u32 {
500 // Yüksek skor
500 // High score
}
}
pub struct MockPerwerdeScoreProvider;
impl pezpallet_trust::PerwerdeScoreProvider<AccountId> for MockPerwerdeScoreProvider {
fn get_perwerde_score(_account: &AccountId) -> u32 {
750 // Yüksek skor
750 // High score
}
}
pub struct MockTikiScoreProvider;
// `pezpallet_trust` için implementasyon
// Implementation for `pezpallet_trust`
impl pezpallet_trust::TikiScoreProvider<AccountId> for MockTikiScoreProvider {
fn get_tiki_score(_account: &AccountId) -> u32 {
100
}
}
// `pezpallet_welati`'nin ihtiyaç duyduğu `pezpallet_tiki` için implementasyon
// Implementation for `pezpallet_tiki`, which `pezpallet_welati` requires
impl pezpallet_tiki::TikiScoreProvider<AccountId> for MockTikiScoreProvider {
fn get_tiki_score(_account: &AccountId) -> u32 {
1000 // Yüksek Tiki score - tüm kontrolleri geçer
1000 // High Tiki score - passes all checks
}
}
pub struct MockCitizenshipStatusProvider;
impl pezpallet_trust::CitizenshipStatusProvider<AccountId> for MockCitizenshipStatusProvider {
fn is_citizen(_account: &AccountId) -> bool {
true // Herkes vatandaş
true // Everyone is a citizen
}
}
// MOCK TRUST PROVIDER - HERKES İÇİN YÜKSEK SKOR
// MOCK TRUST PROVIDER - HIGH SCORE FOR EVERYONE
pub struct MockTrustProvider;
impl pezpallet_trust::TrustScoreProvider<AccountId> for MockTrustProvider {
fn trust_score_of(_account: &AccountId) -> u128 {
1000u128 // Herkes için yüksek trust score
1000u128 // High trust score for everyone
}
}
@@ -403,7 +417,7 @@ impl pezpallet_trust::Config for Test {
type CitizenshipSource = MockCitizenshipStatusProvider;
}
// Welati Configuration - SADECE BİR KEZ TANIMLA
// Welati Configuration - DEFINE ONLY ONCE
parameter_types! {
pub const ParliamentSize: u32 = 201;
pub const DiwanSize: u32 = 11;
@@ -421,9 +435,9 @@ impl pezpallet_welati::Config for Test {
type WeightInfo = ();
type Randomness = MockRandomness;
type RuntimeCall = RuntimeCall;
type TrustScoreSource = MockTrustProvider; // Mock provider kullan
type TikiSource = MockTikiScoreProvider; // Mock Tiki provider kullan
type CitizenSource = MockTrustProvider; // Mock provider kullan
type TrustScoreSource = MockTrustProvider; // Use the mock provider
type TikiSource = MockTikiScoreProvider; // Use the mock Tiki provider
type CitizenSource = MockTrustProvider; // Use the mock provider
type KycSource = IdentityKyc;
type ParliamentSize = ParliamentSize;
type DiwanSize = DiwanSize;
@@ -438,7 +452,7 @@ impl pezpallet_welati::Config for Test {
type MaxEndorsers = MaxEndorsers;
}
// CRITICAL: CitizenInfo trait implementation - SADECE BİR KEZ TANIMLA
// CRITICAL: CitizenInfo trait implementation - DEFINE ONLY ONCE
impl CitizenInfo for Trust {
fn citizen_count() -> u32 {
110
@@ -476,14 +490,14 @@ impl ExtBuilder {
}
}
// SIMPLIFIED TEST USER SETUP - BOŞ BIRAK, MOCK PROVIDERS YETERLI
// SIMPLIFIED TEST USER SETUP - LEAVE EMPTY, MOCK PROVIDERS ARE SUFFICIENT
pub fn setup_test_users() {
// Mock provider'lar zaten herkesin yüksek trust score'u olmasını sağlıyor
// ve TikiScoreProvider da herkesin Tiki'ye sahip olduğunu söylüyor
// Bu sayede pezpallet-tiki ile uğraşmak zorunda kalmıyoruz
// The mock providers already ensure everyone has a high trust score,
// and the TikiScoreProvider also reports that everyone owns a Tiki.
// This way we do not have to deal with pezpallet-tiki.
// Sadece NFTs collection'ı oluşturuldu, bu yeterli
// Testlerde KYC kontrolü zaten bypass ediliyor
// Only the NFTs collection was created, and that is sufficient.
// The KYC check is already bypassed in tests.
}
// CRITICAL HELPER FUNCTION FOR TESTS
@@ -14,7 +14,7 @@ use crate::{
use pezframe_support::{assert_noop, assert_ok, BoundedVec};
use pezsp_runtime::traits::BadOrigin;
// ===== SEÇİM SİSTEMİ TESTLERİ =====
// ===== ELECTION SYSTEM TESTS =====
#[test]
fn initiate_election_works() {
@@ -155,7 +155,7 @@ fn register_candidate_fails_already_candidate() {
#[test]
fn cast_vote_works() {
ExtBuilder::default().build().execute_with(|| {
// 1. Seçimi başlat
// 1. Start the election
assert_ok!(Welati::initiate_election(
RuntimeOrigin::root(),
ElectionType::Parliamentary,
@@ -163,28 +163,28 @@ fn cast_vote_works() {
None,
));
// 2. Bir aday kaydet (hesap 1)
let endorsers: Vec<u64> = (3..=52).collect(); // 50 destekçi
// 2. Register a candidate (account 1)
let endorsers: Vec<u64> = (3..=52).collect(); // 50 endorsers
assert_ok!(Welati::register_candidate(
RuntimeOrigin::signed(1), // Aday
0, // Seçim ID'si
None, // Bölge ID'si
RuntimeOrigin::signed(1), // Candidate
0, // Election ID
None, // District ID
endorsers,
));
// 3. Oy verme periyoduna ilerle
// 3. Advance to the voting period
run_to_block(86_400 + 259_200 + 1);
// 4. Oy kullan (hesap 2, aday 1'e oy veriyor)
// 4. Cast a vote (account 2 votes for candidate 1)
let candidates_to_vote_for = vec![1];
assert_ok!(Welati::cast_vote(
RuntimeOrigin::signed(2), // Seçmen
0, // Seçim ID'si
candidates_to_vote_for.clone(), // Oy verilen aday(lar)
None, // Bölge ID'si
RuntimeOrigin::signed(2), // Voter
0, // Election ID
candidates_to_vote_for.clone(), // Candidate(s) voted for
None, // District ID
));
// 5. Event'i ve depolama durumunu doğrula
// 5. Verify the event and storage state
assert_eq!(
last_event(),
RuntimeEvent::Welati(WelatiEvent::VoteCast {
@@ -253,9 +253,19 @@ fn finalize_election_works() {
None,
));
// Seçimin bitiş tarihinden sonrasına geç
// A candidate and enough voters to clear the 40% Parliamentary quorum
// (MockTrustProvider's fixed citizen_count() is 110, so 44+ votes).
let endorsers: Vec<u64> = (200..=249).collect();
assert_ok!(Welati::register_candidate(RuntimeOrigin::signed(1), 0, None, endorsers,));
run_to_block(86_400 + 259_200 + 1);
for voter in 300..=343u64 {
assert_ok!(Welati::cast_vote(RuntimeOrigin::signed(voter), 0, vec![1], None,));
}
// Move past the election end date
// candidacy (86_400) + campaign (259_200) + voting (432_000) + 1
run_to_block(86_400 + 259_200 + 432_000 + 10); // Ekstra güvenlik için +10
run_to_block(86_400 + 259_200 + 432_000 + 10); // +10 for extra safety
assert_ok!(Welati::finalize_election(RuntimeOrigin::root(), 0,));
@@ -265,7 +275,7 @@ fn finalize_election_works() {
});
}
// ===== ATAMA SİSTEMİ TESTLERİ =====
// ===== APPOINTMENT SYSTEM TESTS =====
#[test]
fn nominate_official_works() {
@@ -305,7 +315,7 @@ fn approve_appointment_works() {
});
}
// ===== KOLLEKTİF KARAR TESTLERİ =====
// ===== COLLECTIVE DECISION TESTS =====
#[test]
fn submit_proposal_works() {
@@ -313,7 +323,7 @@ fn submit_proposal_works() {
let title = b"Test Proposal".to_vec().try_into().unwrap();
let description = b"Test proposal description".to_vec().try_into().unwrap();
// CRITICAL FIX: Helper fonksiyonu kullan
// CRITICAL FIX: Use the helper function
add_parliament_member(1);
assert_ok!(Welati::submit_proposal(
@@ -336,7 +346,7 @@ fn vote_on_proposal_works() {
let title = b"Test Proposal".to_vec().try_into().unwrap();
let description = b"Test proposal description".to_vec().try_into().unwrap();
// CRITICAL FIX: Helper fonksiyonları kullan
// CRITICAL FIX: Use the helper functions
add_parliament_member(1);
add_parliament_member(2);
@@ -365,7 +375,7 @@ fn vote_on_proposal_works() {
});
}
// ===== HELPER FUNCTION TESTLERİ =====
// ===== HELPER FUNCTION TESTS =====
#[test]
fn get_required_trust_score_works() {
@@ -412,7 +422,7 @@ fn calculate_vote_weight_works() {
});
}
// ===== ERROR CASE TESTLERİ =====
// ===== ERROR CASE TESTS =====
#[test]
fn election_not_found_error_works() {
@@ -434,12 +444,12 @@ fn proposal_not_found_error_works() {
});
}
// ===== INTEGRATION TESTLERİ =====
// ===== INTEGRATION TESTS =====
#[test]
fn complete_election_cycle_works() {
ExtBuilder::default().build().execute_with(|| {
// 1. Seçim başlat
// 1. Start the election
assert_ok!(Welati::initiate_election(
RuntimeOrigin::root(),
ElectionType::Parliamentary,
@@ -447,7 +457,7 @@ fn complete_election_cycle_works() {
None,
));
// 2. Adaylar kaydolsun
// 2. Register candidates
let endorsers1: Vec<u64> = (10..=59).collect();
let endorsers2: Vec<u64> = (60..=109).collect();
@@ -455,15 +465,20 @@ fn complete_election_cycle_works() {
assert_ok!(Welati::register_candidate(RuntimeOrigin::signed(2), 0, None, endorsers2,));
// 3. Voting period'a geç
// 3. Move to the voting period
run_to_block(86_400 + 259_200 + 1);
// 4. Oylar kullanılsın
// 4. Cast votes — enough to clear the 40% Parliamentary quorum
// (MockTrustProvider's fixed citizen_count() is 110, so 44+ votes).
assert_ok!(Welati::cast_vote(RuntimeOrigin::signed(3), 0, vec![1], None,));
assert_ok!(Welati::cast_vote(RuntimeOrigin::signed(4), 0, vec![2], None,));
// 5. Seçimi sonlandır
for voter in 500..=541u64 {
assert_ok!(Welati::cast_vote(RuntimeOrigin::signed(voter), 0, vec![1], None,));
}
// 5. Finalize the election
run_to_block(86_400 + 259_200 + 432_000 + 2);
assert_ok!(Welati::finalize_election(RuntimeOrigin::root(), 0,));
@@ -501,7 +516,7 @@ fn complete_proposal_cycle_works() {
let title = b"Budget Amendment".to_vec().try_into().unwrap();
let description = b"Increase education budget by 10%".to_vec().try_into().unwrap();
// CRITICAL FIX: Helper fonksiyonları kullan
// CRITICAL FIX: Use the helper functions
add_parliament_member(1);
add_parliament_member(2);
add_parliament_member(3);
@@ -528,7 +543,7 @@ fn complete_proposal_cycle_works() {
});
}
// ===== RUNOFF ELECTION TESTLERİ =====
// ===== RUNOFF ELECTION TESTS =====
#[test]
fn initiate_runoff_election_works() {
@@ -787,7 +802,12 @@ fn finalize_election_updates_election_status() {
run_to_block(86_400 + 259_200 + 100);
// Enough votes to clear the 40% Parliamentary quorum (MockTrustProvider's
// fixed citizen_count() is 110, so 44+ votes).
assert_ok!(Welati::cast_vote(RuntimeOrigin::signed(100), 0, vec![1], None,));
for voter in 600..=642u64 {
assert_ok!(Welati::cast_vote(RuntimeOrigin::signed(voter), 0, vec![1], None,));
}
run_to_block(86_400 + 259_200 + 432_000 + 100);
File diff suppressed because it is too large. Load diff
@@ -21,7 +21,7 @@ fn main() {
#[cfg(all(feature = "metadata-hash", feature = "std"))]
fn main() {
bizinikiwi_wasm_builder::WasmBuilder::init_with_defaults()
.enable_metadata_hash("TYR", 12)
.enable_metadata_hash("HEZ", 12)
.build();
}
@@ -31,9 +31,7 @@ use pezframe_support::build_struct_json_patch;
use pezsp_core::crypto::UncheckedInto;
use pezsp_genesis_builder::PresetId;
use pezsp_keyring::Sr25519Keyring;
use testnet_teyrchains_constants::pezkuwichain::{
currency::UNITS as TYR, xcm_version::SAFE_XCM_VERSION,
};
use testnet_teyrchains_constants::pezkuwichain::{currency::UNITS, xcm_version::SAFE_XCM_VERSION};
use teyrchains_common::{AccountId, AssetIdForTrustBackedAssets, AuraId};
use xcm::latest::prelude::*;
use xcm_builder::GlobalConsensusConvertsFor;
@@ -62,16 +60,16 @@ pub const PEZ_DECIMALS: u8 = 12;
pub const WUSDT_DECIMALS: u8 = 6;
/// Treasury allocation: 20.25% = 1,012,500,000 PEZ
pub const PEZ_TREASURY_ALLOCATION: Balance = 1_012_500_000 * TYR;
pub const PEZ_TREASURY_ALLOCATION: Balance = 1_012_500_000 * UNITS;
/// Founder allocation: 1.875% = 93,750,000 PEZ
pub const PEZ_FOUNDER_ALLOCATION: Balance = 93_750_000 * TYR;
pub const PEZ_FOUNDER_ALLOCATION: Balance = 93_750_000 * UNITS;
/// Presale allocation: 1.875% = 93,750,000 PEZ
pub const PEZ_PRESALE_ALLOCATION: Balance = 93_750_000 * TYR;
pub const PEZ_PRESALE_ALLOCATION: Balance = 93_750_000 * UNITS;
/// Rewards pool: 76% = 3,800,000,000 PEZ (distributed via sentetik halving)
pub const PEZ_REWARDS_POOL: Balance = 3_800_000_000 * TYR;
pub const PEZ_REWARDS_POOL: Balance = 3_800_000_000 * UNITS;
/// Total PEZ supply: 5 Billion (derived from allocations)
pub const PEZ_TOTAL_SUPPLY: Balance =
@@ -79,7 +77,7 @@ pub const PEZ_TOTAL_SUPPLY: Balance =
// Compile-time verification that total equals expected 5 billion
const _: () = assert!(
PEZ_TOTAL_SUPPLY == 5_000_000_000 * TYR,
PEZ_TOTAL_SUPPLY == 5_000_000_000 * UNITS,
"PEZ allocations must sum to exactly 5 billion tokens"
);
@@ -322,7 +320,7 @@ pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> {
Sr25519Keyring::AliceStash.to_account_id(),
Sr25519Keyring::BobStash.to_account_id(),
],
TYR * 1_000_000,
UNITS * 1_000_000,
1000.into(),
treasury_account,
founder_account,
@@ -138,7 +138,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
impl_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
authoring_version: 1,
spec_version: 1_020_007,
spec_version: 1_020_011,
impl_version: 0,
apis: RUNTIME_API_VERSIONS,
transaction_version: 16,
@@ -278,7 +278,14 @@ impl pezpallet_staking_async::Config for Runtime {
type Currency = Balances;
type CurrencyBalance = Balance;
type RuntimeHoldReason = RuntimeHoldReason;
type CurrencyToVote = pezsp_staking::currency_to_vote::SaturatingCurrencyToVote;
// U128, not Saturating: HEZ has 12 decimals and total issuance (~204M HEZ = ~2.04e20 planck)
// far exceeds u64::MAX (~1.8446e19). SaturatingCurrencyToVote casts balance->u64 saturating, so
// any stash bonding more than ~18.45M HEZ collapses to u64::MAX. Two large nominators (~41M and
// ~40M HEZ) both saturated to u64::MAX, becoming tied voters; the resulting tied edge weights hit
// the `reduce_4` "duplicate/corrupt input" panic in the offchain election miner, so no solution
// was ever produced and no era exposures were written. U128CurrencyToVote scales by
// (total_issuance / u64::MAX) instead, keeping every stake distinct and inside u64 range.
type CurrencyToVote = pezsp_staking::currency_to_vote::U128CurrencyToVote;
type RewardRemainder = ResolveTo<xcm_config::TreasuryAccount, Balances>;
type Slash = ResolveTo<xcm_config::TreasuryAccount, Balances>;
type Reward = ();
@@ -321,7 +321,7 @@ pub type TrustedReserves = (
/// Cases where a remote origin is accepted as trusted Teleporter for a given asset:
///
/// - TYR with the parent Relay Chain and sibling system teyrchains; and
/// - HEZ with the parent Relay Chain and sibling system teyrchains; and
/// - Sibling teyrchains' assets from where they originate (as `ForeignCreators`).
pub type TrustedTeleporters = (
ConcreteAssetFromSystem<TokenLocation>,
@@ -518,7 +518,7 @@ pub mod bridging {
/// 1) an approximate cost of XCM execution (`ExportMessage` and surroundings) at Pezkuwichain bridge hub;
///
/// 2) the approximate cost of Pezkuwichain -> Zagros message delivery transaction on Zagros Bridge Hub,
/// converted into TYRs using 1:1 conversion rate;
/// converted into HEZ using 1:1 conversion rate;
///
/// 3) the approximate cost of Pezkuwichain -> Zagros message confirmation transaction on Pezkuwichain Bridge Hub.
pub storage XcmBridgeHubRouterBaseFee: Balance =
@@ -616,7 +616,7 @@ pub mod bridging {
parameter_types! {
/// User fee for ERC20 token transfer back to Ethereum.
/// (initially was calculated by test `OutboundQueue::calculate_fees` - ETH/TYR 1/400 and fee_per_gas 20 GWEI = 2200698000000 + *25%)
/// (initially was calculated by test `OutboundQueue::calculate_fees` - ETH/HEZ 1/400 and fee_per_gas 20 GWEI = 2200698000000 + *25%)
/// Needs to be more than fee calculated from DefaultFeeConfig FeeConfigRecord in snowbridge:teyrchain/pallets/outbound-queue/src/lib.rs
/// Pezkuwi uses 10 decimals, Dicle and Pezkuwichain 12 decimals.
pub const DefaultBridgeHubEthereumBaseFee: Balance = 3_833_568_200_000;
@@ -829,7 +829,7 @@ mod asset_hub_pezkuwichain_tests {
);
},
|| {
// `SwapFirstAssetTrader` - staking pot receives xcm fees in TYRs
// `SwapFirstAssetTrader` - staking pot receives xcm fees in HEZ
assert!(
Balances::free_balance(&staking_pot) > ExistentialDeposit::get()
);
@@ -148,7 +148,7 @@ pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> {
dev_stakers,
1000.into(),
vec![
// bridged TYR
// bridged HEZ
(
Location::new(2, [GlobalConsensus(PezkuwichainNetwork::get())]),
GlobalConsensusConvertsFor::<UniversalLocation, AccountId>::convert_location(
@@ -159,7 +159,7 @@ pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> {
),
],
vec![
// bridged TYR to Bob
// bridged HEZ to Bob
(
Location::new(2, [GlobalConsensus(PezkuwichainNetwork::get())]),
Sr25519Keyring::Bob.to_account_id(),
@@ -145,13 +145,15 @@ impl_opaque_keys! {
#[pezsp_version::runtime_version]
pub const VERSION: RuntimeVersion = RuntimeVersion {
// Note: "westmint" is the legacy name for this chain. It has been renamed to
// "asset-hub-zagros". Many wallets/tools depend on the `spec_name`, so it remains "westmint"
// for the time being. Wallets/tools should update to treat "asset-hub-zagros" equally.
spec_name: alloc::borrow::Cow::Borrowed("westmint"),
impl_name: alloc::borrow::Cow::Borrowed("westmint"),
// spec_name is "asset-hub-zagros", consistent with the already-rebranded mainnet
// Asset Hub ("asset-hub-pezkuwichain"). The upstream legacy name was "westmint";
// it is dropped here for full independence. This is a testnet identity change that
// takes effect via a runtime upgrade — tooling keyed on the old "westmint" name
// for the Zagros Asset Hub must update.
spec_name: alloc::borrow::Cow::Borrowed("asset-hub-zagros"),
impl_name: alloc::borrow::Cow::Borrowed("asset-hub-zagros"),
authoring_version: 1,
spec_version: 1_020_004,
spec_version: 1_020_005,
impl_version: 0,
apis: RUNTIME_API_VERSIONS,
transaction_version: 16,
@@ -2469,7 +2471,7 @@ pezpallet_revive::impl_runtime_apis_plus_revive_traits!(
&account,
<Balances as Inspect<_>>::minimum_balance(),
));
// register foreign TYRs
// register foreign HEZ
assert_ok!(ForeignAssets::force_create(
RuntimeOrigin::root(),
roc_id.clone().into(),
@@ -277,7 +277,9 @@ impl pezpallet_staking_async::Config for Runtime {
type Currency = Balances;
type CurrencyBalance = Balance;
type RuntimeHoldReason = RuntimeHoldReason;
type CurrencyToVote = pezsp_staking::currency_to_vote::SaturatingCurrencyToVote;
// U128, not Saturating — see asset-hub-pezkuwichain: Saturating clips any stake above
// ~u64::MAX planck to u64::MAX, producing tied voters that panic the election miner's reduce_4.
type CurrencyToVote = pezsp_staking::currency_to_vote::U128CurrencyToVote;
type RewardRemainder = ();
type Slash = ();
type Reward = ();
@@ -1102,7 +1102,7 @@ fn receive_reserve_asset_deposited_roc_from_asset_hub_pezkuwichain_fees_paid_by_
ExistentialDeposit::get(),
AccountId::from([73; 32]),
block_author_account.clone(),
// receiving TYRs
// receiving HEZ
foreign_asset_create_params,
1000000000000,
|| {
@@ -1131,7 +1131,7 @@ fn receive_reserve_asset_deposited_roc_from_asset_hub_pezkuwichain_fees_paid_by_
);
},
|| {
// `SwapFirstAssetTrader` - staking pot receives xcm fees in TYRs
// `SwapFirstAssetTrader` - staking pot receives xcm fees in HEZ
assert!(
Balances::free_balance(&staking_pot) > ExistentialDeposit::get()
);
@@ -1182,7 +1182,7 @@ fn receive_reserve_asset_deposited_roc_from_asset_hub_pezkuwichain_fees_paid_by_
ExistentialDeposit::get(),
AccountId::from([73; 32]),
block_author_account.clone(),
// receiving TYRs
// receiving HEZ
foreign_asset_create_params,
1000000000000,
|| {
@@ -5,8 +5,8 @@
- [Init bridge and run relayer between BridgeHubpezkuwichain and
BridgeHubzagros](#init-bridge-and-run-relayer-between-bridgehubpezkuwichain-and-bridgehubzagros)
- [Initialize configuration for transfer asset over bridge
(TYRs/WNDs)](#initialize-configuration-for-transfer-asset-over-bridge-rocswnds)
- [Send messages - transfer asset over bridge (TYRs/WNDs)](#send-messages---transfer-asset-over-bridge-rocswnds)
(HEZ/WNDs)](#initialize-configuration-for-transfer-asset-over-bridge-rocswnds)
- [Send messages - transfer asset over bridge (HEZ/WNDs)](#send-messages---transfer-asset-over-bridge-rocswnds)
- [Claim relayer's rewards on BridgeHubpezkuwichain and
BridgeHubzagros](#claim-relayers-rewards-on-bridgehubpezkuwichain-and-bridgehubzagros)
- [How to test local BridgeHubDicle/BridgeHubPezkuwiChain](#how-to-test-local-bridgehubdiclebridgehubpezkuwi)
@@ -127,10 +127,10 @@ cd <pezkuwi-sdk-git-repo-dir>
- zagros teyrchain: - https://pezkuwichain.io/?rpc=ws%3A%2F%2F127.0.0.1%3A8945#/chainstate - Pezpallet:
**bridgepezkuwichainTeyrchains** - Keys: **parasInfo(None)**
### Initialize configuration for transfer asset over bridge (TYRs/WNDs)
### Initialize configuration for transfer asset over bridge (HEZ/WNDs)
This initialization does several things:
- creates `ForeignAssets` for wrappedTYRs/wrappedWNDs
- creates `ForeignAssets` for wrappedHEZ/wrappedWNDs
- drips SA for AssetHubpezkuwichain on AssetHubzagros (and vice versa) which holds reserved assets on source chains
```
cd <pezkuwi-sdk-git-repo-dir>
@@ -141,13 +141,13 @@ cd <pezkuwi-sdk-git-repo-dir>
./bridges/testing/environments/pezkuwichain-zagros/bridges_pezkuwichain_zagros.sh init-bridge-hub-zagros-local
```
### Send messages - transfer asset over bridge (TYRs/WNDs)
### Send messages - transfer asset over bridge (HEZ/WNDs)
Do reserve-backed transfers:
```
cd <pezkuwi-sdk-git-repo-dir>
# TYRs from pezkuwichain's Asset Hub to zagros's.
# HEZ from pezkuwichain's Asset Hub to zagros's.
./bridges/testing/environments/pezkuwichain-zagros/bridges_pezkuwichain_zagros.sh reserve-transfer-assets-from-asset-hub-pezkuwichain-local
```
```
@@ -174,7 +174,7 @@ cd <pezkuwi-sdk-git-repo-dir>
```
cd <pezkuwi-sdk-git-repo-dir>
# wrappedTYRs from zagros's Asset Hub to pezkuwichain's.
# wrappedHEZ from zagros's Asset Hub to pezkuwichain's.
./bridges/testing/environments/pezkuwichain-zagros/bridges_pezkuwichain_zagros.sh withdraw-reserve-assets-from-asset-hub-zagros-local
```
@@ -41,7 +41,7 @@ use pezpallet_bridge_relayers::extension::{
BridgeRelayersTransactionExtension, WithMessagesExtensionConfig,
};
use pezpallet_xcm_bridge_hub::XcmAsPlainPayload;
use testnet_teyrchains_constants::pezkuwichain::currency::UNITS as TYR;
use testnet_teyrchains_constants::pezkuwichain::currency::UNITS as HEZ;
use xcm::{
latest::prelude::*,
prelude::{InteriorLocation, NetworkId},
@@ -75,7 +75,7 @@ parameter_types! {
/// PeoplePezkuwichain location
pub PeoplePezkuwichainLocation: Location = Location::new(1, [Teyrchain(pezkuwichain_runtime_constants::system_teyrchain::PEOPLE_ID)]);
pub storage BridgeDeposit: Balance = 5 * TYR;
pub storage BridgeDeposit: Balance = 5 * HEZ;
}
/// Proof of messages, coming from Pezkuwichain Bulletin chain.
@@ -40,7 +40,7 @@ use pezpallet_bridge_messages::LaneIdOf;
use pezpallet_bridge_relayers::extension::{
BridgeRelayersTransactionExtension, WithMessagesExtensionConfig,
};
use testnet_teyrchains_constants::pezkuwichain::currency::UNITS as TYR;
use testnet_teyrchains_constants::pezkuwichain::currency::UNITS as HEZ;
use teyrchains_common::xcm_config::{AllSiblingSystemTeyrchains, RelayOrOtherSystemTeyrchains};
use xcm::{
latest::{prelude::*, ZAGROS_GENESIS_HASH},
@@ -70,7 +70,7 @@ parameter_types! {
]
);
pub storage BridgeDeposit: Balance = 5 * TYR;
pub storage BridgeDeposit: Balance = 5 * HEZ;
}
/// Proof of messages, coming from Zagros.
@@ -270,12 +270,12 @@ mod tests {
///
/// We want this tip to be large enough (delivery transactions with more messages = less
/// operational costs and a faster bridge), so this value should be significant.
const FEE_BOOST_PER_MESSAGE: Balance = 2 * TYR;
const FEE_BOOST_PER_MESSAGE: Balance = 2 * HEZ;
// see `FEE_BOOST_PER_MESSAGE` comment
const FEE_BOOST_PER_RELAY_HEADER: Balance = 2 * TYR;
const FEE_BOOST_PER_RELAY_HEADER: Balance = 2 * HEZ;
// see `FEE_BOOST_PER_MESSAGE` comment
const FEE_BOOST_PER_TEYRCHAIN_HEADER: Balance = 2 * TYR;
const FEE_BOOST_PER_TEYRCHAIN_HEADER: Balance = 2 * HEZ;
#[test]
fn ensure_bridge_hub_pezkuwichain_message_lane_weights_are_correct() {
@@ -26,13 +26,13 @@ fn main() {
#[cfg(all(feature = "metadata-hash", feature = "std"))]
fn main() {
bizinikiwi_wasm_builder::WasmBuilder::init_with_defaults()
.enable_metadata_hash("TYR", 12)
.enable_metadata_hash("HEZ", 12)
.build();
bizinikiwi_wasm_builder::WasmBuilder::init_with_defaults()
.set_file_name("fast_runtime_binary.rs")
.enable_feature("fast-runtime")
.enable_metadata_hash("TYR", 12)
.enable_metadata_hash("HEZ", 12)
.build();
}
@@ -22,7 +22,7 @@ use pezframe_support::build_struct_json_patch;
use pezsp_genesis_builder::PresetId;
use pezsp_keyring::Sr25519Keyring;
use testnet_teyrchains_constants::pezkuwichain::{
currency::UNITS as TYR, xcm_version::SAFE_XCM_VERSION,
currency::UNITS as HEZ, xcm_version::SAFE_XCM_VERSION,
};
use teyrchains_common::{AccountId, AuraId};
@@ -71,7 +71,7 @@ pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> {
(Sr25519Keyring::Bob.to_account_id(), Sr25519Keyring::Bob.public().into()),
],
Sr25519Keyring::well_known().map(|x| x.to_account_id()).collect(),
TYR * 1_000_000,
HEZ * 1_000_000,
CORETIME_PARA_ID,
),
pezsp_genesis_builder::DEV_RUNTIME_PRESET => coretime_pezkuwichain_genesis(
@@ -83,7 +83,7 @@ pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> {
Sr25519Keyring::AliceStash.to_account_id(),
Sr25519Keyring::BobStash.to_account_id(),
],
TYR * 1_000_000,
HEZ * 1_000_000,
CORETIME_PARA_ID,
),
_ => return None,
@@ -186,7 +186,7 @@ pub type WaivedLocations = (
);
/// Cases where a remote origin is accepted as trusted Teleporter for a given asset:
/// - TYR with the parent Relay Chain and sibling teyrchains.
/// - HEZ with the parent Relay Chain and sibling teyrchains.
pub type TrustedTeleporters = ConcreteAssetFromSystem<RocRelayLocation>;
pub struct XcmConfig;
@@ -196,7 +196,7 @@ impl xcm_executor::Config for XcmConfig {
type XcmEventEmitter = PezkuwiXcm;
type AssetTransactor = AssetTransactors;
type OriginConverter = XcmOriginToTransactDispatchOrigin;
// Coretime chain does not recognize a reserve location for any asset. Users must teleport TYR
// Coretime chain does not recognize a reserve location for any asset. Users must teleport HEZ
// where allowed (e.g. with the Relay Chain).
type IsReserve = ();
type IsTeleporter = TrustedTeleporters;
@@ -232,7 +232,7 @@ impl xcm_executor::Config for XcmConfig {
type XcmEventEmitter = PezkuwiXcm;
type AssetTransactor = AssetTransactors;
type OriginConverter = XcmOriginToTransactDispatchOrigin;
// Coretime chain does not recognize a reserve location for any asset. Users must teleport TYR
// Coretime chain does not recognize a reserve location for any asset. Users must teleport ZGR
// where allowed (e.g. with the Relay Chain).
type IsReserve = ();
type IsTeleporter = TrustedTeleporters;
@@ -29,7 +29,7 @@ use pezsp_core::{crypto::UncheckedInto, H256};
use pezsp_genesis_builder::PresetId;
use pezsp_keyring::Sr25519Keyring;
use testnet_teyrchains_constants::pezkuwichain::{
currency::UNITS as TYR, xcm_version::SAFE_XCM_VERSION,
currency::UNITS as HEZ, xcm_version::SAFE_XCM_VERSION,
};
use teyrchains_common::{AccountId, AuraId};
@@ -162,7 +162,7 @@ pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> {
(Sr25519Keyring::Bob.to_account_id(), Sr25519Keyring::Bob.public().into()),
],
Sr25519Keyring::well_known().map(|x| x.to_account_id()).collect(),
TYR * 1_000_000,
HEZ * 1_000_000,
PEOPLE_PARA_ID,
// Founding citizens: Alice and Bob are founding citizens for testing
vec![
@@ -185,7 +185,7 @@ pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> {
Sr25519Keyring::AliceStash.to_account_id(),
Sr25519Keyring::BobStash.to_account_id(),
],
TYR * 1_000_000,
HEZ * 1_000_000,
PEOPLE_PARA_ID,
// Founding citizen: Alice is the founding citizen for dev
vec![(Sr25519Keyring::Alice.to_account_id(), default_founding_citizen_identity_hash())],
@@ -157,7 +157,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: alloc::borrow::Cow::Borrowed("people-pezkuwichain"),
impl_name: alloc::borrow::Cow::Borrowed("people-pezkuwichain"),
authoring_version: 1,
spec_version: 1_020_009,
spec_version: 1_020_011,
impl_version: 0,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
@@ -312,6 +312,12 @@ parameter_types! {
pub const MaxStudentsPerCourse: u32 = 1000;
pub const MaxCoursesPerStudent: u32 = 50;
pub const MaxPointsPerCourse: u32 = 1000;
/// Pezpallet ID used to derive a keyless sovereign "admin" account for courses created
/// via Root or Council (i.e. no natural signer/AccountId exists for those origins).
/// This is NOT an sr25519 keyring account: nobody holds (or can hold) a private key for
/// a `PalletId`-derived account, so it can never be used to forge `complete_course` /
/// `archive_course` calls the way a well-known dev seed (e.g. `//Alice`) could.
pub const PerwerdeAdminPotId: pezframe_support::PalletId = pezframe_support::PalletId(*b"pez/prwd");
}
/// Admin origin for Perwerde pezpallet that supports progressive decentralization
@@ -321,15 +327,27 @@ parameter_types! {
/// 2. Council (1/2 çoğunluk) - Seçimler sonrası
/// 3. Serok atayabilir - Cumhurbaşkanlığı yetkisi
///
/// Bu origin AccountId döndürür (kurs sahibi olarak kullanılır)
/// Bu origin AccountId döndürür (kurs sahibi olarak kullanılır).
///
/// SECURITY: Root and Council origins carry no real signer/AccountId of their own. This
/// origin previously stood in with the well-known dev keypair `//Alice`, whose private key
/// is public knowledge — anyone could derive it and then sign `complete_course` as the
/// "owner" of any Root/Council-created course, forging arbitrary trust-affecting course
/// completions. It now resolves to a `PalletId`-derived sovereign account instead, which has
/// no private key at all, so it can authenticate privileged *creation/archival* (which also
/// goes through `AdminOrigin`) but can never be used to sign a `complete_course` extrinsic.
/// Only Serok-originated courses (which have a genuine on-chain account) can currently be
/// completed via the owner-signature path; enabling completions for Root/Council-created
/// courses requires a follow-up that lets the privileged caller nominate a real owner
/// account explicitly (e.g. an added `owner` parameter on `create_course`).
pub struct PerwerdeAdminOrigin;
impl pezframe_support::traits::EnsureOrigin<RuntimeOrigin> for PerwerdeAdminOrigin {
type Success = AccountId;
fn try_origin(o: RuntimeOrigin) -> Result<Self::Success, RuntimeOrigin> {
// 1. Root origin kontrolü
if let Ok(_) = pezframe_system::ensure_root(o.clone()) {
// Root için varsayılan admin hesabı
return Ok(pezsp_keyring::Sr25519Keyring::Alice.to_account_id());
// Root için keyless (private key'i olmayan) sovereign hesap
return Ok(PerwerdeAdminPotId::get().into_account_truncating());
}
// 2. Council kontrolü (1/2'den fazla oy)
@@ -340,8 +358,8 @@ impl pezframe_support::traits::EnsureOrigin<RuntimeOrigin> for PerwerdeAdminOrig
2,
>::try_origin(o.clone())
{
// Komisyon için varsayılan admin hesabı
return Ok(pezsp_keyring::Sr25519Keyring::Alice.to_account_id());
// Komisyon için keyless (private key'i olmayan) sovereign hesap
return Ok(PerwerdeAdminPotId::get().into_account_truncating());
}
// 3. Serok (Cumhurbaşkanı) kontrolü
@@ -455,6 +473,13 @@ impl pezpallet_tiki::Config for Runtime {
// Kademeli yetki devri: Root → Teknik Komisyon
// NFT/Rol yönetimi için Teknik Komisyon yetkili
type AdminOrigin = crate::RootOrTechnicalCommittee;
// Elected/Earned roles (Serok, SerokiMeclise, Parlementer, Axa, ...) are meant to
// carry evidence of a real election/exam, not just committee say-so. Until a
// dedicated voting/exam pezpallet exists to escalate to Root on their behalf, require
// full Root here — deliberately stricter than the Technical Committee threshold used
// for ordinary admin appointment via `AdminOrigin`.
type ElectedRoleOrigin = EnsureRoot<AccountId>;
type EarnedRoleOrigin = EnsureRoot<AccountId>;
type WeightInfo = pezpallet_tiki::weights::BizinikiwiWeight<Runtime>;
type TikiCollectionId = TikiCollectionId;
type MaxTikisPerUser = MaxTikisPerUser;
@@ -478,6 +503,32 @@ impl pezpallet_staking_score::NoterCheck<AccountId> for TikiNoterChecker {
fn is_noter(who: &AccountId) -> bool {
pezpallet_tiki::Pezpallet::<Runtime>::has_tiki(who, &pezpallet_tiki::Tiki::Noter)
}
#[cfg(feature = "runtime-benchmarks")]
fn make_noter(who: &AccountId) {
pezpallet_tiki::UserTikis::<Runtime>::mutate(who, |tikis| {
let _ = tikis.try_push(pezpallet_tiki::Tiki::Noter);
});
}
}
parameter_types! {
// Real-world analogy: a notary's bond/insurance. An account must hold the
// Noter tiki *and* post this bond via `register_as_noter` before its
// submissions are accepted. Slashable by `RootOrDiwanOrTechnical` if a
// disputed submission is confirmed fraudulent. The tiki role itself
// supports any number of independently registered accounts — this is not
// a single hardcoded noter, the same way a state can authorize any number
// of notaries.
pub const NoterBondAmount: Balance = 50_000 * UNITS;
// Real-world analogy: a notarized document's recording/contestability
// period — a noter-signed submission only takes effect after this many
// blocks unchallenged. Root/XCM-Transact submissions (chain-authenticated,
// not a personal key) are exempt. One real hour, using this runtime's
// actual `HOURS` constant (`testnet_teyrchains_constants::pezkuwichain`,
// derived from the real 6s slot duration this runtime is configured
// with) — matches staking-score's own internal `HOUR_IN_BLOCKS`.
pub const StakingNoterDisputeWindow: BlockNumber = HOURS;
}
impl pezpallet_staking_score::Config for Runtime {
@@ -485,6 +536,16 @@ impl pezpallet_staking_score::Config for Runtime {
type Balance = Balance;
type OnStakingUpdate = Trust;
type NoterChecker = TikiNoterChecker;
type Currency = Balances;
type NoterBondAmount = NoterBondAmount;
type DisputeWindow = StakingNoterDisputeWindow;
// Lightweight: any single Council member can freeze a suspicious pending
// submission for review — mirrors the existing VetoOrigin pattern below.
type DisputeOrigin = pezpallet_collective::EnsureMember<AccountId, CouncilCollective>;
// Deliberately stronger: slashing a noter's bond needs an actual
// governance decision, not one member's word.
type SlashOrigin = crate::RootOrDiwanOrTechnical;
type SlashDestination = RelayTreasuryAccount;
}
// =============================================================================
@@ -779,7 +840,12 @@ impl pezpallet_democracy::Config for Runtime {
type CancellationOrigin = EnsureRoot<AccountId>;
type BlacklistOrigin = EnsureRoot<AccountId>;
type CancelProposalOrigin = EnsureRoot<AccountId>;
type VetoOrigin = pezframe_system::EnsureSigned<AccountId>;
// VetoOrigin must be a small privileged body, not any signed account (upstream contract:
// see bizinikiwi/bin/node/runtime's `EnsureMember<AccountId, TechnicalCollective>`). This
// runtime has no separate technical collective, so require membership in the Council
// instead — any single Council member may veto/blacklist an external proposal, but an
// arbitrary unprivileged account can no longer do so.
type VetoOrigin = pezpallet_collective::EnsureMember<AccountId, CouncilCollective>;
type Slash = ();
type Scheduler = Scheduler;
type PalletsOrigin = OriginCaller;
@@ -918,8 +984,6 @@ parameter_types! {
pub const PezAssetId: u32 = 1;
/// Incentive Pot Pezpallet ID
pub const IncentivePotId: pezframe_support::PalletId = pezframe_support::PalletId(*b"pez/incv");
/// Clawback recipient (QaziMuhammed account - placeholder)
pub ClawbackRecipient: AccountId = pezsp_keyring::Sr25519Keyring::Bob.to_account_id();
}
/// Trust score source for PEZ Rewards
@@ -936,7 +1000,11 @@ impl pezpallet_pez_rewards::Config for Runtime {
type WeightInfo = pezpallet_pez_rewards::weights::BizinikiwiWeight<Runtime>;
type TrustScoreSource = PezRewardsTrustScoreSource;
type IncentivePotId = IncentivePotId;
type ClawbackRecipient = ClawbackRecipient;
// Clawback recipient: relay chain Treasury pallet sovereign account (governance-controlled,
// no private key), reusing the same `RelayTreasuryAccount` already used above for slashed
// identity deposits. This is NOT a placeholder dev key: an sr25519 keyring account (e.g.
// Bob) must never be used here since its private key is publicly known.
type ClawbackRecipient = RelayTreasuryAccount;
// Kademeli yetki devri: Root → Hazine Komisyonu
// PEZ ödül dağıtımı için Hazine Komisyonu yetkili
type ForceOrigin = crate::RootOrTreasuryCommittee;
@@ -61,7 +61,7 @@ parameter_types! {
pub const MaxAssetsIntoHolding: u32 = 64;
pub const GovernanceLocation: Location = Location::parent();
pub const FellowshipLocation: Location = Location::parent();
/// The asset ID for the asset that we use to pay for message delivery fees. Just TYR.
/// The asset ID for the asset that we use to pay for message delivery fees. Just HEZ.
pub FeeAssetId: AssetId = AssetId(RelayLocation::get());
/// The base fee for the message delivery fees.
pub const BaseDeliveryFee: u128 = CENTS.saturating_mul(3);
@@ -225,10 +225,10 @@ impl xcm_executor::Config for XcmConfig {
type XcmEventEmitter = PezkuwiXcm;
type AssetTransactor = FungibleTransactor;
type OriginConverter = XcmOriginToTransactDispatchOrigin;
// People chain does not recognize a reserve location for any asset. Users must teleport TYR
// People chain does not recognize a reserve location for any asset. Users must teleport HEZ
// where allowed (e.g. with the Relay Chain).
type IsReserve = ();
/// Only allow teleportation of TYR.
/// Only allow teleportation of HEZ.
type IsTeleporter = ConcreteAssetFromSystem<RelayLocation>;
type UniversalLocation = UniversalLocation;
type Barrier = Barrier;
@@ -129,7 +129,7 @@ pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber);
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
pub const TYR: Balance = 1_000_000_000_000;
pub const HEZ: Balance = 1_000_000_000_000;
pub const MILLIROC: Balance = 1_000_000_000;
pub const MICROROC: Balance = 1_000_000;
@@ -426,8 +426,8 @@ pub type XcmOriginToTransactDispatchOrigin = (
parameter_types! {
// One XCM operation is 1_000_000_000 weight - almost certainly a conservative estimate.
pub UnitWeightCost: Weight = Weight::from_parts(1_000_000_000, 64 * 1024);
// One TYR buys 1 second of weight.
pub const WeightPrice: (Location, u128) = (Location::parent(), TYR);
// One HEZ buys 1 second of weight.
pub const WeightPrice: (Location, u128) = (Location::parent(), HEZ);
pub const MaxInstructions: u32 = 100;
}
@@ -480,7 +480,7 @@ impl xcm_executor::Config for XcmConfig {
type AssetTransactor = AssetTransactors;
type OriginConverter = XcmOriginToTransactDispatchOrigin;
type IsReserve = Reserves;
type IsTeleporter = NativeAsset; // <- should be enough to allow teleportation of TYR
type IsTeleporter = NativeAsset; // <- should be enough to allow teleportation of HEZ
type UniversalLocation = UniversalLocation;
type Barrier = Barrier;
type Weigher = FixedWeightBounds<UnitWeightCost, RuntimeCall, MaxInstructions>;
@@ -578,11 +578,11 @@ impl pezcumulus_ping::Config for Runtime {
}
parameter_types! {
pub const AssetDeposit: Balance = TYR;
pub const AssetAccountDeposit: Balance = TYR;
pub const AssetDeposit: Balance = HEZ;
pub const AssetAccountDeposit: Balance = HEZ;
pub const ApprovalDeposit: Balance = 100 * MILLIROC;
pub const AssetsStringLimit: u32 = 50;
pub const MetadataDepositBase: Balance = TYR;
pub const MetadataDepositBase: Balance = HEZ;
pub const MetadataDepositPerByte: Balance = 10 * MILLIROC;
pub const UnitBody: BodyId = BodyId::Unit;
}