Skip to content

feat: implement pallet_msa::withdraw_tokens extrinsic #2402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
49116ab
feat: implement pallet_msa::withdraw_tokens extrinsic
JoeCap08055 May 12, 2025
563baed
fix: doc lint
JoeCap08055 May 13, 2025
a7ac8db
fix: broken unit tests
JoeCap08055 May 13, 2025
551c6c5
feat: bump spec version
JoeCap08055 May 13, 2025
6a19e64
fix: update MSA pallet README
JoeCap08055 May 13, 2025
b57132e
feat: unit tests
JoeCap08055 May 13, 2025
1e78244
feat: add benchmarks for withdraw_tokens extrinsic
JoeCap08055 May 13, 2025
582d13d
Merge branch 'main' into feat/withdraw-tokens-from-msa
JoeCap08055 May 13, 2025
0363457
fix: fix payload signature verification after merge EIP-712 from main
JoeCap08055 May 13, 2025
1c7328f
fix: bump spec version
JoeCap08055 May 14, 2025
fa04839
fix: formatting
JoeCap08055 May 14, 2025
1528d19
feat: free extrinsic
JoeCap08055 May 21, 2025
a09efa1
fix: formatting
JoeCap08055 May 21, 2025
c9e614d
Merge branch 'main' into feat/withdraw-tokens-from-msa
JoeCap08055 May 21, 2025
592ea06
fix: lint
JoeCap08055 May 21, 2025
a504353
fix: signature registry checks in extension, e2e nonce issues
JoeCap08055 May 22, 2025
dc5e272
fix: merge from main
JoeCap08055 May 22, 2025
1670a85
Update pallets/msa/src/lib.rs
JoeCap08055 May 22, 2025
340be0f
fix: revert change to enequeue_signature and update function doc
JoeCap08055 May 23, 2025
a865495
chore: debug eth signature replay
JoeCap08055 May 23, 2025
c857b30
fix: formatting
JoeCap08055 May 23, 2025
0a28176
fix: comment
JoeCap08055 May 23, 2025
217bcc8
fix: docs
JoeCap08055 May 25, 2025
40354f2
fix: docs
JoeCap08055 May 25, 2025
9fb0527
fix: keep signed extension tests with others
JoeCap08055 May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

173 changes: 169 additions & 4 deletions e2e/msa/msaTokens.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import '@frequency-chain/api-augment';
import assert from 'assert';
import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
import { ethereumAddressToKeyringPair } from '../scaffolding/ethereum';
import { AddKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
import { ethereumAddressToKeyringPair, getUnifiedAddress, getUnifiedPublicKey } from '../scaffolding/ethereum';
import { getFundingSource } from '../scaffolding/funding';
import { H160 } from '@polkadot/types/interfaces';
import { bnToU8a, hexToU8a, stringToU8a } from '@polkadot/util';
import { KeyringPair } from '@polkadot/keyring/types';
import { keccak256AsU8a } from '@polkadot/util-crypto';
import { getExistentialDeposit } from '../scaffolding/helpers';
import {
CENTS,
createAndFundKeypair,
createKeys,
DOLLARS,
generateAddKeyPayload,
getExistentialDeposit,
signPayloadSr25519,
Sr25519Signature,
} from '../scaffolding/helpers';
import { u64 } from '@polkadot/types';
import { Codec } from '@polkadot/types/types';

const fundingSource = getFundingSource(import.meta.url);

Expand Down Expand Up @@ -80,7 +91,7 @@ describe('MSAs Holding Tokens', function () {

describe('Send tokens to MSA', function () {
it('should send tokens to the MSA', async function () {
const ed = await getExistentialDeposit();
const ed = getExistentialDeposit();
const transferAmount = 1n + ed;
let accountData = await ExtrinsicHelper.getAccountInfo(ethKeys);
const initialBalance = accountData.data.free.toBigInt();
Expand All @@ -102,4 +113,158 @@ describe('MSAs Holding Tokens', function () {
);
});
});

describe('withdrawTokens', function () {
let keys: KeyringPair;
let msaId: u64;
let msaAddress: H160;
let secondaryKey: KeyringPair;
const defaultPayload: AddKeyData = {};
let payload: AddKeyData;
let ownerSig: Sr25519Signature;
let badSig: Sr25519Signature;
let addKeyData: Codec;

before(async function () {
// Setup an MSA with tokens
keys = await createAndFundKeypair(fundingSource, 5n * CENTS);
const { target } = await ExtrinsicHelper.createMsa(keys).signAndSend();
assert.notEqual(target?.data.msaId, undefined, 'MSA Id not in expected event');
msaId = target!.data.msaId;

const { accountId } = await ExtrinsicHelper.apiPromise.call.msaRuntimeApi.getEthereumAddressForMsaId(msaId);
msaAddress = accountId;

secondaryKey = await createAndFundKeypair(fundingSource, 5n * CENTS);

// Default payload making it easier to test `withdrawTokens`
defaultPayload.msaId = msaId;
defaultPayload.newPublicKey = getUnifiedPublicKey(secondaryKey);
});

beforeEach(async function () {
payload = await generateAddKeyPayload(defaultPayload);
});

it('should fail if origin is not address contained in the payload (NotKeyOwner)', async function () {
const badPayload = { ...payload, newPublicKey: getUnifiedAddress(createKeys()) }; // Invalid MSA ID
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', badPayload);
ownerSig = signPayloadSr25519(keys, addKeyData);
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, badPayload);
await assert.rejects(op.fundAndSend(fundingSource), {
name: 'NotKeyOwner',
});
});

it('should fail if MSA owner signature is invalid (MsaOwnershipInvalidSignature)', async function () {
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload);
badSig = signPayloadSr25519(createKeys(), addKeyData); // Invalid signature
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, badSig, payload);
await assert.rejects(op.fundAndSend(fundingSource), {
name: 'MsaOwnershipInvalidSignature',
});
});

it('should fail if expiration has passed (ProofHasExpired)', async function () {
const newPayload = await generateAddKeyPayload({
...defaultPayload,
expiration: (await ExtrinsicHelper.getLastBlock()).block.header.number.toNumber(),
});
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
ownerSig = signPayloadSr25519(keys, addKeyData);
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, newPayload);
await assert.rejects(op.fundAndSend(fundingSource), {
name: 'ProofHasExpired',
});
});

it('should fail if expiration is not yet valid (ProofNotYetValid)', async function () {
const maxMortality = ExtrinsicHelper.api.consts.msa.mortalityWindowSize.toNumber();
const newPayload = await generateAddKeyPayload({
...defaultPayload,
expiration: (await ExtrinsicHelper.getLastBlock()).block.header.number.toNumber() + maxMortality + 999,
});
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
ownerSig = signPayloadSr25519(keys, addKeyData);
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, newPayload);
await assert.rejects(op.fundAndSend(fundingSource), {
name: 'ProofNotYetValid',
});
});

it('should fail if payload signer does not control the MSA in the signed payload (NotMsaOwner)', async function () {
const newPayload = await generateAddKeyPayload({
...defaultPayload,
msaId: new u64(ExtrinsicHelper.api.registry, 9999),
});
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
ownerSig = signPayloadSr25519(keys, addKeyData);
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, newPayload);
await assert.rejects(op.fundAndSend(fundingSource), {
name: 'NotMsaOwner',
});
});

