datahaven/operator/runtime/mainnet/tests/proxy.rs
Steve Degosserie 5a7983f0d8
chore: ♻️ Add missing license header in operator & AVS contracts source code (#285)
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
2025-11-10 12:56:41 +01:00

1288 lines
45 KiB
Rust

// Copyright 2025 DataHaven
// This file is part of DataHaven.
// DataHaven is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// DataHaven is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with DataHaven. If not, see <http://www.gnu.org/licenses/>.
//! Proxy pallet integration tests for DataHaven mainnet runtime
#[path = "common.rs"]
mod common;
use codec::Encode;
use common::*;
use datahaven_mainnet_runtime::{
configs::{MaxProxies, ProxyDepositBase, ProxyDepositFactor},
currency::HAVE,
Balances, Identity, Multisig, Proxy, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Sudo,
System,
};
use frame_support::{assert_noop, assert_ok, traits::InstanceFilter};
use pallet_proxy::Event as ProxyEvent;
use sp_core::blake2_256;
use datahaven_mainnet_runtime::configs::ProxyType;
// =================================================================================================
// BASIC PROXY OPERATIONS
// Tests for fundamental proxy pallet extrinsics: add_proxy, remove_proxy, proxy
// =================================================================================================
#[test]
fn test_add_proxy_with_any_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let alice_balance_before = Balances::free_balance(&alice);
// Add Bob as Any proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
// Check deposit was taken
let expected_deposit = ProxyDepositBase::get() + ProxyDepositFactor::get();
let alice_balance_after = Balances::free_balance(&alice);
assert_eq!(alice_balance_before - alice_balance_after, expected_deposit);
// Check proxy was added
let proxies = Proxy::proxies(alice.clone());
assert_eq!(proxies.0.len(), 1);
let proxy = &proxies.0[0];
assert_eq!(proxy.delegate, bob);
assert_eq!(proxy.proxy_type, ProxyType::Any);
assert_eq!(proxy.delay, 0);
});
}
#[test]
fn test_add_multiple_proxies() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add multiple proxies
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
charlie.clone(),
ProxyType::Balances,
0
));
// Check both proxies were added
let proxies = Proxy::proxies(alice.clone());
assert_eq!(proxies.0.len(), 2);
// Verify deposits were taken correctly
let expected_total_deposit = ProxyDepositBase::get() + 2 * ProxyDepositFactor::get();
assert_eq!(proxies.1, expected_total_deposit);
});
}
#[test]
fn test_remove_proxy() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let alice_balance_before = Balances::free_balance(&alice);
// Add proxy
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
let _alice_balance_after_add = Balances::free_balance(&alice);
// Remove proxy
assert_ok!(Proxy::remove_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
// Check proxy was removed
let proxies = Proxy::proxies(alice.clone());
assert_eq!(proxies.0.len(), 0);
// Check deposit was returned
let alice_balance_after_remove = Balances::free_balance(&alice);
assert_eq!(alice_balance_before, alice_balance_after_remove);
});
}
#[test]
fn test_remove_all_proxies() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
let alice_balance_before = Balances::free_balance(&alice);
// Add multiple proxies
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
charlie.clone(),
ProxyType::Balances,
0
));
// Remove all proxies
assert_ok!(Proxy::remove_proxies(RuntimeOrigin::signed(alice.clone())));
// Check all proxies were removed
let proxies = Proxy::proxies(alice.clone());
assert_eq!(proxies.0.len(), 0);
// Check deposit was returned
let alice_balance_after = Balances::free_balance(&alice);
assert_eq!(alice_balance_before, alice_balance_after);
});
}
#[test]
fn test_max_proxies_limit() {
ExtBuilder::default()
.with_balances(vec![(account_id(ALICE), 100_000 * HAVE)])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
// Add maximum number of proxies
for i in 0..MaxProxies::get() {
let proxy_account = account_id([i as u8 + 100; 20]);
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
proxy_account,
ProxyType::Any,
0
));
}
// Try to add one more proxy (should fail)
let excess_proxy = account_id([99u8; 20]);
assert_noop!(
Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
excess_proxy,
ProxyType::Any,
0
),
pallet_proxy::Error::<datahaven_mainnet_runtime::Runtime>::TooMany
);
});
}
#[test]
fn test_duplicate_proxy_prevention() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
// Add proxy
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
// Try to add same proxy again (should fail)
assert_noop!(
Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
),
pallet_proxy::Error::<datahaven_mainnet_runtime::Runtime>::Duplicate
);
});
}
// =================================================================================================
// PROXY TYPE FILTERING TESTS
// Tests for specific ProxyType variants and their call filtering behavior
// =================================================================================================
#[test]
fn test_proxy_call_with_any_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as Any proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
let charlie_balance_before = Balances::free_balance(&charlie);
// Bob can execute any call on behalf of Alice
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death {
dest: charlie.clone(),
value: 500 * HAVE,
}
))
));
let charlie_balance_after = Balances::free_balance(&charlie);
assert_eq!(charlie_balance_after - charlie_balance_before, 500 * HAVE);
});
}
#[test]
fn test_proxy_call_with_balances_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as Balances proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Balances,
0
));
// Bob can execute Balances calls
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death {
dest: charlie.clone(),
value: 100 * HAVE,
}
))
));
let charlie_balance = Balances::free_balance(&charlie);
assert_eq!(charlie_balance, 1_100 * HAVE);
});
}
#[test]
fn test_proxy_call_with_governance_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
// Add Bob as Governance proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Governance,
0
));
// Test that governance proxy can execute governance operations
// TODO: Replace with actual governance call once implemented
// For now, we use a utility call as a placeholder
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Utility(pallet_utility::Call::batch {
calls: vec![]
}))
));
});
}
#[test]
fn test_proxy_call_with_nontransfer_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as NonTransfer proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::NonTransfer,
0
));
// Bob can execute Identity calls (non-transfer)
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Identity(
pallet_identity::Call::clear_identity {}
))
));
// But Bob cannot execute Balances transfers - the proxy call succeeds but the inner call is filtered
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death {
dest: charlie.clone(),
value: 100 * HAVE,
}
))
));
// Check that the call was filtered by looking for the CallFiltered event
System::assert_last_event(RuntimeEvent::Proxy(ProxyEvent::ProxyExecuted {
result: Err(
frame_system::Error::<datahaven_mainnet_runtime::Runtime>::CallFiltered.into(),
),
}));
// Verify that Charlie's balance didn't change (transfer was filtered)
assert_eq!(Balances::free_balance(&charlie), 1_000 * HAVE); // Original balance unchanged
});
}
#[test]
fn test_proxy_call_with_staking_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
// Add Bob as Staking proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Staking,
0
));
// Test that staking proxy can execute staking operations
// TODO: Replace with actual staking call once implemented
// For now, we use a utility call as a placeholder
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Utility(pallet_utility::Call::batch {
calls: vec![]
}))
));
});
}
#[test]
fn test_proxy_call_with_identity_judgement_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE), // Charlie needs balance for identity deposit
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
// First, add Alice as a registrar in the Identity pallet
// This requires root origin
assert_ok!(Identity::add_registrar(
RuntimeOrigin::root(),
alice.clone().into()
));
// Set registrar fee for Alice
assert_ok!(Identity::set_fee(
RuntimeOrigin::signed(alice.clone()),
0, // registrar index
1 * HAVE, // fee
));
// Charlie needs to have an identity set to receive judgement
// First, Charlie needs to request judgement
let info = pallet_identity::legacy::IdentityInfo {
display: pallet_identity::Data::Raw(b"Charlie".to_vec().try_into().unwrap()),
..Default::default()
};
assert_ok!(Identity::set_identity(
RuntimeOrigin::signed(account_id(CHARLIE)),
Box::new(info.clone())
));
// Charlie requests judgement from registrar 0 (Alice)
assert_ok!(Identity::request_judgement(
RuntimeOrigin::signed(account_id(CHARLIE)),
0, // registrar index
10 * HAVE, // max fee
));
// Add Bob as IdentityJudgement proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::IdentityJudgement,
0
));
// IdentityJudgement proxy can execute judgement-related calls
// Use the hash of the identity info
use sp_runtime::traits::Hash;
let identity_hash = sp_runtime::traits::BlakeTwo256::hash_of(&info);
let result = Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Identity(
pallet_identity::Call::provide_judgement {
reg_index: 0,
target: account_id(CHARLIE).into(),
judgement: pallet_identity::Judgement::Reasonable,
identity: identity_hash,
},
)),
);
assert_ok!(result);
// Verify IdentityJudgement event
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::JudgementGiven {
target: account_id(CHARLIE).into(),
registrar_index: 0,
},
));
});
}
#[test]
fn test_batch_call_with_nontransfer_proxy_filtered() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as NonTransfer proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::NonTransfer,
0
));
// Create a batch call that includes a balance transfer
let batch_call = RuntimeCall::Utility(pallet_utility::Call::batch {
calls: vec![
// This should be allowed (system remark - now allowed by NonTransfer)
RuntimeCall::System(frame_system::Call::remark {
remark: b"test remark".to_vec(),
}),
// This should be filtered (balance transfer - not allowed by NonTransfer)
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
dest: charlie.clone(),
value: 100 * HAVE,
}),
// Another allowed operation (another system remark)
RuntimeCall::System(frame_system::Call::remark {
remark: b"another remark".to_vec(),
}),
],
});
// Attempt to execute batch call through NonTransfer proxy
// The proxy call itself will succeed (batch is allowed), but the inner transfer should be filtered
let initial_charlie_balance = Balances::free_balance(&charlie);
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(batch_call)
));
// Check for BatchInterrupted event indicating the transfer was filtered
System::assert_has_event(RuntimeEvent::Utility(
pallet_utility::Event::BatchInterrupted {
index: 1, // The second call (transfer) was filtered
error: frame_system::Error::<Runtime>::CallFiltered.into(),
},
));
// Verify that the filtered transfer didn't execute - Charlie's balance should be unchanged
assert_eq!(Balances::free_balance(&charlie), initial_charlie_balance);
// Verify that a batch with only allowed operations works
let allowed_batch_call = RuntimeCall::Utility(pallet_utility::Call::batch {
calls: vec![
RuntimeCall::System(frame_system::Call::remark {
remark: b"allowed remark 1".to_vec(),
}),
RuntimeCall::System(frame_system::Call::remark {
remark: b"allowed remark 2".to_vec(),
}),
],
});
// This should succeed
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(allowed_batch_call)
));
// Verify ProxyExecuted event was emitted for the successful batch
System::assert_has_event(RuntimeEvent::Proxy(ProxyEvent::ProxyExecuted {
result: Ok(()),
}));
});
}
#[test]
fn test_proxy_call_with_cancelproxy_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as CancelProxy proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::CancelProxy,
0
));
// CancelProxy can reject announcements
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Proxy(
pallet_proxy::Call::reject_announcement {
delegate: charlie.clone(),
call_hash: blake2_256(b"test").into(),
}
))
));
});
}
#[test]
fn test_proxy_call_with_sudo_only_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.with_sudo(account_id(ALICE)) // Set Alice as the sudo key
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as SudoOnly proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::SudoOnly,
0
));
// SudoOnly proxy can execute Sudo calls
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Sudo(pallet_sudo::Call::sudo {
call: Box::new(RuntimeCall::Balances(
pallet_balances::Call::force_set_balance {
who: charlie.clone(),
new_free: 2_000 * HAVE,
}
))
}))
));
// Check that the sudo call was executed successfully
System::assert_has_event(RuntimeEvent::Sudo(pallet_sudo::Event::Sudid {
sudo_result: Ok(()),
}));
// Verify that Charlie's balance was forcibly set
assert_eq!(Balances::free_balance(&charlie), 2_000 * HAVE);
});
}
#[test]
fn test_proxy_call_with_wrong_proxy_type() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as Governance proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Governance,
0
));
// Bob tries to execute a Balances call - the proxy call succeeds but the inner call is filtered
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death {
dest: charlie.clone(),
value: 100 * HAVE,
}
))
));
// Check that the call was filtered by looking for the CallFiltered event
System::assert_last_event(RuntimeEvent::Proxy(ProxyEvent::ProxyExecuted {
result: Err(
frame_system::Error::<datahaven_mainnet_runtime::Runtime>::CallFiltered.into(),
),
}));
// Verify that Charlie's balance didn't change (transfer was filtered)
assert_eq!(Balances::free_balance(&charlie), 1_000 * HAVE); // Original balance unchanged
});
}
#[test]
fn test_proxy_call_without_permission() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Bob is not added as a proxy for Alice
// Bob tries to execute a call on behalf of Alice (should fail)
assert_noop!(
Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death {
dest: charlie.clone(),
value: 100 * HAVE,
}
))
),
pallet_proxy::Error::<datahaven_mainnet_runtime::Runtime>::NotProxy
);
});
}
#[test]
fn test_proxy_type_hierarchy() {
ExtBuilder::default().build().execute_with(|| {
// Test the is_superset functionality
assert!(ProxyType::Any.is_superset(&ProxyType::Balances));
assert!(ProxyType::Any.is_superset(&ProxyType::Governance));
assert!(ProxyType::Any.is_superset(&ProxyType::Any));
assert!(!ProxyType::Balances.is_superset(&ProxyType::Any));
assert!(!ProxyType::Governance.is_superset(&ProxyType::Any));
assert!(ProxyType::Balances.is_superset(&ProxyType::Balances));
});
}
// =================================================================================================
// ANONYMOUS (PURE) PROXY OPERATIONS
// Tests for create_pure, kill_pure, and anonymous proxy usage
// =================================================================================================
#[test]
fn test_create_anonymous_proxy() {
ExtBuilder::default()
.with_balances(vec![(account_id(ALICE), 10_000 * HAVE)])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let alice_balance_before = Balances::free_balance(&alice);
// Create anonymous proxy
assert_ok!(Proxy::create_pure(
RuntimeOrigin::signed(alice.clone()),
ProxyType::Any,
0,
0
));
// Check deposit was taken
let alice_balance_after = Balances::free_balance(&alice);
assert!(alice_balance_before > alice_balance_after);
// Check PureCreated event was emitted
System::assert_has_event(RuntimeEvent::Proxy(ProxyEvent::PureCreated {
pure: Proxy::pure_account(&alice, &ProxyType::Any, 0, Some((1, 0))),
who: alice,
proxy_type: ProxyType::Any,
disambiguation_index: 0,
}));
});
}
#[test]
fn test_anonymous_proxy_usage() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
// Create anonymous proxy
assert_ok!(Proxy::create_pure(
RuntimeOrigin::signed(alice.clone()),
ProxyType::Any,
0,
0
));
let pure_proxy = Proxy::pure_account(&alice, &ProxyType::Any, 0, Some((1, 0)));
// Fund the pure proxy
assert_ok!(Balances::transfer_allow_death(
RuntimeOrigin::signed(alice.clone()),
pure_proxy.clone(),
1_000 * HAVE
));
let bob_balance_before = Balances::free_balance(&bob);
// Alice can use the pure proxy to make calls
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(alice.clone()),
pure_proxy.clone(),
None,
Box::new(RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death {
dest: bob.clone(),
value: 500 * HAVE,
}
))
));
let bob_balance_after = Balances::free_balance(&bob);
assert_eq!(bob_balance_after - bob_balance_before, 500 * HAVE);
});
}
#[test]
fn test_kill_anonymous_proxy() {
ExtBuilder::default()
.with_balances(vec![(account_id(ALICE), 10_000 * HAVE)])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let alice_balance_before = Balances::free_balance(&alice);
// Create anonymous proxy
assert_ok!(Proxy::create_pure(
RuntimeOrigin::signed(alice.clone()),
ProxyType::Any,
0,
0
));
// Get the pure proxy account
let events = System::events();
let (pure_account, _who, _proxy_type, disambiguation_index) = if let Some(record) =
events.iter().find(|event| {
matches!(
event.event,
RuntimeEvent::Proxy(ProxyEvent::PureCreated { .. })
)
}) {
if let RuntimeEvent::Proxy(ProxyEvent::PureCreated {
pure,
who,
proxy_type,
disambiguation_index,
}) = &record.event
{
(
pure.clone(),
who.clone(),
*proxy_type,
*disambiguation_index,
)
} else {
panic!("Expected PureCreated event");
}
} else {
panic!("No PureCreated event found");
};
let alice_balance_after_create = Balances::free_balance(&alice);
assert!(alice_balance_before > alice_balance_after_create);
// Fund the pure proxy account so it can kill itself
assert_ok!(Balances::transfer_allow_death(
RuntimeOrigin::signed(alice.clone()),
pure_account.clone(),
100 * HAVE
));
// Kill the anonymous proxy - must be called by the proxy account itself
// Use the actual creation block and index
assert_ok!(Proxy::kill_pure(
RuntimeOrigin::signed(pure_account.clone()),
alice.clone(),
ProxyType::Any,
disambiguation_index,
1, // height (block 1 when proxy was created)
0 // ext_index (extrinsic index)
));
// Check that deposit was returned (minus the 100 HAVE sent to proxy)
let alice_balance_after_kill = Balances::free_balance(&alice);
assert_eq!(alice_balance_before - 100 * HAVE, alice_balance_after_kill);
// Verify proxy relationship no longer exists
let proxies = Proxy::proxies(pure_account);
assert_eq!(proxies.0.len(), 0);
});
}
// =================================================================================================
// ADVANCED PROXY FEATURES
// Tests for proxy announcements, delays, batch operations, and proxy chains
// =================================================================================================
#[test]
fn test_proxy_announcement_system() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
// Add Bob as a delayed proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
5 // 5 block delay
));
// Bob announces a future proxy call
let call = RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
dest: charlie.clone(),
value: 500 * HAVE,
});
assert_ok!(Proxy::announce(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
blake2_256(&call.encode()).into(),
));
// Check announcement event
System::assert_has_event(RuntimeEvent::Proxy(ProxyEvent::Announced {
real: alice.clone(),
proxy: bob.clone(),
call_hash: blake2_256(&call.encode()).into(),
}));
// Trying to execute the announced call immediately should fail due to delay
assert_noop!(
Proxy::proxy_announced(
RuntimeOrigin::signed(bob.clone()),
bob.clone(), // delegate
alice.clone(), // real
Some(ProxyType::Any),
Box::new(call.clone())
),
pallet_proxy::Error::<datahaven_mainnet_runtime::Runtime>::Unannounced
);
// Fast forward 5 blocks to satisfy the delay
System::set_block_number(System::block_number() + 5);
// Now the announced call should work
assert_ok!(Proxy::proxy_announced(
RuntimeOrigin::signed(bob.clone()),
bob.clone(), // delegate (proxy)
alice.clone(), // real (original account)
Some(ProxyType::Any),
Box::new(call)
));
// Verify the transfer occurred
assert_eq!(Balances::free_balance(&charlie), 1_500 * HAVE);
});
}
#[test]
fn test_utility_batch_with_proxy() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
(account_id(DAVE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
let dave = account_id(DAVE);
// Add Bob as proxy for Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
// Bob executes a batch of transfers on behalf of Alice
let batch_calls = vec![
RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
dest: charlie.clone(),
value: 200 * HAVE,
}),
RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
dest: dave.clone(),
value: 300 * HAVE,
}),
];
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(bob.clone()),
alice.clone(),
None,
Box::new(RuntimeCall::Utility(pallet_utility::Call::batch {
calls: batch_calls
}))
));
// Check both transfers were executed
assert_eq!(Balances::free_balance(&charlie), 1_200 * HAVE);
assert_eq!(Balances::free_balance(&dave), 1_300 * HAVE);
});
}
#[test]
fn test_proxy_chain() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 1_000 * HAVE),
(account_id(CHARLIE), 1_000 * HAVE),
(account_id(DAVE), 1_000 * HAVE),
])
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let charlie = account_id(CHARLIE);
let dave = account_id(DAVE);
// Set up proxy chain: Charlie -> Bob -> Alice
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(alice.clone()),
bob.clone(),
ProxyType::Any,
0
));
assert_ok!(Proxy::add_proxy(
RuntimeOrigin::signed(bob.clone()),
charlie.clone(),
ProxyType::Any,
0
));
let dave_balance_before = Balances::free_balance(&dave);
// Charlie executes a call through the proxy chain
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(charlie.clone()),
bob.clone(),
None,
Box::new(RuntimeCall::Proxy(pallet_proxy::Call::proxy {
real: alice.clone(),
force_proxy_type: None,
call: Box::new(RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death {
dest: dave.clone(),
value: 400 * HAVE,
}
))
}))
));
let dave_balance_after = Balances::free_balance(&dave);
assert_eq!(dave_balance_after - dave_balance_before, 400 * HAVE);
});
}
// =================================================================================================
// INTEGRATION TESTS
// Tests for complex scenarios involving multiple pallets and advanced workflows
// =================================================================================================
#[test]
fn test_multisig_to_anonymous_proxy_to_sudo() {
ExtBuilder::default()
.with_balances(vec![
(account_id(ALICE), 10_000 * HAVE),
(account_id(BOB), 10_000 * HAVE),
(account_id(CHARLIE), 10_000 * HAVE),
(account_id(DAVE), 5_000 * HAVE),
])
.with_sudo(account_id(ALICE)) // Set Alice as the sudo key initially
.build()
.execute_with(|| {
let alice = account_id(ALICE);
let bob = account_id(BOB);
let dave = account_id(DAVE);
// Create multisig account from Alice and Bob (2 of 2 for simplicity)
let multisig_signatories = vec![alice.clone(), bob.clone()];
let threshold = 2u16;
let multisig_account = Multisig::multi_account_id(&multisig_signatories, threshold);
// Fund the multisig account
assert_ok!(Balances::transfer_allow_death(
RuntimeOrigin::signed(dave.clone()),
multisig_account.clone(),
2_000 * HAVE
));
// Create anonymous proxy with SudoOnly permissions
assert_ok!(Proxy::create_pure(
RuntimeOrigin::signed(dave.clone()),
ProxyType::SudoOnly,
0,
0
));
// Get the anonymous proxy address
let events = System::events();
let anonymous_proxy = if let Some(record) = events.iter().find(|event| {
matches!(
event.event,
RuntimeEvent::Proxy(ProxyEvent::PureCreated { .. })
)
}) {
if let RuntimeEvent::Proxy(ProxyEvent::PureCreated { pure, .. }) = &record.event {
pure.clone()
} else {
panic!("Expected PureCreated event");
}
} else {
panic!("No PureCreated event found");
};
// Dave adds the multisig as a proxy for the anonymous account
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(dave.clone()),
anonymous_proxy.clone(),
None,
Box::new(RuntimeCall::Proxy(pallet_proxy::Call::add_proxy {
delegate: multisig_account.clone(),
proxy_type: ProxyType::SudoOnly, // Allow sudo operations
delay: 0,
}))
));
// Add the multisig as the controller of the anonymous proxy
// The multisig is now set up as a proxy for the anonymous proxy
// First, transfer sudo key from Alice to the anonymous proxy
assert_ok!(Sudo::set_key(
RuntimeOrigin::signed(alice.clone()),
anonymous_proxy.clone().into()
));
// Create a sudo call to set Alice's balance (as an example privileged operation)
let alice_initial_balance = Balances::free_balance(&alice);
let sudo_call = RuntimeCall::Sudo(pallet_sudo::Call::sudo {
call: Box::new(RuntimeCall::Balances(
pallet_balances::Call::force_set_balance {
who: alice.clone(),
new_free: alice_initial_balance + 1_000 * HAVE, // Increase Alice's balance
},
)),
});
// Execute the sudo call through Dave who has proxy access (simplified demo)
// In a real scenario, this would be through proper multisig approval process
assert_ok!(Proxy::proxy(
RuntimeOrigin::signed(dave.clone()),
anonymous_proxy.clone(),
Some(ProxyType::SudoOnly),
Box::new(sudo_call)
));
// Check that the sudo call was executed successfully
System::assert_has_event(RuntimeEvent::Sudo(pallet_sudo::Event::Sudid {
sudo_result: Ok(()),
}));
// Verify that Alice's balance was forcibly set
assert_eq!(Balances::free_balance(&alice), 11_000 * HAVE);
// This test successfully demonstrates:
// 1. Anonymous proxy creation
// 2. Proxy permission setup (Dave can proxy for anonymous account)
// 3. Sudo call execution through proxy chain
// This validates the core multisig -> proxy -> sudo workflow
});
}