Skip to content

Commit 8fcbeab

Browse files
LGLOkpinter-iohk
andauthored
feat: add burn to address associations pallet to discourage attacks on storage (#869)
--------- Co-authored-by: Krisztian Pinter <[email protected]>
1 parent 501b9bf commit 8fcbeab

File tree

8 files changed

+139
-51
lines changed

8 files changed

+139
-51
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This changelog is based on [Keep A Changelog](https://keepachangelog.com/en/1.1.
44

55
# Unreleased
66

7+
* Added extra constant burn fee in `pallet-address-association` to discourage attacks on pallet storage.
8+
79
## Changed
810

911
## Added

demo/runtime/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,10 +546,17 @@ impl pallet_block_production_log::Config for Runtime {
546546
type BenchmarkHelper = PalletBlockProductionLogBenchmarkHelper;
547547
}
548548

549+
parameter_types! {
550+
/// Amount of tokens to burn when making irreversible, forever association
551+
pub const AddressAssociationBurnAmount: Balance = 1_000_000;
552+
}
553+
549554
impl pallet_address_associations::Config for Runtime {
550555
type WeightInfo = pallet_address_associations::weights::SubstrateWeight<Runtime>;
551556

552557
type PartnerChainAddress = AccountId;
558+
type Currency = Balances;
559+
type BurnAmount = AddressAssociationBurnAmount;
553560

554561
fn genesis_utxo() -> UtxoId {
555562
Sidechain::genesis_utxo()

toolkit/address-associations/pallet/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ hex-literal = { workspace = true, optional = true }
2323
sp-core = { workspace = true, optional = true }
2424

2525
[dev-dependencies]
26+
pallet-balances = { workspace = true }
2627
sp-core = { workspace = true }
2728
sp-io = { workspace = true }
2829
sp-runtime = { workspace = true }
@@ -34,6 +35,7 @@ std = [
3435
"frame-support/std",
3536
"frame-system/std",
3637
"log/std",
38+
"pallet-balances/std",
3739
"parity-scale-codec/std",
3840
"scale-info/std",
3941
"sidechain-domain/std",
@@ -46,5 +48,5 @@ runtime-benchmarks = [
4648
"frame-system/runtime-benchmarks",
4749
"sp-runtime/runtime-benchmarks",
4850
"hex-literal",
49-
"sp-core"
51+
"sp-core",
5052
]

toolkit/address-associations/pallet/src/benchmarking.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::Pallet as AddressAssociations;
1515
#[benchmarks(where <T as Config>::PartnerChainAddress: Ss58Codec)]
1616
mod benchmarks {
1717
use super::*;
18+
use frame_support::traits::{Get, tokens::fungible::Mutate};
1819

1920
#[benchmark]
2021
fn associate_address() {
@@ -31,8 +32,12 @@ mod benchmarks {
3132
"1aa8c1b363a207ddadf0c6242a0632f5a557690a327d0245f9d473b983b3d8e1c95a3dd804cab41123c36ddbcb7137b8261c35d5c8ef04ce9d0f8d5c4b3ca607"
3233
));
3334

35+
// Create an account and fund it with sufficient balance
36+
let caller: T::AccountId = account("caller", 0, 0);
37+
let _ = T::Currency::mint_into(&caller, T::BurnAmount::get() * 2u32.into());
38+
3439
#[extrinsic_call]
35-
_(RawOrigin::None, pc_address, signature, stake_public_key);
40+
_(RawOrigin::Signed(caller), pc_address, signature, stake_public_key);
3641
}
3742

3843
impl_benchmark_test_suite!(AddressAssociations, crate::mock::new_test_ext(), crate::mock::Test);

toolkit/address-associations/pallet/src/lib.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
//! Pallet storing associations from main chain public key to parter chain address.
1+
//! Pallet storing associations from main chain public key to partner chain address.
22
//!
33
//! ## Purpose of this pallet
44
//!
55
//! This pallet establishes a many-to-one mapping from Cardano staking keys to Partner Chain addresses.
66
//! The purpose of this mapping is primarily to indicate the local PC address to be the recipient of any
77
//! block production rewards or cross-chain token transfers credited to a Cardano key holders on a Partner
8-
//! Chain. Some intended scenarios inlude:
9-
//! 1. ADA delegators become eligible for block rewards due to their stake poool's operator participating
8+
//! Chain. Some intended scenarios include:
9+
//! 1. ADA delegators become eligible for block rewards due to their stake pool's operator participating
1010
//! in a Partner Chain network. The on-chain payout mechanism uses data provided by this pallet to
1111
//! identify each delegator's Partner Chain address based on their Cardano staking key.
1212
//! 2. A Partner Chain develops its own cross-chain bridge from Cardano. A Cardano user associates their
1313
//! Cardano public key with a Partner Chain address that they control. The user then uses the bridge
1414
//! to send some tokens to themselves. The receiving logic in the Partner Chain ledger then uses this
15-
//! pallet's data to identify the user's PC account and comlete the transfer.
15+
//! pallet's data to identify the user's PC account and complete the transfer.
1616
//!
1717
//! ## Usage - PC Builder
1818
//!
@@ -27,7 +27,7 @@
2727
//!
2828
//! ## Usage - PC User
2929
//!
30-
//! This pallet expoes a single extrinsic `associate_address` accepting the Cardano public key
30+
//! This pallet exposes a single extrinsic `associate_address` accepting the Cardano public key
3131
//! and Partner Chain address to be associated together with a signature confirming that the submitter is
3232
//! the owner of the associated Cardano public key.
3333
//!
@@ -89,11 +89,13 @@ pub mod pallet {
8989
use super::*;
9090
use crate::weights::WeightInfo;
9191
use frame_support::pallet_prelude::*;
92-
use frame_system::pallet_prelude::OriginFor;
92+
use frame_support::traits::fungible::{Inspect, Mutate};
93+
use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
94+
use frame_system::{ensure_signed, pallet_prelude::OriginFor};
9395
use sidechain_domain::{MainchainKeyHash, StakeKeySignature, StakePublicKey, UtxoId};
9496

9597
/// Current version of the pallet
96-
pub const PALLET_VERSION: u32 = 1;
98+
pub const PALLET_VERSION: u32 = 2;
9799

98100
#[pallet::pallet]
99101
pub struct Pallet<T>(_);
@@ -107,6 +109,13 @@ pub mod pallet {
107109
/// account ID, or some address type specific to the Partner Chain.
108110
type PartnerChainAddress: Member + Parameter + MaxEncodedLen;
109111

112+
/// The currency used for burning tokens when an address association is made
113+
type Currency: Mutate<Self::AccountId>;
114+
115+
/// The amount of tokens to burn when upserting metadata
116+
#[pallet::constant]
117+
type BurnAmount: Get<<Self::Currency as Inspect<Self::AccountId>>::Balance>;
118+
110119
/// Function returning the genesis UTXO of the Partner Chain.
111120
/// This typically should be wired with the `genesis_utxo` function exposed by `pallet_sidechain`.
112121
fn genesis_utxo() -> UtxoId;
@@ -133,6 +142,8 @@ pub mod pallet {
133142
MainchainKeyAlreadyAssociated,
134143
/// Signals an invalid Cardano key signature
135144
InvalidMainchainSignature,
145+
/// Could not burn additional fee for occupying space
146+
InsufficientBalance,
136147
}
137148

138149
#[pallet::call]
@@ -145,11 +156,12 @@ pub mod pallet {
145156
#[pallet::call_index(0)]
146157
#[pallet::weight(T::WeightInfo::associate_address())]
147158
pub fn associate_address(
148-
_origin: OriginFor<T>,
159+
origin: OriginFor<T>,
149160
partnerchain_address: T::PartnerChainAddress,
150161
signature: StakeKeySignature,
151162
stake_public_key: StakePublicKey,
152163
) -> DispatchResult {
164+
let origin_account_id = ensure_signed(origin)?;
153165
let genesis_utxo = T::genesis_utxo();
154166

155167
let stake_key_hash = stake_public_key.hash();
@@ -158,6 +170,14 @@ pub mod pallet {
158170
!AddressAssociations::<T>::contains_key(&stake_key_hash),
159171
Error::<T>::MainchainKeyAlreadyAssociated
160172
);
173+
T::Currency::burn_from(
174+
&origin_account_id,
175+
T::BurnAmount::get(),
176+
Preservation::Preserve,
177+
Precision::Exact,
178+
Fortitude::Force,
179+
)
180+
.map_err(|_| Error::<T>::InsufficientBalance)?;
161181

162182
let address_association_message = AddressAssociationSignedMessage {
163183
stake_public_key: stake_public_key.clone(),

toolkit/address-associations/pallet/src/mock.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
use frame_support::parameter_types;
12
use frame_support::traits::ConstU32;
23
use frame_support::{
34
construct_runtime,
45
traits::{ConstU16, ConstU64},
56
};
67
use hex_literal::hex;
78
use sidechain_domain::*;
8-
use sp_core::H256;
9+
use sp_core::crypto::Ss58Codec;
10+
use sp_core::{ConstU128, H256};
911
use sp_runtime::{
1012
AccountId32, BuildStorage,
1113
traits::{BlakeTwo256, IdentityLookup},
@@ -35,7 +37,7 @@ pub mod mock_pallet {
3537
partner_chain_address: PartnerChainAddress,
3638
main_chain_key_hash: MainchainKeyHash,
3739
) {
38-
LastNewAssociation::<T>::put((partner_chain_address, main_chain_key_hash))
40+
LastNewAssociation::<T>::put((partner_chain_address, main_chain_key_hash));
3941
}
4042
}
4143
}
@@ -44,6 +46,7 @@ construct_runtime! {
4446
pub enum Test {
4547
System: frame_system,
4648
AddressAssociations: crate::pallet,
49+
Balances: pallet_balances,
4750
MockPallet: mock_pallet
4851
}
4952
}
@@ -65,7 +68,7 @@ impl frame_system::Config for Test {
6568
type BlockHashCount = ConstU64<250>;
6669
type Version = ();
6770
type PalletInfo = PalletInfo;
68-
type AccountData = ();
71+
type AccountData = pallet_balances::AccountData<u128>;
6972
type OnNewAccount = ();
7073
type OnKilledAccount = ();
7174
type SystemWeightInfo = ();
@@ -83,16 +86,58 @@ impl frame_system::Config for Test {
8386
type PostTransactions = ();
8487
}
8588

89+
impl pallet_balances::Config for Test {
90+
type MaxLocks = ConstU32<50>;
91+
type MaxReserves = ();
92+
type ReserveIdentifier = [u8; 8];
93+
type Balance = u128;
94+
type RuntimeEvent = RuntimeEvent;
95+
type DustRemoval = ();
96+
type ExistentialDeposit = ConstU128<1>;
97+
type AccountStore = System;
98+
type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>;
99+
type FreezeIdentifier = ();
100+
type MaxFreezes = ();
101+
type RuntimeHoldReason = RuntimeHoldReason;
102+
type RuntimeFreezeReason = ();
103+
type DoneSlashHandler = ();
104+
}
105+
106+
parameter_types! {
107+
pub const AssociationFeeBurn: u128 = 1000;
108+
}
109+
110+
pub(crate) const FUNDED_ACCOUNT: AccountId32 = AccountId32::new([1; 32]);
111+
112+
pub(crate) const STAKE_PUBLIC_KEY: StakePublicKey =
113+
StakePublicKey(hex!("2bebcb7fbc74a6e0fd6e00a311698b047b7b659f0e047ff5349dbd984aefc52c"));
114+
115+
pub(crate) fn pc_address() -> AccountId32 {
116+
AccountId32::from_ss58check("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY").unwrap()
117+
}
118+
119+
pub(crate) const VALID_SIGNATURE: [u8; 64] = hex!(
120+
"1aa8c1b363a207ddadf0c6242a0632f5a557690a327d0245f9d473b983b3d8e1c95a3dd804cab41123c36ddbcb7137b8261c35d5c8ef04ce9d0f8d5c4b3ca607"
121+
);
122+
86123
impl crate::pallet::Config for Test {
87124
type WeightInfo = ();
88125
type PartnerChainAddress = PartnerChainAddress;
89126
fn genesis_utxo() -> UtxoId {
90127
UtxoId::new(hex!("59104061ffa0d66f9ba0135d6fc6a884a395b10f8ae9cb276fc2c3bfdfedc260"), 1)
91128
}
92-
129+
type Currency = Balances;
130+
type BurnAmount = AssociationFeeBurn;
93131
type OnNewAssociation = MockPallet;
94132
}
95133

96134
pub fn new_test_ext() -> sp_io::TestExternalities {
97-
frame_system::GenesisConfig::<Test>::default().build_storage().unwrap().into()
135+
let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
136+
pallet_balances::GenesisConfig::<Test> {
137+
balances: vec![(FUNDED_ACCOUNT, 100_000)],
138+
dev_accounts: None,
139+
}
140+
.assimilate_storage(&mut t)
141+
.unwrap();
142+
t.into()
98143
}

toolkit/address-associations/pallet/src/tests.rs

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,32 @@ use frame_system::pallet_prelude::OriginFor;
44
use hex_literal::hex;
55
use mock::*;
66
use sidechain_domain::*;
7-
use sp_core::crypto::Ss58Codec;
87
use sp_runtime::AccountId32;
98

109
#[test]
1110
fn saves_new_address_association() {
1211
new_test_ext().execute_with(|| {
13-
// Alice
14-
let stake_public_key = StakePublicKey(hex!(
15-
"2bebcb7fbc74a6e0fd6e00a311698b047b7b659f0e047ff5349dbd984aefc52c"
16-
));
17-
let mc_signature = hex!("1aa8c1b363a207ddadf0c6242a0632f5a557690a327d0245f9d473b983b3d8e1c95a3dd804cab41123c36ddbcb7137b8261c35d5c8ef04ce9d0f8d5c4b3ca607");
12+
assert_eq!(mock_pallet::LastNewAssociation::<Test>::get(), None);
13+
let initial_balance = Balances::free_balance(&FUNDED_ACCOUNT);
14+
assert_ok!(super::Pallet::<Test>::associate_address(
15+
OriginFor::<Test>::signed(FUNDED_ACCOUNT),
16+
pc_address(),
17+
VALID_SIGNATURE.into(),
18+
STAKE_PUBLIC_KEY.clone(),
19+
));
1820

19-
// Alice
20-
let pc_address =
21-
AccountId32::from_ss58check("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY").unwrap();
22-
23-
assert_eq!(mock_pallet::LastNewAssociation::<Test>::get(), None);
24-
25-
assert_ok!(
26-
super::Pallet::<Test>::associate_address(
27-
OriginFor::<Test>::signed(AccountId32::new([1; 32])),
28-
pc_address.clone(),
29-
mc_signature.into(),
30-
stake_public_key.clone(),
31-
)
32-
);
33-
34-
assert_eq!(
35-
Pallet::<Test>::get_partner_chain_address_for(&stake_public_key),
36-
Some(pc_address.clone())
37-
);
21+
assert_eq!(
22+
Pallet::<Test>::get_partner_chain_address_for(&STAKE_PUBLIC_KEY),
23+
Some(pc_address())
24+
);
3825

39-
assert_eq!(mock_pallet::LastNewAssociation::<Test>::get(), Some((pc_address,stake_public_key.hash())));
40-
})
26+
assert_eq!(
27+
mock_pallet::LastNewAssociation::<Test>::get(),
28+
Some((pc_address(), STAKE_PUBLIC_KEY.hash()))
29+
);
30+
let final_balance = Balances::free_balance(&FUNDED_ACCOUNT);
31+
assert_eq!(final_balance, initial_balance - AssociationFeeBurn::get());
32+
})
4133
}
4234

4335
#[test]
@@ -56,7 +48,7 @@ fn rejects_duplicate_key_association() {
5648
);
5749
assert_eq!(
5850
super::Pallet::<Test>::associate_address(
59-
OriginFor::<Test>::signed(AccountId32::new([1; 32])),
51+
OriginFor::<Test>::signed(FUNDED_ACCOUNT),
6052
AccountId32::new([0; 32]),
6153
signature,
6254
stake_public_key,
@@ -70,20 +62,34 @@ fn rejects_duplicate_key_association() {
7062
#[test]
7163
fn rejects_invalid_mainchain_signature() {
7264
new_test_ext().execute_with(|| {
73-
let stake_public_key = StakePublicKey(hex!(
74-
"fc014cb5f071f5d6a36cb5a7e5f168c86555989445a23d4abec33d280f71aca4"
75-
));
76-
let signature = StakeKeySignature(hex!("c50828c31d1a61e05fdb943847efd42ce2eadda9c7d21dd2d035e8de66bc56de7f6b1297fba6cb7305f2aac97b5f9168894fb10295c503de6d5fb6ae70bd9a0d"));
65+
66+
let invalid_signature = StakeKeySignature(hex!("c50828c31d1a61e05fdb943847efd42ce2eadda9c7d21dd2d035e8de66bc56de7f6b1297fba6cb7305f2aac97b5f9168894fb10295c503de6d5fb6ae70bd9a0d"));
7767

7868
assert_eq!(
7969
super::Pallet::<Test>::associate_address(
80-
OriginFor::<Test>::signed(AccountId32::new([1; 32])),
81-
AccountId32::new([0; 32]),
82-
signature,
83-
stake_public_key,
70+
OriginFor::<Test>::signed(FUNDED_ACCOUNT),
71+
pc_address(),
72+
invalid_signature,
73+
STAKE_PUBLIC_KEY,
8474
)
8575
.unwrap_err(),
8676
Error::<Test>::InvalidMainchainSignature.into()
8777
);
8878
})
8979
}
80+
81+
#[test]
82+
fn rejects_extrinsic_when_origin_account_cannot_pay_extra_fee() {
83+
new_test_ext().execute_with(|| {
84+
assert_eq!(
85+
super::Pallet::<Test>::associate_address(
86+
OriginFor::<Test>::signed(AccountId32::new([3; 32])),
87+
pc_address(),
88+
VALID_SIGNATURE.into(),
89+
STAKE_PUBLIC_KEY,
90+
)
91+
.unwrap_err(),
92+
Error::<Test>::InsufficientBalance.into()
93+
);
94+
})
95+
}

0 commit comments

Comments
 (0)