it('should fail if payload signer is not an MSA control key (NoKeyExists)', async function () {
const badSigner = createKeys();
const newPayload = await generateAddKeyPayload({
...defaultPayload,
msaId: new u64(ExtrinsicHelper.api.registry, 9999),
});
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
ownerSig = signPayloadSr25519(badSigner, addKeyData);
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, badSigner, ownerSig, newPayload);
await assert.rejects(op.fundAndSend(fundingSource), {
name: 'NoKeyExists',
});
});

it('should fail if MSA does not have a balance', async function () {
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload);
ownerSig = signPayloadSr25519(keys, addKeyData);
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, payload);
await assert.rejects(op.fundAndSend(fundingSource), {
name: 'InsufficientBalanceToWithdraw',
});
});

it('should succeed', async function () {
// Fund receiver with known amount to pay for transaction
const startingAmount = 1n * DOLLARS;
const transferAmount = 1n * DOLLARS;
const tertiaryKeys = await createAndFundKeypair(fundingSource, startingAmount);
const {
data: { free: startingBalance },
} = await ExtrinsicHelper.getAccountInfo(tertiaryKeys);

// Send tokens to MSA
try {
const { target: transferEvent } = await ExtrinsicHelper.transferFunds(
fundingSource,
ethereumAddressToKeyringPair(msaAddress),
transferAmount
).signAndSend();
assert.notEqual(transferEvent, undefined, 'should have transferred tokens to MSA');
} catch (err: any) {
console.error('Error sending tokens to MSA', err.message);
}

const newPayload = await generateAddKeyPayload({ ...payload, newPublicKey: getUnifiedPublicKey(tertiaryKeys) });
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
ownerSig = signPayloadSr25519(keys, addKeyData);
const op = ExtrinsicHelper.withdrawTokens(tertiaryKeys, keys, ownerSig, newPayload);
const { eventMap } = await op.fundAndSend(fundingSource);
const feeAmount = (eventMap['transactionPayment.TransactionFeePaid'].data as unknown as any).actualFee;

// Destination account should have had balance increased
const {
data: { free: endingBalance },
} = await ExtrinsicHelper.getAccountInfo(tertiaryKeys);

assert(
startingBalance.toBigInt() + transferAmount - feeAmount.toBigInt() === endingBalance.toBigInt(),
'balance of recieve should have increased by the transfer amount minus fee'
);
});
});
});
12 changes: 12 additions & 0 deletions e2e/scaffolding/extrinsicHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,4 +913,16 @@ export class ExtrinsicHelper {
ExtrinsicHelper.api.events.passkey.TransactionExecutionSuccess
);
}

public static withdrawTokens(
keys: KeyringPair,
ownerKeys: KeyringPair,
ownerSignature: MultiSignatureType,
payload: AddKeyData
) {
return new Extrinsic(
() => ExtrinsicHelper.api.tx.msa.withdrawTokens(getUnifiedPublicKey(ownerKeys), ownerSignature, payload),
keys
);
}
}
2 changes: 1 addition & 1 deletion e2e/scaffolding/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export async function getBlockNumber(): Promise<number> {

let cacheED: null | bigint = null;

export async function getExistentialDeposit(): Promise<bigint> {
export function getExistentialDeposit(): bigint {
if (cacheED !== null) return cacheED;
return (cacheED = ExtrinsicHelper.api.consts.balances.existentialDeposit.toBigInt());
}
Expand Down
1 change: 1 addition & 0 deletions pallets/capacity/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ impl pallet_msa::Config for Test {
type CreateProviderViaGovernanceOrigin = EnsureSigned<u64>;
/// This MUST ALWAYS be MaxSignaturesPerBucket * NumberOfBuckets.
type MaxSignaturesStored = ConstU32<8000>;
type Currency = pallet_balances::Pallet<Self>;
}

// not used yet
Expand Down
1 change: 1 addition & 0 deletions pallets/frequency-tx-payment/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl pallet_msa::Config for Test {
type CreateProviderViaGovernanceOrigin = EnsureSigned<u64>;
/// This MUST ALWAYS be MaxSignaturesPerBucket * NumberOfBuckets.
type MaxSignaturesStored = ConstU32<8000>;
type Currency = pallet_balances::Pallet<Self>;
}

// Needs parameter_types! for the impls below
Expand Down
1 change: 1 addition & 0 deletions pallets/msa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-weights = { workspace = true }
pallet-balances = { workspace = true }
# Frequency related dependencies
common-primitives = { default-features = false, path = "../../common/primitives" }
serde = { workspace = true, features = ["derive"] }
Expand Down
1 change: 1 addition & 0 deletions pallets/msa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The MSA pallet provides for:
| `retire_msa`<br />Remove all keys and mark the MSA as retired | Delegator | Free | [`PublicKeyDeleted`](https://frequency-chain.github.io/frequency/pallet_msa/pallet/enum.Event.html#variant.PublicKeyDeleted), [`MsaRetired`](https://frequency-chain.github.io/frequency/pallet_msa/pallet/enum.Event.html#variant.MsaRetired) | 18 |
| `revoke_delegation_by_delegator`<br />Remove delegation | Delegator | Free | [`DelegationRevoked`](https://frequency-chain.github.io/frequency/pallet_msa/pallet/enum.Event.html#variant.DelegationRevoked) | 1 |
| `revoke_delegation_by_provider`<br />Remove delegation | Provider | Free | [`DelegationRevoked`](https://frequency-chain.github.io/frequency/pallet_msa/pallet/enum.Event.html#variant.DelegationRevoked) | 1 |
| `withdraw_tokens`<br />Withdraw all tokens from an MSA | Token Account | Tokens | [`Transfer`](https://paritytech.github.io/polkadot-sdk/master/pallet_balances/pallet/enum.Event.html#variant.Transfer) | 158 |

See [Rust Docs](https://frequency-chain.github.io/frequency/pallet_msa/pallet/struct.Pallet.html) for more details.

Expand Down
36 changes: 35 additions & 1 deletion pallets/msa/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::types::EMPTY_FUNCTION;
#[allow(unused)]
use crate::Pallet as Msa;
use frame_benchmarking::{account, v2::*};
use frame_support::assert_ok;
use frame_support::{assert_ok, traits::fungible::Inspect};
use frame_system::RawOrigin;
use sp_core::{crypto::KeyTypeId, Encode};
use sp_runtime::RuntimeAppPublic;
Expand Down Expand Up @@ -369,6 +369,40 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn withdraw_tokens() -> Result<(), BenchmarkError> {
prep_signature_registry::<T>();

let (msa_public_key, msa_key_pair, msa_id) = create_msa_account_and_keys::<T>();

let eth_account_id: H160 = Msa::<T>::msa_id_to_eth_address(msa_id);
let mut bytes = &EthereumAddressMapper::to_bytes32(&eth_account_id.0)[..];
let msa_account_id = <T as frame_system::Config>::AccountId::decode(&mut bytes).unwrap();

// Fund MSA
// let balance = <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance.from(10_000_000u128);
let balance = <T as Config>::Currency::minimum_balance();
T::Currency::set_balance(&msa_account_id, balance);
assert_eq!(T::Currency::balance(&msa_account_id), balance);

let (add_key_payload, _, new_account_id) = add_key_payload_and_signature::<T>(msa_id);

let encoded_add_key_payload = wrap_binary_data(add_key_payload.encode());
let owner_signature =
MultiSignature::Sr25519(msa_key_pair.sign(&encoded_add_key_payload).unwrap().into());

#[extrinsic_call]
_(
RawOrigin::Signed(new_account_id.clone()),
msa_public_key.clone(),
owner_signature,
add_key_payload,
);

assert_eq!(T::Currency::balance(&msa_account_id), Zero::zero());
Ok(())
}

impl_benchmark_test_suite!(
Msa,
crate::tests::mock::new_test_ext_keystore(),
Expand Down
Loading