From 88ba4f6bd54aca2413974039f75123be4a190961 Mon Sep 17 00:00:00 2001 From: siandreev Date: Mon, 20 Nov 2023 13:53:48 +0100 Subject: [PATCH 001/121] chore: build script --- scripts/printWalletCode.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/printWalletCode.ts b/scripts/printWalletCode.ts index ae953378..e7e3f507 100644 --- a/scripts/printWalletCode.ts +++ b/scripts/printWalletCode.ts @@ -2,10 +2,12 @@ import { compile } from '@ton-community/blueprint'; import { LibraryDeployer } from '../wrappers/library-deployer'; export async function run() { - const code = LibraryDeployer.exportLibCode(await compile('wallet_v5')); + const walletCode = await compile('wallet_v5'); + const code = LibraryDeployer.exportLibCode(walletCode); console.log('WALLET CODE HEX', code.toBoc().toString('hex'), '\n'); - console.log('WALLET CODE BASE64', code.toBoc().toString('base64')); + console.log('WALLET CODE BASE64', code.toBoc().toString('base64'), '\n'); + console.log('WALLET FULL CODE BASE64', walletCode.toBoc().toString('base64')); } run(); From 0b95549d5a2637be917bac80cf2510caf20f40e6 Mon Sep 17 00:00:00 2001 From: siandreev Date: Tue, 21 Nov 2023 18:40:31 +0100 Subject: [PATCH 002/121] chore: ci updated --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34f98df2..20001d42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Run tests on: push: - branches: [ "main" ] + branches: [ "main", "feature/optimisation" ] pull_request: - branches: [ "main" ] + branches: [ "main", "feature/optimisation" ] jobs: test: From 6a245a2da520cc7e11626774e1b6829ad272e1a7 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 02:12:12 +0200 Subject: [PATCH 003/121] Added root cell repacking algorithm with enforced Asm version --- contracts/imports/entrypoint.fc | 33 --------------- contracts/imports/root_cell_repacker.fc | 53 +++++++++++++++++++++++++ contracts/wallet_v5.fc | 10 ++--- 3 files changed, 57 insertions(+), 39 deletions(-) delete mode 100644 contracts/imports/entrypoint.fc create mode 100644 contracts/imports/root_cell_repacker.fc diff --git a/contracts/imports/entrypoint.fc b/contracts/imports/entrypoint.fc deleted file mode 100644 index f41b014a..00000000 --- a/contracts/imports/entrypoint.fc +++ /dev/null @@ -1,33 +0,0 @@ -;; Basic entry point for received messages (both external and internal). -;; recv_internal and recv_external are NOT implicitly used anymore! -;; use msg_value or full_msg ONLY if you are sure these are supplied - if is_external = 0! ~ a very thin ice here! - -;; N.B. If compiled by an compiler that does not support entry_point_recv this method will be ignored and the -;; contract would compile, however the gas usage will be higher due to recv_internal/external not inlined in root - -;; Not supported in the main (conservative) branch. -;; Please use the entrypoint branch for the best gas optimizations and usage! -{- - Direct gas comparison on a specific commit between main and entrypoint: - main entrypoint - External test case: 2699 2707 (a bit more due to if ordering, making it way around messes up cell slicing completely) - Internal test case: 3165 2963 - Extension test case: 1828 1626 - - main entrypoint - External global ctr: 59108 58893 (not all is bad with external messages overall) - Internal global ctr: 63031 61011 - Extension global ctr: 34141 32929 --} - - -{- -() entry_point_recv(int msg_value, cell full_msg, slice body, int is_external) impure { - ifnot (is_external) { - recv_internal(msg_value, full_msg, body); - return(); - } - recv_external(body); - ignore_int_params(msg_value, full_msg); ;; does nothing but prevents dropping them which will cause stack underflow -} --} \ No newline at end of file diff --git a/contracts/imports/root_cell_repacker.fc b/contracts/imports/root_cell_repacker.fc new file mode 100644 index 00000000..d7cd869f --- /dev/null +++ b/contracts/imports/root_cell_repacker.fc @@ -0,0 +1,53 @@ +() __install_root_cell_repacker_hook__() impure asm """ +// Require the Fift Assembler version that this code is known to work with +// (this version of FunC is required in the contract code anyway) +"0.4.4" require-asm-fif-version +// Install a hook that will execute in context of PROGRAM in the Fift assembler +// The hook will redefine }END>c function in order to modify the resulting root cell +@atend @ 1 { execute + // Replace current words dictionary with (Asm) context one to redefine }END>c word + current@ context@ current! + { + // Get the original root cell as the result of original program assembling + // by calling }END> that will return builder, and completing builder to cell + }END> b> + // Now the top entry on stack contains tha original root cell that constists of + // some boilerplate code and one and only reference to methods dictionary + + DUP INC // The copy of method_id+1 will be 0 if method_id = -1 + IFNOTJMP:<{ + // This branch is executed only if method_id = -1 (external messages) + DROP // Remove the second copy of method_id created for further decisions + over -1 swap @procdictkeylen idict@ // Extract recv_external from methods dict + { "recv_external is not defined" abort } ifnot // Fail if method is not found + @addop // Otherwise, if everything is OK, embed recv_external into root code cell + }> + // If the execution is at this moment, either this is internal method call (that are strictly + // discouraged if using root cell repacker) or a getter method (which's gas is not too relevant) + swap // Select methods dictionary for further operations (remove embedded recv_... methods) + 0 swap @procdictkeylen idict- drop // remove recv_internal from methods dictionary + -1 swap @procdictkeylen idict- drop // remove recv_external from methods dictionary + @procdictkeylen DICTPUSHCONST DICTIGETJMPZ 11 THROWARG // add methods dictionary and call the (get-)method + }> + b> + } + // The words will be redefined in "current" dictionary that is actually Asm context + : }END>c + // Restore the current words dictionary to context and restore original current dict + current@ context! current! +// Actually attach the hook installer to the end of current procedure definition +} does @atend ! +"""; \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index b0c3bb62..0a68a1f9 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -1,6 +1,7 @@ #pragma version =0.4.4; #include "imports/stdlib.fc"; +#include "imports/root_cell_repacker.fc"; ;; SDCNTTRAIL1 will count trailing ones in slice. If bounced flag (last bit) is set it will be non-zero, else zero. () slicy_return_if_bounce(slice s_flags) impure asm "SDCNTTRAIL1" "IFRET"; @@ -141,6 +142,9 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) } () recv_internal(cell full_msg, slice body) impure inline { + + __install_root_cell_repacker_hook__(); ;; this function from root_cell_repacker can be called in any method + ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. var full_msg_slice = full_msg.begin_parse(); @@ -183,12 +187,6 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) } -;; N.B. If compiled by an compiler that does not support entry_point_recv this method will be ignored and the -;; contract would compile, however the gas usage will be higher due to recv_internal/external not inlined in root - -;; Moved to separate file to make it easier to backport stuff from entrypoint branch to main branch -#include "imports/entrypoint.fc"; - ;; Get methods int seqno() method_id { From e70ff08b6fee0a517174b052bb4c6577cfdbe714 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 02:16:39 +0200 Subject: [PATCH 004/121] Added size constants and changed stored seqno type from ui32 to i33 --- contracts/wallet_v5.fc | 40 +++++++++++++++++++++----------- tests/wallet-v5-external.spec.ts | 4 ++-- tests/wallet-v5-internal.spec.ts | 4 ++-- wrappers/wallet-v5.ts | 2 +- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 0a68a1f9..886e1356 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -3,6 +3,16 @@ #include "imports/stdlib.fc"; #include "imports/root_cell_repacker.fc"; +const int size::stored_seqno = 33; +const int size::stored_subwallet = 80; +const int size::public_key = 256; + +const int size::subwallet_id = 80; +const int size::valid_until = 32; +const int size::msg_seqno = 32; + +const int size::flags = 4; + ;; SDCNTTRAIL1 will count trailing ones in slice. If bounced flag (last bit) is set it will be non-zero, else zero. () slicy_return_if_bounce(slice s_flags) impure asm "SDCNTTRAIL1" "IFRET"; @@ -51,7 +61,7 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) int packed_addr = pack_address((wc, hash) ); var ds = get_data().begin_parse(); - var data_bits = ds~load_bits(32 + 80 + 256); + var data_bits = ds~load_bits(size::stored_seqno + size::stored_subwallet + size::public_key); var extensions = ds.preload_dict(); ;; Add extension @@ -103,13 +113,13 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) var signature = body~load_bits(512); var cs = body; - var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(80), cs~load_uint(32), cs~load_uint(32)); + var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); - var stored_seqno = ds~load_uint(32); + var stored_seqno = ds~load_int(size::stored_seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - var stored_subwallet = ds~load_uint(80); - var public_key = ds.preload_uint(256); + var stored_subwallet = ds~load_uint(size::stored_subwallet); + var public_key = ds.preload_uint(size::public_key); ;; Only such checking order results in least amount of gas throw_unless(35, check_signature(slice_hash(body), signature, public_key)); @@ -122,7 +132,7 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() - .store_uint(stored_seqno, 32) + .store_int(stored_seqno, size::stored_seqno) .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); @@ -148,7 +158,7 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. var full_msg_slice = full_msg.begin_parse(); - var s_flags = full_msg_slice~load_bits(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... + var s_flags = full_msg_slice~load_bits(size::flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... slicy_return_if_bounce(s_flags); ;; slicy_return_if_bounce(begin_cell().store_uint(3, 4).end_cell().begin_parse()); ;; TEST!!! @@ -167,7 +177,7 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) var ds = get_data().begin_parse(); ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed - var extensions = ds.skip_bits(32 + 80 + 256).preload_dict(); + var extensions = ds.skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key).preload_dict(); ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). @@ -190,21 +200,25 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) ;; Get methods int seqno() method_id { - return get_data().begin_parse().preload_uint(32); + return get_data().begin_parse().preload_int(size::stored_seqno); } int get_wallet_id() method_id { - return get_data().begin_parse().skip_bits(32).preload_uint(80); + return get_data().begin_parse().skip_bits(size::stored_seqno).preload_uint(size::stored_subwallet); } int get_public_key() method_id { - var cs = get_data().begin_parse().skip_bits(32 + 80); - return cs.preload_uint(256); + var cs = get_data().begin_parse().skip_bits(size::stored_seqno + size::stored_subwallet); + return cs.preload_uint(size::public_key); } ;; Returns raw dictionary (or null if empty) where keys are packed addresses and the `wc` is stored in leafs. ;; User should unpack the address using the same packing function using `wc` to restore the original address. cell get_extensions() method_id { - var ds = get_data().begin_parse().skip_bits(32 + 80 + 256); + var ds = get_data().begin_parse().skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key); return ds~load_dict(); } + +int is_public_key_enabled() method_id { + return get_data().begin_parse().preload_int(size::stored_seqno) > 0; +} \ No newline at end of file diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 2a52a807..0d97bffc 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -355,7 +355,7 @@ describe('Wallet V5 sign auth external', () => { const msg2 = createMsgInternal({ dest: testReceiver2, value: forwardValue2 }); const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), + new ActionSetData(beginCell().storeInt(239, 33).endCell()), new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg1), new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg2) ]); @@ -408,7 +408,7 @@ describe('Wallet V5 sign auth external', () => { ); const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), + new ActionSetData(beginCell().storeInt(239, 33).endCell()), ...msges.map(msg => new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)) ]); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 7fd13d79..1c9a0a11 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -331,7 +331,7 @@ describe('Wallet V5 sign auth internal', () => { const msg2 = createMsgInternal({ dest: testReceiver2, value: forwardValue2 }); const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), + new ActionSetData(beginCell().storeInt(239, 33).endCell()), new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg1), new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg2) ]); @@ -383,7 +383,7 @@ describe('Wallet V5 sign auth internal', () => { ); const actionsList = packActionsList([ - new ActionSetData(beginCell().storeUint(239, 32).endCell()), + new ActionSetData(beginCell().storeInt(239, 33).endCell()), ...msges.map(msg => new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)) ]); diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 1eca81b5..3599b77c 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -23,7 +23,7 @@ export type WalletV5Config = { export function walletV5ConfigToCell(config: WalletV5Config): Cell { return beginCell() - .storeUint(config.seqno, 32) + .storeInt(config.seqno, 33) .storeUint(config.walletId, 80) .storeBuffer(config.publicKey, 32) .storeDict(config.extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8)) From 194f80359110703614dea037ca05ff31fac43973 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 02:18:06 +0200 Subject: [PATCH 005/121] Changed sign op for internal messages to sint to avoid replay attacks --- contracts/wallet_v5.fc | 4 ++-- wrappers/wallet-v5.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 886e1356..83b67ab7 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -21,7 +21,7 @@ const int size::flags = 4; (slice) enforce_and_remove_sign_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; (slice, int) check_and_remove_extn_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; -(slice) check_and_remove_sign_prefix_or_ret(slice body) impure asm "x{7369676E} SDBEGINSQ" "IFNOTRET"; +(slice) check_and_remove_sint_prefix_or_ret(slice body) impure asm "x{73696E74} SDBEGINSQ" "IFNOTRET"; ;; Extensible wallet contract v5 @@ -189,7 +189,7 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) } - body = check_and_remove_sign_prefix_or_ret(body); ;; 0x7369676E ("sign") + body = check_and_remove_sint_prefix_or_ret(body); ;; 0x73696E74 ("sint") - sign internal ;; Process the rest of the slice just like the signed request. process_signed_request(body); diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 3599b77c..957b4f5c 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -37,7 +37,8 @@ export const Opcodes = { action_extended_add_extension: 0x1c40db9f, action_extended_remove_extension: 0x5eaef4a4, auth_extension: 0x6578746e, - auth_signed: 0x7369676e + auth_signed: 0x7369676e, + auth_signed_internal: 0x73696e74 }; export class WalletId { @@ -136,7 +137,7 @@ export class WalletV5 implements Contract { value: opts.value, sendMode: SendMode.PAY_GAS_SEPARATELY, body: beginCell() - .storeUint(Opcodes.auth_signed, 32) + .storeUint(Opcodes.auth_signed_internal, 32) .storeSlice(opts.body.beginParse()) .endCell() }); From 04a363d576db42b7f18a7e0408b09325a5ef8b41 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 02:25:47 +0200 Subject: [PATCH 006/121] Removed set_data and temporarily disabled the relevant tests --- contracts/wallet_v5.fc | 5 ----- tests/wallet-v5-external.spec.ts | 9 ++++++--- tests/wallet-v5-internal.spec.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 83b67ab7..d4e604b5 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -48,11 +48,6 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) while (cs~load_uint(1)) { int op = cs~load_uint(32); - ;; Raw set_data - if (op == 0x1ff8ea0b) { - set_data(cs~load_ref()); - } - var is_add_ext = (op == 0x1c40db9f); var is_del_ext = (op == 0x5eaef4a4); ;; Add/remove extensions diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 0d97bffc..47d1d2d8 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -15,11 +15,8 @@ import { ActionAddExtension, ActionRemoveExtension, ActionSendMsg, - ActionSetCode, - ActionSetData, packActionsList } from './actions'; -import { WalletV4 } from '../wrappers/wallet-v4'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; @@ -341,6 +338,7 @@ describe('Wallet V5 sign auth external', () => { ); }); + /* TODO: Rewrite as a negative test it('Set data and do two transfers', async () => { const testReceiver1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue1 = toNano(0.001); @@ -388,7 +386,9 @@ describe('Wallet V5 sign auth external', () => { const storedSeqno = await walletV5.getSeqno(); expect(storedSeqno).toEqual(239); }); + */ + /* TODO: Rewrite as a negative test it('Send 255 transfers and do set data', async () => { await ( await blockchain.treasury('mass-messages') @@ -438,6 +438,7 @@ describe('Wallet V5 sign auth external', () => { const storedSeqno = await walletV5.getSeqno(); expect(storedSeqno).toEqual(239); }); + */ it('Remove extension', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); @@ -469,6 +470,7 @@ describe('Wallet V5 sign auth external', () => { accountForGas(receipt2.transactions); }); + /* TODO: Rewrite as a negative test it('Change code and data to wallet v4', async () => { const code_v4 = await compile('wallet_v4'); const data_v4 = beginCell() @@ -537,6 +539,7 @@ describe('Wallet V5 sign auth external', () => { value: forwardValue }); }); + */ it('Should fail adding existing extension', async () => { const testExtension = Address.parseRaw('0:' + '0'.repeat(64)); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 1c9a0a11..67af7342 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -9,11 +9,8 @@ import { ActionAddExtension, ActionRemoveExtension, ActionSendMsg, - ActionSetCode, - ActionSetData, packActionsList } from './actions'; -import { WalletV4 } from '../wrappers/wallet-v4'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; @@ -317,6 +314,7 @@ describe('Wallet V5 sign auth internal', () => { ); }); + /* TODO: Rewrite as a negative test it('Set data and do two transfers', async () => { const testReceiver1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue1 = toNano(0.001); @@ -367,7 +365,9 @@ describe('Wallet V5 sign auth internal', () => { const storedSeqno = await walletV5.getSeqno(); expect(storedSeqno).toEqual(239); }); + */ + /* TODO: Rewrite as a negative test it('Send 255 transfers and do set data', async () => { const range = [...new Array(255)].map((_, index) => index); @@ -416,6 +416,7 @@ describe('Wallet V5 sign auth internal', () => { const storedSeqno = await walletV5.getSeqno(); expect(storedSeqno).toEqual(239); }); + */ it('Remove extension', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); @@ -453,6 +454,7 @@ describe('Wallet V5 sign auth internal', () => { accountForGas(receipt2.transactions); }); + /* TODO: Rewrite as a negative test it('Change code and data to wallet v4', async () => { const code_v4 = await compile('wallet_v4'); const data_v4 = beginCell() @@ -525,6 +527,7 @@ describe('Wallet V5 sign auth internal', () => { value: forwardValue }); }); + */ it('Should fail adding existing extension', async () => { const testExtension = Address.parseRaw('0:' + '0'.repeat(64)); From 322237273c0522062a0831dafbafc39d64884c25 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 03:16:02 +0200 Subject: [PATCH 007/121] Added actions checking (send_msg w/ mode 2 enforced) and set pk enabled --- contracts/wallet_v5.fc | 55 ++++++++++++++++++++++++++------ tests/actions.ts | 2 +- tests/wallet-v5-external.spec.ts | 2 +- tests/wallet-v5-internal.spec.ts | 2 +- wrappers/wallet-v5.ts | 1 + 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index d4e604b5..af243a99 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -15,6 +15,7 @@ const int size::flags = 4; ;; SDCNTTRAIL1 will count trailing ones in slice. If bounced flag (last bit) is set it will be non-zero, else zero. () slicy_return_if_bounce(slice s_flags) impure asm "SDCNTTRAIL1" "IFRET"; +() slicy_throw_if_ends_with_0(slice s) impure asm "SDCNTTRAIL0" "37 THROWIF"; (slice) udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET IFNOTRET"; @@ -23,6 +24,12 @@ const int size::flags = 4; (slice) check_and_remove_sint_prefix_or_ret(slice body) impure asm "x{73696E74} SDBEGINSQ" "IFNOTRET"; +(slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; +(slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; +(slice, int) check_and_remove_set_public_key_enabled_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; + +(slice) enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; + ;; Extensible wallet contract v5 ;; Compresses 8+256-bit address into 256-bit uint by cutting off one bit from sha256. @@ -35,21 +42,32 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) {- ifnot (cs.preload_uint(1)) { - set_actions(cs.preload_ref()); + set_actions(cs.preload_ref().verify_actions()); return (); } -} -() set_actions_if_simple(slice cs) impure asm "x{4_} SDBEGINSQ" "IFJMP:<{ PLDREF c5 POP }>" "DROP"; +() set_actions_if_simple(slice cs) impure asm "x{4_} SDBEGINSQ" "IFJMP:<{ PLDREF verify_actions INLINECALLDICT c5 POP }>" "DROP"; + +cell verify_actions(cell c5) inline { + slice c5s = c5.begin_parse(); + while (~ c5s.slice_empty?()) { + ;; only send_msg is allowed, set_code or reserve_currency are not + c5s = c5s.enforce_and_remove_action_send_msg_prefix(); + ;; enforce that send_mode has 2 bit set + ;; for that load 7 bits and make sure that they end with 1 + slicy_throw_if_ends_with_0(c5s.preload_bits(7)); + c5s = c5s.preload_ref().begin_parse(); + } + return c5; +} ;; Dispatches already authenticated request. () dispatch_complex_request_inline(slice cs) impure inline { ;; Recurse into extended actions until we reach standard actions while (cs~load_uint(1)) { - int op = cs~load_uint(32); - - var is_add_ext = (op == 0x1c40db9f); - var is_del_ext = (op == 0x5eaef4a4); + var is_add_ext = cs~check_and_remove_add_extension_prefix(); + var is_del_ext = cs~check_and_remove_remove_extension_prefix(); ;; Add/remove extensions if ((is_add_ext) | (is_del_ext)) { (int wc, int hash) = parse_std_addr(cs~load_msg_addr()); @@ -64,8 +82,7 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) (extensions, int success?) = extensions.udict_add_builder?(256, packed_addr, begin_cell().store_int(wc,8)); throw_unless(39, success?); } else - ;; Remove extension - ;; if (op == 0x5eaef4a4) + ;; Remove extension if (op == 0x5eaef4a4) ;; It can be ONLY 0x1c40db9f OR 0x5eaef4a4 here. No need for second check. { (extensions, int success?) = extensions.udict_delete?(256, packed_addr); @@ -78,13 +95,33 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) .end_cell()); } + if (cs~check_and_remove_set_public_key_enabled_prefix()) { + var enable = cs~load_uint(1); + var ds = get_data().begin_parse(); + var stored_seqno = ds~load_int(size::stored_seqno); + var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions + if (enable) { + if (stored_seqno <= 0) { + stored_seqno = (- stored_seqno) + 1; + } + } else { + if (stored_seqno >= 0) { + stored_seqno = - (stored_seqno + 1); + } + } + set_data(begin_cell() + .store_int(stored_seqno, size::stored_seqno) + .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions + .end_cell()); + } + ;; Other actions are no-op ;; FIXME: is it costlier to check for unsupported actions and throw? cs = cs.preload_ref().begin_parse(); } ;; At this point we are at `action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0;` ;; Simply set the C5 register with all pre-computed actions: - set_actions(cs.preload_ref()); + set_actions(cs.preload_ref().verify_actions()); return (); } diff --git a/tests/actions.ts b/tests/actions.ts index 28fe6295..189b956c 100644 --- a/tests/actions.ts +++ b/tests/actions.ts @@ -21,7 +21,7 @@ export class ActionSendMsg { public serialize(): Cell { return beginCell() .storeUint(this.tag, 32) - .storeUint(this.mode, 8) + .storeUint(this.mode | SendMode.IGNORE_ERRORS, 8) .storeRef(beginCell().store(storeMessageRelaxed(this.outMsg)).endCell()) .endCell(); } diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 47d1d2d8..42cfd54a 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -137,7 +137,7 @@ describe('Wallet V5 sign auth external', () => { const sendTxactionAction = beginCell() .storeUint(Opcodes.action_send_msg, 32) - .storeInt(SendMode.PAY_GAS_SEPARATELY, 8) + .storeInt(SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, 8) .storeRef(sendTxMsg) .endCell(); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 67af7342..e9c0d6f6 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -131,7 +131,7 @@ describe('Wallet V5 sign auth internal', () => { const sendTxactionAction = beginCell() .storeUint(Opcodes.action_send_msg, 32) - .storeInt(SendMode.PAY_GAS_SEPARATELY, 8) + .storeInt(SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, 8) .storeRef(sendTxMsg) .endCell(); diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 957b4f5c..0aa5a311 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -36,6 +36,7 @@ export const Opcodes = { action_extended_set_data: 0x1ff8ea0b, action_extended_add_extension: 0x1c40db9f, action_extended_remove_extension: 0x5eaef4a4, + action_extended_set_public_key_enabled: 0x20cbb95a, auth_extension: 0x6578746e, auth_signed: 0x7369676e, auth_signed_internal: 0x73696e74 From 54a28d3e49eae7add532381eba291523286d81b0 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 10:14:29 +0200 Subject: [PATCH 008/121] Add guidelines and code to be able to make unsafe version --- contracts/wallet_v5.fc | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index af243a99..c2893285 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -24,6 +24,7 @@ const int size::flags = 4; (slice) check_and_remove_sint_prefix_or_ret(slice body) impure asm "x{73696E74} SDBEGINSQ" "IFNOTRET"; +(slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; (slice, int) check_and_remove_set_public_key_enabled_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; @@ -49,6 +50,8 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) () set_actions_if_simple(slice cs) impure asm "x{4_} SDBEGINSQ" "IFJMP:<{ PLDREF verify_actions INLINECALLDICT c5 POP }>" "DROP"; cell verify_actions(cell c5) inline { + ;; Comment out code starting from here to disable checks (unsafe version) + ;; {- slice c5s = c5.begin_parse(); while (~ c5s.slice_empty?()) { ;; only send_msg is allowed, set_code or reserve_currency are not @@ -58,6 +61,7 @@ cell verify_actions(cell c5) inline { slicy_throw_if_ends_with_0(c5s.preload_bits(7)); c5s = c5s.preload_ref().begin_parse(); } + ;; -} return c5; } @@ -94,8 +98,7 @@ cell verify_actions(cell c5) inline { .store_dict(extensions) .end_cell()); } - - if (cs~check_and_remove_set_public_key_enabled_prefix()) { + elseif (cs~check_and_remove_set_public_key_enabled_prefix()) { var enable = cs~load_uint(1); var ds = get_data().begin_parse(); var stored_seqno = ds~load_int(size::stored_seqno); @@ -114,13 +117,18 @@ cell verify_actions(cell c5) inline { .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); } - + ;; Uncomment to allow set_data (for unsafe version) + {- + elseif (cs~check_and_remove_set_data_prefix()) { + set_data(cs~load_ref()); + } + -} ;; Other actions are no-op ;; FIXME: is it costlier to check for unsupported actions and throw? cs = cs.preload_ref().begin_parse(); } ;; At this point we are at `action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0;` - ;; Simply set the C5 register with all pre-computed actions: + ;; Simply set the C5 register with all pre-computed actions after verification: set_actions(cs.preload_ref().verify_actions()); return (); } From 7abb84d70f1d445005ad6621832748bf65499c41 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 10:17:45 +0200 Subject: [PATCH 009/121] Optimized verify_actions by rearranging loop and conditions --- contracts/wallet_v5.fc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index c2893285..8b67e1f8 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -13,6 +13,8 @@ const int size::msg_seqno = 32; const int size::flags = 4; +() return_if(int cond) impure asm "IFRET"; + ;; SDCNTTRAIL1 will count trailing ones in slice. If bounced flag (last bit) is set it will be non-zero, else zero. () slicy_return_if_bounce(slice s_flags) impure asm "SDCNTTRAIL1" "IFRET"; () slicy_throw_if_ends_with_0(slice s) impure asm "SDCNTTRAIL0" "37 THROWIF"; @@ -53,14 +55,15 @@ cell verify_actions(cell c5) inline { ;; Comment out code starting from here to disable checks (unsafe version) ;; {- slice c5s = c5.begin_parse(); - while (~ c5s.slice_empty?()) { + return_if(c5s.slice_empty?()); + do { ;; only send_msg is allowed, set_code or reserve_currency are not c5s = c5s.enforce_and_remove_action_send_msg_prefix(); ;; enforce that send_mode has 2 bit set ;; for that load 7 bits and make sure that they end with 1 slicy_throw_if_ends_with_0(c5s.preload_bits(7)); c5s = c5s.preload_ref().begin_parse(); - } + } until (c5s.slice_empty?()); ;; -} return c5; } From 12b6e05cb0dc178101d53e35083e8e80b5fe376a Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 10:26:43 +0200 Subject: [PATCH 010/121] Added more info about sequence and fixed some cases in getters --- contracts/wallet_v5.fc | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 8b67e1f8..888f6516 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -107,12 +107,18 @@ cell verify_actions(cell c5) inline { var stored_seqno = ds~load_int(size::stored_seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions if (enable) { - if (stored_seqno <= 0) { - stored_seqno = (- stored_seqno) + 1; + if (stored_seqno < 0) { + ;; Can't be disabled with 0 because disabling increments seqno + ;; -123 -> 123 -> 124 + stored_seqno = - stored_seqno; + stored_seqno = stored_seqno + 1; } } else { if (stored_seqno >= 0) { - stored_seqno = - (stored_seqno + 1); + ;; Corner case: 0 -> 1 -> -1 + ;; 123 -> 124 -> -124 + stored_seqno = stored_seqno + 1; + stored_seqno = - stored_seqno; } } set_data(begin_cell() @@ -243,7 +249,7 @@ cell verify_actions(cell c5) inline { ;; Get methods int seqno() method_id { - return get_data().begin_parse().preload_int(size::stored_seqno); + return abs(get_data().begin_parse().preload_int(size::stored_seqno)); } int get_wallet_id() method_id { @@ -263,5 +269,5 @@ cell get_extensions() method_id { } int is_public_key_enabled() method_id { - return get_data().begin_parse().preload_int(size::stored_seqno) > 0; + return get_data().begin_parse().preload_int(size::stored_seqno) >= 0; } \ No newline at end of file From c5c65d19b5979037a6dd595ea3c5feb3235eef03 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 10:38:57 +0200 Subject: [PATCH 011/121] Adjusted comments to be more correct and precise --- contracts/wallet_v5.fc | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 888f6516..d744cddc 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -149,8 +149,8 @@ cell verify_actions(cell c5) inline { } () dispatch_extension_request(slice cs, var dummy1) impure inline { - set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref()); return (); } - ;; <<<<<<<<<<---------- CONTEST TEST CUTOFF POINT ---------->>>>>>>>>> + set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref().verify_actions()); return (); } + ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> ;; dummy1~impure_touch(); ;; DROP merged to 2DROP! dispatch_complex_request_iref(cs); @@ -172,6 +172,10 @@ cell verify_actions(cell c5) inline { ;; Only such checking order results in least amount of gas throw_unless(35, check_signature(slice_hash(body), signature, public_key)); + ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 + ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 + ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed + ;; throw_if(33, stored_seqno < 0); throw_unless(33, msg_seqno == stored_seqno); throw_unless(34, subwallet_id == stored_subwallet); throw_if(36, valid_until <= now()); @@ -187,15 +191,16 @@ cell verify_actions(cell c5) inline { commit(); - set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref()); return (); } - ;; <<<<<<<<<<---------- CONTEST TEST CUTOFF POINT ---------->>>>>>>>>> + set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref().verify_actions()); return (); } + ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> ;; inline_ref required because otherwise it will produce undesirable JMPREF dispatch_complex_request_iref(cs); } () recv_external(slice body) impure inline { - body = enforce_and_remove_sign_prefix(body); ;; 0x7369676E ("sign") + ;; 0x7369676E ("sign") external message authenticated by signature + body = enforce_and_remove_sign_prefix(body); process_signed_request(body); return(); } @@ -214,11 +219,11 @@ cell verify_actions(cell c5) inline { ;; We accept two kinds of authenticated messages: ;; - 0x6578746E "extn" authenticated by extension - ;; - 0x7369676E "sign" authenticated by signature + ;; - 0x73696E74 "sint" internal message authenticated by signature (body, int is_extn) = check_and_remove_extn_prefix(body); ;; 0x6578746E ("extn") - ;; IFJMPREF + ;; IFJMPREF because unconditionally returns inside if (is_extn) { ;; "extn" authenticated by extension ;; Authenticate extension by its address. From 0c9fa87d963daa4fe227bfd1be1fc2125096359a Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 10:40:20 +0200 Subject: [PATCH 012/121] Removed unneccessary noop middleware and renamed method to classic name --- contracts/wallet_v5.fc | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index d744cddc..52e88667 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -69,7 +69,9 @@ cell verify_actions(cell c5) inline { } ;; Dispatches already authenticated request. -() dispatch_complex_request_inline(slice cs) impure inline { +;; this function is explicitly included as an inline reference - not completely inlined +;; completely inlining it causes undesirable code split and noticeable gas increase in some paths +() dispatch_complex_request(slice cs) impure inline_ref { ;; Recurse into extended actions until we reach standard actions while (cs~load_uint(1)) { @@ -142,18 +144,12 @@ cell verify_actions(cell c5) inline { return (); } -() dispatch_complex_request_iref(slice cs) impure inline_ref { - ;; this function is explicitly included as an inline reference - not completely inlined - ;; completely inlining it causes undesirable code split and noticeable gas increase in some paths - dispatch_complex_request_inline(cs); -} - () dispatch_extension_request(slice cs, var dummy1) impure inline { set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref().verify_actions()); return (); } ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> ;; dummy1~impure_touch(); ;; DROP merged to 2DROP! - dispatch_complex_request_iref(cs); + dispatch_complex_request(cs); } ;; Verifies signed request, prevents replays and proceeds with `dispatch_request`. @@ -195,7 +191,7 @@ cell verify_actions(cell c5) inline { ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> ;; inline_ref required because otherwise it will produce undesirable JMPREF - dispatch_complex_request_iref(cs); + dispatch_complex_request(cs); } () recv_external(slice body) impure inline { From 74cc7f6efd229eb6f06b553adf97425d4ed966e7 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 10:51:40 +0200 Subject: [PATCH 013/121] Removed throwif from the comment about seqno --- contracts/wallet_v5.fc | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 52e88667..a8c852b6 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -171,7 +171,6 @@ cell verify_actions(cell c5) inline { ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed - ;; throw_if(33, stored_seqno < 0); throw_unless(33, msg_seqno == stored_seqno); throw_unless(34, subwallet_id == stored_subwallet); throw_if(36, valid_until <= now()); From 4217b8abefc05973124f0a4c57068e7d1b1644ba Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 14:16:50 +0200 Subject: [PATCH 014/121] Made internal messages bounce-safe (replaced throws with soft checks) --- contracts/wallet_v5.fc | 45 +++++++++++++++++++++++++++++++- tests/wallet-v5-internal.spec.ts | 10 +++---- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index a8c852b6..e1e9c707 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -14,6 +14,7 @@ const int size::msg_seqno = 32; const int size::flags = 4; () return_if(int cond) impure asm "IFRET"; +() return_unless(int cond) impure asm "IFNOTRET"; ;; SDCNTTRAIL1 will count trailing ones in slice. If bounced flag (last bit) is set it will be non-zero, else zero. () slicy_return_if_bounce(slice s_flags) impure asm "SDCNTTRAIL1" "IFRET"; @@ -193,6 +194,48 @@ cell verify_actions(cell c5) inline { dispatch_complex_request(cs); } +;; Same logic as above function but with return_* instead of throw_* and additional checks to prevent bounces +() process_signed_request_from_internal_message(slice body) impure inline { + ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) + return_if(body.slice_bits() < 512 + size::subwallet_id + size::valid_until + size::msg_seqno + 1); + + ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. + var signature = body~load_bits(512); + + var cs = body; + var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); + + var ds = get_data().begin_parse(); + var stored_seqno = ds~load_int(size::stored_seqno); + var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions + var stored_subwallet = ds~load_uint(size::stored_subwallet); + var public_key = ds.preload_uint(size::public_key); + + ;; Only such checking order results in least amount of gas + return_unless(check_signature(slice_hash(body), signature, public_key)); + ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 + ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 + ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed + return_unless(msg_seqno == stored_seqno); + return_unless(subwallet_id == stored_subwallet); + return_if(valid_until <= now()); + + ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. + stored_seqno = stored_seqno + 1; + set_data(begin_cell() + .store_int(stored_seqno, size::stored_seqno) + .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions + .end_cell()); + + commit(); + + set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref().verify_actions()); return (); } + ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> + + ;; inline_ref required because otherwise it will produce undesirable JMPREF + dispatch_complex_request(cs); +} + () recv_external(slice body) impure inline { ;; 0x7369676E ("sign") external message authenticated by signature body = enforce_and_remove_sign_prefix(body); @@ -241,7 +284,7 @@ cell verify_actions(cell c5) inline { body = check_and_remove_sint_prefix_or_ret(body); ;; 0x73696E74 ("sint") - sign internal ;; Process the rest of the slice just like the signed request. - process_signed_request(body); + process_signed_request_from_internal_message(body); return (); ;; Explicit returns escape function faster and const less gas (suddenly!) } diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index e9c0d6f6..6a829890 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -636,7 +636,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(35); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -682,7 +682,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(35); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -726,7 +726,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(33); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -770,7 +770,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(36); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -814,7 +814,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(34); + ).toEqual(0); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, From dc6b4e98f0a0db4a69ca46ee288aeab2cbe1f748 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 14:53:29 +0200 Subject: [PATCH 015/121] Added quick return for empty internal messages --- contracts/wallet_v5.fc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index e1e9c707..d31546c4 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -247,6 +247,8 @@ cell verify_actions(cell c5) inline { __install_root_cell_repacker_hook__(); ;; this function from root_cell_repacker can be called in any method + return_if(body.slice_empty?()); ;; return right away if the body is empty (simple transfer) + ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. var full_msg_slice = full_msg.begin_parse(); From 3ad3a7a313b1086e336c431c6803630c5d5e7d61 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 15:23:57 +0200 Subject: [PATCH 016/121] [Docs] Moved contest note to separate file and tidied up README a bit --- Contest.md | 26 ++++++++++++++++++++++++++ README.md | 33 ++++----------------------------- 2 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 Contest.md diff --git a/Contest.md b/Contest.md new file mode 100644 index 00000000..c3be22f3 --- /dev/null +++ b/Contest.md @@ -0,0 +1,26 @@ +## Contest note! + +
+Contest logo +
+ +**Because of the extreme amount of optimizations, developer's discretion is advised!** *Evil laugh* + +The build system is the same as in the original Wallet V5, **no security features have been sacrificed** +for performance improvements, that is - there are **practically no tradeoffs or compromises**. + +Message and storage layouts were **not changed**, although some rearragement might squeeze a little more gas, +but that may break existing optimizations due to stack reordering. + +Also, **tests were improved** - a **Global Gas Counter** mechanism was added that accounts for gas in all transactions +of all test suites and cases (except for negative and getter ones). This allows to keep an eye on other non-contest +cases to track how bad is tradeoff when performing optimizations here and there. + +Another utility that was developed for contest is ***scalpel script***, that allows for a detailed, *really* detailed optimizations +of the code by comparing lines of code function by function, printing out diffs, and providing detailed TVM files with +stack comments and rewrites. This utility allowed to make some latter optimizations, since with each optimization +next one becomes exponentionally harder to make. While result is not entirely precise and is needed to be verified +by tests, this allows to instantly estimate whether there is some progress or not, since scalpel is executed immediately, +while tests take approximately 10 seconds to execute. + +### Details of optimizations, their rationale and explanations, comparison of consumed gas both in test cases and not in test cases (global gas counter) are provided on a dedicated page: [Gas improvements](Improvements.rst). diff --git a/README.md b/README.md index 82b17284..e86386c9 100644 --- a/README.md +++ b/README.md @@ -4,41 +4,16 @@ This is an extensible wallet specification aimed at replacing V4 and allowing ar Wallet V5 has 25% lower fees, can delegate payments for gas to third parties and supports flexible extension mechanism. -## Warning! Contest note! - -
-Contest logo -
- -**Because of the extreme amount of optimizations, developer's discretion is advised!** *Evil laugh* - -The build system is the same as in the original Wallet V5, **no security features have been sacrificed** -for performance improvements, that is - there are **practically no tradeoffs or compromises**. - -Message and storage layouts were **not changed**, although some rearragement might squeeze a little more gas, -but that may break existing optimizations due to stack reordering. - -Also, **tests were improved** - a **Global Gas Counter** mechanism was added that accounts for gas in all transactions -of all test suites and cases (except for negative and getter ones). This allows to keep an eye on other non-contest -cases to track how bad is tradeoff when performing optimizations here and there. - -Another utility that was developed for contest is ***scalpel script***, that allows for a detailed, *really* detailed optimizations -of the code by comparing lines of code function by function, printing out diffs, and providing detailed TVM files with -stack comments and rewrites. This utility allowed to make some latter optimizations, since with each optimization -next one becomes exponentionally harder to make. While result is not entirely precise and is needed to be verified -by tests, this allows to instantly estimate whether there is some progress or not, since scalpel is executed immediately, -while tests take approximately 10 seconds to execute. - -### Details of optimizations, their rationale and explanations, comparison of consumed gas both in test cases and not in test cases (global gas counter) are provided on a dedicated page: [Gas improvements](Improvements.rst). - ## Project structure - [Specification](Specification.md) - `contracts` - source code of all the smart contracts of the project and their dependencies. - `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions. - `tests` - tests for the contracts. -- `scripts` - scripts used by the project, mainly the deployment scripts. -- **[Gas improvements](Improvements.rst)** (detailed by contest paths, global gas counters per commit) +- `scripts` - scripts used by the project, mainly the deployment scripts, additionally contains utilities for gas optimisation. +- `fift` - contains standard Fift v0.4.4 library including the assembler and disassembler for gas optimisation utilities. +- [Gas improvements](Improvements.rst) - a log of improvements, detailed by primary code paths, global gas counters per commit. +- [Contest note](Contest.md) - a note showing some information about interesting improvements during the optimisation contest. ## How to use From 0d9ffef2a0ae463160acdd0d859d6bf2386dfc3b Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 15:26:42 +0200 Subject: [PATCH 017/121] [Docs] Slight readme tidy-up --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e86386c9..af25b151 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,11 @@ Wallet V5 has 25% lower fees, can delegate payments for gas to third parties and - `tests` - tests for the contracts. - `scripts` - scripts used by the project, mainly the deployment scripts, additionally contains utilities for gas optimisation. - `fift` - contains standard Fift v0.4.4 library including the assembler and disassembler for gas optimisation utilities. + +### Additional documentation + - [Gas improvements](Improvements.rst) - a log of improvements, detailed by primary code paths, global gas counters per commit. -- [Contest note](Contest.md) - a note showing some information about interesting improvements during the optimisation contest. +- [Contest](Contest.md) - a note showing some information about interesting improvements during the optimisation contest. ## How to use From 507b821b15a86f2f2bdf9f07d98a21920f7272d1 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 16:03:54 +0200 Subject: [PATCH 018/121] [Docs] Updated specification in accordance with new contract features --- Specification.md | 69 +++++++++++++----------------------------------- 1 file changed, 18 insertions(+), 51 deletions(-) diff --git a/Specification.md b/Specification.md index 89238060..51af729f 100644 --- a/Specification.md +++ b/Specification.md @@ -19,16 +19,18 @@ Thanks to [Andrew Gutarev](https://github.com/pyAndr3w) for the idea to set c5 r Thanks to [@subden](https://t.me/subden), [@botpult](https://t.me/botpult) and [@tvorogme](https://t.me/tvorogme) for ideas and discussion. +Thanks to [Skydev](https://github.com/Skydev0h) for optimization and preparing the second revision of the contract. + ## Features * 25% smaller computation fees. * Arbitrary amount of outgoing messages is supported via action list. -* Wallet code can be upgraded transparently without breaking user's address in the future. * Wallet code can be extended by anyone in a decentralized and conflict-free way: multiple feature extensions can co-exist. * Extensions can perform the same operations as the signer: emit arbitrary messages on behalf of the owner, add and remove extensions. * Signed requests can be delivered via internal message to allow 3rd party pay for gas. * For consistency and ease of indexing, external messages also receive a 32-bit opcode. +* To lay foundation for support of scenarios like 2FA or access recovery it is possible to disable signature authentication. ## Overview @@ -39,8 +41,8 @@ Authentication: * by extension Operation types: -* standard output actions -* “set data” operation +* standard output send message action +* enable or disable public key (signature authentication) * install extension * remove extension @@ -63,40 +65,12 @@ User may delegate this job to other apps via extensions. ### Extending the wallet -**A. Use extensions** - The best way to extend functionality of the wallet is to use the extensions mechanism that permit delegating access to the wallet to other contracts. From the perspective of the wallet, every extension can perform the same actions as the owner of a private key. Therefore limits and capabilities can be embedded in such an extension with a custom storage scheme. Extensions can co-exist simultaneously, so experimental capabilities can be deployed and tested independently from each other. -**B. Code optimization** - -Backwards compatible code optimization **can be performed** with a single `set_code` action (`action_set_code#ad4de08e`) signed by the user. That is, hypothetical upgrade from `v5R1` to `v5R2` can be done in-place without forcing users to change their wallet address. - -If the optimized code requires changes to the data layout (e.g. reordering fields) the user can sign a request with two actions: `set_code` (in the standard action) and `set_data` (an extended action per this specification). Note that `set_data` action must make sure `seqno` is properly incremented after the upgrade as to prevent replays. Also, `set_data` must be performed right before the standard actions to not get overwritten by extension actions. The updated wallet **must** have the new subwallet ID to prevent accidental repeated migrations. - -User agents **should not** make `set_code` and `set_data` actions available via general-purpose API to prevent misuse and mistakes. Instead, they should be used as a part of migration logic for a specific wallet code. - -To restore the wallet by a seed phrase, user agent should use the original code and should expect the upgraded code to work in exactly the same way as previously. - -**C. Emergency upgrades** - -This is a variant of (B), so the same consideration apply. The difference is that functionality may be modified as to prevent user from suffering loss of funds. E.g. some previously possible actions or signed messages would lead to a failure. - -Just like with (B), user agents **should not** make `set_code` and `set_data` actions available via general-purpose API to prevent misuse and mistakes. Instead, they should be used as a part of migration logic for a specific wallet code. - -New users’ wallets **should not** be deployed with upgraded code. Instead, the improved wallet code should also be released as a new wallet version (e.g. v6, with a separate subwallet ID) and new wallets should be deployed with that code. This way `set_code` would be used as an emergency patch for existing wallets, while new wallets would be deployed directly with the major next version. - - -**D. Substantial upgrades** - -We **do not recommend** performing substantial wallet upgrades in-place using `set_code`/`set_data` actions. Instead, user agents should have support for multiple accounts and easy switching between them. - -In-place migration requires maintaining backwards compatibility for all wallet features, which in turn could lead to increase in code size and higher gas and rent costs. - - ### Can the wallet outsource payment for gas fees? Yes! You can deliver signed messages via an internal message from a 3rd party wallet. Also, the message is handled exactly like an external one: after the basic checks the wallet takes care of the fees itself, so that 3rd party does not need to overpay for users who actually do have TONs. @@ -123,6 +97,8 @@ You need to put two requests in your message body: Yes. We have considered constant-size schemes where the wallet only stores trusted extension code. However, extension authentication becomes combursome and expensive: plugin needs to transmit additional data and each request needs to recompute plugin’s address. We estimate that for the reasonably sized wallets (less than 100 plugins) authentication via the dictionary lookup would not exceed costs of indirect address authentication. +### Why it can be useful to disable signature authentication? + ### What is library on masterchain? Library is a special code storage mechanism that allows to reduce storage cost for a new Wallet V5 contract instance. Wallet V5 contract code is stored into a masterchain library. @@ -143,7 +119,7 @@ wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = Wa - `global_id` is a TON chain identifier. TON Mainnet `global_id = -239` and TON Testnet `global_id = -3`. - `wc` is a Workchain. -1 for Masterchain and 0 for Basechain. - `version`: current version of wallet v5 is `0`. -- `subwallet_number` can be used to get multiplie wallet contracts binded to the single keypair. +- `subwallet_number` can be used to get multiple wallet contracts bound to the single keypair. ## Packed address @@ -166,25 +142,16 @@ Action types: ```tl-b // Standard actions from block.tlb: out_list_empty$_ = OutList 0; -out_list$_ {n:#} prev:^(OutList n) action:OutAction - = OutList (n + 1); -action_send_msg#0ec3c86d mode:(## 8) - out_msg:^(MessageRelaxed Any) = OutAction; -action_set_code#ad4de08e new_code:^Cell = OutAction; -action_reserve_currency#36e6b809 mode:(## 8) - currency:CurrencyCollection = OutAction; -libref_hash$0 lib_hash:bits256 = LibRef; -libref_ref$1 library:^Cell = LibRef; -action_change_library#26fa1dd4 mode:(## 7) { mode <= 2 } - libref:LibRef = OutAction; +out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); +action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; // Extended actions in W5: action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0; action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); -action_set_data#1ff8ea0b data:^Cell = ExtendedAction; action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; +action_set_public_key_enabled#20cbb95a enabled:(## 1) = ExtendedAction; ``` Authentication modes: @@ -192,14 +159,14 @@ Authentication modes: ```tl-b signed_request$_ signature: bits512 // 512 - subwallet_id: uint32 // 512+32 - valid_until: uint32 // 512+32+32 - msg_seqno: uint32 // 512+32+32+32 = 608 + wallet_id: (## 80) // 512+80 + valid_until: (## 32) // 512+80+32 + msg_seqno: (## 32) // 512+80+32+32 = 656 inner: InnerRequest = SignedRequest; -internal_signed#7369676E signed:SignedRequest = InternalMsgBody; -internal_extension#6578746E inner:InnerRequest = InternalMsgBody; -external_signed#7369676E signed:SignedRequest = ExternalMsgBody; +internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; +internal_extension#6578746e inner:InnerRequest = InternalMsgBody; +external_signed#7369676e signed:SignedRequest = ExternalMsgBody; actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; ``` @@ -207,7 +174,7 @@ actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; Contract state: ```tl-b wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; -contract_state$_ seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; +contract_state$_ seqno:int33 wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; ``` ## Source code From b5bdf247404bf26cc973abe0da9b970add21e583 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Wed, 24 Jan 2024 16:17:46 +0200 Subject: [PATCH 019/121] [Docs] Update types.tlb --- types.tlb | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/types.tlb b/types.tlb index 13460898..d21f0957 100644 --- a/types.tlb +++ b/types.tlb @@ -1,33 +1,24 @@ // Standard actions from block.tlb: out_list_empty$_ = OutList 0; -out_list$_ {n:#} prev:^(OutList n) action:OutAction - = OutList (n + 1); -action_send_msg#0ec3c86d mode:(## 8) - out_msg:^(MessageRelaxed Any) = OutAction; -action_set_code#ad4de08e new_code:^Cell = OutAction; -action_reserve_currency#36e6b809 mode:(## 8) - currency:CurrencyCollection = OutAction; -libref_hash$0 lib_hash:bits256 = LibRef; -libref_ref$1 library:^Cell = LibRef; -action_change_library#26fa1dd4 mode:(## 7) { mode <= 2 } - libref:LibRef = OutAction; +out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); +action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; // Extended actions in W5: action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0; action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); -action_set_data#1ff8ea0b data:^Cell = ExtendedAction; action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; +action_set_public_key_enabled#20cbb95a enabled:(## 1) = ExtendedAction; signed_request$_ signature: bits512 // 512 - subwallet_id: uint32 // 512+32 - valid_until: uint32 // 512+32+32 - msg_seqno: uint32 // 512+32+32+32 = 608 + wallet_id: (## 80) // 512+80 + valid_until: (## 32) // 512+80+32 + msg_seqno: (## 32) // 512+80+32+32 = 656 inner: InnerRequest = SignedRequest; -internal_signed#7369676e signed:SignedRequest = InternalMsgBody; +internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; internal_extension#6578746e inner:InnerRequest = InternalMsgBody; external_signed#7369676e signed:SignedRequest = ExternalMsgBody; @@ -35,4 +26,4 @@ actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; // Contract state wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; -contract_state$_ seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; +contract_state$_ seqno:int33 wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; From f2601ae27052ee4172bd7042699b62a432b27de2 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Sat, 27 Jan 2024 17:25:43 +0200 Subject: [PATCH 020/121] [Docs] Add use cases for disabling public key --- Specification.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Specification.md b/Specification.md index 51af729f..8c499580 100644 --- a/Specification.md +++ b/Specification.md @@ -97,7 +97,13 @@ You need to put two requests in your message body: Yes. We have considered constant-size schemes where the wallet only stores trusted extension code. However, extension authentication becomes combursome and expensive: plugin needs to transmit additional data and each request needs to recompute plugin’s address. We estimate that for the reasonably sized wallets (less than 100 plugins) authentication via the dictionary lookup would not exceed costs of indirect address authentication. -### Why it can be useful to disable signature authentication? +### Why it can be useful to disable signature authentication mode? + +Ability to disable authentication by signature enables two related use-cases: + +1. Two-factor authentication schemes: where control over wallet is fully delegated to an extension that checks two signatures: the user’s one and the signature from the auth service. Naturally, if the signature authentication in the wallet remains enabled, the second factor check is bypassed. + +2. Account recovery: delegating full control to another wallet in case of key compromise or loss. Wallet may contain larger amount of assets and its address could be tied to long-term contracts, therefore delegation to another controlling account is preferred to simply transferring the assets. ### What is library on masterchain? From 95451204394529315e283a984a800c5fbcd52ffe Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Sat, 27 Jan 2024 19:29:55 +0200 Subject: [PATCH 021/121] Updated actions, allow naming, empty ref check, docs, actions wrapper --- Specification.md | 8 ++-- contracts/wallet_v5.fc | 34 +++++++++++---- tests/actions.ts | 96 ++++++++++-------------------------------- types.tlb | 2 +- wrappers/wallet-v5.ts | 2 +- 5 files changed, 55 insertions(+), 87 deletions(-) diff --git a/Specification.md b/Specification.md index 8c499580..5225291d 100644 --- a/Specification.md +++ b/Specification.md @@ -97,11 +97,11 @@ You need to put two requests in your message body: Yes. We have considered constant-size schemes where the wallet only stores trusted extension code. However, extension authentication becomes combursome and expensive: plugin needs to transmit additional data and each request needs to recompute plugin’s address. We estimate that for the reasonably sized wallets (less than 100 plugins) authentication via the dictionary lookup would not exceed costs of indirect address authentication. -### Why it can be useful to disable signature authentication mode? +### Why it can be useful to disallow authentication with signature? -Ability to disable authentication by signature enables two related use-cases: +Ability to disallow authentication with signature enables two related use-cases: -1. Two-factor authentication schemes: where control over wallet is fully delegated to an extension that checks two signatures: the user’s one and the signature from the auth service. Naturally, if the signature authentication in the wallet remains enabled, the second factor check is bypassed. +1. Two-factor authentication schemes: where control over wallet is fully delegated to an extension that checks two signatures: the user’s one and the signature from the auth service. Naturally, if the signature authentication in the wallet remains allowed, the second factor check is bypassed. 2. Account recovery: delegating full control to another wallet in case of key compromise or loss. Wallet may contain larger amount of assets and its address could be tied to long-term contracts, therefore delegation to another controlling account is preferred to simply transferring the assets. @@ -157,7 +157,7 @@ action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; -action_set_public_key_enabled#20cbb95a enabled:(## 1) = ExtendedAction; +action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; ``` Authentication modes: diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index d31546c4..836956af 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -30,7 +30,7 @@ const int size::flags = 4; (slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; -(slice, int) check_and_remove_set_public_key_enabled_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; +(slice, int) check_and_remove_set_signature_auth_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; (slice) enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; @@ -50,7 +50,15 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) return (); } -} -() set_actions_if_simple(slice cs) impure asm "x{4_} SDBEGINSQ" "IFJMP:<{ PLDREF verify_actions INLINECALLDICT c5 POP }>" "DROP"; +() set_actions_if_simple(slice cs) impure asm """ + x{4_} SDBEGINSQ + IFJMP:<{ + PLDREF + verify_actions INLINECALLDICT + c5 POP + }> + DROP +"""; cell verify_actions(cell c5) inline { ;; Comment out code starting from here to disable checks (unsafe version) @@ -104,12 +112,13 @@ cell verify_actions(cell c5) inline { .store_dict(extensions) .end_cell()); } - elseif (cs~check_and_remove_set_public_key_enabled_prefix()) { - var enable = cs~load_uint(1); + elseif (cs~check_and_remove_set_signature_auth_allowed_prefix()) { + var allow = cs~load_uint(1); var ds = get_data().begin_parse(); var stored_seqno = ds~load_int(size::stored_seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - if (enable) { + if (allow) { + ;; allow if (stored_seqno < 0) { ;; Can't be disabled with 0 because disabling increments seqno ;; -123 -> 123 -> 124 @@ -117,7 +126,11 @@ cell verify_actions(cell c5) inline { stored_seqno = stored_seqno + 1; } } else { + ;; disallow if (stored_seqno >= 0) { + ds = ds.skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key); + var extensions_is_not_null = ds.preload_uint(1); + throw_unless(42, extensions_is_not_null); ;; Corner case: 0 -> 1 -> -1 ;; 123 -> 124 -> -124 stored_seqno = stored_seqno + 1; @@ -245,9 +258,13 @@ cell verify_actions(cell c5) inline { () recv_internal(cell full_msg, slice body) impure inline { - __install_root_cell_repacker_hook__(); ;; this function from root_cell_repacker can be called in any method + ;; this function from root_cell_repacker can be called in any method + ;; no opcodes are added by this function to recv_internal directly + __install_root_cell_repacker_hook__(); - return_if(body.slice_empty?()); ;; return right away if the body is empty (simple transfer) + ;; return right away if there are no references + ;; correct messages always have a ref, because any code paths ends with preload_ref + return_if(body.slice_refs_empty?()); ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. var full_msg_slice = full_msg.begin_parse(); @@ -294,6 +311,7 @@ cell verify_actions(cell c5) inline { ;; Get methods int seqno() method_id { + ;; Use absolute value to do not confuse apps with negative seqno if key is disabled return abs(get_data().begin_parse().preload_int(size::stored_seqno)); } @@ -313,6 +331,6 @@ cell get_extensions() method_id { return ds~load_dict(); } -int is_public_key_enabled() method_id { +int get_is_signature_auth_allowed() method_id { return get_data().begin_parse().preload_int(size::stored_seqno) >= 0; } \ No newline at end of file diff --git a/tests/actions.ts b/tests/actions.ts index 189b956c..504898ed 100644 --- a/tests/actions.ts +++ b/tests/actions.ts @@ -1,15 +1,4 @@ -import { - Address, - beginCell, - Cell, - CurrencyCollection, - MessageRelaxed, - SendMode, - storeMessageRelaxed, - storeCurrencyCollection -} from 'ton-core'; - -export type LibRef = Cell | bigint; +import { Address, beginCell, Cell, MessageRelaxed, SendMode, storeMessageRelaxed } from 'ton-core'; export class ActionSendMsg { public static readonly tag = 0x0ec3c86d; @@ -27,63 +16,6 @@ export class ActionSendMsg { } } -export class ActionSetCode { - public static readonly tag = 0xad4de08e; - - public readonly tag = ActionSetCode.tag; - - constructor(public readonly newCode: Cell) {} - - public serialize(): Cell { - return beginCell().storeUint(this.tag, 32).storeRef(this.newCode).endCell(); - } -} - -export class ActionReserveCurrency { - public static readonly tag = 0x36e6b809; - - public readonly tag = ActionReserveCurrency.tag; - - constructor(public readonly mode: SendMode, public readonly currency: CurrencyCollection) {} - - public serialize(): Cell { - return beginCell() - .storeUint(this.tag, 32) - .storeUint(this.mode, 8) - .store(storeCurrencyCollection(this.currency)) - .endCell(); - } -} - -export class ActionChangeLibrary { - public static readonly tag = 0x26fa1dd4; - - public readonly tag = ActionChangeLibrary.tag; - - constructor(public readonly mode: number, public readonly libRef: LibRef) {} - - public serialize(): Cell { - const cell = beginCell().storeUint(this.tag, 32).storeUint(this.mode, 7); - if (typeof this.libRef === 'bigint') { - return cell.storeUint(0, 1).storeUint(this.libRef, 256).endCell(); - } - - return cell.storeUint(1, 1).storeRef(this.libRef).endCell(); - } -} - -export class ActionSetData { - public static readonly tag = 0x1ff8ea0b; - - public readonly tag = ActionSetData.tag; - - constructor(public readonly data: Cell) {} - - public serialize(): Cell { - return beginCell().storeUint(this.tag, 32).storeRef(this.data).endCell(); - } -} - export class ActionAddExtension { public static readonly tag = 0x1c40db9f; @@ -108,14 +40,32 @@ export class ActionRemoveExtension { } } -export type OutAction = ActionSendMsg | ActionSetCode | ActionReserveCurrency | ActionChangeLibrary; -export type ExtendedAction = ActionSetData | ActionAddExtension | ActionRemoveExtension; +export class ActionSetSignatureAuthAllowed { + public static readonly tag = 0x20cbb95a; + + public readonly tag = ActionSetSignatureAuthAllowed.tag; + + constructor(public readonly allowed: Boolean) {} + + public serialize(): Cell { + return beginCell() + .storeUint(this.tag, 32) + .storeUint(this.allowed ? 1 : 0, 1) + .endCell(); + } +} + +export type OutAction = ActionSendMsg; +export type ExtendedAction = + | ActionAddExtension + | ActionRemoveExtension + | ActionSetSignatureAuthAllowed; export function isExtendedAction(action: OutAction | ExtendedAction): action is ExtendedAction { return ( - action.tag === ActionSetData.tag || action.tag === ActionAddExtension.tag || - action.tag === ActionRemoveExtension.tag + action.tag === ActionRemoveExtension.tag || + action.tag === ActionSetSignatureAuthAllowed.tag ); } diff --git a/types.tlb b/types.tlb index d21f0957..9c654f5e 100644 --- a/types.tlb +++ b/types.tlb @@ -9,7 +9,7 @@ action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; -action_set_public_key_enabled#20cbb95a enabled:(## 1) = ExtendedAction; +action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; signed_request$_ signature: bits512 // 512 diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 0aa5a311..72366a6b 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -36,7 +36,7 @@ export const Opcodes = { action_extended_set_data: 0x1ff8ea0b, action_extended_add_extension: 0x1c40db9f, action_extended_remove_extension: 0x5eaef4a4, - action_extended_set_public_key_enabled: 0x20cbb95a, + action_extended_set_signature_auth_allowed: 0x20cbb95a, auth_extension: 0x6578746e, auth_signed: 0x7369676e, auth_signed_internal: 0x73696e74 From c7bd87ac92ab485e975f8295b8e4b7806ae8a277 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Sun, 28 Jan 2024 14:12:48 +0200 Subject: [PATCH 022/121] Added gas tests for incoming messages --- tests/wallet-v5-internal.spec.ts | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 6a829890..6fdab19e 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -676,6 +676,13 @@ describe('Wallet V5 sign auth internal', () => { value: toNano(0.1), body }); + console.debug( + 'SINGLE WRONG SIGNATURE INTERNAL TRANSFER GAS USED:', + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).gasUsed + ); expect( ( @@ -906,5 +913,38 @@ describe('Wallet V5 sign auth internal', () => { .computePhase as TransactionComputeVm ).exitCode ).toEqual(0); + + console.debug( + 'SINGLE SIMPLE INTERNAL TRANSFER GAS USED:', + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).gasUsed + ); + }); + + it('Should skip message with longer text comment', async () => { + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: beginCell().storeUint(0, 32).storeStringTail('Hello world'.repeat(20)).endCell() + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + console.debug( + 'SINGLE LONGER SIMPLE INTERNAL TRANSFER GAS USED:', + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).gasUsed + ); }); }); From 916ed76de53ba2fb8068d351d7886b096cc7de90 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Sun, 28 Jan 2024 14:16:18 +0200 Subject: [PATCH 023/121] Removed fift optimizations that might create problems for tooling --- contracts/imports/root_cell_repacker.fc | 53 ------------------------- contracts/wallet_v5.fc | 35 +++++----------- 2 files changed, 11 insertions(+), 77 deletions(-) delete mode 100644 contracts/imports/root_cell_repacker.fc diff --git a/contracts/imports/root_cell_repacker.fc b/contracts/imports/root_cell_repacker.fc deleted file mode 100644 index d7cd869f..00000000 --- a/contracts/imports/root_cell_repacker.fc +++ /dev/null @@ -1,53 +0,0 @@ -() __install_root_cell_repacker_hook__() impure asm """ -// Require the Fift Assembler version that this code is known to work with -// (this version of FunC is required in the contract code anyway) -"0.4.4" require-asm-fif-version -// Install a hook that will execute in context of PROGRAM in the Fift assembler -// The hook will redefine }END>c function in order to modify the resulting root cell -@atend @ 1 { execute - // Replace current words dictionary with (Asm) context one to redefine }END>c word - current@ context@ current! - { - // Get the original root cell as the result of original program assembling - // by calling }END> that will return builder, and completing builder to cell - }END> b> - // Now the top entry on stack contains tha original root cell that constists of - // some boilerplate code and one and only reference to methods dictionary - - DUP INC // The copy of method_id+1 will be 0 if method_id = -1 - IFNOTJMP:<{ - // This branch is executed only if method_id = -1 (external messages) - DROP // Remove the second copy of method_id created for further decisions - over -1 swap @procdictkeylen idict@ // Extract recv_external from methods dict - { "recv_external is not defined" abort } ifnot // Fail if method is not found - @addop // Otherwise, if everything is OK, embed recv_external into root code cell - }> - // If the execution is at this moment, either this is internal method call (that are strictly - // discouraged if using root cell repacker) or a getter method (which's gas is not too relevant) - swap // Select methods dictionary for further operations (remove embedded recv_... methods) - 0 swap @procdictkeylen idict- drop // remove recv_internal from methods dictionary - -1 swap @procdictkeylen idict- drop // remove recv_external from methods dictionary - @procdictkeylen DICTPUSHCONST DICTIGETJMPZ 11 THROWARG // add methods dictionary and call the (get-)method - }> - b> - } - // The words will be redefined in "current" dictionary that is actually Asm context - : }END>c - // Restore the current words dictionary to context and restore original current dict - current@ context! current! -// Actually attach the hook installer to the end of current procedure definition -} does @atend ! -"""; \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 836956af..3c05fda7 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -1,7 +1,6 @@ #pragma version =0.4.4; #include "imports/stdlib.fc"; -#include "imports/root_cell_repacker.fc"; const int size::stored_seqno = 33; const int size::stored_subwallet = 80; @@ -27,7 +26,7 @@ const int size::flags = 4; (slice) check_and_remove_sint_prefix_or_ret(slice body) impure asm "x{73696E74} SDBEGINSQ" "IFNOTRET"; -(slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; +;; (slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; (slice, int) check_and_remove_set_signature_auth_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; @@ -44,21 +43,7 @@ int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) ;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. () set_actions(cell action_list) impure asm "c5 POP"; -{- - ifnot (cs.preload_uint(1)) { - set_actions(cs.preload_ref().verify_actions()); - return (); - } --} -() set_actions_if_simple(slice cs) impure asm """ - x{4_} SDBEGINSQ - IFJMP:<{ - PLDREF - verify_actions INLINECALLDICT - c5 POP - }> - DROP -"""; +int count_leading_zeroes(slice cs) asm "SDCNTLEAD0"; cell verify_actions(cell c5) inline { ;; Comment out code starting from here to disable checks (unsafe version) @@ -159,7 +144,9 @@ cell verify_actions(cell c5) inline { } () dispatch_extension_request(slice cs, var dummy1) impure inline { - set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref().verify_actions()); return (); } + if (count_leading_zeroes(cs)) { ;; starts with bit 0 + return set_actions(cs.preload_ref().verify_actions()); + } ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> ;; dummy1~impure_touch(); ;; DROP merged to 2DROP! @@ -200,7 +187,9 @@ cell verify_actions(cell c5) inline { commit(); - set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref().verify_actions()); return (); } + if (count_leading_zeroes(cs)) { ;; starts with bit 0 + return set_actions(cs.preload_ref().verify_actions()); + } ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> ;; inline_ref required because otherwise it will produce undesirable JMPREF @@ -242,7 +231,9 @@ cell verify_actions(cell c5) inline { commit(); - set_actions_if_simple(cs); ;; <- ifnot (cs.preload_uint(1)) { set_actions(cs.preload_ref().verify_actions()); return (); } + if (count_leading_zeroes(cs)) { ;; starts with bit 0 + return set_actions(cs.preload_ref().verify_actions()); + } ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> ;; inline_ref required because otherwise it will produce undesirable JMPREF @@ -258,10 +249,6 @@ cell verify_actions(cell c5) inline { () recv_internal(cell full_msg, slice body) impure inline { - ;; this function from root_cell_repacker can be called in any method - ;; no opcodes are added by this function to recv_internal directly - __install_root_cell_repacker_hook__(); - ;; return right away if there are no references ;; correct messages always have a ref, because any code paths ends with preload_ref return_if(body.slice_refs_empty?()); From 43eba0d195480e14703abfa42d25d4e0b84cb068 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Sun, 28 Jan 2024 14:33:02 +0200 Subject: [PATCH 024/121] Flattened and split some asm functions for understanding and toolings --- contracts/wallet_v5.fc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 3c05fda7..d4c628cd 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -15,16 +15,11 @@ const int size::flags = 4; () return_if(int cond) impure asm "IFRET"; () return_unless(int cond) impure asm "IFNOTRET"; -;; SDCNTTRAIL1 will count trailing ones in slice. If bounced flag (last bit) is set it will be non-zero, else zero. -() slicy_return_if_bounce(slice s_flags) impure asm "SDCNTTRAIL1" "IFRET"; -() slicy_throw_if_ends_with_0(slice s) impure asm "SDCNTTRAIL0" "37 THROWIF"; - -(slice) udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET IFNOTRET"; +(slice) udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET" "IFNOTRET"; (slice) enforce_and_remove_sign_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; (slice, int) check_and_remove_extn_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; - -(slice) check_and_remove_sint_prefix_or_ret(slice body) impure asm "x{73696E74} SDBEGINSQ" "IFNOTRET"; +(slice, int) check_and_remove_sint_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; ;; (slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; @@ -38,12 +33,14 @@ const int size::flags = 4; ;; Compresses 8+256-bit address into 256-bit uint by cutting off one bit from sha256. ;; This allows us to save on wrapping the address in a cell and make plugin requests cheaper. ;; This method also unpacks address hash if you pass packed hash with the original wc. -int pack_address((int, int) address) impure asm "SWAP INC XOR"; ;; hash ^ (wc+1) +int pack_address((int, int) address) impure asm "SWAP" "INC" "XOR"; ;; hash ^ (wc+1) ;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. () set_actions(cell action_list) impure asm "c5 POP"; int count_leading_zeroes(slice cs) asm "SDCNTLEAD0"; +int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; +int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; cell verify_actions(cell c5) inline { ;; Comment out code starting from here to disable checks (unsafe version) @@ -55,7 +52,7 @@ cell verify_actions(cell c5) inline { c5s = c5s.enforce_and_remove_action_send_msg_prefix(); ;; enforce that send_mode has 2 bit set ;; for that load 7 bits and make sure that they end with 1 - slicy_throw_if_ends_with_0(c5s.preload_bits(7)); + throw_if(37, count_trailing_zeroes(c5s.preload_bits(7))); c5s = c5s.preload_ref().begin_parse(); } until (c5s.slice_empty?()); ;; -} @@ -257,7 +254,9 @@ cell verify_actions(cell c5) inline { var full_msg_slice = full_msg.begin_parse(); var s_flags = full_msg_slice~load_bits(size::flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... - slicy_return_if_bounce(s_flags); + + ;; If bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. + return_if(count_trailing_ones(s_flags)); ;; slicy_return_if_bounce(begin_cell().store_uint(3, 4).end_cell().begin_parse()); ;; TEST!!! @@ -287,7 +286,8 @@ cell verify_actions(cell c5) inline { } - body = check_and_remove_sint_prefix_or_ret(body); ;; 0x73696E74 ("sint") - sign internal + (body, int is_sint) = check_and_remove_sint_prefix(body); ;; 0x73696E74 ("sint") - sign internal + return_unless(is_sint); ;; Process the rest of the slice just like the signed request. process_signed_request_from_internal_message(body); From 167347ae6dcb75181ac1153c6eeee6b28ff59559 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Tue, 30 Jan 2024 16:35:33 +0200 Subject: [PATCH 025/121] Add external message bit to signed message part --- Specification.md | 7 ++--- contracts/wallet_v5.fc | 43 +++++++++++++++++------------- tests/wallet-v5-extensions.spec.ts | 1 + tests/wallet-v5-external.spec.ts | 1 + tests/wallet-v5-internal.spec.ts | 2 ++ types.tlb | 7 ++--- 6 files changed, 37 insertions(+), 24 deletions(-) diff --git a/Specification.md b/Specification.md index 5225291d..4e86569f 100644 --- a/Specification.md +++ b/Specification.md @@ -165,9 +165,10 @@ Authentication modes: ```tl-b signed_request$_ signature: bits512 // 512 - wallet_id: (## 80) // 512+80 - valid_until: (## 32) // 512+80+32 - msg_seqno: (## 32) // 512+80+32+32 = 656 + is_external: (## 1) // 512+1 + wallet_id: (## 80) // 512+1+80 + valid_until: (## 32) // 512+1+80+32 + msg_seqno: (## 32) // 512+1+80+32+32 = 657 inner: InnerRequest = SignedRequest; internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index d4c628cd..44a1e749 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -140,22 +140,15 @@ cell verify_actions(cell c5) inline { return (); } -() dispatch_extension_request(slice cs, var dummy1) impure inline { - if (count_leading_zeroes(cs)) { ;; starts with bit 0 - return set_actions(cs.preload_ref().verify_actions()); - } - ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> - ;; - dummy1~impure_touch(); ;; DROP merged to 2DROP! - dispatch_complex_request(cs); -} +;; ------------------------------------------------------------------------------------------------ ;; Verifies signed request, prevents replays and proceeds with `dispatch_request`. -() process_signed_request(slice body) impure inline { +() process_signed_request_from_external_message(slice body) impure inline { ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. var signature = body~load_bits(512); var cs = body; + throw_unless(38, cs~load_uint(1)); ;; signed external messages must begin with 1 prefix var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); @@ -193,15 +186,35 @@ cell verify_actions(cell c5) inline { dispatch_complex_request(cs); } +() recv_external(slice body) impure inline { + ;; 0x7369676E ("sign") external message authenticated by signature + body = enforce_and_remove_sign_prefix(body); + process_signed_request_from_external_message(body); + return(); +} + +;; ------------------------------------------------------------------------------------------------ + +() dispatch_extension_request(slice cs, var dummy1) impure inline { + if (count_leading_zeroes(cs)) { ;; starts with bit 0 + return set_actions(cs.preload_ref().verify_actions()); + } + ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> + ;; + dummy1~impure_touch(); ;; DROP merged to 2DROP! + dispatch_complex_request(cs); +} + ;; Same logic as above function but with return_* instead of throw_* and additional checks to prevent bounces () process_signed_request_from_internal_message(slice body) impure inline { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(body.slice_bits() < 512 + size::subwallet_id + size::valid_until + size::msg_seqno + 1); + return_if(body.slice_bits() < 512 + 1 + size::subwallet_id + size::valid_until + size::msg_seqno + 1); ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. var signature = body~load_bits(512); var cs = body; + return_if(cs~load_uint(1)); ;; signed internal messages must begin with 0 prefix var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); @@ -237,13 +250,6 @@ cell verify_actions(cell c5) inline { dispatch_complex_request(cs); } -() recv_external(slice body) impure inline { - ;; 0x7369676E ("sign") external message authenticated by signature - body = enforce_and_remove_sign_prefix(body); - process_signed_request(body); - return(); -} - () recv_internal(cell full_msg, slice body) impure inline { ;; return right away if there are no references @@ -295,6 +301,7 @@ cell verify_actions(cell c5) inline { } +;; ------------------------------------------------------------------------------------------------ ;; Get methods int seqno() method_id { diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 2b46b4d6..4c178ad8 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -44,6 +44,7 @@ describe('Wallet V5 extensions auth', () => { function createBody(actionsList: Cell) { const payload = beginCell() + .storeUint(0, 1) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 42cfd54a..3422ea35 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -74,6 +74,7 @@ describe('Wallet V5 sign auth external', () => { function createBody(actionsList: Cell) { const payload = beginCell() + .storeUint(1, 1) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 6fdab19e..59215dc0 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -68,6 +68,7 @@ describe('Wallet V5 sign auth internal', () => { function createBody(actionsList: Cell) { const payload = beginCell() + .storeUint(0, 1) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -658,6 +659,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(0, 1) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno diff --git a/types.tlb b/types.tlb index 9c654f5e..d22c8860 100644 --- a/types.tlb +++ b/types.tlb @@ -13,9 +13,10 @@ action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; signed_request$_ signature: bits512 // 512 - wallet_id: (## 80) // 512+80 - valid_until: (## 32) // 512+80+32 - msg_seqno: (## 32) // 512+80+32+32 = 656 + is_external: (## 1) // 512+1 + wallet_id: (## 80) // 512+1+80 + valid_until: (## 32) // 512+1+80+32 + msg_seqno: (## 32) // 512+1+80+32+32 = 657 inner: InnerRequest = SignedRequest; internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; From e580224665809b69d0fc52406fb27396984c027d Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Sun, 4 Feb 2024 00:31:22 +0200 Subject: [PATCH 026/121] Changed signed message prefix from 1 bit to 32-bit message tag --- Specification.md | 8 ++++---- contracts/wallet_v5.fc | 9 ++++++--- tests/wallet-v5-extensions.spec.ts | 2 +- tests/wallet-v5-external.spec.ts | 2 +- tests/wallet-v5-internal.spec.ts | 4 ++-- types.tlb | 8 ++++---- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Specification.md b/Specification.md index 4e86569f..bd2e53c1 100644 --- a/Specification.md +++ b/Specification.md @@ -165,10 +165,10 @@ Authentication modes: ```tl-b signed_request$_ signature: bits512 // 512 - is_external: (## 1) // 512+1 - wallet_id: (## 80) // 512+1+80 - valid_until: (## 32) // 512+1+80+32 - msg_seqno: (## 32) // 512+1+80+32+32 = 657 + request_type: (## 32) // 512+32 + wallet_id: (## 80) // 512+32+80 + valid_until: (## 32) // 512+32+80+32 + msg_seqno: (## 32) // 512+32+80+32+32 = 688 inner: InnerRequest = SignedRequest; internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 44a1e749..eb229213 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -28,6 +28,9 @@ const int size::flags = 4; (slice) enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; +(slice, int) check_and_remove_internal_signed_prefix(slice body) impure asm "x{73696e74} SDBEGINSQ"; +(slice, int) check_and_remove_external_signed_prefix(slice body) impure asm "x{7369676e} SDBEGINSQ"; + ;; Extensible wallet contract v5 ;; Compresses 8+256-bit address into 256-bit uint by cutting off one bit from sha256. @@ -148,7 +151,7 @@ cell verify_actions(cell c5) inline { var signature = body~load_bits(512); var cs = body; - throw_unless(38, cs~load_uint(1)); ;; signed external messages must begin with 1 prefix + throw_unless(38, cs~check_and_remove_external_signed_prefix()); var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); @@ -208,13 +211,13 @@ cell verify_actions(cell c5) inline { ;; Same logic as above function but with return_* instead of throw_* and additional checks to prevent bounces () process_signed_request_from_internal_message(slice body) impure inline { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(body.slice_bits() < 512 + 1 + size::subwallet_id + size::valid_until + size::msg_seqno + 1); + return_if(body.slice_bits() < 512 + 32 + size::subwallet_id + size::valid_until + size::msg_seqno + 1); ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. var signature = body~load_bits(512); var cs = body; - return_if(cs~load_uint(1)); ;; signed internal messages must begin with 0 prefix + return_unless(cs~check_and_remove_internal_signed_prefix()); var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 4c178ad8..246628a7 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -44,7 +44,7 @@ describe('Wallet V5 extensions auth', () => { function createBody(actionsList: Cell) { const payload = beginCell() - .storeUint(0, 1) + .storeUint(0x73696e74, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 3422ea35..4145e0ed 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -74,7 +74,7 @@ describe('Wallet V5 sign auth external', () => { function createBody(actionsList: Cell) { const payload = beginCell() - .storeUint(1, 1) + .storeUint(0x7369676e, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 59215dc0..6c7297bc 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -68,7 +68,7 @@ describe('Wallet V5 sign auth internal', () => { function createBody(actionsList: Cell) { const payload = beginCell() - .storeUint(0, 1) + .storeUint(0x73696e74, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -659,7 +659,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() - .storeUint(0, 1) + .storeUint(0x73696e74, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno diff --git a/types.tlb b/types.tlb index d22c8860..6f5cb23e 100644 --- a/types.tlb +++ b/types.tlb @@ -13,10 +13,10 @@ action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; signed_request$_ signature: bits512 // 512 - is_external: (## 1) // 512+1 - wallet_id: (## 80) // 512+1+80 - valid_until: (## 32) // 512+1+80+32 - msg_seqno: (## 32) // 512+1+80+32+32 = 657 + request_type: (## 32) // 512+32 + wallet_id: (## 80) // 512+32+80 + valid_until: (## 32) // 512+32+80+32 + msg_seqno: (## 32) // 512+32+80+32+32 = 688 inner: InnerRequest = SignedRequest; internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; From 41f30632fde9b942a62a60e911c9b865367fa943 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 9 Feb 2024 03:23:06 +0200 Subject: [PATCH 027/121] Moved signature to the end of the message (includes opcode) --- contracts/wallet_v5.fc | 39 +++++++++++++++++------------- tests/wallet-v5-extensions.spec.ts | 6 ++--- tests/wallet-v5-external.spec.ts | 9 ++++--- tests/wallet-v5-internal.spec.ts | 14 ++++++----- wrappers/wallet-v5.ts | 7 ++++-- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index eb229213..614e1c8e 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -28,9 +28,6 @@ const int size::flags = 4; (slice) enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; -(slice, int) check_and_remove_internal_signed_prefix(slice body) impure asm "x{73696e74} SDBEGINSQ"; -(slice, int) check_and_remove_external_signed_prefix(slice body) impure asm "x{7369676e} SDBEGINSQ"; - ;; Extensible wallet contract v5 ;; Compresses 8+256-bit address into 256-bit uint by cutting off one bit from sha256. @@ -45,6 +42,12 @@ int count_leading_zeroes(slice cs) asm "SDCNTLEAD0"; int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; +;; (slice, slice) split(slice s, int bits, int refs) asm "SPLIT"; +;; (slice, slice, int) split?(slice s, int bits, int refs) asm "SPLIT" "NULLSWAPIFNOT"; + +slice get_last_bits(slice s, int n) asm "SDCUTLAST"; +slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; + cell verify_actions(cell c5) inline { ;; Comment out code starting from here to disable checks (unsafe version) ;; {- @@ -146,12 +149,12 @@ cell verify_actions(cell c5) inline { ;; ------------------------------------------------------------------------------------------------ ;; Verifies signed request, prevents replays and proceeds with `dispatch_request`. -() process_signed_request_from_external_message(slice body) impure inline { +() process_signed_request_from_external_message(slice full_body) impure inline { ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. - var signature = body~load_bits(512); + slice signature = full_body.get_last_bits(512); + slice signed = full_body.remove_last_bits(512); - var cs = body; - throw_unless(38, cs~check_and_remove_external_signed_prefix()); + var cs = signed.skip_bits(32); var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); @@ -161,7 +164,7 @@ cell verify_actions(cell c5) inline { var public_key = ds.preload_uint(size::public_key); ;; Only such checking order results in least amount of gas - throw_unless(35, check_signature(slice_hash(body), signature, public_key)); + throw_unless(35, check_signature(slice_hash(signed), signature, public_key)); ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed @@ -190,9 +193,10 @@ cell verify_actions(cell c5) inline { } () recv_external(slice body) impure inline { + slice full_body = body; ;; 0x7369676E ("sign") external message authenticated by signature body = enforce_and_remove_sign_prefix(body); - process_signed_request_from_external_message(body); + process_signed_request_from_external_message(full_body); return(); } @@ -209,15 +213,15 @@ cell verify_actions(cell c5) inline { } ;; Same logic as above function but with return_* instead of throw_* and additional checks to prevent bounces -() process_signed_request_from_internal_message(slice body) impure inline { +() process_signed_request_from_internal_message(slice full_body) impure inline { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(body.slice_bits() < 512 + 32 + size::subwallet_id + size::valid_until + size::msg_seqno + 1); + return_if(full_body.slice_bits() < 32 + size::subwallet_id + size::valid_until + size::msg_seqno + 1 + 512); ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. - var signature = body~load_bits(512); + slice signature = full_body.get_last_bits(512); + slice signed = full_body.remove_last_bits(512); - var cs = body; - return_unless(cs~check_and_remove_internal_signed_prefix()); + var cs = signed.skip_bits(32); var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); @@ -227,7 +231,7 @@ cell verify_actions(cell c5) inline { var public_key = ds.preload_uint(size::public_key); ;; Only such checking order results in least amount of gas - return_unless(check_signature(slice_hash(body), signature, public_key)); + return_unless(check_signature(slice_hash(signed), signature, public_key)); ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed @@ -295,11 +299,12 @@ cell verify_actions(cell c5) inline { } - (body, int is_sint) = check_and_remove_sint_prefix(body); ;; 0x73696E74 ("sint") - sign internal + slice full_body = body; + (_, int is_sint) = check_and_remove_sint_prefix(body); ;; 0x73696E74 ("sint") - sign internal return_unless(is_sint); ;; Process the rest of the slice just like the signed request. - process_signed_request_from_internal_message(body); + process_signed_request_from_internal_message(full_body); return (); ;; Explicit returns escape function faster and const less gas (suddenly!) } diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 246628a7..910bdba9 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -1,6 +1,6 @@ import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton-community/sandbox'; import { Address, beginCell, Cell, Dictionary, Sender, SendMode, toNano } from 'ton-core'; -import { WalletId, WalletV5 } from '../wrappers/wallet-v5'; +import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; import '@ton-community/test-utils'; import { compile } from '@ton-community/blueprint'; import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from 'ton-crypto'; @@ -44,7 +44,7 @@ describe('Wallet V5 extensions auth', () => { function createBody(actionsList: Cell) { const payload = beginCell() - .storeUint(0x73696e74, 32) + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -54,8 +54,8 @@ describe('Wallet V5 extensions auth', () => { const signature = sign(payload.hash(), keypair.secretKey); seqno++; return beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); } diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 4145e0ed..cab6c7ec 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -74,7 +74,7 @@ describe('Wallet V5 sign auth external', () => { function createBody(actionsList: Cell) { const payload = beginCell() - .storeUint(0x7369676e, 32) + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -84,8 +84,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); seqno++; return beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); } @@ -770,7 +770,8 @@ describe('Wallet V5 sign auth external', () => { const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); - const payload = beginCell() + const payload = beginCell() // TODO: Seems to pass with auth_signed, need analysis! + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -779,8 +780,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 6c7297bc..03dc6185 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -68,7 +68,7 @@ describe('Wallet V5 sign auth internal', () => { function createBody(actionsList: Cell) { const payload = beginCell() - .storeUint(0x73696e74, 32) + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -78,8 +78,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); seqno++; return beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); } @@ -659,7 +659,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() - .storeUint(0x73696e74, 32) + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -670,8 +670,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), fakeKeypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -713,6 +713,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno + 1, 32) // seqno @@ -844,7 +845,8 @@ describe('Wallet V5 sign auth internal', () => { const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); - const payload = beginCell() + const payload = beginCell() // TODO: Seems to pass with auth_signed_internal, need analysis! + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -853,8 +855,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternal(sender, { diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 72366a6b..4ab93317 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -138,7 +138,7 @@ export class WalletV5 implements Contract { value: opts.value, sendMode: SendMode.PAY_GAS_SEPARATELY, body: beginCell() - .storeUint(Opcodes.auth_signed_internal, 32) + // .storeUint(Opcodes.auth_signed_internal, 32) // Is signed inside message .storeSlice(opts.body.beginParse()) .endCell() }); @@ -172,7 +172,10 @@ export class WalletV5 implements Contract { async sendExternalSignedMessage(provider: ContractProvider, body: Cell) { await provider.external( - beginCell().storeUint(Opcodes.auth_signed, 32).storeSlice(body.beginParse()).endCell() + beginCell() + // .storeUint(Opcodes.auth_signed, 32) // Is signed inside message + .storeSlice(body.beginParse()) + .endCell() ); } From 033b63e002a6fa43c6457f6ac649fa8e9767c50b Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 9 Feb 2024 13:18:57 +0200 Subject: [PATCH 028/121] Adjusted TLB definitions to match signature at the end --- Specification.md | 16 ++++++++-------- types.tlb | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Specification.md b/Specification.md index bd2e53c1..0e1906cf 100644 --- a/Specification.md +++ b/Specification.md @@ -163,13 +163,13 @@ action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; Authentication modes: ```tl-b -signed_request$_ - signature: bits512 // 512 - request_type: (## 32) // 512+32 - wallet_id: (## 80) // 512+32+80 - valid_until: (## 32) // 512+32+80+32 - msg_seqno: (## 32) // 512+32+80+32+32 = 688 - inner: InnerRequest = SignedRequest; +signed_request$_ // 32 (opcode from outer) + wallet_id: WalletID // 80 + valid_until: # // 32 + msg_seqno: # // 32 + inner: InnerRequest // 1 .. (1 + 32 + 256) + ^Cell + signature: bits512 // 512 += SignedRequest; // Total: 688 .. 976 + ^Cell internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; internal_extension#6578746e inner:InnerRequest = InternalMsgBody; @@ -180,7 +180,7 @@ actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; Contract state: ```tl-b -wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; +wallet_id$_ global_id:# wc:int8 version:(## 8) subwallet_number:# = WalletID; contract_state$_ seqno:int33 wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; ``` diff --git a/types.tlb b/types.tlb index 6f5cb23e..0ac3940a 100644 --- a/types.tlb +++ b/types.tlb @@ -11,13 +11,13 @@ action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; -signed_request$_ - signature: bits512 // 512 - request_type: (## 32) // 512+32 - wallet_id: (## 80) // 512+32+80 - valid_until: (## 32) // 512+32+80+32 - msg_seqno: (## 32) // 512+32+80+32+32 = 688 - inner: InnerRequest = SignedRequest; +signed_request$_ // 32 (opcode from outer) + wallet_id: WalletID // 80 + valid_until: # // 32 + msg_seqno: # // 32 + inner: InnerRequest // 1 .. (1 + 32 + 256) + ^Cell + signature: bits512 // 512 += SignedRequest; // Total: 688 .. 976 + ^Cell internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; internal_extension#6578746e inner:InnerRequest = InternalMsgBody; From 34a85561da6c1a0bed76d74ecd486e4da3fff24b Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 9 Feb 2024 13:22:37 +0200 Subject: [PATCH 029/121] Adjusted all tests to match new signature place (in end) --- tests/wallet-v5-external.spec.ts | 15 ++++++++++----- tests/wallet-v5-internal.spec.ts | 13 ++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index cab6c7ec..518e8e2a 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -619,6 +619,7 @@ describe('Wallet V5 sign auth external', () => { .endCell(); const fakePayload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(vu, 32) .storeUint(seqno + 1, 32) // seqno @@ -627,8 +628,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(fakePayload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -649,6 +650,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno @@ -659,8 +661,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), fakeKeypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -681,6 +683,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno + 1, 32) // seqno @@ -689,8 +692,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -711,6 +714,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(Math.round(Date.now() / 1000) - 600, 32) .storeUint(seqno, 32) @@ -719,8 +723,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => @@ -741,6 +745,7 @@ describe('Wallet V5 sign auth external', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed, 32) .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -749,8 +754,8 @@ describe('Wallet V5 sign auth external', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); await disableConsoleError(() => diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 03dc6185..cd94f66b 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -608,6 +608,7 @@ describe('Wallet V5 sign auth internal', () => { const vu = validUntil(); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(vu, 32) .storeUint(seqno, 32) // seqno @@ -623,8 +624,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(fakePayload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -713,7 +714,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() - + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno + 1, 32) // seqno @@ -722,8 +723,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -758,6 +759,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(Math.round(Date.now() / 1000) - 600, 32) .storeUint(seqno, 32) @@ -766,8 +768,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { @@ -802,6 +804,7 @@ describe('Wallet V5 sign auth internal', () => { const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); const payload = beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 80) .storeUint(validUntil(), 32) .storeUint(seqno, 32) @@ -810,8 +813,8 @@ describe('Wallet V5 sign auth internal', () => { const signature = sign(payload.hash(), keypair.secretKey); const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) .storeSlice(payload.beginParse()) + .storeUint(bufferToBigInt(signature), 512) .endCell(); const receipt = await walletV5.sendInternalSignedMessage(sender, { From c724855d566b2be097c18fe311f2b7228562f825 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Mon, 12 Feb 2024 11:52:31 +0200 Subject: [PATCH 030/121] Fixed accounting for seqno twice on signature disabling --- contracts/wallet_v5.fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 614e1c8e..3f31b853 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -116,7 +116,7 @@ cell verify_actions(cell c5) inline { } else { ;; disallow if (stored_seqno >= 0) { - ds = ds.skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key); + ds = ds.skip_bits(size::stored_subwallet + size::public_key); var extensions_is_not_null = ds.preload_uint(1); throw_unless(42, extensions_is_not_null); ;; Corner case: 0 -> 1 -> -1 From 4bf0feb9cb26095df2a1ef81c878ece68cfaefb7 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Thu, 15 Feb 2024 02:35:25 +0200 Subject: [PATCH 031/121] Added throw for unknown ext-act, setcode/data neg tests, test-only acts --- contracts/wallet_v5.fc | 3 + tests/actions.ts | 9 +- tests/test-only-actions.ts | 82 +++++++++++++ tests/wallet-v5-external.spec.ts | 193 +++++------------------------- tests/wallet-v5-internal.spec.ts | 198 +++++-------------------------- 5 files changed, 147 insertions(+), 338 deletions(-) create mode 100644 tests/test-only-actions.ts diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 3f31b853..b0944ca5 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -136,6 +136,9 @@ cell verify_actions(cell c5) inline { set_data(cs~load_ref()); } -} + else { + throw(41); ;; unsupported action + } ;; Other actions are no-op ;; FIXME: is it costlier to check for unsupported actions and throw? cs = cs.preload_ref().begin_parse(); diff --git a/tests/actions.ts b/tests/actions.ts index 504898ed..6212e603 100644 --- a/tests/actions.ts +++ b/tests/actions.ts @@ -1,4 +1,5 @@ import { Address, beginCell, Cell, MessageRelaxed, SendMode, storeMessageRelaxed } from 'ton-core'; +import { isTestOnlyExtendedAction, TestOnlyExtendedAction, TestOnlyOutAction } from './test-only-actions'; export class ActionSendMsg { public static readonly tag = 0x0ec3c86d; @@ -55,17 +56,19 @@ export class ActionSetSignatureAuthAllowed { } } -export type OutAction = ActionSendMsg; +export type OutAction = ActionSendMsg | TestOnlyOutAction; export type ExtendedAction = | ActionAddExtension | ActionRemoveExtension - | ActionSetSignatureAuthAllowed; + | ActionSetSignatureAuthAllowed + | TestOnlyExtendedAction; export function isExtendedAction(action: OutAction | ExtendedAction): action is ExtendedAction { return ( action.tag === ActionAddExtension.tag || action.tag === ActionRemoveExtension.tag || - action.tag === ActionSetSignatureAuthAllowed.tag + action.tag === ActionSetSignatureAuthAllowed.tag || + isTestOnlyExtendedAction(action) ); } diff --git a/tests/test-only-actions.ts b/tests/test-only-actions.ts new file mode 100644 index 00000000..75173517 --- /dev/null +++ b/tests/test-only-actions.ts @@ -0,0 +1,82 @@ +import { + Address, + beginCell, + Cell, + CurrencyCollection, + MessageRelaxed, + SendMode, + storeCurrencyCollection, + storeMessageRelaxed +} from 'ton-core'; +import { + ExtendedAction, + OutAction +} from './actions'; + +export type LibRef = Cell | bigint; + +export class ActionSetCode { + public static readonly tag = 0xad4de08e; + + public readonly tag = ActionSetCode.tag; + + constructor(public readonly newCode: Cell) {} + + public serialize(): Cell { + return beginCell().storeUint(this.tag, 32).storeRef(this.newCode).endCell(); + } +} + +export class ActionReserveCurrency { + public static readonly tag = 0x36e6b809; + + public readonly tag = ActionReserveCurrency.tag; + + constructor(public readonly mode: SendMode, public readonly currency: CurrencyCollection) {} + + public serialize(): Cell { + return beginCell() + .storeUint(this.tag, 32) + .storeUint(this.mode, 8) + .store(storeCurrencyCollection(this.currency)) + .endCell(); + } +} + +export class ActionChangeLibrary { + public static readonly tag = 0x26fa1dd4; + + public readonly tag = ActionChangeLibrary.tag; + + constructor(public readonly mode: number, public readonly libRef: LibRef) {} + + public serialize(): Cell { + const cell = beginCell().storeUint(this.tag, 32).storeUint(this.mode, 7); + if (typeof this.libRef === 'bigint') { + return cell.storeUint(0, 1).storeUint(this.libRef, 256).endCell(); + } + + return cell.storeUint(1, 1).storeRef(this.libRef).endCell(); + } +} + +export class ActionSetData { + public static readonly tag = 0x1ff8ea0b; + + public readonly tag = ActionSetData.tag; + + constructor(public readonly data: Cell) {} + + public serialize(): Cell { + return beginCell().storeUint(this.tag, 32).storeRef(this.data).endCell(); + } +} + +export type TestOnlyOutAction = ActionSetCode | ActionReserveCurrency | ActionChangeLibrary; +export type TestOnlyExtendedAction = ActionSetData; + +export function isTestOnlyExtendedAction(action: OutAction | ExtendedAction): action is ExtendedAction { + return ( + action.tag === ActionSetData.tag + ); +} \ No newline at end of file diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 518e8e2a..ffcb718d 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -21,6 +21,8 @@ import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDes import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; import { default as config } from './config'; +import { ActionSetCode, ActionSetData } from './test-only-actions'; +import { WalletV4 } from '../wrappers/wallet-v4'; const WALLET_ID = new WalletId({ networkGlobalId: -239, workChain: -1, subwalletNumber: 0 }); @@ -339,108 +341,6 @@ describe('Wallet V5 sign auth external', () => { ); }); - /* TODO: Rewrite as a negative test - it('Set data and do two transfers', async () => { - const testReceiver1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue1 = toNano(0.001); - - const testReceiver2 = Address.parse('EQCgYDKqfTh7zVj9BQwOIPs4SuOhM7wnIjb6bdtM2AJf_Z9G'); - const forwardValue2 = toNano(0.0012); - - const receiver1BalanceBefore = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceBefore = (await blockchain.getContract(testReceiver2)).balance; - - const msg1 = createMsgInternal({ dest: testReceiver1, value: forwardValue1 }); - const msg2 = createMsgInternal({ dest: testReceiver2, value: forwardValue2 }); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeInt(239, 33).endCell()), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg1), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg2) - ]); - - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - - expect(receipt.transactions.length).toEqual(3); - accountForGas(receipt.transactions); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver1, - value: forwardValue1 - }); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver2, - value: forwardValue2 - }); - - const fee1 = receipt.transactions[1].totalFees.coins; - const fee2 = receipt.transactions[2].totalFees.coins; - - const receiver1BalanceAfter = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceAfter = (await blockchain.getContract(testReceiver2)).balance; - expect(receiver1BalanceAfter).toEqual(receiver1BalanceBefore + forwardValue1 - fee1); - expect(receiver2BalanceAfter).toEqual(receiver2BalanceBefore + forwardValue2 - fee2); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - */ - - /* TODO: Rewrite as a negative test - it('Send 255 transfers and do set data', async () => { - await ( - await blockchain.treasury('mass-messages') - ).send({ to: walletV5.address, value: toNano(100) }); - - const range = [...new Array(255)].map((_, index) => index); - - const receivers = range.map(i => Address.parseRaw('0:' + i.toString().padStart(64, '0'))); - const balancesBefore = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const forwardValues = range.map(i => BigInt(toNano(0.000001 * i))); - - const msges = receivers.map((dest, i) => - createMsgInternal({ dest: dest, value: forwardValues[i] }) - ); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeInt(239, 33).endCell()), - ...msges.map(msg => new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)) - ]); - - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - - expect(receipt.transactions.length).toEqual(range.length + 1); - accountForGas(receipt.transactions); - - receivers.forEach((to, i) => { - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to, - value: forwardValues[i] - }); - }); - - const balancesAfter = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const fees = receipt.transactions.slice(1).map(tx => tx.totalFees.coins); - - balancesAfter.forEach((balanceAfter, i) => { - expect(balanceAfter).toEqual(balancesBefore[i] + forwardValues[i] - fees[i]); - }); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - */ - it('Remove extension', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); @@ -471,76 +371,37 @@ describe('Wallet V5 sign auth external', () => { accountForGas(receipt2.transactions); }); - /* TODO: Rewrite as a negative test - it('Change code and data to wallet v4', async () => { - const code_v4 = await compile('wallet_v4'); - const data_v4 = beginCell() - .storeUint(0, 32) - .storeUint(0, 32) - .storeBuffer(keypair.publicKey, 32) - .storeDict(Dictionary.empty()) - .endCell(); + it('Should fail SetData action', async () => { + const cell = beginCell().endCell(); const actionsList = packActionsList([ - new ActionSetData(data_v4), - new ActionSetCode(code_v4) + new ActionSetData(cell) ]); - const receipt1 = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - accountForGas(receipt1.transactions); - - const walletV4 = blockchain.openContract(WalletV4.createFromAddress(walletV5.address)); - const seqno = await walletV4.getSeqno(); - const subwalletId = await walletV4.getSubWalletID(); - const publicKey = await walletV4.getPublicKey(); - const extensions = Dictionary.loadDirect( - Dictionary.Keys.Address(), - Dictionary.Values.BigInt(0), - await walletV4.getExtensions() - ); - - expect(seqno).toEqual(0); - expect(subwalletId).toEqual(0); - expect(publicKey).toEqual(bufferToBigInt(keypair.publicKey)); - expect(extensions.size).toEqual(0); - - const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue = toNano(0.001); - - const sendTxMsg = beginCell() - .storeUint(0x10, 6) - .storeAddress(testReceiver) - .storeCoins(forwardValue) - .storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .storeRef(beginCell().endCell()) - .endCell(); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - const mesagesCell = beginCell() - .storeUint(0, 8) - .storeUint(SendMode.PAY_GAS_SEPARATELY, 8) - .storeRef(sendTxMsg) - .endCell(); + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(41); + }); - const payload = beginCell() - .storeUint(0, 32) - .storeUint(validUntil(), 32) - .storeUint(0, 32) - .storeSlice(mesagesCell.beginParse()) - .endCell(); + it('Should fail SetCode action', async () => { + const cell = beginCell().endCell(); - const signature = sign(payload.hash(), keypair.secretKey); - const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) - .storeSlice(payload.beginParse()) - .endCell(); + const actionsList = packActionsList([ + new ActionSetCode(cell) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - const receipt = await walletV4.sendExternalSignedMessage(body); - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver, - value: forwardValue - }); + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(9); }); - */ it('Should fail adding existing extension', async () => { const testExtension = Address.parseRaw('0:' + '0'.repeat(64)); @@ -775,7 +636,7 @@ describe('Wallet V5 sign auth external', () => { const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); - const payload = beginCell() // TODO: Seems to pass with auth_signed, need analysis! + const payload = beginCell() // auth_signed_internal used instead of auth_signed .storeUint(Opcodes.auth_signed_internal, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) @@ -792,7 +653,7 @@ describe('Wallet V5 sign auth external', () => { await disableConsoleError(() => expect( walletV5.sendExternal( - beginCell().storeUint(1111, 32).storeSlice(body.beginParse()).endCell() + beginCell().storeSlice(body.beginParse()).endCell() ) ).rejects.toThrow() ); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index cd94f66b..758c18b4 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -15,6 +15,7 @@ import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDes import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; import { default as config } from './config'; +import { ActionSetCode, ActionSetData } from './test-only-actions'; const WALLET_ID = new WalletId({ networkGlobalId: -239, workChain: 0, subwalletNumber: 0 }); @@ -315,110 +316,6 @@ describe('Wallet V5 sign auth internal', () => { ); }); - /* TODO: Rewrite as a negative test - it('Set data and do two transfers', async () => { - const testReceiver1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue1 = toNano(0.001); - - const testReceiver2 = Address.parse('EQCgYDKqfTh7zVj9BQwOIPs4SuOhM7wnIjb6bdtM2AJf_Z9G'); - const forwardValue2 = toNano(0.0012); - - const receiver1BalanceBefore = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceBefore = (await blockchain.getContract(testReceiver2)).balance; - - const msg1 = createMsgInternal({ dest: testReceiver1, value: forwardValue1 }); - const msg2 = createMsgInternal({ dest: testReceiver2, value: forwardValue2 }); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeInt(239, 33).endCell()), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg1), - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg2) - ]); - - const receipt = await walletV5.sendInternalSignedMessage(sender, { - value: toNano(0.1), - body: createBody(actionsList) - }); - - expect(receipt.transactions.length).toEqual(4); - accountForGas(receipt.transactions); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver1, - value: forwardValue1 - }); - - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver2, - value: forwardValue2 - }); - - const fee1 = receipt.transactions[2].totalFees.coins; - const fee2 = receipt.transactions[3].totalFees.coins; - - const receiver1BalanceAfter = (await blockchain.getContract(testReceiver1)).balance; - const receiver2BalanceAfter = (await blockchain.getContract(testReceiver2)).balance; - expect(receiver1BalanceAfter).toEqual(receiver1BalanceBefore + forwardValue1 - fee1); - expect(receiver2BalanceAfter).toEqual(receiver2BalanceBefore + forwardValue2 - fee2); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - */ - - /* TODO: Rewrite as a negative test - it('Send 255 transfers and do set data', async () => { - const range = [...new Array(255)].map((_, index) => index); - - const receivers = range.map(i => Address.parseRaw('0:' + i.toString().padStart(64, '0'))); - const balancesBefore = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const forwardValues = range.map(i => BigInt(toNano(0.0001 * i))); - - const msges = receivers.map((dest, i) => - createMsgInternal({ dest: dest, value: forwardValues[i] }) - ); - - const actionsList = packActionsList([ - new ActionSetData(beginCell().storeInt(239, 33).endCell()), - ...msges.map(msg => new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)) - ]); - - const receipt = await walletV5.sendInternalSignedMessage(sender, { - value: toNano(10), - body: createBody(actionsList) - }); - - expect(receipt.transactions.length).toEqual(range.length + 2); - accountForGas(receipt.transactions); - - receivers.forEach((to, i) => { - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to, - value: forwardValues[i] - }); - }); - - const balancesAfter = ( - await Promise.all(receivers.map(r => blockchain.getContract(r))) - ).map(i => i.balance); - - const fees = receipt.transactions.slice(2).map(tx => tx.totalFees.coins); - - balancesAfter.forEach((balanceAfter, i) => { - expect(balanceAfter).toEqual(balancesBefore[i] + forwardValues[i] - fees[i]); - }); - - const storedSeqno = await walletV5.getSeqno(); - expect(storedSeqno).toEqual(239); - }); - */ - it('Remove extension', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); @@ -455,80 +352,43 @@ describe('Wallet V5 sign auth internal', () => { accountForGas(receipt2.transactions); }); - /* TODO: Rewrite as a negative test - it('Change code and data to wallet v4', async () => { - const code_v4 = await compile('wallet_v4'); - const data_v4 = beginCell() - .storeUint(0, 32) - .storeUint(0, 32) - .storeBuffer(keypair.publicKey, 32) - .storeDict(Dictionary.empty()) - .endCell(); + it('Should fail SetData action', async () => { + const cell = beginCell().endCell(); const actionsList = packActionsList([ - new ActionSetData(data_v4), - new ActionSetCode(code_v4) + new ActionSetData(cell) ]); - const receipt1 = await walletV5.sendInternalSignedMessage(sender, { + const receipt = await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body: createBody(actionsList) }); - accountForGas(receipt1.transactions); - - const walletV4 = blockchain.openContract(WalletV4.createFromAddress(walletV5.address)); - const seqno = await walletV4.getSeqno(); - const subwalletId = await walletV4.getSubWalletID(); - const publicKey = await walletV4.getPublicKey(); - const extensions = Dictionary.loadDirect( - Dictionary.Keys.Address(), - Dictionary.Values.BigInt(0), - await walletV4.getExtensions() - ); - - expect(seqno).toEqual(0); - expect(subwalletId).toEqual(0); - expect(publicKey).toEqual(bufferToBigInt(keypair.publicKey)); - expect(extensions.size).toEqual(0); - - const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue = toNano(0.001); - - const sendTxMsg = beginCell() - .storeUint(0x10, 6) - .storeAddress(testReceiver) - .storeCoins(forwardValue) - .storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .storeRef(beginCell().endCell()) - .endCell(); - - const mesagesCell = beginCell() - .storeUint(0, 8) - .storeUint(SendMode.PAY_GAS_SEPARATELY, 8) - .storeRef(sendTxMsg) - .endCell(); - - const payload = beginCell() - .storeUint(0, 32) - .storeUint(validUntil(), 32) - .storeUint(0, 32) - .storeSlice(mesagesCell.beginParse()) - .endCell(); + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(41); + }); - const signature = sign(payload.hash(), keypair.secretKey); - const body = beginCell() - .storeUint(bufferToBigInt(signature), 512) - .storeSlice(payload.beginParse()) - .endCell(); + it('Should fail SetCode action', async () => { + const cell = beginCell().endCell(); - const receipt = await walletV4.sendExternalSignedMessage(body); - expect(receipt.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver, - value: forwardValue + const actionsList = packActionsList([ + new ActionSetCode(cell) + ]); + const receipt = await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(actionsList) }); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(9); }); - */ it('Should fail adding existing extension', async () => { const testExtension = Address.parseRaw('0:' + '0'.repeat(64)); @@ -848,7 +708,7 @@ describe('Wallet V5 sign auth internal', () => { const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); const actionsList = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); - const payload = beginCell() // TODO: Seems to pass with auth_signed_internal, need analysis! + const payload = beginCell() // auth_signed used instead of auth_signed_internal .storeUint(Opcodes.auth_signed, 32) .storeUint(WALLET_ID.serialized, 80) .storeUint(validUntil(), 32) @@ -865,7 +725,7 @@ describe('Wallet V5 sign auth internal', () => { const receipt = await walletV5.sendInternal(sender, { sendMode: SendMode.PAY_GAS_SEPARATELY, value: toNano(0.1), - body: beginCell().storeUint(1111, 32).storeSlice(body.beginParse()).endCell() + body: beginCell().storeSlice(body.beginParse()).endCell() }); expect(receipt.transactions.length).toEqual(2); From 3f76dbb3826a3f9fef3ce794774b1fede0441314 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Thu, 15 Feb 2024 04:12:36 +0200 Subject: [PATCH 032/121] Added tests for signature disallow cases, incorrect allow act now throws --- contracts/wallet_v5.fc | 33 ++- tests/wallet-v5-extensions.spec.ts | 108 ++++++++- tests/wallet-v5-external.spec.ts | 229 +++++++++++++++++- tests/wallet-v5-internal.spec.ts | 368 ++++++++++++++++++++++++++++- wrappers/wallet-v5.ts | 10 + 5 files changed, 727 insertions(+), 21 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index b0944ca5..637b3455 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -107,23 +107,21 @@ cell verify_actions(cell c5) inline { var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions if (allow) { ;; allow - if (stored_seqno < 0) { - ;; Can't be disabled with 0 because disabling increments seqno - ;; -123 -> 123 -> 124 - stored_seqno = - stored_seqno; - stored_seqno = stored_seqno + 1; - } + throw_unless(43, stored_seqno < 0); + ;; Can't be disallowed with 0 because disallowing increments seqno + ;; -123 -> 123 -> 124 + stored_seqno = - stored_seqno; + stored_seqno = stored_seqno + 1; } else { ;; disallow - if (stored_seqno >= 0) { - ds = ds.skip_bits(size::stored_subwallet + size::public_key); - var extensions_is_not_null = ds.preload_uint(1); - throw_unless(42, extensions_is_not_null); - ;; Corner case: 0 -> 1 -> -1 - ;; 123 -> 124 -> -124 - stored_seqno = stored_seqno + 1; - stored_seqno = - stored_seqno; - } + throw_unless(43, stored_seqno >= 0); + ds = ds.skip_bits(size::stored_subwallet + size::public_key); + var extensions_is_not_null = ds.preload_uint(1); + throw_unless(42, extensions_is_not_null); + ;; Corner case: 0 -> 1 -> -1 + ;; 123 -> 124 -> -124 + stored_seqno = stored_seqno + 1; + stored_seqno = - stored_seqno; } set_data(begin_cell() .store_int(stored_seqno, size::stored_seqno) @@ -137,10 +135,9 @@ cell verify_actions(cell c5) inline { } -} else { + ;; need to throw on unsupported actions for correct flow and for testability throw(41); ;; unsupported action } - ;; Other actions are no-op - ;; FIXME: is it costlier to check for unsupported actions and throw? cs = cs.preload_ref().begin_parse(); } ;; At this point we are at `action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0;` @@ -166,6 +163,7 @@ cell verify_actions(cell c5) inline { var stored_subwallet = ds~load_uint(size::stored_subwallet); var public_key = ds.preload_uint(size::public_key); + ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! ;; Only such checking order results in least amount of gas throw_unless(35, check_signature(slice_hash(signed), signature, public_key)); ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 @@ -233,6 +231,7 @@ cell verify_actions(cell c5) inline { var stored_subwallet = ds~load_uint(size::stored_subwallet); var public_key = ds.preload_uint(size::public_key); + ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! ;; Only such checking order results in least amount of gas return_unless(check_signature(slice_hash(signed), signature, public_key)); ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 910bdba9..78c94f0a 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -8,7 +8,7 @@ import { bufferToBigInt, createMsgInternal, packAddress, validUntil } from './ut import { ActionAddExtension, ActionRemoveExtension, - ActionSendMsg, + ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; @@ -320,4 +320,110 @@ describe('Wallet V5 extensions auth', () => { const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; expect(receiverBalanceAfter).toEqual(receiverBalanceBefore); }); + + it('Disallow signature auth and do a transfer from extension', async () => { + await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!), + new ActionSetSignatureAuthAllowed(false) + ])) + }); + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actions = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); + + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actions + }); + + expect(receipt.transactions.length).toEqual(3); + accountForGas(receipt.transactions); + + expect(receipt.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); + + it('Disallow signature auth; re-allow and self-delete by extension; do signed transfer', async () => { + await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!), + new ActionSetSignatureAuthAllowed(false) + ])) + }); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionRemoveExtension(sender.address!), + new ActionSetSignatureAuthAllowed(true) + ]) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed1 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed1).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(3); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); }); diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index ffcb718d..1dadade0 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -14,7 +14,7 @@ import { import { ActionAddExtension, ActionRemoveExtension, - ActionSendMsg, + ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; @@ -690,4 +690,231 @@ describe('Wallet V5 sign auth external', () => { expect(walletBalanceBefore).toEqual(walletBalanceAfter); }); + + it('Should fail disallowing signature auth with no exts', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(42); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should fail allowing signature auth when allowed', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(true) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should add ext and disallow signature auth', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext and disallow signature auth in separate txs', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const actionsList2 = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); + accountForGas(receipt2.transactions); + + expect( + ( + (receipt2.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed2 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed2).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext, disallow sign, remove ext, allow sign in one tx; send in other', async () => { + // N.B. Test that zero extensions do not prevent re-allowing the signature authentication + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionRemoveExtension(testExtension), + new ActionSetSignatureAuthAllowed(true) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); + + expect(receipt2.transactions.length).toEqual(2); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[1].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); + + it('Should fail disallowing signature auth twice in tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); // throw when handling, packet is dropped + }); + + it('Should add ext, disallow sig auth; fail different signed tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + + await disableConsoleError(() => + expect(walletV5.sendExternalSignedMessage(createBody(packActionsList([])))).rejects.toThrow() + ); + }); }); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 758c18b4..b86f75e9 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -4,11 +4,11 @@ import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; import '@ton-community/test-utils'; import { compile } from '@ton-community/blueprint'; import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from 'ton-crypto'; -import { bufferToBigInt, createMsgInternal, packAddress, validUntil } from './utils'; +import { bufferToBigInt, createMsgInternal, disableConsoleError, packAddress, validUntil } from './utils'; import { ActionAddExtension, ActionRemoveExtension, - ActionSendMsg, + ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; @@ -765,6 +765,43 @@ describe('Wallet V5 sign auth internal', () => { ).toEqual(0); }); + it('Should not revert on short "sint" messages', async () => { + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: beginCell().storeUint(Opcodes.auth_signed_internal, 32).endCell() + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + }); + + it('Should not revert on long incorrect "sint" messages', async () => { + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: beginCell() + .storeUint(Opcodes.auth_signed_internal, 32) + .storeUint(0, 657) + .endCell() + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + }); + it('Should skip message with simple text comment', async () => { const receipt = await walletV5.sendInternal(sender, { sendMode: SendMode.PAY_GAS_SEPARATELY, @@ -814,4 +851,331 @@ describe('Wallet V5 sign auth internal', () => { ).gasUsed ); }); + + it('Should fail disallowing signature auth with no exts', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(42); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should fail allowing signature auth when allowed', async () => { + const actionsList = packActionsList([ + new ActionSetSignatureAuthAllowed(true) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + + it('Should add ext and disallow signature auth', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + accountForGas(receipt.transactions); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext and disallow signature auth in separate txs', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + + accountForGas(receipt.transactions); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const actionsList2 = packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(2); + + accountForGas(receipt2.transactions); + + expect( + ( + (receipt2.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed2 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed2).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + }); + + it('Should add ext, disallow sign, remove ext, allow sign in one tx; send in other', async () => { + // N.B. Test that zero extensions do not prevent re-allowing the signature authentication + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionRemoveExtension(testExtension), + new ActionSetSignatureAuthAllowed(true) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(3); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); + + it('Should fail disallowing signature auth twice in tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(43); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); // throw when handling, packet is dropped + }); + + it('Should add ext, disallow sig auth; fail different signed tx', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + const extensionsDict = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + await walletV5.getExtensions() + ); + + expect(extensionsDict.size).toEqual(1); + + expect(extensionsDict.get(packAddress(testExtension))).toEqual( + BigInt(testExtension.workChain) + ); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(0); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 1); + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + const actionsList2 = packActionsList([new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg)]); + + const receipt2 = await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect( + ( + (receipt2.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + expect(receipt2.transactions).not.toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore); + }); }); diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 4ab93317..fe0c7085 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -198,6 +198,16 @@ export class WalletV5 implements Contract { } } + async getIsSignatureAuthAllowed(provider: ContractProvider) { + const state = await provider.getState(); + if (state.state.type === 'active') { + let res = await provider.get('get_is_signature_auth_allowed', []); + return res.stack.readNumber(); + } else { + return -1; + } + } + async getWalletId(provider: ContractProvider) { const result = await provider.get('get_wallet_id', []); return WalletId.deserialize(result.stack.readBigNumber()); From 7f786ea2f02b94d7ebd71ac9675ab46c6cc22118 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Thu, 15 Feb 2024 20:45:55 +0200 Subject: [PATCH 033/121] Added a test for separate auth disable by extension --- tests/wallet-v5-extensions.spec.ts | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 78c94f0a..4d2f30d2 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -426,4 +426,93 @@ describe('Wallet V5 extensions auth', () => { const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); }); + + it('Add ext; disallow signature auth by ext; re-allow and self-delete by extension; do signed transfer', async () => { + await walletV5.sendInternalSignedMessage(sender, { + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + + const receipt0 = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]) + }); + + expect(receipt0.transactions.length).toEqual(2); + accountForGas(receipt0.transactions); + + expect( + ( + (receipt0.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed0 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed0).toEqual(0); + + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionRemoveExtension(sender.address!), + new ActionSetSignatureAuthAllowed(true) + ]) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(0); + + const isSignatureAuthAllowed1 = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed1).toEqual(-1); + + const contract_seqno = await walletV5.getSeqno(); + expect(contract_seqno).toEqual(seqno + 2); + + // Allowing or disallowing signature auth increments seqno, need to re-read + seqno = contract_seqno; + + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const forwardValue = toNano(0.001); + + const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + + const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + + const actionsList2 = packActionsList([ + new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + ]); + + const receipt2 = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList2) + }); + + expect(receipt2.transactions.length).toEqual(3); + accountForGas(receipt2.transactions); + + expect(receipt2.transactions).toHaveTransaction({ + from: walletV5.address, + to: testReceiver, + value: forwardValue + }); + + const fee = receipt2.transactions[2].totalFees.coins; + const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + }); }); From 773909916aa1e885f1c77356d62930df688ff9ea Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 20 Feb 2024 11:23:44 +0100 Subject: [PATCH 034/121] improved overview wording --- Specification.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Specification.md b/Specification.md index 0e1906cf..ed44015b 100644 --- a/Specification.md +++ b/Specification.md @@ -34,21 +34,20 @@ Thanks to [Skydev](https://github.com/Skydev0h) for optimization and preparing t ## Overview -Wallet V5 supports **2 authentication modes**, all standard output actions (send message, set library, code replacement) plus additional **3 operation types**. +Wallet V5 supports **2 authentication modes** and **3 operations types**. Authentication: * by signature * by extension -Operation types: -* standard output send message action -* enable or disable public key (signature authentication) -* install extension -* remove extension +Operations: +* standard "send message" action (up to 255 messages at once), +* enable/disable signature authentication, +* install/remove extension. Signed messages can be delivered both by external and internal messages. -All operation types are available to all authentication modes. +All operations are available to all authentication modes. ## Discussion From c2734f0f620d75b6efcf2252e1117891ee252613 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 20 Feb 2024 16:33:32 +0100 Subject: [PATCH 035/121] Bounce internal signed messages to protect relayer against abuse --- contracts/wallet_v5.fc | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 637b3455..7308ed52 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -231,15 +231,23 @@ cell verify_actions(cell c5) inline { var stored_subwallet = ds~load_uint(size::stored_subwallet); var public_key = ds.preload_uint(size::public_key); + ;; Note on bouncing/nonbouncing behaviour: + ;; In principle, the wallet should not bounce incoming messages as to avoid + ;; returning deposits back to the sender due to opcode misinterpretation. + ;; However, specifically for "gasless" transactions (signed messages relayed by a 3rd party), + ;; there is a risk for the relaying party to be abused: their coins should be bounced back in case of a race condition or delays. + ;; We resolve this dilemma by silently failing at the signature check (therefore ordinary deposits with arbitrary opcodes never bounce), + ;; but failing with exception (therefore bouncing) after the signature check. + ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! ;; Only such checking order results in least amount of gas return_unless(check_signature(slice_hash(signed), signature, public_key)); ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed - return_unless(msg_seqno == stored_seqno); - return_unless(subwallet_id == stored_subwallet); - return_if(valid_until <= now()); + throw_unless(msg_seqno == stored_seqno); + throw_unless(subwallet_id == stored_subwallet); + throw_if(valid_until <= now()); ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; @@ -337,4 +345,4 @@ cell get_extensions() method_id { int get_is_signature_auth_allowed() method_id { return get_data().begin_parse().preload_int(size::stored_seqno) >= 0; -} \ No newline at end of file +} From b233fb78dceee987a8f316b9301677a15f15fe45 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 20 Feb 2024 16:43:37 +0100 Subject: [PATCH 036/121] fix throw argument --- contracts/wallet_v5.fc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 7308ed52..3ea6c99e 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -245,9 +245,9 @@ cell verify_actions(cell c5) inline { ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed - throw_unless(msg_seqno == stored_seqno); - throw_unless(subwallet_id == stored_subwallet); - throw_if(valid_until <= now()); + throw_unless(33, msg_seqno == stored_seqno); + throw_unless(34, subwallet_id == stored_subwallet); + throw_if(36, valid_until <= now()); ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; From 4cd918189546a991ad3a703e8c140246195b3bdb Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Tue, 20 Feb 2024 23:50:42 +0200 Subject: [PATCH 037/121] Fixed failing tests after adding back throw (bounce) for sint after sig --- tests/wallet-v5-internal.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index b86f75e9..8dc09e3b 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -597,7 +597,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(0); + ).toEqual(33); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -642,7 +642,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(0); + ).toEqual(36); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -687,7 +687,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(0); + ).toEqual(34); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -1166,7 +1166,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt2.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(0); + ).toEqual(33); expect(receipt2.transactions).not.toHaveTransaction({ from: walletV5.address, From 8661ce2ef47f11d6cca6d5545340e9aa5132c557 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Thu, 22 Feb 2024 01:52:32 +0200 Subject: [PATCH 038/121] Added safeguard against deleting last ext with disabled key (need tests) --- contracts/wallet_v5.fc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 3ea6c99e..b3c83a2f 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -81,6 +81,7 @@ cell verify_actions(cell c5) inline { var ds = get_data().begin_parse(); var data_bits = ds~load_bits(size::stored_seqno + size::stored_subwallet + size::public_key); + var stored_seqno = data_bits.preload_int(size::stored_seqno); var extensions = ds.preload_dict(); ;; Add extension @@ -93,6 +94,7 @@ cell verify_actions(cell c5) inline { { (extensions, int success?) = extensions.udict_delete?(256, packed_addr); throw_unless(40, success?); + throw_if(44, null?(extensions) & (stored_seqno < 0)); } set_data(begin_cell() From 4458be0092adb610e6b95459cfbaca3da4bf5b46 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 23 Feb 2024 02:43:08 +0200 Subject: [PATCH 039/121] Fix prefix clashing bug and adjust code style (reported by @behrang) --- contracts/wallet_v5.fc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index b3c83a2f..e8497e4a 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -71,11 +71,11 @@ cell verify_actions(cell c5) inline { () dispatch_complex_request(slice cs) impure inline_ref { ;; Recurse into extended actions until we reach standard actions - while (cs~load_uint(1)) { - var is_add_ext = cs~check_and_remove_add_extension_prefix(); - var is_del_ext = cs~check_and_remove_remove_extension_prefix(); + while (cs~load_int(1)) { + var is_add_ext? = cs~check_and_remove_add_extension_prefix(); + var is_del_ext? = is_add_ext? ? 0 : cs~check_and_remove_remove_extension_prefix(); ;; Add/remove extensions - if ((is_add_ext) | (is_del_ext)) { + if (is_add_ext? | is_del_ext?) { (int wc, int hash) = parse_std_addr(cs~load_msg_addr()); int packed_addr = pack_address((wc, hash) ); @@ -85,7 +85,7 @@ cell verify_actions(cell c5) inline { var extensions = ds.preload_dict(); ;; Add extension - if (is_add_ext) { + if (is_add_ext?) { (extensions, int success?) = extensions.udict_add_builder?(256, packed_addr, begin_cell().store_int(wc,8)); throw_unless(39, success?); } else @@ -103,11 +103,11 @@ cell verify_actions(cell c5) inline { .end_cell()); } elseif (cs~check_and_remove_set_signature_auth_allowed_prefix()) { - var allow = cs~load_uint(1); + var allow? = cs~load_int(1); var ds = get_data().begin_parse(); var stored_seqno = ds~load_int(size::stored_seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - if (allow) { + if (allow?) { ;; allow throw_unless(43, stored_seqno < 0); ;; Can't be disallowed with 0 because disallowing increments seqno @@ -289,10 +289,10 @@ cell verify_actions(cell c5) inline { ;; - 0x6578746E "extn" authenticated by extension ;; - 0x73696E74 "sint" internal message authenticated by signature - (body, int is_extn) = check_and_remove_extn_prefix(body); ;; 0x6578746E ("extn") + (body, int is_extn?) = check_and_remove_extn_prefix(body); ;; 0x6578746E ("extn") ;; IFJMPREF because unconditionally returns inside - if (is_extn) { ;; "extn" authenticated by extension + if (is_extn?) { ;; "extn" authenticated by extension ;; Authenticate extension by its address. int packed_sender_addr = pack_address(parse_std_addr(full_msg_slice~load_msg_addr())); ;; no PLDMSGADDR exists @@ -312,8 +312,8 @@ cell verify_actions(cell c5) inline { } slice full_body = body; - (_, int is_sint) = check_and_remove_sint_prefix(body); ;; 0x73696E74 ("sint") - sign internal - return_unless(is_sint); + (_, int is_sint?) = check_and_remove_sint_prefix(body); ;; 0x73696E74 ("sint") - sign internal + return_unless(is_sint?); ;; Process the rest of the slice just like the signed request. process_signed_request_from_internal_message(full_body); From 51358adde0d12290ee3c4a1c5bbb98f992f62c8b Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 23 Feb 2024 02:56:39 +0200 Subject: [PATCH 040/121] Fixed tests and added cases for deleting last ext with disallowed pubkey --- tests/wallet-v5-extensions.spec.ts | 8 +++---- tests/wallet-v5-external.spec.ts | 29 +++++++++++++++++++++---- tests/wallet-v5-internal.spec.ts | 34 +++++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 4d2f30d2..ae91fdd6 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -372,8 +372,8 @@ describe('Wallet V5 extensions auth', () => { const receipt = await walletV5.sendInternalMessageFromExtension(sender, { value: toNano('0.1'), body: packActionsList([ - new ActionRemoveExtension(sender.address!), - new ActionSetSignatureAuthAllowed(true) + new ActionSetSignatureAuthAllowed(true), + new ActionRemoveExtension(sender.address!) ]) }); @@ -461,8 +461,8 @@ describe('Wallet V5 extensions auth', () => { const receipt = await walletV5.sendInternalMessageFromExtension(sender, { value: toNano('0.1'), body: packActionsList([ - new ActionRemoveExtension(sender.address!), - new ActionSetSignatureAuthAllowed(true) + new ActionSetSignatureAuthAllowed(true), + new ActionRemoveExtension(sender.address!) ]) }); diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 1dadade0..3d17d1d3 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -800,15 +800,14 @@ describe('Wallet V5 sign auth external', () => { expect(contract_seqno).toEqual(seqno + 1); }); - it('Should add ext, disallow sign, remove ext, allow sign in one tx; send in other', async () => { - // N.B. Test that zero extensions do not prevent re-allowing the signature authentication + it('Should add ext, disallow sign, allow sign, remove ext in one tx; send in other', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ new ActionAddExtension(testExtension), new ActionSetSignatureAuthAllowed(false), - new ActionRemoveExtension(testExtension), - new ActionSetSignatureAuthAllowed(true) + new ActionSetSignatureAuthAllowed(true), + new ActionRemoveExtension(testExtension) ]); const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); accountForGas(receipt.transactions); @@ -856,6 +855,28 @@ describe('Wallet V5 sign auth external', () => { expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); }); + it('Should fail removing last extension with signature auth disabled', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionRemoveExtension(testExtension) + ]); + const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[0].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(44); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + it('Should fail disallowing signature auth twice in tx', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 8dc09e3b..61f79f07 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -1011,15 +1011,14 @@ describe('Wallet V5 sign auth internal', () => { expect(contract_seqno).toEqual(seqno + 1); }); - it('Should add ext, disallow sign, remove ext, allow sign in one tx; send in other', async () => { - // N.B. Test that zero extensions do not prevent re-allowing the signature authentication + it('Should add ext, disallow sign, allow sign, remove ext in one tx; send in other', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ new ActionAddExtension(testExtension), new ActionSetSignatureAuthAllowed(false), + new ActionSetSignatureAuthAllowed(true), new ActionRemoveExtension(testExtension), - new ActionSetSignatureAuthAllowed(true) ]); const receipt = await walletV5.sendInternal(sender, { @@ -1078,6 +1077,35 @@ describe('Wallet V5 sign auth internal', () => { expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); }); + it('Should fail removing last extension with signature auth disabled', async () => { + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + + const actionsList = packActionsList([ + new ActionAddExtension(testExtension), + new ActionSetSignatureAuthAllowed(false), + new ActionRemoveExtension(testExtension) + ]); + + const receipt = await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(actionsList) + }); + + expect(receipt.transactions.length).toEqual(2); + accountForGas(receipt.transactions); + + expect( + ( + (receipt.transactions[1].description as TransactionDescriptionGeneric) + .computePhase as TransactionComputeVm + ).exitCode + ).toEqual(44); + + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + expect(isSignatureAuthAllowed).toEqual(-1); + }); + it('Should fail disallowing signature auth twice in tx', async () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); From d373fb0ba92241826481c578c6cd67c4e5081764 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Thu, 13 Jun 2024 16:09:00 +0200 Subject: [PATCH 041/121] Introduction improvements --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af25b151..c695619c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Wallet V5 +# 🔥W5: wallet v5 standard This is an extensible wallet specification aimed at replacing V4 and allowing arbitrary extensions. -Wallet V5 has 25% lower fees, can delegate payments for gas to third parties and supports flexible extension mechanism. +W5 has **25% lower fees**, supports **gasless transactions** (via third party relayers) and implements a **flexible extension mechanism**. ## Project structure From 92471dda68cb115c008d9d9dc788e4de48ddd8ca Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 14 Jun 2024 18:30:35 +0300 Subject: [PATCH 042/121] Some improvements and changes to the contract 1) Internal messages can use any send_mode 2) Action list verification accounts for exotic cells 4) Removed unneccessary (and maybe even harming) shortcuts 5) Deduplicated process_signed_xxx code into single fun --- contracts/wallet_v5.fc | 124 +++++++++++++---------------------------- 1 file changed, 38 insertions(+), 86 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index e8497e4a..5f771c0c 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -17,6 +17,8 @@ const int size::flags = 4; (slice) udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET" "IFNOTRET"; +(slice, int) begin_parse_xc(cell c) asm "XCTOS"; + (slice) enforce_and_remove_sign_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; (slice, int) check_and_remove_extn_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; (slice, int) check_and_remove_sint_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; @@ -48,18 +50,18 @@ int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; slice get_last_bits(slice s, int n) asm "SDCUTLAST"; slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; -cell verify_actions(cell c5) inline { +cell verify_actions(cell c5, int is_external) inline { ;; Comment out code starting from here to disable checks (unsafe version) ;; {- - slice c5s = c5.begin_parse(); + (slice c5s, _) = c5.begin_parse_xc(); return_if(c5s.slice_empty?()); do { ;; only send_msg is allowed, set_code or reserve_currency are not c5s = c5s.enforce_and_remove_action_send_msg_prefix(); ;; enforce that send_mode has 2 bit set ;; for that load 7 bits and make sure that they end with 1 - throw_if(37, count_trailing_zeroes(c5s.preload_bits(7))); - c5s = c5s.preload_ref().begin_parse(); + throw_if(37, is_external & count_trailing_zeroes(c5s.preload_bits(7))); + (c5s, _) = c5s.preload_ref().begin_parse_xc(); } until (c5s.slice_empty?()); ;; -} return c5; @@ -68,7 +70,7 @@ cell verify_actions(cell c5) inline { ;; Dispatches already authenticated request. ;; this function is explicitly included as an inline reference - not completely inlined ;; completely inlining it causes undesirable code split and noticeable gas increase in some paths -() dispatch_complex_request(slice cs) impure inline_ref { +() dispatch_complex_request(slice cs, int is_external) impure inline_ref { ;; Recurse into extended actions until we reach standard actions while (cs~load_int(1)) { @@ -144,81 +146,18 @@ cell verify_actions(cell c5) inline { } ;; At this point we are at `action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0;` ;; Simply set the C5 register with all pre-computed actions after verification: - set_actions(cs.preload_ref().verify_actions()); + set_actions(cs.preload_ref().verify_actions(is_external)); return (); } ;; ------------------------------------------------------------------------------------------------ ;; Verifies signed request, prevents replays and proceeds with `dispatch_request`. -() process_signed_request_from_external_message(slice full_body) impure inline { - ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. - slice signature = full_body.get_last_bits(512); - slice signed = full_body.remove_last_bits(512); - - var cs = signed.skip_bits(32); - var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); - - var ds = get_data().begin_parse(); - var stored_seqno = ds~load_int(size::stored_seqno); - var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - var stored_subwallet = ds~load_uint(size::stored_subwallet); - var public_key = ds.preload_uint(size::public_key); - - ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! - ;; Only such checking order results in least amount of gas - throw_unless(35, check_signature(slice_hash(signed), signature, public_key)); - ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 - ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 - ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed - throw_unless(33, msg_seqno == stored_seqno); - throw_unless(34, subwallet_id == stored_subwallet); - throw_if(36, valid_until <= now()); - - accept_message(); - - ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. - stored_seqno = stored_seqno + 1; - set_data(begin_cell() - .store_int(stored_seqno, size::stored_seqno) - .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions - .end_cell()); - - commit(); - - if (count_leading_zeroes(cs)) { ;; starts with bit 0 - return set_actions(cs.preload_ref().verify_actions()); +() process_signed_request(slice full_body, int is_external) impure inline { + ifnot (is_external) { + ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) + return_if(full_body.slice_bits() < 32 + size::subwallet_id + size::valid_until + size::msg_seqno + 1 + 512); } - ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> - - ;; inline_ref required because otherwise it will produce undesirable JMPREF - dispatch_complex_request(cs); -} - -() recv_external(slice body) impure inline { - slice full_body = body; - ;; 0x7369676E ("sign") external message authenticated by signature - body = enforce_and_remove_sign_prefix(body); - process_signed_request_from_external_message(full_body); - return(); -} - -;; ------------------------------------------------------------------------------------------------ - -() dispatch_extension_request(slice cs, var dummy1) impure inline { - if (count_leading_zeroes(cs)) { ;; starts with bit 0 - return set_actions(cs.preload_ref().verify_actions()); - } - ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> - ;; - dummy1~impure_touch(); ;; DROP merged to 2DROP! - dispatch_complex_request(cs); -} - -;; Same logic as above function but with return_* instead of throw_* and additional checks to prevent bounces -() process_signed_request_from_internal_message(slice full_body) impure inline { - ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(full_body.slice_bits() < 32 + size::subwallet_id + size::valid_until + size::msg_seqno + 1 + 512); ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. slice signature = full_body.get_last_bits(512); @@ -234,16 +173,22 @@ cell verify_actions(cell c5) inline { var public_key = ds.preload_uint(size::public_key); ;; Note on bouncing/nonbouncing behaviour: - ;; In principle, the wallet should not bounce incoming messages as to avoid + ;; In principle, the wallet should not bounce incoming messages as to avoid ;; returning deposits back to the sender due to opcode misinterpretation. ;; However, specifically for "gasless" transactions (signed messages relayed by a 3rd party), ;; there is a risk for the relaying party to be abused: their coins should be bounced back in case of a race condition or delays. ;; We resolve this dilemma by silently failing at the signature check (therefore ordinary deposits with arbitrary opcodes never bounce), ;; but failing with exception (therefore bouncing) after the signature check. - + ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! - ;; Only such checking order results in least amount of gas - return_unless(check_signature(slice_hash(signed), signature, public_key)); + int signature_is_valid = check_signature(slice_hash(signed), signature, public_key); + if (is_external) { + throw_unless(35, signature_is_valid); + } else { + ifnot (signature_is_valid) { + return(); + } + } ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed @@ -251,6 +196,10 @@ cell verify_actions(cell c5) inline { throw_unless(34, subwallet_id == stored_subwallet); throw_if(36, valid_until <= now()); + if (is_external) { + accept_message(); + } + ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() @@ -260,15 +209,19 @@ cell verify_actions(cell c5) inline { commit(); - if (count_leading_zeroes(cs)) { ;; starts with bit 0 - return set_actions(cs.preload_ref().verify_actions()); - } - ;; <<<<<<<<<<---------- Simple primary cases gas evaluation ends here ---------->>>>>>>>>> + dispatch_complex_request(cs, is_external); +} - ;; inline_ref required because otherwise it will produce undesirable JMPREF - dispatch_complex_request(cs); +() recv_external(slice body) impure inline { + slice full_body = body; + ;; 0x7369676E ("sign") external message authenticated by signature + body = enforce_and_remove_sign_prefix(body); + process_signed_request(full_body, true); + return(); } +;; ------------------------------------------------------------------------------------------------ + () recv_internal(cell full_msg, slice body) impure inline { ;; return right away if there are no references @@ -305,8 +258,7 @@ cell verify_actions(cell c5) inline { ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). var wc = extensions.udict_get_or_return(256, packed_sender_addr); ;; kindof ifnot (success?) { return(); } - ;; auth_kind and wc are passed into dispatch_extension_request and later are dropped in batch with 3 BLKDROP - dispatch_extension_request(body, wc); ;; Special route for external address authenticated request + dispatch_complex_request(body, false); return (); } @@ -316,7 +268,7 @@ cell verify_actions(cell c5) inline { return_unless(is_sint?); ;; Process the rest of the slice just like the signed request. - process_signed_request_from_internal_message(full_body); + process_signed_request(full_body, false); return (); ;; Explicit returns escape function faster and const less gas (suddenly!) } From 7d1d14494925ce629bc0d8bbe17783decf476c7a Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 14 Jun 2024 18:49:36 +0300 Subject: [PATCH 043/121] Plugins now bound to same workchain as the wallet, no packing (6) --- contracts/wallet_v5.fc | 20 ++++++++++---------- tests/utils.ts | 3 +-- tests/wallet-v5-external.spec.ts | 2 +- tests/wallet-v5-get.spec.ts | 4 ++-- tests/wallet-v5-internal.spec.ts | 2 +- wrappers/wallet-v5.ts | 2 +- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 5f771c0c..79f3fe66 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -32,11 +32,6 @@ const int size::flags = 4; ;; Extensible wallet contract v5 -;; Compresses 8+256-bit address into 256-bit uint by cutting off one bit from sha256. -;; This allows us to save on wrapping the address in a cell and make plugin requests cheaper. -;; This method also unpacks address hash if you pass packed hash with the original wc. -int pack_address((int, int) address) impure asm "SWAP" "INC" "XOR"; ;; hash ^ (wc+1) - ;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. () set_actions(cell action_list) impure asm "c5 POP"; @@ -79,7 +74,9 @@ cell verify_actions(cell c5, int is_external) inline { ;; Add/remove extensions if (is_add_ext? | is_del_ext?) { (int wc, int hash) = parse_std_addr(cs~load_msg_addr()); - int packed_addr = pack_address((wc, hash) ); + (int my_wc, _) = parse_std_addr(my_address()); + + throw_unless(45, my_wc == wc); var ds = get_data().begin_parse(); var data_bits = ds~load_bits(size::stored_seqno + size::stored_subwallet + size::public_key); @@ -88,13 +85,13 @@ cell verify_actions(cell c5, int is_external) inline { ;; Add extension if (is_add_ext?) { - (extensions, int success?) = extensions.udict_add_builder?(256, packed_addr, begin_cell().store_int(wc,8)); + (extensions, int success?) = extensions.udict_add_builder?(256, hash, begin_cell().store_int(wc,8)); throw_unless(39, success?); } else ;; Remove extension if (op == 0x5eaef4a4) ;; It can be ONLY 0x1c40db9f OR 0x5eaef4a4 here. No need for second check. { - (extensions, int success?) = extensions.udict_delete?(256, packed_addr); + (extensions, int success?) = extensions.udict_delete?(256, hash); throw_unless(40, success?); throw_if(44, null?(extensions) & (stored_seqno < 0)); } @@ -248,7 +245,10 @@ cell verify_actions(cell c5, int is_external) inline { if (is_extn?) { ;; "extn" authenticated by extension ;; Authenticate extension by its address. - int packed_sender_addr = pack_address(parse_std_addr(full_msg_slice~load_msg_addr())); ;; no PLDMSGADDR exists + (int sender_wc, int sender_hash) = parse_std_addr(full_msg_slice~load_msg_addr()); ;; no PLDMSGADDR exists + (int my_wc, _) = parse_std_addr(my_address()); + + return_unless(my_wc == sender_wc); var ds = get_data().begin_parse(); ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed @@ -256,7 +256,7 @@ cell verify_actions(cell c5, int is_external) inline { ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). - var wc = extensions.udict_get_or_return(256, packed_sender_addr); ;; kindof ifnot (success?) { return(); } + extensions.udict_get_or_return(256, sender_hash); ;; kindof ifnot (success?) { return(); } dispatch_complex_request(body, false); return (); diff --git a/tests/utils.ts b/tests/utils.ts index 8257d4b9..d309bf9e 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -5,8 +5,7 @@ export function bufferToBigInt(buffer: Buffer): bigint { } export function packAddress(address: Address) { - const wcPlus = address.workChain + 1; - return bufferToBigInt(address.hash) ^ BigInt(wcPlus); + return bufferToBigInt(address.hash); } export function validUntil(ttlMs = 1000 * 60 * 3) { diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 3d17d1d3..69e26c29 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -293,7 +293,7 @@ describe('Wallet V5 sign auth external', () => { }); it('Add two extensions and do a transfer', async () => { - const testExtension1 = Address.parse('Ef82pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX28X'); + const testExtension1 = Address.parse('EQA2pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX5Bf'); const testExtension2 = Address.parse('EQCgYDKqfTh7zVj9BQwOIPs4SuOhM7wnIjb6bdtM2AJf_Z9G'); const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); diff --git a/tests/wallet-v5-get.spec.ts b/tests/wallet-v5-get.spec.ts index c3fac3ad..6ec99d20 100644 --- a/tests/wallet-v5-get.spec.ts +++ b/tests/wallet-v5-get.spec.ts @@ -107,7 +107,7 @@ describe('Wallet V5 get methods', () => { it('Get extensions dict', async () => { const plugin1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const plugin2 = Address.parse('Ef82pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX28X'); + const plugin2 = Address.parse('EQA2pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX5Bf'); const extensions: Dictionary = Dictionary.empty( Dictionary.Keys.BigUint(256), @@ -127,7 +127,7 @@ describe('Wallet V5 get methods', () => { it('Get extensions array', async () => { const plugin1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const plugin2 = Address.parse('Ef82pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX28X'); + const plugin2 = Address.parse('EQA2pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX5Bf'); const extensions: Dictionary = Dictionary.empty( Dictionary.Keys.BigUint(256), diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 61f79f07..64595209 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -266,7 +266,7 @@ describe('Wallet V5 sign auth internal', () => { }); it('Add two extensions and do a transfer', async () => { - const testExtension1 = Address.parse('Ef82pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX28X'); + const testExtension1 = Address.parse('EQA2pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX5Bf'); const testExtension2 = Address.parse('EQCgYDKqfTh7zVj9BQwOIPs4SuOhM7wnIjb6bdtM2AJf_Z9G'); const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index fe0c7085..89c991b5 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -232,7 +232,7 @@ export class WalletV5 implements Contract { return dict.keys().map(key => { const wc = dict.get(key)!; - const addressHex = key ^ (wc + 1n); + const addressHex = key; return Address.parseRaw(`${wc}:${addressHex.toString(16)}`); }); } From 1f9b0a3a19edfab4ae4d6bfd23eb0fea1e5fc181 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 14 Jun 2024 19:15:57 +0300 Subject: [PATCH 044/121] Moved signature_auth_disable to separate variable from seq_no sign (3) --- Specification.md | 2 +- contracts/wallet_v5.fc | 57 +++++++++++++++--------------- scripts/deployWalletV5.ts | 1 + tests/wallet-v5-extensions.spec.ts | 11 ++---- tests/wallet-v5-external.spec.ts | 13 ++++--- tests/wallet-v5-get.spec.ts | 1 + tests/wallet-v5-internal.spec.ts | 15 ++++---- types.tlb | 2 +- wrappers/wallet-v5.ts | 4 ++- 9 files changed, 52 insertions(+), 54 deletions(-) diff --git a/Specification.md b/Specification.md index ed44015b..fdfef5d3 100644 --- a/Specification.md +++ b/Specification.md @@ -180,7 +180,7 @@ actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; Contract state: ```tl-b wallet_id$_ global_id:# wc:int8 version:(## 8) subwallet_number:# = WalletID; -contract_state$_ seqno:int33 wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; +contract_state$_ signature_auth_disabled:(## 1) seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; ``` ## Source code diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 79f3fe66..e18faf8a 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -2,7 +2,8 @@ #include "imports/stdlib.fc"; -const int size::stored_seqno = 33; +const int size::signature_auth_disabled = 1; +const int size::stored_seqno = 32; const int size::stored_subwallet = 80; const int size::public_key = 256; @@ -79,8 +80,8 @@ cell verify_actions(cell c5, int is_external) inline { throw_unless(45, my_wc == wc); var ds = get_data().begin_parse(); - var data_bits = ds~load_bits(size::stored_seqno + size::stored_subwallet + size::public_key); - var stored_seqno = data_bits.preload_int(size::stored_seqno); + var data_bits = ds~load_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet + size::public_key); + var signature_auth_disabled = data_bits.preload_int(size::signature_auth_disabled); var extensions = ds.preload_dict(); ;; Add extension @@ -93,7 +94,7 @@ cell verify_actions(cell c5, int is_external) inline { { (extensions, int success?) = extensions.udict_delete?(256, hash); throw_unless(40, success?); - throw_if(44, null?(extensions) & (stored_seqno < 0)); + throw_if(44, null?(extensions) & (signature_auth_disabled)); } set_data(begin_cell() @@ -104,28 +105,24 @@ cell verify_actions(cell c5, int is_external) inline { elseif (cs~check_and_remove_set_signature_auth_allowed_prefix()) { var allow? = cs~load_int(1); var ds = get_data().begin_parse(); - var stored_seqno = ds~load_int(size::stored_seqno); + var signature_auth_disabled = ds~load_int(size::signature_auth_disabled); + var stored_seqno = ds~load_uint(size::stored_seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions if (allow?) { ;; allow - throw_unless(43, stored_seqno < 0); - ;; Can't be disallowed with 0 because disallowing increments seqno - ;; -123 -> 123 -> 124 - stored_seqno = - stored_seqno; - stored_seqno = stored_seqno + 1; + throw_unless(43, signature_auth_disabled); + signature_auth_disabled = false; } else { ;; disallow - throw_unless(43, stored_seqno >= 0); + throw_if(43, signature_auth_disabled); ds = ds.skip_bits(size::stored_subwallet + size::public_key); var extensions_is_not_null = ds.preload_uint(1); throw_unless(42, extensions_is_not_null); - ;; Corner case: 0 -> 1 -> -1 - ;; 123 -> 124 -> -124 - stored_seqno = stored_seqno + 1; - stored_seqno = - stored_seqno; + signature_auth_disabled = true; } set_data(begin_cell() - .store_int(stored_seqno, size::stored_seqno) + .store_int(signature_auth_disabled, size::signature_auth_disabled) + .store_uint(stored_seqno, size::stored_seqno) .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); } @@ -153,7 +150,7 @@ cell verify_actions(cell c5, int is_external) inline { () process_signed_request(slice full_body, int is_external) impure inline { ifnot (is_external) { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(full_body.slice_bits() < 32 + size::subwallet_id + size::valid_until + size::msg_seqno + 1 + 512); + return_if(full_body.slice_bits() < 32 + size::signature_auth_disabled + size::subwallet_id + size::valid_until + size::msg_seqno + 1 + 512); } ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. @@ -164,7 +161,9 @@ cell verify_actions(cell c5, int is_external) inline { var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); var ds = get_data().begin_parse(); - var stored_seqno = ds~load_int(size::stored_seqno); + var signature_auth_disabled = ds~load_int(size::signature_auth_disabled); + + var stored_seqno = ds~load_uint(size::stored_seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions var stored_subwallet = ds~load_uint(size::stored_subwallet); var public_key = ds.preload_uint(size::public_key); @@ -180,15 +179,16 @@ cell verify_actions(cell c5, int is_external) inline { ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! int signature_is_valid = check_signature(slice_hash(signed), signature, public_key); if (is_external) { + throw_if(32, signature_auth_disabled); throw_unless(35, signature_is_valid); } else { + if (signature_auth_disabled) { + return(); + } ifnot (signature_is_valid) { return(); } } - ;; If public key is disabled, stored_seqno is strictly less than zero: stored_seqno < 0 - ;; However, msg_seqno is uint, therefore it can be only greater or equal to zero: msg_seqno >= 0 - ;; Thus, if public key is disabled, these two domains NEVER intersect, and additional check is not needed throw_unless(33, msg_seqno == stored_seqno); throw_unless(34, subwallet_id == stored_subwallet); throw_if(36, valid_until <= now()); @@ -200,7 +200,8 @@ cell verify_actions(cell c5, int is_external) inline { ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() - .store_int(stored_seqno, size::stored_seqno) + .store_int(false, size::signature_auth_disabled) ;; it cannot be true, otherwise execution would not get here + .store_uint(stored_seqno, size::stored_seqno) .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); @@ -252,7 +253,7 @@ cell verify_actions(cell c5, int is_external) inline { var ds = get_data().begin_parse(); ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed - var extensions = ds.skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key).preload_dict(); + var extensions = ds.skip_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet + size::public_key).preload_dict(); ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). @@ -278,25 +279,25 @@ cell verify_actions(cell c5, int is_external) inline { int seqno() method_id { ;; Use absolute value to do not confuse apps with negative seqno if key is disabled - return abs(get_data().begin_parse().preload_int(size::stored_seqno)); + return get_data().begin_parse().skip_bits(size::signature_auth_disabled).preload_uint(size::stored_seqno); } int get_wallet_id() method_id { - return get_data().begin_parse().skip_bits(size::stored_seqno).preload_uint(size::stored_subwallet); + return get_data().begin_parse().skip_bits(size::signature_auth_disabled + size::stored_seqno).preload_uint(size::stored_subwallet); } int get_public_key() method_id { - var cs = get_data().begin_parse().skip_bits(size::stored_seqno + size::stored_subwallet); + var cs = get_data().begin_parse().skip_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet); return cs.preload_uint(size::public_key); } ;; Returns raw dictionary (or null if empty) where keys are packed addresses and the `wc` is stored in leafs. ;; User should unpack the address using the same packing function using `wc` to restore the original address. cell get_extensions() method_id { - var ds = get_data().begin_parse().skip_bits(size::stored_seqno + size::stored_subwallet + size::public_key); + var ds = get_data().begin_parse().skip_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet + size::public_key); return ds~load_dict(); } int get_is_signature_auth_allowed() method_id { - return get_data().begin_parse().preload_int(size::stored_seqno) >= 0; + return ~ get_data().begin_parse().preload_int(size::signature_auth_disabled); } diff --git a/scripts/deployWalletV5.ts b/scripts/deployWalletV5.ts index 39f87e4e..ce66fbbe 100644 --- a/scripts/deployWalletV5.ts +++ b/scripts/deployWalletV5.ts @@ -15,6 +15,7 @@ export async function run(provider: NetworkProvider) { const walletV5 = provider.open( WalletV5.createFromConfig( { + signature_auth_disabled: false, seqno: 0, walletId: new WalletId({ networkGlobalId: -3 }).serialized, // testnet publicKey: keypair.publicKey, diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index ae91fdd6..9b8522db 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -68,6 +68,7 @@ describe('Wallet V5 extensions auth', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { + signature_auth_disabled: false, seqno: 0, walletId: WALLET_ID.serialized, publicKey: keypair.publicKey, @@ -391,10 +392,7 @@ describe('Wallet V5 extensions auth', () => { expect(isSignatureAuthAllowed1).toEqual(-1); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 2); - - // Allowing or disallowing signature auth increments seqno, need to re-read - seqno = contract_seqno; + expect(contract_seqno).toEqual(seqno); const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue = toNano(0.001); @@ -480,10 +478,7 @@ describe('Wallet V5 extensions auth', () => { expect(isSignatureAuthAllowed1).toEqual(-1); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 2); - - // Allowing or disallowing signature auth increments seqno, need to re-read - seqno = contract_seqno; + expect(contract_seqno).toEqual(seqno); const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue = toNano(0.001); diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 69e26c29..5c6ee973 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -58,6 +58,7 @@ describe('Wallet V5 sign auth external', () => { const _walletV5 = blockchain.openContract( WalletV5.createFromConfig( { + signature_auth_disabled: params?.signature_auth_disabled ?? false, seqno: params?.seqno ?? 0, walletId: params?.walletId ?? WALLET_ID.serialized, publicKey: params?.publicKey ?? _keypair.publicKey, @@ -100,6 +101,7 @@ describe('Wallet V5 sign auth external', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { + signature_auth_disabled: false, seqno: 0, walletId: WALLET_ID.serialized, publicKey: keypair.publicKey, @@ -746,7 +748,7 @@ describe('Wallet V5 sign auth external', () => { expect(isSignatureAuthAllowed).toEqual(0); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 1); + expect(contract_seqno).toEqual(seqno); }); it('Should add ext and disallow signature auth in separate txs', async () => { @@ -797,7 +799,7 @@ describe('Wallet V5 sign auth external', () => { expect(isSignatureAuthAllowed2).toEqual(0); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 1); + expect(contract_seqno).toEqual(seqno); }); it('Should add ext, disallow sign, allow sign, remove ext in one tx; send in other', async () => { @@ -823,10 +825,7 @@ describe('Wallet V5 sign auth external', () => { expect(isSignatureAuthAllowed).toEqual(-1); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 2); - - // Allowing or disallowing signature auth increments seqno, need to re-read - seqno = contract_seqno; + expect(contract_seqno).toEqual(seqno); const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue = toNano(0.001); @@ -932,7 +931,7 @@ describe('Wallet V5 sign auth external', () => { expect(isSignatureAuthAllowed).toEqual(0); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 1); + expect(contract_seqno).toEqual(seqno); await disableConsoleError(() => expect(walletV5.sendExternalSignedMessage(createBody(packActionsList([])))).rejects.toThrow() diff --git a/tests/wallet-v5-get.spec.ts b/tests/wallet-v5-get.spec.ts index 6ec99d20..c6bca5ee 100644 --- a/tests/wallet-v5-get.spec.ts +++ b/tests/wallet-v5-get.spec.ts @@ -31,6 +31,7 @@ describe('Wallet V5 get methods', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { + signature_auth_disabled: params?.signature_auth_disabled ?? false, seqno: params?.seqno ?? 0, walletId: params?.walletId ?? WALLET_ID.serialized, publicKey: params?.publicKey ?? keypair.publicKey, diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 64595209..dc51b7ce 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -51,6 +51,7 @@ describe('Wallet V5 sign auth internal', () => { const _walletV5 = blockchain.openContract( WalletV5.createFromConfig( { + signature_auth_disabled: params?.signature_auth_disabled ?? false, seqno: params?.seqno ?? 0, walletId: params?.walletId ?? WALLET_ID.serialized, publicKey: params?.publicKey ?? _keypair.publicKey, @@ -93,6 +94,7 @@ describe('Wallet V5 sign auth internal', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { + signature_auth_disabled: false, seqno: 0, walletId: WALLET_ID.serialized, publicKey: keypair.publicKey, @@ -941,7 +943,7 @@ describe('Wallet V5 sign auth internal', () => { expect(isSignatureAuthAllowed).toEqual(0); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 1); + expect(contract_seqno).toEqual(seqno); }); it('Should add ext and disallow signature auth in separate txs', async () => { @@ -1008,7 +1010,7 @@ describe('Wallet V5 sign auth internal', () => { expect(isSignatureAuthAllowed2).toEqual(0); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 1); + expect(contract_seqno).toEqual(seqno); }); it('Should add ext, disallow sign, allow sign, remove ext in one tx; send in other', async () => { @@ -1041,10 +1043,7 @@ describe('Wallet V5 sign auth internal', () => { expect(isSignatureAuthAllowed).toEqual(-1); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 2); - - // Allowing or disallowing signature auth increments seqno, need to re-read - seqno = contract_seqno; + expect(contract_seqno).toEqual(seqno); const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue = toNano(0.001); @@ -1175,7 +1174,7 @@ describe('Wallet V5 sign auth internal', () => { expect(isSignatureAuthAllowed).toEqual(0); const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno + 1); + expect(contract_seqno).toEqual(seqno); const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue = toNano(0.001); @@ -1194,7 +1193,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt2.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(33); + ).toEqual(0); expect(receipt2.transactions).not.toHaveTransaction({ from: walletV5.address, diff --git a/types.tlb b/types.tlb index 0ac3940a..6390684b 100644 --- a/types.tlb +++ b/types.tlb @@ -27,4 +27,4 @@ actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; // Contract state wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; -contract_state$_ seqno:int33 wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; +contract_state$_ signature_auth_disabled:(## 1) seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 89c991b5..3d5fd456 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -15,6 +15,7 @@ import { import { bufferToBigInt } from '../tests/utils'; export type WalletV5Config = { + signature_auth_disabled: boolean; seqno: number; walletId: bigint; publicKey: Buffer; @@ -23,7 +24,8 @@ export type WalletV5Config = { export function walletV5ConfigToCell(config: WalletV5Config): Cell { return beginCell() - .storeInt(config.seqno, 33) + .storeBit(config.signature_auth_disabled) + .storeUint(config.seqno, 32) .storeUint(config.walletId, 80) .storeBuffer(config.publicKey, 32) .storeDict(config.extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8)) From a210caabc58b737d4c2fbe6e3676020b5c88dda3 Mon Sep 17 00:00:00 2001 From: Skydev0h Date: Fri, 14 Jun 2024 19:38:39 +0300 Subject: [PATCH 045/121] Fix `getExtensionsArray()` method when address hash starts with `00` Co-authored-by: Dmytro Polunin --- tests/wallet-v5-get.spec.ts | 11 ++++++++--- wrappers/wallet-v5.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/wallet-v5-get.spec.ts b/tests/wallet-v5-get.spec.ts index c6bca5ee..cb36bea0 100644 --- a/tests/wallet-v5-get.spec.ts +++ b/tests/wallet-v5-get.spec.ts @@ -127,8 +127,11 @@ describe('Wallet V5 get methods', () => { }); it('Get extensions array', async () => { - const plugin1 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const plugin2 = Address.parse('EQA2pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX5Bf'); + const plugin1 = Address.parse( + '0:0000F5851B4A185F5F63C0D0CD0412F5ACA353F577DA18FF47C936F99DBD0000' + ); + const plugin2 = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + const plugin3 = Address.parse('EQA2pT4d8T7TyRsjW2BpGpGYga-lMA4JjQb4D2tc1PXMX5Bf'); const extensions: Dictionary = Dictionary.empty( Dictionary.Keys.BigUint(256), @@ -136,13 +139,15 @@ describe('Wallet V5 get methods', () => { ); extensions.set(packAddress(plugin1), BigInt(plugin1.workChain)); extensions.set(packAddress(plugin2), BigInt(plugin2.workChain)); + extensions.set(packAddress(plugin3), BigInt(plugin3.workChain)); await deploy({ extensions }); const actual = await walletV5.getExtensionsArray(); - expect(actual.length).toBe(2); + expect(actual.length).toBe(3); expect(actual[0].equals(plugin1)).toBeTruthy(); expect(actual[1].equals(plugin2)).toBeTruthy(); + expect(actual[2].equals(plugin3)).toBeTruthy(); }); it('Get empty extensions array', async () => { diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 3d5fd456..b62870ef 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -235,7 +235,7 @@ export class WalletV5 implements Contract { return dict.keys().map(key => { const wc = dict.get(key)!; const addressHex = key; - return Address.parseRaw(`${wc}:${addressHex.toString(16)}`); + return Address.parseRaw(`${wc}:${addressHex.toString(16).padStart(64, '0')}`); }); } } From ba7e312deea7b24681b404b6c6bbb5dd9d5bea54 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 10:27:55 +0400 Subject: [PATCH 046/121] add builds to git --- .gitignore | 1 - build/library-deployer.compiled.json | 1 + build/wallet_v5.compiled.json | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 build/library-deployer.compiled.json create mode 100644 build/wallet_v5.compiled.json diff --git a/.gitignore b/.gitignore index ef9f336c..6f054627 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules temp -build .idea .env diff --git a/build/library-deployer.compiled.json b/build/library-deployer.compiled.json new file mode 100644 index 00000000..3acdc095 --- /dev/null +++ b/build/library-deployer.compiled.json @@ -0,0 +1 @@ +{"hex":"b5ee9c72410106010030000114ff00f4a413f4bcf2c80b0102012003020006f2f0010202d1050400193b511cbec1b232483ec13b552000053c00601cfc59c2"} \ No newline at end of file diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json new file mode 100644 index 00000000..823ea0f9 --- /dev/null +++ b/build/wallet_v5.compiled.json @@ -0,0 +1 @@ +{"hex":"b5ee9c7241021301000226000114ff00f4a413f4bcf2c80b0102012004020102f203011420d728239b4b3b74307f0f0201480e050201200706001bbe5f0f6a2684080b8eb90fa021840201200b080201200a090019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760d0c0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff028ed020c702dc01d0d60301c713dc01d72c232bc3a3748ea101fa4030fa44f828fa443058badded44d0810171d721f4058307f40edd3070db3c8e8c3120d72c239b4b73a431dd70e2100f01f08ef5eda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f910289602f26001f2a39e02945f09db31e001945f08db31e1e25122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd81002cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011211005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009001fa4001fa44f828fa443022baf2aded44d0810171d71821d70a0001f405069d3002c8ca0740148307f453f2a79e33048307f45bf2a8206e58b0f26ce2c85003cf1612f400c9ed545d9452a0"} \ No newline at end of file From 1d447047e4d221132cd76734bd91103eccb4629c Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 10:36:52 +0400 Subject: [PATCH 047/121] cosmetic: rename constants (no changes in compiled code) --- contracts/wallet_v5.fc | 54 +++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index e18faf8a..085f50af 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -2,16 +2,12 @@ #include "imports/stdlib.fc"; -const int size::signature_auth_disabled = 1; -const int size::stored_seqno = 32; -const int size::stored_subwallet = 80; +const int size::bool = 1; +const int size::seqno = 32; +const int size::wallet_id = 80; const int size::public_key = 256; - -const int size::subwallet_id = 80; const int size::valid_until = 32; -const int size::msg_seqno = 32; - -const int size::flags = 4; +const int size::message_flags = 4; () return_if(int cond) impure asm "IFRET"; () return_unless(int cond) impure asm "IFNOTRET"; @@ -80,8 +76,8 @@ cell verify_actions(cell c5, int is_external) inline { throw_unless(45, my_wc == wc); var ds = get_data().begin_parse(); - var data_bits = ds~load_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet + size::public_key); - var signature_auth_disabled = data_bits.preload_int(size::signature_auth_disabled); + var data_bits = ds~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); + var signature_auth_disabled = data_bits.preload_int(size::bool); var extensions = ds.preload_dict(); ;; Add extension @@ -105,8 +101,8 @@ cell verify_actions(cell c5, int is_external) inline { elseif (cs~check_and_remove_set_signature_auth_allowed_prefix()) { var allow? = cs~load_int(1); var ds = get_data().begin_parse(); - var signature_auth_disabled = ds~load_int(size::signature_auth_disabled); - var stored_seqno = ds~load_uint(size::stored_seqno); + var signature_auth_disabled = ds~load_int(size::bool); + var stored_seqno = ds~load_uint(size::seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions if (allow?) { ;; allow @@ -115,14 +111,14 @@ cell verify_actions(cell c5, int is_external) inline { } else { ;; disallow throw_if(43, signature_auth_disabled); - ds = ds.skip_bits(size::stored_subwallet + size::public_key); + ds = ds.skip_bits(size::wallet_id + size::public_key); var extensions_is_not_null = ds.preload_uint(1); throw_unless(42, extensions_is_not_null); signature_auth_disabled = true; } set_data(begin_cell() - .store_int(signature_auth_disabled, size::signature_auth_disabled) - .store_uint(stored_seqno, size::stored_seqno) + .store_int(signature_auth_disabled, size::bool) + .store_uint(stored_seqno, size::seqno) .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); } @@ -150,7 +146,7 @@ cell verify_actions(cell c5, int is_external) inline { () process_signed_request(slice full_body, int is_external) impure inline { ifnot (is_external) { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(full_body.slice_bits() < 32 + size::signature_auth_disabled + size::subwallet_id + size::valid_until + size::msg_seqno + 1 + 512); + return_if(full_body.slice_bits() < 32 + size::bool + size::wallet_id + size::valid_until + size::seqno + 1 + 512); } ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. @@ -158,14 +154,14 @@ cell verify_actions(cell c5, int is_external) inline { slice signed = full_body.remove_last_bits(512); var cs = signed.skip_bits(32); - var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::subwallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::msg_seqno)); + var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); var ds = get_data().begin_parse(); - var signature_auth_disabled = ds~load_int(size::signature_auth_disabled); + var signature_auth_disabled = ds~load_int(size::bool); - var stored_seqno = ds~load_uint(size::stored_seqno); + var stored_seqno = ds~load_uint(size::seqno); var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - var stored_subwallet = ds~load_uint(size::stored_subwallet); + var stored_subwallet = ds~load_uint(size::wallet_id); var public_key = ds.preload_uint(size::public_key); ;; Note on bouncing/nonbouncing behaviour: @@ -200,8 +196,8 @@ cell verify_actions(cell c5, int is_external) inline { ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() - .store_int(false, size::signature_auth_disabled) ;; it cannot be true, otherwise execution would not get here - .store_uint(stored_seqno, size::stored_seqno) + .store_int(false, size::bool) ;; it cannot be true, otherwise execution would not get here + .store_uint(stored_seqno, size::seqno) .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); @@ -229,7 +225,7 @@ cell verify_actions(cell c5, int is_external) inline { ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. var full_msg_slice = full_msg.begin_parse(); - var s_flags = full_msg_slice~load_bits(size::flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... + var s_flags = full_msg_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... ;; If bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. return_if(count_trailing_ones(s_flags)); @@ -253,7 +249,7 @@ cell verify_actions(cell c5, int is_external) inline { var ds = get_data().begin_parse(); ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed - var extensions = ds.skip_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet + size::public_key).preload_dict(); + var extensions = ds.skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key).preload_dict(); ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). @@ -279,25 +275,25 @@ cell verify_actions(cell c5, int is_external) inline { int seqno() method_id { ;; Use absolute value to do not confuse apps with negative seqno if key is disabled - return get_data().begin_parse().skip_bits(size::signature_auth_disabled).preload_uint(size::stored_seqno); + return get_data().begin_parse().skip_bits(size::bool).preload_uint(size::seqno); } int get_wallet_id() method_id { - return get_data().begin_parse().skip_bits(size::signature_auth_disabled + size::stored_seqno).preload_uint(size::stored_subwallet); + return get_data().begin_parse().skip_bits(size::bool + size::seqno).preload_uint(size::wallet_id); } int get_public_key() method_id { - var cs = get_data().begin_parse().skip_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet); + var cs = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id); return cs.preload_uint(size::public_key); } ;; Returns raw dictionary (or null if empty) where keys are packed addresses and the `wc` is stored in leafs. ;; User should unpack the address using the same packing function using `wc` to restore the original address. cell get_extensions() method_id { - var ds = get_data().begin_parse().skip_bits(size::signature_auth_disabled + size::stored_seqno + size::stored_subwallet + size::public_key); + var ds = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); return ds~load_dict(); } int get_is_signature_auth_allowed() method_id { - return ~ get_data().begin_parse().preload_int(size::signature_auth_disabled); + return ~ get_data().begin_parse().preload_int(size::bool); } From 99493bc8fe9a0897aa035d642a4a9844e925c338 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 10:40:22 +0400 Subject: [PATCH 048/121] cosmetic: types (no changes in compiled code) --- contracts/wallet_v5.fc | 58 +++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 085f50af..34787208 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -12,11 +12,11 @@ const int size::message_flags = 4; () return_if(int cond) impure asm "IFRET"; () return_unless(int cond) impure asm "IFNOTRET"; -(slice) udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET" "IFNOTRET"; +slice udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET" "IFNOTRET"; (slice, int) begin_parse_xc(cell c) asm "XCTOS"; -(slice) enforce_and_remove_sign_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; +slice enforce_and_remove_sign_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; (slice, int) check_and_remove_extn_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; (slice, int) check_and_remove_sint_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; @@ -25,7 +25,7 @@ const int size::message_flags = 4; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; (slice, int) check_and_remove_set_signature_auth_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; -(slice) enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; +slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; ;; Extensible wallet contract v5 @@ -66,8 +66,8 @@ cell verify_actions(cell c5, int is_external) inline { ;; Recurse into extended actions until we reach standard actions while (cs~load_int(1)) { - var is_add_ext? = cs~check_and_remove_add_extension_prefix(); - var is_del_ext? = is_add_ext? ? 0 : cs~check_and_remove_remove_extension_prefix(); + int is_add_ext? = cs~check_and_remove_add_extension_prefix(); + int is_del_ext? = is_add_ext? ? 0 : cs~check_and_remove_remove_extension_prefix(); ;; Add/remove extensions if (is_add_ext? | is_del_ext?) { (int wc, int hash) = parse_std_addr(cs~load_msg_addr()); @@ -75,10 +75,10 @@ cell verify_actions(cell c5, int is_external) inline { throw_unless(45, my_wc == wc); - var ds = get_data().begin_parse(); - var data_bits = ds~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); - var signature_auth_disabled = data_bits.preload_int(size::bool); - var extensions = ds.preload_dict(); + slice ds = get_data().begin_parse(); + slice data_bits = ds~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); + int signature_auth_disabled = data_bits.preload_int(size::bool); + cell extensions = ds.preload_dict(); ;; Add extension if (is_add_ext?) { @@ -99,11 +99,11 @@ cell verify_actions(cell c5, int is_external) inline { .end_cell()); } elseif (cs~check_and_remove_set_signature_auth_allowed_prefix()) { - var allow? = cs~load_int(1); - var ds = get_data().begin_parse(); - var signature_auth_disabled = ds~load_int(size::bool); - var stored_seqno = ds~load_uint(size::seqno); - var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions + int allow? = cs~load_int(1); + slice ds = get_data().begin_parse(); + int signature_auth_disabled = ds~load_int(size::bool); + int stored_seqno = ds~load_uint(size::seqno); + slice immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions if (allow?) { ;; allow throw_unless(43, signature_auth_disabled); @@ -112,7 +112,7 @@ cell verify_actions(cell c5, int is_external) inline { ;; disallow throw_if(43, signature_auth_disabled); ds = ds.skip_bits(size::wallet_id + size::public_key); - var extensions_is_not_null = ds.preload_uint(1); + int extensions_is_not_null = ds.preload_uint(1); throw_unless(42, extensions_is_not_null); signature_auth_disabled = true; } @@ -153,16 +153,16 @@ cell verify_actions(cell c5, int is_external) inline { slice signature = full_body.get_last_bits(512); slice signed = full_body.remove_last_bits(512); - var cs = signed.skip_bits(32); - var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); + slice cs = signed.skip_bits(32); + (int subwallet_id, int valid_until, int msg_seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); - var ds = get_data().begin_parse(); - var signature_auth_disabled = ds~load_int(size::bool); + slice ds = get_data().begin_parse(); + int signature_auth_disabled = ds~load_int(size::bool); - var stored_seqno = ds~load_uint(size::seqno); - var immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - var stored_subwallet = ds~load_uint(size::wallet_id); - var public_key = ds.preload_uint(size::public_key); + int stored_seqno = ds~load_uint(size::seqno); + slice immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions + int stored_subwallet = ds~load_uint(size::wallet_id); + int public_key = ds.preload_uint(size::public_key); ;; Note on bouncing/nonbouncing behaviour: ;; In principle, the wallet should not bounce incoming messages as to avoid @@ -223,9 +223,9 @@ cell verify_actions(cell c5, int is_external) inline { return_if(body.slice_refs_empty?()); ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. - var full_msg_slice = full_msg.begin_parse(); + slice full_msg_slice = full_msg.begin_parse(); - var s_flags = full_msg_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... + slice s_flags = full_msg_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... ;; If bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. return_if(count_trailing_ones(s_flags)); @@ -247,9 +247,9 @@ cell verify_actions(cell c5, int is_external) inline { return_unless(my_wc == sender_wc); - var ds = get_data().begin_parse(); + slice ds = get_data().begin_parse(); ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed - var extensions = ds.skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key).preload_dict(); + cell extensions = ds.skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key).preload_dict(); ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). @@ -283,14 +283,14 @@ int get_wallet_id() method_id { } int get_public_key() method_id { - var cs = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id); + slice cs = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id); return cs.preload_uint(size::public_key); } ;; Returns raw dictionary (or null if empty) where keys are packed addresses and the `wc` is stored in leafs. ;; User should unpack the address using the same packing function using `wc` to restore the original address. cell get_extensions() method_id { - var ds = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); + slice ds = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); return ds~load_dict(); } From 0d5cc02ee1a240acb3c8fe6f02735c9d394f313b Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 11:22:50 +0400 Subject: [PATCH 049/121] cosmetic: rename vars, avoid abbreviations (no changes in compiled code) --- contracts/wallet_v5.fc | 200 ++++++++++++++++++++--------------------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 34787208..b77b5c47 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -9,28 +9,28 @@ const int size::public_key = 256; const int size::valid_until = 32; const int size::message_flags = 4; -() return_if(int cond) impure asm "IFRET"; -() return_unless(int cond) impure asm "IFNOTRET"; +() return_if(int condition) impure asm "IFRET"; +() return_unless(int condition) impure asm "IFNOTRET"; -slice udict_get_or_return(cell dict, int key_len, int index) impure asm(index dict key_len) "DICTUGET" "IFNOTRET"; +slice udict_get_or_return(cell dict, int key_length, int index) impure asm(index dict key_length) "DICTUGET" "IFNOTRET"; -(slice, int) begin_parse_xc(cell c) asm "XCTOS"; +(slice, int) begin_parse_raw(cell c) asm "XCTOS"; -slice enforce_and_remove_sign_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; -(slice, int) check_and_remove_extn_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; -(slice, int) check_and_remove_sint_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; +slice enforce_and_remove_signed_external_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; +(slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; +(slice, int) check_and_remove_signed_internal_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; ;; (slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; -(slice, int) check_and_remove_set_signature_auth_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; +(slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; ;; Extensible wallet contract v5 ;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. -() set_actions(cell action_list) impure asm "c5 POP"; +() set_c5_actions(cell action_list) impure asm "c5 POP"; int count_leading_zeroes(slice cs) asm "SDCNTLEAD0"; int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; @@ -42,19 +42,19 @@ int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; slice get_last_bits(slice s, int n) asm "SDCUTLAST"; slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; -cell verify_actions(cell c5, int is_external) inline { +cell verify_c5_actions(cell c5, int is_external) inline { ;; Comment out code starting from here to disable checks (unsafe version) ;; {- - (slice c5s, _) = c5.begin_parse_xc(); - return_if(c5s.slice_empty?()); + (slice cs, _) = c5.begin_parse_raw(); + return_if(cs.slice_empty?()); do { ;; only send_msg is allowed, set_code or reserve_currency are not - c5s = c5s.enforce_and_remove_action_send_msg_prefix(); + cs = cs.enforce_and_remove_action_send_msg_prefix(); ;; enforce that send_mode has 2 bit set ;; for that load 7 bits and make sure that they end with 1 - throw_if(37, is_external & count_trailing_zeroes(c5s.preload_bits(7))); - (c5s, _) = c5s.preload_ref().begin_parse_xc(); - } until (c5s.slice_empty?()); + throw_if(37, is_external & count_trailing_zeroes(cs.preload_bits(7))); + (cs, _) = cs.preload_ref().begin_parse_raw(); + } until (cs.slice_empty?()); ;; -} return c5; } @@ -62,64 +62,64 @@ cell verify_actions(cell c5, int is_external) inline { ;; Dispatches already authenticated request. ;; this function is explicitly included as an inline reference - not completely inlined ;; completely inlining it causes undesirable code split and noticeable gas increase in some paths -() dispatch_complex_request(slice cs, int is_external) impure inline_ref { +() process_actions(slice cs, int is_external) impure inline_ref { ;; Recurse into extended actions until we reach standard actions while (cs~load_int(1)) { - int is_add_ext? = cs~check_and_remove_add_extension_prefix(); - int is_del_ext? = is_add_ext? ? 0 : cs~check_and_remove_remove_extension_prefix(); + int is_add_extension = cs~check_and_remove_add_extension_prefix(); + int is_remove_extension = is_add_extension ? 0 : cs~check_and_remove_remove_extension_prefix(); ;; Add/remove extensions - if (is_add_ext? | is_del_ext?) { - (int wc, int hash) = parse_std_addr(cs~load_msg_addr()); - (int my_wc, _) = parse_std_addr(my_address()); + if (is_add_extension | is_remove_extension) { + (int address_wc, int address_hash) = parse_std_addr(cs~load_msg_addr()); + (int my_address_wc, _) = parse_std_addr(my_address()); - throw_unless(45, my_wc == wc); + throw_unless(45, my_address_wc == address_wc); - slice ds = get_data().begin_parse(); - slice data_bits = ds~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); - int signature_auth_disabled = data_bits.preload_int(size::bool); - cell extensions = ds.preload_dict(); + slice data_slice = get_data().begin_parse(); + slice data_slice_before_extensions = data_slice~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); + int is_signature_disabled = data_slice_before_extensions.preload_int(size::bool); + cell extensions = data_slice.preload_dict(); ;; Add extension - if (is_add_ext?) { - (extensions, int success?) = extensions.udict_add_builder?(256, hash, begin_cell().store_int(wc,8)); - throw_unless(39, success?); + if (is_add_extension) { + (extensions, int is_success) = extensions.udict_add_builder?(256, address_hash, begin_cell().store_int(address_wc,8)); + throw_unless(39, is_success); } else ;; Remove extension if (op == 0x5eaef4a4) ;; It can be ONLY 0x1c40db9f OR 0x5eaef4a4 here. No need for second check. { - (extensions, int success?) = extensions.udict_delete?(256, hash); - throw_unless(40, success?); - throw_if(44, null?(extensions) & (signature_auth_disabled)); + (extensions, int is_success) = extensions.udict_delete?(256, address_hash); + throw_unless(40, is_success); + throw_if(44, null?(extensions) & is_signature_disabled); } set_data(begin_cell() - .store_slice(data_bits) + .store_slice(data_slice_before_extensions) .store_dict(extensions) .end_cell()); } - elseif (cs~check_and_remove_set_signature_auth_allowed_prefix()) { - int allow? = cs~load_int(1); - slice ds = get_data().begin_parse(); - int signature_auth_disabled = ds~load_int(size::bool); - int stored_seqno = ds~load_uint(size::seqno); - slice immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - if (allow?) { + elseif (cs~check_and_remove_set_signature_allowed_prefix()) { + int allow_signature = cs~load_int(1); + slice data_slice = get_data().begin_parse(); + int is_signature_disabled = data_slice~load_int(size::bool); + int stored_seqno = data_slice~load_uint(size::seqno); + slice data_tail = data_slice; ;; stored_subwallet ~ public_key ~ extensions + if (allow_signature) { ;; allow - throw_unless(43, signature_auth_disabled); - signature_auth_disabled = false; + throw_unless(43, is_signature_disabled); + is_signature_disabled = false; } else { ;; disallow - throw_if(43, signature_auth_disabled); - ds = ds.skip_bits(size::wallet_id + size::public_key); - int extensions_is_not_null = ds.preload_uint(1); - throw_unless(42, extensions_is_not_null); - signature_auth_disabled = true; + throw_if(43, is_signature_disabled); + data_slice = data_slice.skip_bits(size::wallet_id + size::public_key); + int is_extensions_not_empty = data_slice.preload_uint(1); + throw_unless(42, is_extensions_not_empty); + is_signature_disabled = true; } set_data(begin_cell() - .store_int(signature_auth_disabled, size::bool) + .store_int(is_signature_disabled, size::bool) .store_uint(stored_seqno, size::seqno) - .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions + .store_slice(data_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); } ;; Uncomment to allow set_data (for unsafe version) @@ -136,33 +136,33 @@ cell verify_actions(cell c5, int is_external) inline { } ;; At this point we are at `action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0;` ;; Simply set the C5 register with all pre-computed actions after verification: - set_actions(cs.preload_ref().verify_actions(is_external)); + set_c5_actions(cs.preload_ref().verify_c5_actions(is_external)); return (); } ;; ------------------------------------------------------------------------------------------------ ;; Verifies signed request, prevents replays and proceeds with `dispatch_request`. -() process_signed_request(slice full_body, int is_external) impure inline { +() process_signed_request(slice in_msg_body, int is_external) impure inline { ifnot (is_external) { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(full_body.slice_bits() < 32 + size::bool + size::wallet_id + size::valid_until + size::seqno + 1 + 512); + return_if(in_msg_body.slice_bits() < 32 + size::bool + size::wallet_id + size::valid_until + size::seqno + 1 + 512); } ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. - slice signature = full_body.get_last_bits(512); - slice signed = full_body.remove_last_bits(512); + slice signature = in_msg_body.get_last_bits(512); + slice signed_slice = in_msg_body.remove_last_bits(512); - slice cs = signed.skip_bits(32); - (int subwallet_id, int valid_until, int msg_seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); + slice cs = signed_slice.skip_bits(32); ;; skip signed_internal or signer_external prefix + (int wallet_id, int valid_until, int seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); - slice ds = get_data().begin_parse(); - int signature_auth_disabled = ds~load_int(size::bool); + slice data_slice = get_data().begin_parse(); + int is_signature_disabled = data_slice~load_int(size::bool); - int stored_seqno = ds~load_uint(size::seqno); - slice immutable_tail = ds; ;; stored_subwallet ~ public_key ~ extensions - int stored_subwallet = ds~load_uint(size::wallet_id); - int public_key = ds.preload_uint(size::public_key); + int stored_seqno = data_slice~load_uint(size::seqno); + slice data_tail = data_slice; ;; stored_subwallet ~ public_key ~ extensions + int stored_wallet_id = data_slice~load_uint(size::wallet_id); + int public_key = data_slice.preload_uint(size::public_key); ;; Note on bouncing/nonbouncing behaviour: ;; In principle, the wallet should not bounce incoming messages as to avoid @@ -173,20 +173,20 @@ cell verify_actions(cell c5, int is_external) inline { ;; but failing with exception (therefore bouncing) after the signature check. ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! - int signature_is_valid = check_signature(slice_hash(signed), signature, public_key); + int is_signature_valid = check_signature(slice_hash(signed_slice), signature, public_key); if (is_external) { - throw_if(32, signature_auth_disabled); - throw_unless(35, signature_is_valid); + throw_if(32, is_signature_disabled); + throw_unless(35, is_signature_valid); } else { - if (signature_auth_disabled) { - return(); + if (is_signature_disabled) { + return (); } - ifnot (signature_is_valid) { - return(); + ifnot (is_signature_valid) { + return (); } } - throw_unless(33, msg_seqno == stored_seqno); - throw_unless(34, subwallet_id == stored_subwallet); + throw_unless(33, seqno == stored_seqno); + throw_unless(34, wallet_id == stored_wallet_id); throw_if(36, valid_until <= now()); if (is_external) { @@ -198,37 +198,37 @@ cell verify_actions(cell c5, int is_external) inline { set_data(begin_cell() .store_int(false, size::bool) ;; it cannot be true, otherwise execution would not get here .store_uint(stored_seqno, size::seqno) - .store_slice(immutable_tail) ;; stored_subwallet ~ public_key ~ extensions + .store_slice(data_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); commit(); - dispatch_complex_request(cs, is_external); + process_actions(cs, is_external); } -() recv_external(slice body) impure inline { - slice full_body = body; +() recv_external(slice in_msg_body) impure inline { + slice body = in_msg_body; ;; 0x7369676E ("sign") external message authenticated by signature - body = enforce_and_remove_sign_prefix(body); - process_signed_request(full_body, true); + in_msg_body = enforce_and_remove_signed_external_prefix(in_msg_body); + process_signed_request(body, true); return(); } ;; ------------------------------------------------------------------------------------------------ -() recv_internal(cell full_msg, slice body) impure inline { +() recv_internal(cell in_msg_full, slice in_msg_body) impure inline { ;; return right away if there are no references ;; correct messages always have a ref, because any code paths ends with preload_ref - return_if(body.slice_refs_empty?()); + return_if(in_msg_body.slice_refs_empty?()); ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. - slice full_msg_slice = full_msg.begin_parse(); + slice in_msg_full_slice = in_msg_full.begin_parse(); - slice s_flags = full_msg_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... + slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... ;; If bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. - return_if(count_trailing_ones(s_flags)); + return_if(count_trailing_ones(message_flags_slice)); ;; slicy_return_if_bounce(begin_cell().store_uint(3, 4).end_cell().begin_parse()); ;; TEST!!! @@ -236,36 +236,36 @@ cell verify_actions(cell c5, int is_external) inline { ;; - 0x6578746E "extn" authenticated by extension ;; - 0x73696E74 "sint" internal message authenticated by signature - (body, int is_extn?) = check_and_remove_extn_prefix(body); ;; 0x6578746E ("extn") + (in_msg_body, int is_extension_action) = check_and_remove_extension_action_prefix(in_msg_body); ;; 0x6578746E ("extn") ;; IFJMPREF because unconditionally returns inside - if (is_extn?) { ;; "extn" authenticated by extension + if (is_extension_action) { ;; "extn" authenticated by extension ;; Authenticate extension by its address. - (int sender_wc, int sender_hash) = parse_std_addr(full_msg_slice~load_msg_addr()); ;; no PLDMSGADDR exists - (int my_wc, _) = parse_std_addr(my_address()); + (int sender_address_wc, int sender_address_hash) = parse_std_addr(in_msg_full_slice~load_msg_addr()); ;; no PLDMSGADDR exists + (int my_address_wc, _) = parse_std_addr(my_address()); - return_unless(my_wc == sender_wc); + return_unless(my_address_wc == sender_address_wc); - slice ds = get_data().begin_parse(); + slice data_slice = get_data().begin_parse(); ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed - cell extensions = ds.skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key).preload_dict(); + cell extensions = data_slice.skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key).preload_dict(); ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). - extensions.udict_get_or_return(256, sender_hash); ;; kindof ifnot (success?) { return(); } + extensions.udict_get_or_return(256, sender_address_hash); ;; kindof ifnot (success?) { return(); } - dispatch_complex_request(body, false); + process_actions(in_msg_body, false); return (); } - slice full_body = body; - (_, int is_sint?) = check_and_remove_sint_prefix(body); ;; 0x73696E74 ("sint") - sign internal - return_unless(is_sint?); + slice body = in_msg_body; + (_, int is_signed_internal) = check_and_remove_signed_internal_prefix(in_msg_body); ;; 0x73696E74 ("sint") - sign internal + return_unless(is_signed_internal); ;; Process the rest of the slice just like the signed request. - process_signed_request(full_body, false); + process_signed_request(body, false); return (); ;; Explicit returns escape function faster and const less gas (suddenly!) } @@ -283,15 +283,15 @@ int get_wallet_id() method_id { } int get_public_key() method_id { - slice cs = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id); - return cs.preload_uint(size::public_key); + slice data_slice = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id); + return data_slice.preload_uint(size::public_key); } ;; Returns raw dictionary (or null if empty) where keys are packed addresses and the `wc` is stored in leafs. ;; User should unpack the address using the same packing function using `wc` to restore the original address. cell get_extensions() method_id { - slice ds = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); - return ds~load_dict(); + slice data_slice = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); + return data_slice~load_dict(); } int get_is_signature_auth_allowed() method_id { From ce56813db3f2910ac5f138ec8f6f7b9999b89c69 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 11:32:20 +0400 Subject: [PATCH 050/121] cosmetic: remove comments about optimizations (no changes in compiled code) --- contracts/wallet_v5.fc | 96 +++++++++--------------------------------- 1 file changed, 21 insertions(+), 75 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index b77b5c47..d6d1acea 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -20,51 +20,38 @@ slice enforce_and_remove_signed_external_prefix(slice body) impure asm "x{736967 (slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; (slice, int) check_and_remove_signed_internal_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; -;; (slice, int) check_and_remove_set_data_prefix(slice body) impure asm "x{1ff8ea0b} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; (slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; -;; Extensible wallet contract v5 - ;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. () set_c5_actions(cell action_list) impure asm "c5 POP"; -int count_leading_zeroes(slice cs) asm "SDCNTLEAD0"; int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; -;; (slice, slice) split(slice s, int bits, int refs) asm "SPLIT"; -;; (slice, slice, int) split?(slice s, int bits, int refs) asm "SPLIT" "NULLSWAPIFNOT"; - slice get_last_bits(slice s, int n) asm "SDCUTLAST"; slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; cell verify_c5_actions(cell c5, int is_external) inline { - ;; Comment out code starting from here to disable checks (unsafe version) - ;; {- (slice cs, _) = c5.begin_parse_raw(); return_if(cs.slice_empty?()); + do { ;; only send_msg is allowed, set_code or reserve_currency are not cs = cs.enforce_and_remove_action_send_msg_prefix(); - ;; enforce that send_mode has 2 bit set - ;; for that load 7 bits and make sure that they end with 1 + ;; enforce that send_mode has 2 bit set, for that load 7 bits and make sure that they end with 1 throw_if(37, is_external & count_trailing_zeroes(cs.preload_bits(7))); (cs, _) = cs.preload_ref().begin_parse_raw(); } until (cs.slice_empty?()); - ;; -} + return c5; } -;; Dispatches already authenticated request. -;; this function is explicitly included as an inline reference - not completely inlined -;; completely inlining it causes undesirable code split and noticeable gas increase in some paths () process_actions(slice cs, int is_external) impure inline_ref { - - ;; Recurse into extended actions until we reach standard actions + ;; Loop extended actions until we reach standard actions while (cs~load_int(1)) { int is_add_extension = cs~check_and_remove_add_extension_prefix(); int is_remove_extension = is_add_extension ? 0 : cs~check_and_remove_remove_extension_prefix(); @@ -84,10 +71,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { if (is_add_extension) { (extensions, int is_success) = extensions.udict_add_builder?(256, address_hash, begin_cell().store_int(address_wc,8)); throw_unless(39, is_success); - } else - ;; Remove extension if (op == 0x5eaef4a4) - ;; It can be ONLY 0x1c40db9f OR 0x5eaef4a4 here. No need for second check. - { + } else { ;; Remove extension (extensions, int is_success) = extensions.udict_delete?(256, address_hash); throw_unless(40, is_success); throw_if(44, null?(extensions) & is_signature_disabled); @@ -97,19 +81,17 @@ cell verify_c5_actions(cell c5, int is_external) inline { .store_slice(data_slice_before_extensions) .store_dict(extensions) .end_cell()); - } - elseif (cs~check_and_remove_set_signature_allowed_prefix()) { + + } elseif (cs~check_and_remove_set_signature_allowed_prefix()) { int allow_signature = cs~load_int(1); slice data_slice = get_data().begin_parse(); int is_signature_disabled = data_slice~load_int(size::bool); int stored_seqno = data_slice~load_uint(size::seqno); slice data_tail = data_slice; ;; stored_subwallet ~ public_key ~ extensions - if (allow_signature) { - ;; allow + if (allow_signature) { ;; allow throw_unless(43, is_signature_disabled); is_signature_disabled = false; - } else { - ;; disallow + } else { ;; disallow throw_if(43, is_signature_disabled); data_slice = data_slice.skip_bits(size::wallet_id + size::public_key); int is_extensions_not_empty = data_slice.preload_uint(1); @@ -121,20 +103,11 @@ cell verify_c5_actions(cell c5, int is_external) inline { .store_uint(stored_seqno, size::seqno) .store_slice(data_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); - } - ;; Uncomment to allow set_data (for unsafe version) - {- - elseif (cs~check_and_remove_set_data_prefix()) { - set_data(cs~load_ref()); - } - -} - else { - ;; need to throw on unsupported actions for correct flow and for testability + } else { throw(41); ;; unsupported action } cs = cs.preload_ref().begin_parse(); } - ;; At this point we are at `action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0;` ;; Simply set the C5 register with all pre-computed actions after verification: set_c5_actions(cs.preload_ref().verify_c5_actions(is_external)); return (); @@ -142,14 +115,12 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ -;; Verifies signed request, prevents replays and proceeds with `dispatch_request`. () process_signed_request(slice in_msg_body, int is_external) impure inline { ifnot (is_external) { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) return_if(in_msg_body.slice_bits() < 32 + size::bool + size::wallet_id + size::valid_until + size::seqno + 1 + 512); } - ;; The precise order of operations here is VERY important. Any other order results in unneccessary stack shuffles. slice signature = in_msg_body.get_last_bits(512); slice signed_slice = in_msg_body.remove_last_bits(512); @@ -164,15 +135,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { int stored_wallet_id = data_slice~load_uint(size::wallet_id); int public_key = data_slice.preload_uint(size::public_key); - ;; Note on bouncing/nonbouncing behaviour: - ;; In principle, the wallet should not bounce incoming messages as to avoid - ;; returning deposits back to the sender due to opcode misinterpretation. - ;; However, specifically for "gasless" transactions (signed messages relayed by a 3rd party), - ;; there is a risk for the relaying party to be abused: their coins should be bounced back in case of a race condition or delays. - ;; We resolve this dilemma by silently failing at the signature check (therefore ordinary deposits with arbitrary opcodes never bounce), - ;; but failing with exception (therefore bouncing) after the signature check. - - ;; TODO: Consider moving signed into separate ref, slice_hash consumes 500 gas just like cell creation! int is_signature_valid = check_signature(slice_hash(signed_slice), signature, public_key); if (is_external) { throw_if(32, is_signature_disabled); @@ -196,7 +158,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() - .store_int(false, size::bool) ;; it cannot be true, otherwise execution would not get here + .store_int(false, size::bool) .store_uint(stored_seqno, size::seqno) .store_slice(data_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); @@ -208,7 +170,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { () recv_external(slice in_msg_body) impure inline { slice body = in_msg_body; - ;; 0x7369676E ("sign") external message authenticated by signature in_msg_body = enforce_and_remove_signed_external_prefix(in_msg_body); process_signed_request(body, true); return(); @@ -217,43 +178,31 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ () recv_internal(cell in_msg_full, slice in_msg_body) impure inline { + return_if(in_msg_body.slice_refs_empty?()); ;; message with actions always have a ref - ;; return right away if there are no references - ;; correct messages always have a ref, because any code paths ends with preload_ref - return_if(in_msg_body.slice_refs_empty?()); - - ;; Any attempt to postpone msg_value deletion will result in s2 POP -> SWAP change. No use at all. slice in_msg_full_slice = in_msg_full.begin_parse(); - slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt ... + slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool - ;; If bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. + ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. return_if(count_trailing_ones(message_flags_slice)); - ;; slicy_return_if_bounce(begin_cell().store_uint(3, 4).end_cell().begin_parse()); ;; TEST!!! + (in_msg_body, int is_extension_action) = check_and_remove_extension_action_prefix(in_msg_body); - ;; We accept two kinds of authenticated messages: - ;; - 0x6578746E "extn" authenticated by extension - ;; - 0x73696E74 "sint" internal message authenticated by signature - - (in_msg_body, int is_extension_action) = check_and_remove_extension_action_prefix(in_msg_body); ;; 0x6578746E ("extn") - - ;; IFJMPREF because unconditionally returns inside - if (is_extension_action) { ;; "extn" authenticated by extension + if (is_extension_action) { ;; Authenticate extension by its address. - (int sender_address_wc, int sender_address_hash) = parse_std_addr(in_msg_full_slice~load_msg_addr()); ;; no PLDMSGADDR exists + (int sender_address_wc, int sender_address_hash) = parse_std_addr(in_msg_full_slice~load_msg_addr()); (int my_address_wc, _) = parse_std_addr(my_address()); return_unless(my_address_wc == sender_address_wc); slice data_slice = get_data().begin_parse(); - ;; It is not required to read this data here, maybe ext is doing simple transfer where those are not needed cell extensions = data_slice.skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key).preload_dict(); ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). - extensions.udict_get_or_return(256, sender_address_hash); ;; kindof ifnot (success?) { return(); } + extensions.udict_get_or_return(256, sender_address_hash); process_actions(in_msg_body, false); return (); @@ -261,12 +210,11 @@ cell verify_c5_actions(cell c5, int is_external) inline { } slice body = in_msg_body; - (_, int is_signed_internal) = check_and_remove_signed_internal_prefix(in_msg_body); ;; 0x73696E74 ("sint") - sign internal + (_, int is_signed_internal) = check_and_remove_signed_internal_prefix(in_msg_body); return_unless(is_signed_internal); - ;; Process the rest of the slice just like the signed request. process_signed_request(body, false); - return (); ;; Explicit returns escape function faster and const less gas (suddenly!) + return (); } @@ -274,7 +222,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Get methods int seqno() method_id { - ;; Use absolute value to do not confuse apps with negative seqno if key is disabled return get_data().begin_parse().skip_bits(size::bool).preload_uint(size::seqno); } @@ -287,8 +234,7 @@ int get_public_key() method_id { return data_slice.preload_uint(size::public_key); } -;; Returns raw dictionary (or null if empty) where keys are packed addresses and the `wc` is stored in leafs. -;; User should unpack the address using the same packing function using `wc` to restore the original address. +;; Returns raw dictionary (or null if empty) where keys are address hashes. Workchains of extensions are same with wallet smart contract workchain cell get_extensions() method_id { slice data_slice = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); return data_slice~load_dict(); From b58bbb97731cd6a348691798f2c2ca3d3087f17f Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 11:37:23 +0400 Subject: [PATCH 051/121] cosmetic: add additional consts for sizes (no changes in compiled code) --- contracts/wallet_v5.fc | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index d6d1acea..f5443b76 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -8,6 +8,9 @@ const int size::wallet_id = 80; const int size::public_key = 256; const int size::valid_until = 32; const int size::message_flags = 4; +const int size::signature = 512; +const int size::message_type_prefix = 32; +const int size::address_hash_size = 256; () return_if(int condition) impure asm "IFRET"; () return_unless(int condition) impure asm "IFNOTRET"; @@ -69,10 +72,10 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Add extension if (is_add_extension) { - (extensions, int is_success) = extensions.udict_add_builder?(256, address_hash, begin_cell().store_int(address_wc,8)); + (extensions, int is_success) = extensions.udict_add_builder?(size::address_hash_size, address_hash, begin_cell().store_int(address_wc,8)); throw_unless(39, is_success); } else { ;; Remove extension - (extensions, int is_success) = extensions.udict_delete?(256, address_hash); + (extensions, int is_success) = extensions.udict_delete?(size::address_hash_size, address_hash); throw_unless(40, is_success); throw_if(44, null?(extensions) & is_signature_disabled); } @@ -118,13 +121,13 @@ cell verify_c5_actions(cell c5, int is_external) inline { () process_signed_request(slice in_msg_body, int is_external) impure inline { ifnot (is_external) { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(in_msg_body.slice_bits() < 32 + size::bool + size::wallet_id + size::valid_until + size::seqno + 1 + 512); + return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::bool + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); } - slice signature = in_msg_body.get_last_bits(512); - slice signed_slice = in_msg_body.remove_last_bits(512); + slice signature = in_msg_body.get_last_bits(size::signature); + slice signed_slice = in_msg_body.remove_last_bits(size::signature); - slice cs = signed_slice.skip_bits(32); ;; skip signed_internal or signer_external prefix + slice cs = signed_slice.skip_bits(size::message_type_prefix); ;; skip signed_internal or signer_external prefix (int wallet_id, int valid_until, int seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); slice data_slice = get_data().begin_parse(); @@ -202,7 +205,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). - extensions.udict_get_or_return(256, sender_address_hash); + extensions.udict_get_or_return(size::address_hash_size, sender_address_hash); process_actions(in_msg_body, false); return (); From f1f4cb4bd969f7e167e638dabd58685ffc9d1692 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 11:48:46 +0400 Subject: [PATCH 052/121] cosmetic: add error const (no changes in compiled code) --- contracts/wallet_v5.fc | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index f5443b76..19d44dd9 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -2,6 +2,20 @@ #include "imports/stdlib.fc"; +const int error::invalid_c5_action = 37; +const int error::extension_wrong_workchain = 45; +const int error::add_extension_error = 39; +const int error::remove_extension_error = 40; +const int error::disable_signature_when_extensions_is_empty = 44; +const int error::this_signature_mode_already_set = 43; +const int error::remove_last_extension_when_signature_disabled = 42; +const int error::unspported_action = 41; +const int error::signature_disabled = 32; +const int error::invalid_signature = 35; +const int error::invalid_seqno = 33; +const int error::invalid_wallet_id = 34; +const int error::expired = 36; + const int size::bool = 1; const int size::seqno = 32; const int size::wallet_id = 80; @@ -46,7 +60,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; only send_msg is allowed, set_code or reserve_currency are not cs = cs.enforce_and_remove_action_send_msg_prefix(); ;; enforce that send_mode has 2 bit set, for that load 7 bits and make sure that they end with 1 - throw_if(37, is_external & count_trailing_zeroes(cs.preload_bits(7))); + throw_if(error::invalid_c5_action, is_external & count_trailing_zeroes(cs.preload_bits(7))); (cs, _) = cs.preload_ref().begin_parse_raw(); } until (cs.slice_empty?()); @@ -63,7 +77,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { (int address_wc, int address_hash) = parse_std_addr(cs~load_msg_addr()); (int my_address_wc, _) = parse_std_addr(my_address()); - throw_unless(45, my_address_wc == address_wc); + throw_unless(error::extension_wrong_workchain, my_address_wc == address_wc); slice data_slice = get_data().begin_parse(); slice data_slice_before_extensions = data_slice~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); @@ -73,11 +87,11 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Add extension if (is_add_extension) { (extensions, int is_success) = extensions.udict_add_builder?(size::address_hash_size, address_hash, begin_cell().store_int(address_wc,8)); - throw_unless(39, is_success); + throw_unless( error::add_extension_error, is_success); } else { ;; Remove extension (extensions, int is_success) = extensions.udict_delete?(size::address_hash_size, address_hash); - throw_unless(40, is_success); - throw_if(44, null?(extensions) & is_signature_disabled); + throw_unless(error::remove_extension_error, is_success); + throw_if(error::disable_signature_when_extensions_is_empty, null?(extensions) & is_signature_disabled); } set_data(begin_cell() @@ -92,13 +106,13 @@ cell verify_c5_actions(cell c5, int is_external) inline { int stored_seqno = data_slice~load_uint(size::seqno); slice data_tail = data_slice; ;; stored_subwallet ~ public_key ~ extensions if (allow_signature) { ;; allow - throw_unless(43, is_signature_disabled); + throw_unless(error::this_signature_mode_already_set, is_signature_disabled); is_signature_disabled = false; } else { ;; disallow - throw_if(43, is_signature_disabled); + throw_if(error::this_signature_mode_already_set, is_signature_disabled); data_slice = data_slice.skip_bits(size::wallet_id + size::public_key); int is_extensions_not_empty = data_slice.preload_uint(1); - throw_unless(42, is_extensions_not_empty); + throw_unless(error::remove_last_extension_when_signature_disabled, is_extensions_not_empty); is_signature_disabled = true; } set_data(begin_cell() @@ -107,7 +121,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { .store_slice(data_tail) ;; stored_subwallet ~ public_key ~ extensions .end_cell()); } else { - throw(41); ;; unsupported action + throw(error::unspported_action); } cs = cs.preload_ref().begin_parse(); } @@ -140,8 +154,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { int is_signature_valid = check_signature(slice_hash(signed_slice), signature, public_key); if (is_external) { - throw_if(32, is_signature_disabled); - throw_unless(35, is_signature_valid); + throw_if(error::signature_disabled, is_signature_disabled); + throw_unless( error::invalid_signature, is_signature_valid); } else { if (is_signature_disabled) { return (); @@ -150,9 +164,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { return (); } } - throw_unless(33, seqno == stored_seqno); - throw_unless(34, wallet_id == stored_wallet_id); - throw_if(36, valid_until <= now()); + throw_unless(error::invalid_seqno, seqno == stored_seqno); + throw_unless(error::invalid_wallet_id, wallet_id == stored_wallet_id); + throw_if(error::expired, valid_until <= now()); if (is_external) { accept_message(); From 0537f52437da10a96f12351fe11e3e6aab3f8726 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 11:54:55 +0400 Subject: [PATCH 053/121] cosmetic: subwallet_id->wallet_id in comments (no changes in compiled code) --- contracts/wallet_v5.fc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 19d44dd9..5097ae1f 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -104,7 +104,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice data_slice = get_data().begin_parse(); int is_signature_disabled = data_slice~load_int(size::bool); int stored_seqno = data_slice~load_uint(size::seqno); - slice data_tail = data_slice; ;; stored_subwallet ~ public_key ~ extensions + slice data_tail = data_slice; ;; wallet_id, public_key, extensions if (allow_signature) { ;; allow throw_unless(error::this_signature_mode_already_set, is_signature_disabled); is_signature_disabled = false; @@ -118,7 +118,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { set_data(begin_cell() .store_int(is_signature_disabled, size::bool) .store_uint(stored_seqno, size::seqno) - .store_slice(data_tail) ;; stored_subwallet ~ public_key ~ extensions + .store_slice(data_tail) ;; wallet_id, public_key, extensions .end_cell()); } else { throw(error::unspported_action); @@ -148,7 +148,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { int is_signature_disabled = data_slice~load_int(size::bool); int stored_seqno = data_slice~load_uint(size::seqno); - slice data_tail = data_slice; ;; stored_subwallet ~ public_key ~ extensions + slice data_tail = data_slice; ;; wallet_id, public_key, extensions int stored_wallet_id = data_slice~load_uint(size::wallet_id); int public_key = data_slice.preload_uint(size::public_key); @@ -177,7 +177,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { set_data(begin_cell() .store_int(false, size::bool) .store_uint(stored_seqno, size::seqno) - .store_slice(data_tail) ;; stored_subwallet ~ public_key ~ extensions + .store_slice(data_tail) ;; wallet_id, public_key, extensions .end_cell()); commit(); From 0574f376ab9df5321b783015312bf610d624067b Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 12:11:36 +0400 Subject: [PATCH 054/121] don't store workchain in extensions dict values --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- tests/wallet-v5-extensions.spec.ts | 18 +++++++------- tests/wallet-v5-external.spec.ts | 36 ++++++++++++++-------------- tests/wallet-v5-get.spec.ts | 16 ++++++------- tests/wallet-v5-internal.spec.ts | 38 +++++++++++++++--------------- wrappers/wallet-v5.ts | 6 ++--- 7 files changed, 59 insertions(+), 59 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 823ea0f9..41b1cf84 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021301000226000114ff00f4a413f4bcf2c80b0102012004020102f203011420d728239b4b3b74307f0f0201480e050201200706001bbe5f0f6a2684080b8eb90fa021840201200b080201200a090019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760d0c0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff028ed020c702dc01d0d60301c713dc01d72c232bc3a3748ea101fa4030fa44f828fa443058badded44d0810171d721f4058307f40edd3070db3c8e8c3120d72c239b4b73a431dd70e2100f01f08ef5eda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f910289602f26001f2a39e02945f09db31e001945f08db31e1e25122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd81002cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011211005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009001fa4001fa44f828fa443022baf2aded44d0810171d71821d70a0001f405069d3002c8ca0740148307f453f2a79e33048307f45bf2a8206e58b0f26ce2c85003cf1612f400c9ed545d9452a0"} \ No newline at end of file +{"hex":"b5ee9c7241021301000229000114ff00f4a413f4bcf2c80b0102012004020102f203011420d728239b4b3b74307f0f0201480e050201200706001bbe5f0f6a2684080b8eb90fa021840201200b080201200a090019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760d0c0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff028ed020c702dc01d0d60301c713dc01d72c232bc3a3748ea101fa4030fa44f828fa443058badded44d0810171d721f4058307f40edd3070db3c8e8c3120d72c239b4b73a431dd70e2100f01f08ef5eda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f910289602f26001f2a39e02945f09db31e001945f08db31e1e25122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd81002cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011211005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009601fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1050248307f45bf2a8206e5004b0f26c02e2c85003cf1612f400c9ed54c2158adf"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 5097ae1f..8196e975 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -86,7 +86,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Add extension if (is_add_extension) { - (extensions, int is_success) = extensions.udict_add_builder?(size::address_hash_size, address_hash, begin_cell().store_int(address_wc,8)); + (extensions, int is_success) = extensions.udict_add_builder?(size::address_hash_size, address_hash, begin_cell().store_int(-1, 1)); throw_unless( error::add_extension_error, is_success); } else { ;; Remove extension (extensions, int is_success) = extensions.udict_delete?(size::address_hash_size, address_hash); diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 9b8522db..0d5e331d 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -197,17 +197,17 @@ describe('Wallet V5 extensions auth', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(2); expect(extensionsDict.get(packAddress(sender.address!))).toEqual( - BigInt(sender.address!.workChain) + -1n ); expect(extensionsDict.get(packAddress(testOtherExtension))).toEqual( - BigInt(testOtherExtension.workChain) + -1n ); }); @@ -230,15 +230,15 @@ describe('Wallet V5 extensions auth', () => { const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict1.size).toEqual(2); expect(extensionsDict1.get(packAddress(sender.address!))).toEqual( - BigInt(sender.address!.workChain) + -1n ); expect(extensionsDict1.get(packAddress(otherExtension))).toEqual( - BigInt(otherExtension.workChain) + -1n ); const actions2 = packActionsList([new ActionRemoveExtension(otherExtension)]); @@ -252,12 +252,12 @@ describe('Wallet V5 extensions auth', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(1); expect(extensionsDict.get(packAddress(sender.address!))).toEqual( - BigInt(sender.address!.workChain) + -1n ); expect(extensionsDict.get(packAddress(otherExtension))).toEqual(undefined); }); @@ -279,7 +279,7 @@ describe('Wallet V5 extensions auth', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(0); diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 5c6ee973..1e224995 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -208,14 +208,14 @@ describe('Wallet V5 sign auth external', () => { const extensions = await walletV5.getExtensions(); const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), extensions ); expect(extensionsDict.size).toEqual(1); - const storedWC = extensionsDict.get(packAddress(testExtension)); - expect(storedWC).toEqual(BigInt(testExtension.workChain)); + const dictValue = extensionsDict.get(packAddress(testExtension)); + expect(dictValue).toEqual(-1n); }); it('Send single transfers to a deployed wallet', async () => { @@ -328,7 +328,7 @@ describe('Wallet V5 sign auth external', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); @@ -336,10 +336,10 @@ describe('Wallet V5 sign auth external', () => { accountForGas(receipt.transactions); expect(extensionsDict.get(packAddress(testExtension1))).toEqual( - BigInt(testExtension1.workChain) + -1n ); expect(extensionsDict.get(packAddress(testExtension2))).toEqual( - BigInt(testExtension2.workChain) + -1n ); }); @@ -350,19 +350,19 @@ describe('Wallet V5 sign auth external', () => { const receipt1 = await walletV5.sendExternalSignedMessage(createBody(actionsList1)); const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict1.size).toEqual(1); expect(extensionsDict1.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); const actionsList2 = packActionsList([new ActionRemoveExtension(testExtension)]); const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); const extensionsDict2 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); @@ -413,12 +413,12 @@ describe('Wallet V5 sign auth external', () => { accountForGas(receipt1.transactions); const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict1.size).toEqual(1); expect(extensionsDict1.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); const actionsList2 = packActionsList([new ActionAddExtension(testExtension)]); @@ -433,12 +433,12 @@ describe('Wallet V5 sign auth external', () => { const extensionsDict2 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict2.size).toEqual(1); expect(extensionsDict2.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); }); @@ -457,7 +457,7 @@ describe('Wallet V5 sign auth external', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(0); @@ -769,14 +769,14 @@ describe('Wallet V5 sign auth external', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(1); expect(extensionsDict.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); @@ -917,14 +917,14 @@ describe('Wallet V5 sign auth external', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(1); expect(extensionsDict.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); diff --git a/tests/wallet-v5-get.spec.ts b/tests/wallet-v5-get.spec.ts index cb36bea0..e919724a 100644 --- a/tests/wallet-v5-get.spec.ts +++ b/tests/wallet-v5-get.spec.ts @@ -112,16 +112,16 @@ describe('Wallet V5 get methods', () => { const extensions: Dictionary = Dictionary.empty( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8) + Dictionary.Values.BigInt(1) ); - extensions.set(packAddress(plugin1), BigInt(plugin1.workChain)); - extensions.set(packAddress(plugin2), BigInt(plugin2.workChain)); + extensions.set(packAddress(plugin1), -1n); + extensions.set(packAddress(plugin2), -1n); await deploy({ extensions }); const actual = await walletV5.getExtensions(); const expected = beginCell() - .storeDictDirect(extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8)) + .storeDictDirect(extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(1)) .endCell(); expect(actual?.equals(expected)).toBeTruthy(); }); @@ -135,11 +135,11 @@ describe('Wallet V5 get methods', () => { const extensions: Dictionary = Dictionary.empty( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8) + Dictionary.Values.BigInt(1) ); - extensions.set(packAddress(plugin1), BigInt(plugin1.workChain)); - extensions.set(packAddress(plugin2), BigInt(plugin2.workChain)); - extensions.set(packAddress(plugin3), BigInt(plugin3.workChain)); + extensions.set(packAddress(plugin1), -1n); + extensions.set(packAddress(plugin2), -1n); + extensions.set(packAddress(plugin3), -1n); await deploy({ extensions }); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index dc51b7ce..ecc7f256 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -207,14 +207,14 @@ describe('Wallet V5 sign auth internal', () => { const extensions = await walletV5.getExtensions(); const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), extensions ); expect(extensionsDict.size).toEqual(1); const storedWC = extensionsDict.get(packAddress(testExtension)); - expect(storedWC).toEqual(BigInt(testExtension.workChain)); + expect(storedWC).toEqual(-1n); }); it('Send two transfers', async () => { @@ -304,17 +304,17 @@ describe('Wallet V5 sign auth internal', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(2); expect(extensionsDict.get(packAddress(testExtension1))).toEqual( - BigInt(testExtension1.workChain) + -1n ); expect(extensionsDict.get(packAddress(testExtension2))).toEqual( - BigInt(testExtension2.workChain) + -1n ); }); @@ -328,12 +328,12 @@ describe('Wallet V5 sign auth internal', () => { }); const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict1.size).toEqual(1); expect(extensionsDict1.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); const actionsList2 = packActionsList([new ActionRemoveExtension(testExtension)]); @@ -343,7 +343,7 @@ describe('Wallet V5 sign auth internal', () => { }); const extensionsDict2 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); @@ -402,12 +402,12 @@ describe('Wallet V5 sign auth internal', () => { }); const extensionsDict1 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict1.size).toEqual(1); expect(extensionsDict1.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); const actionsList2 = packActionsList([new ActionAddExtension(testExtension)]); @@ -425,12 +425,12 @@ describe('Wallet V5 sign auth internal', () => { const extensionsDict2 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict2.size).toEqual(1); expect(extensionsDict2.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); }); @@ -452,7 +452,7 @@ describe('Wallet V5 sign auth internal', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(0); @@ -922,14 +922,14 @@ describe('Wallet V5 sign auth internal', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(1); expect(extensionsDict.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); expect( @@ -965,14 +965,14 @@ describe('Wallet V5 sign auth internal', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(1); expect(extensionsDict.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); expect( @@ -1153,14 +1153,14 @@ describe('Wallet V5 sign auth internal', () => { const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), await walletV5.getExtensions() ); expect(extensionsDict.size).toEqual(1); expect(extensionsDict.get(packAddress(testExtension))).toEqual( - BigInt(testExtension.workChain) + -1n ); expect( diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index b62870ef..648f6a9b 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -28,7 +28,7 @@ export function walletV5ConfigToCell(config: WalletV5Config): Cell { .storeUint(config.seqno, 32) .storeUint(config.walletId, 80) .storeBuffer(config.publicKey, 32) - .storeDict(config.extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(8)) + .storeDict(config.extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(1)) .endCell(); } @@ -228,12 +228,12 @@ export class WalletV5 implements Contract { const dict: Dictionary = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(8), + Dictionary.Values.BigInt(1), extensions ); return dict.keys().map(key => { - const wc = dict.get(key)!; + const wc = 0n; const addressHex = key; return Address.parseRaw(`${wc}:${addressHex.toString(16).padStart(64, '0')}`); }); From 2f20a49f459aa6a5eec5b6588565776c26df372f Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 12:18:16 +0400 Subject: [PATCH 055/121] remove unnecessary micro-optimiazations --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 41b1cf84..b2ddf86f 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021301000229000114ff00f4a413f4bcf2c80b0102012004020102f203011420d728239b4b3b74307f0f0201480e050201200706001bbe5f0f6a2684080b8eb90fa021840201200b080201200a090019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760d0c0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff028ed020c702dc01d0d60301c713dc01d72c232bc3a3748ea101fa4030fa44f828fa443058badded44d0810171d721f4058307f40edd3070db3c8e8c3120d72c239b4b73a431dd70e2100f01f08ef5eda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f910289602f26001f2a39e02945f09db31e001945f08db31e1e25122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd81002cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011211005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009601fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1050248307f45bf2a8206e5004b0f26c02e2c85003cf1612f400c9ed54c2158adf"} \ No newline at end of file +{"hex":"b5ee9c724102130100022b000114ff00f4a413f4bcf2c80b0102012004020102f203011420d728239b4b3b74307f0f0201480e050201200706001bbe5f0f6a2684080b8eb90fa021840201200b080201200a090019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760d0c0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff0292d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3c8e8c3120d72c239b4b73a431dd70e2100f01f08ef5eda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f910289602f26001f2a39e02945f09db31e001945f08db31e1e25122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd81002cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011211005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009601fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1050248307f45bf2a8206e5004b0f26c02e2c85003cf1612f400c9ed54b93c17e2"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 8196e975..651740b3 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -29,8 +29,6 @@ const int size::address_hash_size = 256; () return_if(int condition) impure asm "IFRET"; () return_unless(int condition) impure asm "IFNOTRET"; -slice udict_get_or_return(cell dict, int key_length, int index) impure asm(index dict key_length) "DICTUGET" "IFNOTRET"; - (slice, int) begin_parse_raw(cell c) asm "XCTOS"; slice enforce_and_remove_signed_external_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; @@ -127,7 +125,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { } ;; Simply set the C5 register with all pre-computed actions after verification: set_c5_actions(cs.preload_ref().verify_c5_actions(is_external)); - return (); } ;; ------------------------------------------------------------------------------------------------ @@ -189,7 +186,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice body = in_msg_body; in_msg_body = enforce_and_remove_signed_external_prefix(in_msg_body); process_signed_request(body, true); - return(); } ;; ------------------------------------------------------------------------------------------------ @@ -219,7 +215,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). - extensions.udict_get_or_return(size::address_hash_size, sender_address_hash); + (_, int extension_found) = extensions.udict_get?(size::address_hash_size, sender_address_hash); + return_unless(extension_found); process_actions(in_msg_body, false); return (); @@ -231,8 +228,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { return_unless(is_signed_internal); process_signed_request(body, false); - return (); - } ;; ------------------------------------------------------------------------------------------------ From edef7977d48bf8432a1fb955aa64610fb4c53429 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 12:25:50 +0400 Subject: [PATCH 056/121] simplify --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 13 +++++-------- tests/wallet-v5-internal.spec.ts | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index b2ddf86f..f4fdfbcf 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102130100022b000114ff00f4a413f4bcf2c80b0102012004020102f203011420d728239b4b3b74307f0f0201480e050201200706001bbe5f0f6a2684080b8eb90fa021840201200b080201200a090019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760d0c0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff0292d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3c8e8c3120d72c239b4b73a431dd70e2100f01f08ef5eda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f910289602f26001f2a39e02945f09db31e001945f08db31e1e25122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd81002cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011211005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009601fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1050248307f45bf2a8206e5004b0f26c02e2c85003cf1612f400c9ed54b93c17e2"} \ No newline at end of file +{"hex":"b5ee9c7241021201000291000114ff00f4a413f4bcf2c80b01020120030201f8f220d728239b4b3b74307f8eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2605122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d040201200605001bbe5f0f6a2684080b8eb90fa021840201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01fa3120d72c239b4b73a431dd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2605122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd80f02cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009601fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1050248307f45bf2a8206e5004b0f26c02e2c85003cf1612f400c9ed548a58b220"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 651740b3..28a82c63 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -150,17 +150,14 @@ cell verify_c5_actions(cell c5, int is_external) inline { int public_key = data_slice.preload_uint(size::public_key); int is_signature_valid = check_signature(slice_hash(signed_slice), signature, public_key); - if (is_external) { - throw_if(error::signature_disabled, is_signature_disabled); - throw_unless( error::invalid_signature, is_signature_valid); - } else { - if (is_signature_disabled) { - return (); - } - ifnot (is_signature_valid) { + ifnot (is_signature_valid) { + if (is_external) { + throw_unless(error::invalid_signature, is_signature_valid); + } else { return (); } } + throw_if(error::signature_disabled, is_signature_disabled); throw_unless(error::invalid_seqno, seqno == stored_seqno); throw_unless(error::invalid_wallet_id, wallet_id == stored_wallet_id); throw_if(error::expired, valid_until <= now()); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index ecc7f256..46c12a66 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -1193,7 +1193,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt2.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(0); + ).toEqual(32); expect(receipt2.transactions).not.toHaveTransaction({ from: walletV5.address, From 4f773954295bbd02727bf6147c630d932716799c Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 12:53:03 +0400 Subject: [PATCH 057/121] refactor signature_allowed --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 30 ++++++++++++++---------------- tests/wallet-v5-extensions.spec.ts | 2 +- tests/wallet-v5-external.spec.ts | 4 ++-- tests/wallet-v5-get.spec.ts | 2 +- tests/wallet-v5-internal.spec.ts | 4 ++-- wrappers/wallet-v5.ts | 4 ++-- 7 files changed, 23 insertions(+), 25 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index f4fdfbcf..64b57125 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021201000291000114ff00f4a413f4bcf2c80b01020120030201f8f220d728239b4b3b74307f8eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2605122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d040201200605001bbe5f0f6a2684080b8eb90fa021840201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0012a880ed44d0d70a00b30018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01fa3120d72c239b4b73a431dd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2605122baf2a15036baf2a2f823bbf2642292f800dea470c8ca00cb1f01cf16c9ed54f80fdb3cd80f02cc9401d200018edbd72c20e206dcfc2091709901d72c22f577a52412e25210b18e3b30d72c21065dcad48e2dd200ed44d0d200d31f5205953001f2ab709f02f26b01810150d721d70b00f2aa7fe2c8ca00cb1f58cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009601fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1050248307f45bf2a8206e5004b0f26c02e2c85003cf1612f400c9ed548a58b220"} \ No newline at end of file +{"hex":"b5ee9c724102120100028b000114ff00f4a413f4bcf2c80b01020120030201f8f220d728239b4b3b74307f8eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d040201200605001bbe5f0f6a2684080b8eb90fa021840201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01fa3120d72c239b4b73a431dd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d20052049430f26b7f9e01f2ab810150d721d70b00f2aa70e2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5435df666e"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 28a82c63..b7e3ccd7 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -79,7 +79,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice data_slice = get_data().begin_parse(); slice data_slice_before_extensions = data_slice~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); - int is_signature_disabled = data_slice_before_extensions.preload_int(size::bool); + int is_signature_allowed = data_slice_before_extensions.preload_int(size::bool); cell extensions = data_slice.preload_dict(); ;; Add extension @@ -89,7 +89,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { } else { ;; Remove extension (extensions, int is_success) = extensions.udict_delete?(size::address_hash_size, address_hash); throw_unless(error::remove_extension_error, is_success); - throw_if(error::disable_signature_when_extensions_is_empty, null?(extensions) & is_signature_disabled); + throw_if(error::disable_signature_when_extensions_is_empty, null?(extensions) & (~ is_signature_allowed)); } set_data(begin_cell() @@ -100,22 +100,20 @@ cell verify_c5_actions(cell c5, int is_external) inline { } elseif (cs~check_and_remove_set_signature_allowed_prefix()) { int allow_signature = cs~load_int(1); slice data_slice = get_data().begin_parse(); - int is_signature_disabled = data_slice~load_int(size::bool); - int stored_seqno = data_slice~load_uint(size::seqno); - slice data_tail = data_slice; ;; wallet_id, public_key, extensions + int is_signature_allowed = data_slice~load_int(size::bool); + slice data_tail = data_slice; ;; seqno, wallet_id, public_key, extensions if (allow_signature) { ;; allow - throw_unless(error::this_signature_mode_already_set, is_signature_disabled); - is_signature_disabled = false; + throw_if(error::this_signature_mode_already_set, is_signature_allowed); + is_signature_allowed = true; } else { ;; disallow - throw_if(error::this_signature_mode_already_set, is_signature_disabled); - data_slice = data_slice.skip_bits(size::wallet_id + size::public_key); + throw_unless(error::this_signature_mode_already_set, is_signature_allowed); + data_slice = data_slice.skip_bits(size::seqno + size::wallet_id + size::public_key); int is_extensions_not_empty = data_slice.preload_uint(1); throw_unless(error::remove_last_extension_when_signature_disabled, is_extensions_not_empty); - is_signature_disabled = true; + is_signature_allowed = false; } set_data(begin_cell() - .store_int(is_signature_disabled, size::bool) - .store_uint(stored_seqno, size::seqno) + .store_int(is_signature_allowed, size::bool) .store_slice(data_tail) ;; wallet_id, public_key, extensions .end_cell()); } else { @@ -142,7 +140,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { (int wallet_id, int valid_until, int seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); slice data_slice = get_data().begin_parse(); - int is_signature_disabled = data_slice~load_int(size::bool); + int is_signature_allowed = data_slice~load_int(size::bool); int stored_seqno = data_slice~load_uint(size::seqno); slice data_tail = data_slice; ;; wallet_id, public_key, extensions @@ -157,7 +155,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return (); } } - throw_if(error::signature_disabled, is_signature_disabled); + throw_unless(error::signature_disabled, is_signature_allowed); throw_unless(error::invalid_seqno, seqno == stored_seqno); throw_unless(error::invalid_wallet_id, wallet_id == stored_wallet_id); throw_if(error::expired, valid_until <= now()); @@ -169,7 +167,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() - .store_int(false, size::bool) + .store_int(true, size::bool) ;; is_signature_allowed .store_uint(stored_seqno, size::seqno) .store_slice(data_tail) ;; wallet_id, public_key, extensions .end_cell()); @@ -250,5 +248,5 @@ cell get_extensions() method_id { } int get_is_signature_auth_allowed() method_id { - return ~ get_data().begin_parse().preload_int(size::bool); + return get_data().begin_parse().preload_int(size::bool); } diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 0d5e331d..d4aa8e98 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -68,7 +68,7 @@ describe('Wallet V5 extensions auth', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { - signature_auth_disabled: false, + signatureAllowed: true, seqno: 0, walletId: WALLET_ID.serialized, publicKey: keypair.publicKey, diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 1e224995..a71ed3fb 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -58,7 +58,7 @@ describe('Wallet V5 sign auth external', () => { const _walletV5 = blockchain.openContract( WalletV5.createFromConfig( { - signature_auth_disabled: params?.signature_auth_disabled ?? false, + signatureAllowed: true, seqno: params?.seqno ?? 0, walletId: params?.walletId ?? WALLET_ID.serialized, publicKey: params?.publicKey ?? _keypair.publicKey, @@ -101,7 +101,7 @@ describe('Wallet V5 sign auth external', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { - signature_auth_disabled: false, + signatureAllowed: true, seqno: 0, walletId: WALLET_ID.serialized, publicKey: keypair.publicKey, diff --git a/tests/wallet-v5-get.spec.ts b/tests/wallet-v5-get.spec.ts index e919724a..d6f32517 100644 --- a/tests/wallet-v5-get.spec.ts +++ b/tests/wallet-v5-get.spec.ts @@ -31,7 +31,7 @@ describe('Wallet V5 get methods', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { - signature_auth_disabled: params?.signature_auth_disabled ?? false, + signatureAllowed: true, seqno: params?.seqno ?? 0, walletId: params?.walletId ?? WALLET_ID.serialized, publicKey: params?.publicKey ?? keypair.publicKey, diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 46c12a66..6e1e3ff7 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -51,7 +51,7 @@ describe('Wallet V5 sign auth internal', () => { const _walletV5 = blockchain.openContract( WalletV5.createFromConfig( { - signature_auth_disabled: params?.signature_auth_disabled ?? false, + signatureAllowed: true, seqno: params?.seqno ?? 0, walletId: params?.walletId ?? WALLET_ID.serialized, publicKey: params?.publicKey ?? _keypair.publicKey, @@ -94,7 +94,7 @@ describe('Wallet V5 sign auth internal', () => { walletV5 = blockchain.openContract( WalletV5.createFromConfig( { - signature_auth_disabled: false, + signatureAllowed: true, seqno: 0, walletId: WALLET_ID.serialized, publicKey: keypair.publicKey, diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 648f6a9b..9c55ad86 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -15,7 +15,7 @@ import { import { bufferToBigInt } from '../tests/utils'; export type WalletV5Config = { - signature_auth_disabled: boolean; + signatureAllowed: boolean; seqno: number; walletId: bigint; publicKey: Buffer; @@ -24,7 +24,7 @@ export type WalletV5Config = { export function walletV5ConfigToCell(config: WalletV5Config): Cell { return beginCell() - .storeBit(config.signature_auth_disabled) + .storeBit(config.signatureAllowed) .storeUint(config.seqno, 32) .storeUint(config.walletId, 80) .storeBuffer(config.publicKey, 32) From 9869feffa55d72046e24b3942a0a16842160d8d0 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 13:01:00 +0400 Subject: [PATCH 058/121] preload_dict --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 64b57125..32b31081 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100028b000114ff00f4a413f4bcf2c80b01020120030201f8f220d728239b4b3b74307f8eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d040201200605001bbe5f0f6a2684080b8eb90fa021840201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01fa3120d72c239b4b73a431dd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d20052049430f26b7f9e01f2ab810150d721d70b00f2aa70e2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5435df666e"} \ No newline at end of file +{"hex":"b5ee9c724102120100028a000114ff00f4a413f4bcf2c80b01020120030201f8f220d728239b4b3b74307f8eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01fa3120d72c239b4b73a431dd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d20052049430f26b7f9e01f2ab810170d721d70b00f2aa70e2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5450b275e0"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index b7e3ccd7..cf6ecb3e 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -31,6 +31,8 @@ const int size::address_hash_size = 256; (slice, int) begin_parse_raw(cell c) asm "XCTOS"; +cell preload_dict(slice s) asm "PLDDICT"; + slice enforce_and_remove_signed_external_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; (slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; (slice, int) check_and_remove_signed_internal_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; @@ -244,7 +246,7 @@ int get_public_key() method_id { ;; Returns raw dictionary (or null if empty) where keys are address hashes. Workchains of extensions are same with wallet smart contract workchain cell get_extensions() method_id { slice data_slice = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); - return data_slice~load_dict(); + return data_slice.preload_dict(); } int get_is_signature_auth_allowed() method_id { From 5a218427d285ac65afc2e806d40d2a4fa2aa2f10 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 13:03:21 +0400 Subject: [PATCH 059/121] get-methods cosmetic --- contracts/wallet_v5.fc | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index cf6ecb3e..af00864a 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -230,25 +230,32 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ ;; Get methods +int get_is_signature_auth_allowed() method_id { + return get_data().begin_parse() + .preload_int(size::bool); +} + int seqno() method_id { - return get_data().begin_parse().skip_bits(size::bool).preload_uint(size::seqno); + return get_data().begin_parse() + .skip_bits(size::bool) + .preload_uint(size::seqno); } int get_wallet_id() method_id { - return get_data().begin_parse().skip_bits(size::bool + size::seqno).preload_uint(size::wallet_id); + return get_data().begin_parse() + .skip_bits(size::bool + size::seqno) + .preload_uint(size::wallet_id); } int get_public_key() method_id { - slice data_slice = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id); - return data_slice.preload_uint(size::public_key); + return get_data().begin_parse() + .skip_bits(size::bool + size::seqno + size::wallet_id) + .preload_uint(size::public_key); } ;; Returns raw dictionary (or null if empty) where keys are address hashes. Workchains of extensions are same with wallet smart contract workchain cell get_extensions() method_id { - slice data_slice = get_data().begin_parse().skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key); - return data_slice.preload_dict(); -} - -int get_is_signature_auth_allowed() method_id { - return get_data().begin_parse().preload_int(size::bool); -} + return get_data().begin_parse() + .skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key) + .preload_dict(); +} \ No newline at end of file From ad1d90b57842ec404a872a3ba32474377d640c3d Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 13:12:09 +0400 Subject: [PATCH 060/121] simplify --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 20 ++++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 32b31081..e96ea133 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100028a000114ff00f4a413f4bcf2c80b01020120030201f8f220d728239b4b3b74307f8eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01fa3120d72c239b4b73a431dd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d20052049430f26b7f9e01f2ab810170d721d70b00f2aa70e2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5450b275e0"} \ No newline at end of file +{"hex":"b5ee9c7241021401000296000114ff00f4a413f4bcf2c80b0102012005020102f203011c20d70b1f82107369676ebaf2af7f0401e28eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd8110201480f0602012008070019be5f0f6a2684080b8eb90fa02c0201200c090201200b0a0019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760e0d0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e111001fe3120d70b1f821073696e74badd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd81102c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d20052049430f26b7f9e01f2ab810170d721d70b00f2aa70e2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011312005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5434d7f9d7"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index af00864a..ad96ac6d 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -15,6 +15,7 @@ const int error::invalid_signature = 35; const int error::invalid_seqno = 33; const int error::invalid_wallet_id = 34; const int error::expired = 36; +const int error::invalid_message_type = 47; const int size::bool = 1; const int size::seqno = 32; @@ -26,16 +27,15 @@ const int size::signature = 512; const int size::message_type_prefix = 32; const int size::address_hash_size = 256; +const int signed_external = 0x7369676E; +const int signed_internal = 0x73696E74; + () return_if(int condition) impure asm "IFRET"; () return_unless(int condition) impure asm "IFNOTRET"; (slice, int) begin_parse_raw(cell c) asm "XCTOS"; -cell preload_dict(slice s) asm "PLDDICT"; - -slice enforce_and_remove_signed_external_prefix(slice body) impure asm "x{7369676E} SDBEGINS"; (slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; -(slice, int) check_and_remove_signed_internal_prefix(slice body) impure asm "x{73696E74} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; @@ -180,9 +180,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { } () recv_external(slice in_msg_body) impure inline { - slice body = in_msg_body; - in_msg_body = enforce_and_remove_signed_external_prefix(in_msg_body); - process_signed_request(body, true); + throw_unless(error::invalid_message_type, in_msg_body.preload_uint(size::message_type_prefix) == signed_external); + process_signed_request(in_msg_body, true); } ;; ------------------------------------------------------------------------------------------------ @@ -220,11 +219,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { } - slice body = in_msg_body; - (_, int is_signed_internal) = check_and_remove_signed_internal_prefix(in_msg_body); - return_unless(is_signed_internal); - - process_signed_request(body, false); + return_unless(in_msg_body.preload_uint(size::message_type_prefix) == signed_internal); + process_signed_request(in_msg_body, false); } ;; ------------------------------------------------------------------------------------------------ From 2e31a01f9ee40240b6a39717342dc412f2677329 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 13:39:59 +0400 Subject: [PATCH 061/121] simplify --- contracts/wallet_v5.fc | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index ad96ac6d..cb38bbf4 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -43,15 +43,15 @@ const int signed_internal = 0x73696E74; slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; -;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. -() set_c5_actions(cell action_list) impure asm "c5 POP"; - int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; slice get_last_bits(slice s, int n) asm "SDCUTLAST"; slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; +;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. +() set_c5_actions(cell action_list) impure asm "c5 POP"; + cell verify_c5_actions(cell c5, int is_external) inline { (slice cs, _) = c5.begin_parse_raw(); return_if(cs.slice_empty?()); @@ -103,17 +103,15 @@ cell verify_c5_actions(cell c5, int is_external) inline { int allow_signature = cs~load_int(1); slice data_slice = get_data().begin_parse(); int is_signature_allowed = data_slice~load_int(size::bool); + throw_if(error::this_signature_mode_already_set, is_signature_allowed == allow_signature); + is_signature_allowed = allow_signature; + slice data_tail = data_slice; ;; seqno, wallet_id, public_key, extensions - if (allow_signature) { ;; allow - throw_if(error::this_signature_mode_already_set, is_signature_allowed); - is_signature_allowed = true; - } else { ;; disallow - throw_unless(error::this_signature_mode_already_set, is_signature_allowed); - data_slice = data_slice.skip_bits(size::seqno + size::wallet_id + size::public_key); - int is_extensions_not_empty = data_slice.preload_uint(1); + ifnot (allow_signature) { ;; disallow + int is_extensions_not_empty = data_slice.skip_bits(size::seqno + size::wallet_id + size::public_key).preload_uint(1); throw_unless(error::remove_last_extension_when_signature_disabled, is_extensions_not_empty); - is_signature_allowed = false; } + set_data(begin_cell() .store_int(is_signature_allowed, size::bool) .store_slice(data_tail) ;; wallet_id, public_key, extensions @@ -190,9 +188,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return_if(in_msg_body.slice_refs_empty?()); ;; message with actions always have a ref slice in_msg_full_slice = in_msg_full.begin_parse(); - slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool - ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. return_if(count_trailing_ones(message_flags_slice)); @@ -206,8 +202,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { return_unless(my_address_wc == sender_address_wc); - slice data_slice = get_data().begin_parse(); - cell extensions = data_slice.skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key).preload_dict(); + cell extensions = get_data().begin_parse() + .skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key) + .preload_dict(); ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). From 5424092acd0ef857ece3d848f40ca87767477daa Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 14:09:42 +0400 Subject: [PATCH 062/121] c5 comments --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index e96ea133..339a04bc 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021401000296000114ff00f4a413f4bcf2c80b0102012005020102f203011c20d70b1f82107369676ebaf2af7f0401e28eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd8110201480f0602012008070019be5f0f6a2684080b8eb90fa02c0201200c090201200b0a0019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760e0d0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e111001fe3120d70b1f821073696e74badd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd81102c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d20052049430f26b7f9e01f2ab810170d721d70b00f2aa70e2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011312005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5434d7f9d7"} \ No newline at end of file +{"hex":"b5ee9c7241021401000296000114ff00f4a413f4bcf2c80b0102012005020102f203011c20d70b1f82107369676ebaf2af7f0401e28eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd8110201480f0602012008070019be5f0f6a2684080b8eb90fa02c0201200c090201200b0a0019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760e0d0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e111001fe3120d70b1f821073696e74badd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd81102c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011312005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5481665378"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index cb38bbf4..bea3f118 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -33,23 +33,28 @@ const int signed_internal = 0x73696E74; () return_if(int condition) impure asm "IFRET"; () return_unless(int condition) impure asm "IFNOTRET"; -(slice, int) begin_parse_raw(cell c) asm "XCTOS"; - (slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; (slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; -slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; - int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; slice get_last_bits(slice s, int n) asm "SDCUTLAST"; slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; -;; Stores pre-computed list of actions (mostly `action_send_msg`) in the actions register. + +(slice, int) begin_parse_raw(cell c) asm "XCTOS"; + +;; `action_send_msg` has 0x0ec3c86d prefix +;; https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L380 +slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; + +;; Put raw list of OutActions to C5 register. +;; OutList TLB-schema - https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L378 +;; C5 register - https://docs.ton.org/tvm.pdf, page 11 () set_c5_actions(cell action_list) impure asm "c5 POP"; cell verify_c5_actions(cell c5, int is_external) inline { @@ -57,10 +62,14 @@ cell verify_c5_actions(cell c5, int is_external) inline { return_if(cs.slice_empty?()); do { - ;; only send_msg is allowed, set_code or reserve_currency are not + ;; only `action_send_msg` is allowed, `action_set_code` or `action_reserve_currency` are not cs = cs.enforce_and_remove_action_send_msg_prefix(); - ;; enforce that send_mode has 2 bit set, for that load 7 bits and make sure that they end with 1 + + ;; action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; + ;; https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L380 + ;; enforce that send_mode has +2 bit (ignore errors) set for external message. For that load 7 bits and make sure that they end with 1 throw_if(error::invalid_c5_action, is_external & count_trailing_zeroes(cs.preload_bits(7))); + (cs, _) = cs.preload_ref().begin_parse_raw(); } until (cs.slice_empty?()); @@ -141,7 +150,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice data_slice = get_data().begin_parse(); int is_signature_allowed = data_slice~load_int(size::bool); - int stored_seqno = data_slice~load_uint(size::seqno); slice data_tail = data_slice; ;; wallet_id, public_key, extensions int stored_wallet_id = data_slice~load_uint(size::wallet_id); From b3d712dfb2365727a92303d3a1325f3432b1a057 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 14:12:39 +0400 Subject: [PATCH 063/121] fix size check --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 339a04bc..e39b6974 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021401000296000114ff00f4a413f4bcf2c80b0102012005020102f203011c20d70b1f82107369676ebaf2af7f0401e28eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd8110201480f0602012008070019be5f0f6a2684080b8eb90fa02c0201200c090201200b0a0019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760e0d0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e111001fe3120d70b1f821073696e74badd708eeeeda2edfb209821d7498102b2b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd81102c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011312005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5481665378"} \ No newline at end of file +{"hex":"b5ee9c7241021401000296000114ff00f4a413f4bcf2c80b0102012005020102f203011c20d70b1f82107369676ebaf2af7f0401e28eeeeda2edfb209821d7498102b1b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd8110201480f0602012008070019be5f0f6a2684080b8eb90fa02c0201200c090201200b0a0019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760e0d0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e111001fe3120d70b1f821073696e74badd708eeeeda2edfb209821d7498102b1b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd81102c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011312005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5422d94c60"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index bea3f118..eade0a7e 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -139,7 +139,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { () process_signed_request(slice in_msg_body, int is_external) impure inline { ifnot (is_external) { ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::bool + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); + return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); } slice signature = in_msg_body.get_last_bits(size::signature); From 520dec7cf700583adeb071ec21c1c92e54053762 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 14:14:42 +0400 Subject: [PATCH 064/121] fix error naming --- contracts/wallet_v5.fc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index eade0a7e..7dbba041 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -6,9 +6,9 @@ const int error::invalid_c5_action = 37; const int error::extension_wrong_workchain = 45; const int error::add_extension_error = 39; const int error::remove_extension_error = 40; -const int error::disable_signature_when_extensions_is_empty = 44; +const int error::disable_signature_when_extensions_is_empty = 42; const int error::this_signature_mode_already_set = 43; -const int error::remove_last_extension_when_signature_disabled = 42; +const int error::remove_last_extension_when_signature_disabled = 44; const int error::unspported_action = 41; const int error::signature_disabled = 32; const int error::invalid_signature = 35; @@ -100,7 +100,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { } else { ;; Remove extension (extensions, int is_success) = extensions.udict_delete?(size::address_hash_size, address_hash); throw_unless(error::remove_extension_error, is_success); - throw_if(error::disable_signature_when_extensions_is_empty, null?(extensions) & (~ is_signature_allowed)); + throw_if(error::remove_last_extension_when_signature_disabled, null?(extensions) & (~ is_signature_allowed)); } set_data(begin_cell() @@ -118,12 +118,12 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice data_tail = data_slice; ;; seqno, wallet_id, public_key, extensions ifnot (allow_signature) { ;; disallow int is_extensions_not_empty = data_slice.skip_bits(size::seqno + size::wallet_id + size::public_key).preload_uint(1); - throw_unless(error::remove_last_extension_when_signature_disabled, is_extensions_not_empty); + throw_unless(error::disable_signature_when_extensions_is_empty, is_extensions_not_empty); } set_data(begin_cell() .store_int(is_signature_allowed, size::bool) - .store_slice(data_tail) ;; wallet_id, public_key, extensions + .store_slice(data_tail) ;; seqno, wallet_id, public_key, extensions .end_cell()); } else { throw(error::unspported_action); From 243d7d200d0cdf5e9603c413e2e99954afdd198a Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 14:18:06 +0400 Subject: [PATCH 065/121] simplify --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index e39b6974..82acd73f 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021401000296000114ff00f4a413f4bcf2c80b0102012005020102f203011c20d70b1f82107369676ebaf2af7f0401e28eeeeda2edfb209821d7498102b1b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd8110201480f0602012008070019be5f0f6a2684080b8eb90fa02c0201200c090201200b0a0019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760e0d0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e111001fe3120d70b1f821073696e74badd708eeeeda2edfb209821d7498102b1b9dcdf218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd81102c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011312005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed5422d94c60"} \ No newline at end of file +{"hex":"b5ee9c7241021201000282000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed549bc93548"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 7dbba041..61eccadf 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -137,11 +137,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ () process_signed_request(slice in_msg_body, int is_external) impure inline { - ifnot (is_external) { - ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); - } - slice signature = in_msg_body.get_last_bits(size::signature); slice signed_slice = in_msg_body.remove_last_bits(size::signature); @@ -225,6 +220,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { } return_unless(in_msg_body.preload_uint(size::message_type_prefix) == signed_internal); + ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) + return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); process_signed_request(in_msg_body, false); } From a39b7b09091c4dac2b723a488707381721532920 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 14:25:53 +0400 Subject: [PATCH 066/121] simplify --- contracts/wallet_v5.fc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 61eccadf..bfd012c4 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -195,9 +195,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. return_if(count_trailing_ones(message_flags_slice)); - (in_msg_body, int is_extension_action) = check_and_remove_extension_action_prefix(in_msg_body); - - if (is_extension_action) { + if (in_msg_body~check_and_remove_extension_action_prefix()) { ;; Authenticate extension by its address. (int sender_address_wc, int sender_address_hash) = parse_std_addr(in_msg_full_slice~load_msg_addr()); From fe128dde8bfb8eb658790d4e7baa3f5945f5b801 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 21 Jun 2024 14:33:23 +0400 Subject: [PATCH 067/121] cosmetic-optimization --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 82acd73f..06ccffc4 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021201000282000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55009801fa4001fa44f828fa443058baf2aded44d0810171d71821d70a0001f405059d307fc8ca0040048307f453f2a78e1150248307f45bf2a8206e04b314b0f26c02e2c85003cf1612f400c9ed549bc93548"} \ No newline at end of file +{"hex":"b5ee9c724102120100027d000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed543fe9ade5"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index bfd012c4..8421606e 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -90,7 +90,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice data_slice = get_data().begin_parse(); slice data_slice_before_extensions = data_slice~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); - int is_signature_allowed = data_slice_before_extensions.preload_int(size::bool); cell extensions = data_slice.preload_dict(); ;; Add extension @@ -100,6 +99,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { } else { ;; Remove extension (extensions, int is_success) = extensions.udict_delete?(size::address_hash_size, address_hash); throw_unless(error::remove_extension_error, is_success); + int is_signature_allowed = data_slice_before_extensions.preload_int(size::bool); throw_if(error::remove_last_extension_when_signature_disabled, null?(extensions) & (~ is_signature_allowed)); } From ab0233d08f811facafac7b09413e25a8f6a17aba Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 12:41:14 +0400 Subject: [PATCH 068/121] explicit () in binary operations --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 06ccffc4..7359f89e 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100027d000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9102091309928945f0adb31e1f2a3e201f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70b00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d7393020c700dc8e1ad72820761e436c20d71d06c7125220b0f265d74cd7393020c700e65bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed543fe9ade5"} \ No newline at end of file +{"hex":"b5ee9c724102120100027b000114ff00f4a413f4bcf2c80b01020120030201e4f220d70b1f82107369676ebaf2af7f8ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff027cd020c702dc01d0d60301c713c200dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f23120d70b1f821073696e74badd20d7498102b1b9dc708ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005421d7393020c700dc8e1cd72820761e436c20d71d06c712c2005220b0f265d74cd7393020c700e65bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed548c2eefa4"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 8421606e..69c819b2 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -39,11 +39,15 @@ const int signed_internal = 0x73696E74; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; (slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; -int count_trailing_zeroes(slice cs) asm "SDCNTTRAIL0"; -int count_trailing_ones(slice cs) asm "SDCNTTRAIL1"; +;;; returns the number of trailing zeroes in slice s. +int count_trailing_zeroes(slice s) asm "SDCNTTRAIL0"; +;;; returns the number of trailing ones in slice s. +int count_trailing_ones(slice s) asm "SDCNTTRAIL1"; -slice get_last_bits(slice s, int n) asm "SDCUTLAST"; -slice remove_last_bits(slice s, int n) asm "SDSKIPLAST"; +;;; returns the last 0 ≤ l ≤ 1023 bits of s. +slice get_last_bits(slice s, int l) asm "SDCUTLAST"; +;;; returns all but the last 0 ≤ l ≤ 1023 bits of s. +slice remove_last_bits(slice s, int l) asm "SDSKIPLAST"; (slice, int) begin_parse_raw(cell c) asm "XCTOS"; @@ -68,7 +72,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; ;; https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L380 ;; enforce that send_mode has +2 bit (ignore errors) set for external message. For that load 7 bits and make sure that they end with 1 - throw_if(error::invalid_c5_action, is_external & count_trailing_zeroes(cs.preload_bits(7))); + throw_if(error::invalid_c5_action, is_external & (count_trailing_zeroes(cs.preload_bits(7)) > 0)); (cs, _) = cs.preload_ref().begin_parse_raw(); } until (cs.slice_empty?()); @@ -117,7 +121,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice data_tail = data_slice; ;; seqno, wallet_id, public_key, extensions ifnot (allow_signature) { ;; disallow - int is_extensions_not_empty = data_slice.skip_bits(size::seqno + size::wallet_id + size::public_key).preload_uint(1); + int is_extensions_not_empty = data_slice.skip_bits(size::seqno + size::wallet_id + size::public_key).preload_int(1); throw_unless(error::disable_signature_when_extensions_is_empty, is_extensions_not_empty); } @@ -153,7 +157,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { int is_signature_valid = check_signature(slice_hash(signed_slice), signature, public_key); ifnot (is_signature_valid) { if (is_external) { - throw_unless(error::invalid_signature, is_signature_valid); + throw(error::invalid_signature); } else { return (); } @@ -193,7 +197,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice in_msg_full_slice = in_msg_full.begin_parse(); slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. - return_if(count_trailing_ones(message_flags_slice)); + return_if(count_trailing_ones(message_flags_slice) > 0); if (in_msg_body~check_and_remove_extension_action_prefix()) { From 477bf6d0175e88904188ff28ddec6ebf6c2f8500 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 12:48:11 +0400 Subject: [PATCH 069/121] cosmetic --- contracts/wallet_v5.fc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 69c819b2..e5206474 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -27,11 +27,13 @@ const int size::signature = 512; const int size::message_type_prefix = 32; const int size::address_hash_size = 256; -const int signed_external = 0x7369676E; -const int signed_internal = 0x73696E74; +const int prefix::signed_external = 0x7369676E; +const int prefix::signed_internal = 0x73696E74; -() return_if(int condition) impure asm "IFRET"; -() return_unless(int condition) impure asm "IFNOTRET"; +;;; performs a RET, but only if integer f is non-zero. If f is a NaN, throws an integer overflow exception. +() return_if(int f) impure asm "IFRET"; +;;; performs a RET, but only if integer f is zero +() return_unless(int f) impure asm "IFNOTRET"; (slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; @@ -144,7 +146,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice signature = in_msg_body.get_last_bits(size::signature); slice signed_slice = in_msg_body.remove_last_bits(size::signature); - slice cs = signed_slice.skip_bits(size::message_type_prefix); ;; skip signed_internal or signer_external prefix + slice cs = signed_slice.skip_bits(size::message_type_prefix); ;; skip signed_internal or signed_external prefix (int wallet_id, int valid_until, int seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); slice data_slice = get_data().begin_parse(); @@ -185,7 +187,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { } () recv_external(slice in_msg_body) impure inline { - throw_unless(error::invalid_message_type, in_msg_body.preload_uint(size::message_type_prefix) == signed_external); + throw_unless(error::invalid_message_type, in_msg_body.preload_uint(size::message_type_prefix) == prefix::signed_external); process_signed_request(in_msg_body, true); } @@ -221,7 +223,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { } - return_unless(in_msg_body.preload_uint(size::message_type_prefix) == signed_internal); + return_unless(in_msg_body.preload_uint(size::message_type_prefix) == prefix::signed_internal); ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); process_signed_request(in_msg_body, false); From 029e60a7009eb5d3887425eff3296d598d07fd9f Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 12:56:16 +0400 Subject: [PATCH 070/121] fix return --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 7359f89e..cc3eecab 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100027b000114ff00f4a413f4bcf2c80b01020120030201e4f220d70b1f82107369676ebaf2af7f8ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff027cd020c702dc01d0d60301c713c200dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f23120d70b1f821073696e74badd20d7498102b1b9dc708ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005421d7393020c700dc8e1cd72820761e436c20d71d06c712c2005220b0f265d74cd7393020c700e65bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed548c2eefa4"} \ No newline at end of file +{"hex":"b5ee9c724102120100027f000114ff00f4a413f4bcf2c80b01020120030201e4f220d70b1f82107369676ebaf2af7f8ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff027cd020c702dc01d0d60301c713c200dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f23120d70b1f821073696e74badd20d7498102b1b9dc708ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005c21d7393020c700915b8e208e1cd72820761e436c20d71d06c712c2005220b0f265d74cd7393020c700e65be2ed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54dd2a9a2b"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index e5206474..34343af1 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -65,7 +65,9 @@ slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c8 cell verify_c5_actions(cell c5, int is_external) inline { (slice cs, _) = c5.begin_parse_raw(); - return_if(cs.slice_empty?()); + if (cs.slice_empty?()) { + return c5; + } do { ;; only `action_send_msg` is allowed, `action_set_code` or `action_reserve_currency` are not From 59108961442ac43f42f462a62581d28ad76f479f Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 14:00:17 +0400 Subject: [PATCH 071/121] simplify --- contracts/wallet_v5.fc | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 34343af1..4528a5d6 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -65,11 +65,8 @@ slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c8 cell verify_c5_actions(cell c5, int is_external) inline { (slice cs, _) = c5.begin_parse_raw(); - if (cs.slice_empty?()) { - return c5; - } - do { + while (~ cs.slice_empty?()) { ;; only `action_send_msg` is allowed, `action_set_code` or `action_reserve_currency` are not cs = cs.enforce_and_remove_action_send_msg_prefix(); @@ -79,7 +76,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { throw_if(error::invalid_c5_action, is_external & (count_trailing_zeroes(cs.preload_bits(7)) > 0)); (cs, _) = cs.preload_ref().begin_parse_raw(); - } until (cs.slice_empty?()); + } return c5; } @@ -201,7 +198,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice in_msg_full_slice = in_msg_full.begin_parse(); slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. - return_if(count_trailing_ones(message_flags_slice) > 0); + return_if(count_trailing_ones(message_flags_slice)); if (in_msg_body~check_and_remove_extension_action_prefix()) { From 2ba7fdaea517e36978ca20375a9e429d1970f6c1 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 14:06:27 +0400 Subject: [PATCH 072/121] commit only for external --- contracts/wallet_v5.fc | 4 +++- tests/wallet-v5-internal.spec.ts | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 4528a5d6..6e2c3cd4 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -180,7 +180,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { .store_slice(data_tail) ;; wallet_id, public_key, extensions .end_cell()); - commit(); + if (is_external) { + commit(); + } process_actions(cs, is_external); } diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 6e1e3ff7..e0eea790 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -865,7 +865,7 @@ describe('Wallet V5 sign auth internal', () => { body: createBody(actionsList) }); - expect(receipt.transactions.length).toEqual(2); + expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced expect( ( @@ -889,7 +889,7 @@ describe('Wallet V5 sign auth internal', () => { body: createBody(actionsList) }); - expect(receipt.transactions.length).toEqual(2); + expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced expect( ( @@ -1091,7 +1091,7 @@ describe('Wallet V5 sign auth internal', () => { body: createBody(actionsList) }); - expect(receipt.transactions.length).toEqual(2); + expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced accountForGas(receipt.transactions); expect( @@ -1120,7 +1120,7 @@ describe('Wallet V5 sign auth internal', () => { body: createBody(actionsList) }); - expect(receipt.transactions.length).toEqual(2); + expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced accountForGas(receipt.transactions); expect( From c10105a8d2540d963615f69feda57a5668d69f49 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 14:23:09 +0400 Subject: [PATCH 073/121] xctos comment --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index cc3eecab..1c19892b 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100027f000114ff00f4a413f4bcf2c80b01020120030201e4f220d70b1f82107369676ebaf2af7f8ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff027cd020c702dc01d0d60301c713c200dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f23120d70b1f821073696e74badd20d7498102b1b9dc708ee0eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed54f80fdb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005c21d7393020c700915b8e208e1cd72820761e436c20d71d06c712c2005220b0f265d74cd7393020c700e65be2ed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54dd2a9a2b"} \ No newline at end of file +{"hex":"b5ee9c724102120100027d000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54fe4d9457"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 6e2c3cd4..3bb49146 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -51,9 +51,6 @@ slice get_last_bits(slice s, int l) asm "SDCUTLAST"; ;;; returns all but the last 0 ≤ l ≤ 1023 bits of s. slice remove_last_bits(slice s, int l) asm "SDSKIPLAST"; - -(slice, int) begin_parse_raw(cell c) asm "XCTOS"; - ;; `action_send_msg` has 0x0ec3c86d prefix ;; https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L380 slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; @@ -63,6 +60,10 @@ slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c8 ;; C5 register - https://docs.ton.org/tvm.pdf, page 11 () set_c5_actions(cell action_list) impure asm "c5 POP"; +;; XCTOS doesn't automatically load exotic cells (unlike CTOS `begin_parse`). We use it in `verify_c5_actions` because during action phase processing exotic cells in c5 won't be unfolded too. +;;; transforms an ordinary or exotic cell into a Slice, as if it were an ordinary cell. A flag is returned indicating whether c is exotic. If that be the case, its type can later be deserialized from the first eight bits of s. +(slice, int) begin_parse_raw(cell c) asm "XCTOS"; + cell verify_c5_actions(cell c5, int is_external) inline { (slice cs, _) = c5.begin_parse_raw(); @@ -70,9 +71,12 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; only `action_send_msg` is allowed, `action_set_code` or `action_reserve_currency` are not cs = cs.enforce_and_remove_action_send_msg_prefix(); + ;; enforce that send_mode has +2 bit (ignore errors) set for external message. + ;; if such send_mode is not set and sending fails at the action phase (for example due to insufficient balance) then the seqno will not be increased and the external message will be processed again and again. + ;; action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; ;; https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L380 - ;; enforce that send_mode has +2 bit (ignore errors) set for external message. For that load 7 bits and make sure that they end with 1 + ;; load 7 bits and make sure that they end with 1 throw_if(error::invalid_c5_action, is_external & (count_trailing_zeroes(cs.preload_bits(7)) > 0)); (cs, _) = cs.preload_ref().begin_parse_raw(); From 1f3c1770dead1fd4c6466f6b63fb01b25996cd88 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 14:53:43 +0400 Subject: [PATCH 074/121] update stdlib from stablecoin-contract --- contracts/imports/stdlib.fc | 41 +++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/contracts/imports/stdlib.fc b/contracts/imports/stdlib.fc index fa048f67..241993d6 100644 --- a/contracts/imports/stdlib.fc +++ b/contracts/imports/stdlib.fc @@ -1,6 +1,21 @@ ;; Standard library for funC ;; +{- + This file is part of TON FunC Standard Library. + + FunC Standard Library is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + FunC Standard Library 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 Lesser General Public License for more details. + +-} + {- # Tuple manipulation primitives The names and the types are mostly self-explaining. @@ -26,7 +41,7 @@ forall X -> tuple cons(X head, tuple tail) asm "CONS"; forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; ;;; Extracts the tail and the head of lisp-style list. -forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; +forall X -> (tuple, X) list_next(tuple list) asm(-> 1 0) "UNCONS"; ;;; Returns the head of lisp-style list. forall X -> X car(tuple list) asm "CAR"; @@ -244,11 +259,9 @@ cont bless(slice s) impure asm "BLESS"; () commit() impure asm "COMMIT"; ;;; Not implemented -;;() buy_gas(int gram) impure asm "BUYGAS"; - ;;; Computes the amount of gas that can be bought for `amount` nanoTONs, ;;; and sets `gl` accordingly in the same way as [set_gas_limit]. -() buy_gas(int amount) impure asm "BUYGAS"; +;;() buy_gas(int amount) impure asm "BUYGAS"; ;;; Computes the minimum of two integers [x] and [y]. int min(int x, int y) asm "MIN"; @@ -285,12 +298,12 @@ slice begin_parse(cell c) asm "CTOS"; () end_parse(slice s) impure asm "ENDS"; ;;; Loads the first reference from the slice. -(slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; +(slice, cell) load_ref(slice s) asm(-> 1 0) "LDREF"; ;;; Preloads the first reference from the slice. cell preload_ref(slice s) asm "PLDREF"; - {- Functions below are commented because are implemented on compilator level for optimisation -} +{- Functions below are commented because are implemented on compilator level for optimisation -} ;;; Loads a signed [len]-bit integer from a slice [s]. ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; @@ -311,8 +324,8 @@ cell preload_ref(slice s) asm "PLDREF"; ;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; ;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`). -(slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; -(slice, int) load_coins(slice s) asm( -> 1 0) "LDGRAMS"; +(slice, int) load_grams(slice s) asm(-> 1 0) "LDGRAMS"; +(slice, int) load_coins(slice s) asm(-> 1 0) "LDVARUINT16"; ;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s]. slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; @@ -330,7 +343,7 @@ slice slice_last(slice s, int len) asm "SDCUTLAST"; ;;; Loads a dictionary `D` (HashMapE) from `slice` [s]. ;;; (returns `null` if `nothing` constructor is used). -(slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; +(slice, cell) load_dict(slice s) asm(-> 1 0) "LDDICT"; ;;; Preloads a dictionary `D` from `slice` [s]. cell preload_dict(slice s) asm "PLDDICT"; @@ -342,7 +355,7 @@ slice skip_dict(slice s) asm "SKIPDICT"; ;;; In other words loads 1 bit and if it is true ;;; loads first ref and return it with slice remainder ;;; otherwise returns `null` and slice remainder -(slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; +(slice, cell) load_maybe_ref(slice s) asm(-> 1 0) "LDOPTREF"; ;;; Preloads (Maybe ^Cell) from `slice` [s]. cell preload_maybe_ref(slice s) asm "PLDOPTREF"; @@ -434,7 +447,7 @@ builder store_slice(builder b, slice s) asm "STSLICER"; ;;; ;;; Store amounts of TonCoins to the builder as VarUInteger 16 builder store_grams(builder b, int x) asm "STGRAMS"; -builder store_coins(builder b, int x) asm "STGRAMS"; +builder store_coins(builder b, int x) asm "STVARUINT16"; ;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b]. ;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise. @@ -485,7 +498,7 @@ builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; ;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`, ;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices. -(slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; +(slice, slice) load_msg_addr(slice s) asm(-> 1 0) "LDMSGADDR"; ;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`. ;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown. @@ -618,8 +631,6 @@ int get_seed() impure asm "RANDSEED"; () randomize_lt() impure asm "LTIME" "ADDRAND"; ;;; Checks whether the data parts of two slices coinside -int equal_slice_bits(slice a, slice b) asm "SDEQ"; -int equal_slices(slice a, slice b) asm "SDEQ"; - +int equal_slices_bits(slice a, slice b) asm "SDEQ"; ;;; Concatenates two builders builder store_builder(builder to, builder from) asm "STBR"; \ No newline at end of file From 464e68d29f1b200e02667ffffbd07c47248debc3 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sat, 22 Jun 2024 14:55:31 +0400 Subject: [PATCH 075/121] rename get_is_signature_auth_allowed -> is_signature_allowed --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- wrappers/wallet-v5.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 1c19892b..482f3e6e 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100027d000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200a0702012009080019b45d1da89a10043ae43ae169f00017b592fda89a0e3ae43ae163f00202760c0b0010a880ed44d0d70a000018ab9ced44d08071d721d70bff0278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54fe4d9457"} \ No newline at end of file +{"hex":"b5ee9c724102120100027f000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae169f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08071d721d70bff80278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54a956c8eb"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 3bb49146..f8d40e66 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -237,7 +237,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ ;; Get methods -int get_is_signature_auth_allowed() method_id { +int is_signature_allowed() method_id { return get_data().begin_parse() .preload_int(size::bool); } diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 9c55ad86..aa6fece5 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -203,7 +203,7 @@ export class WalletV5 implements Contract { async getIsSignatureAuthAllowed(provider: ContractProvider) { const state = await provider.getState(); if (state.state.type === 'active') { - let res = await provider.get('get_is_signature_auth_allowed', []); + let res = await provider.get('is_signature_allowed', []); return res.stack.readNumber(); } else { return -1; From d5ea3816e89f810119af5c70a44c5f4c3fbdd6b8 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Mon, 24 Jun 2024 10:54:37 +0400 Subject: [PATCH 076/121] comments --- contracts/wallet_v5.fc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index f8d40e66..a8162d3b 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -55,16 +55,18 @@ slice remove_last_bits(slice s, int l) asm "SDSKIPLAST"; ;; https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L380 slice enforce_and_remove_action_send_msg_prefix(slice body) impure asm "x{0ec3c86d} SDBEGINS"; -;; Put raw list of OutActions to C5 register. -;; OutList TLB-schema - https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L378 -;; C5 register - https://docs.ton.org/tvm.pdf, page 11 +;;; put raw list of OutActions to C5 register. +;;; OutList TLB-schema - https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L378 +;;; C5 register - https://docs.ton.org/tvm.pdf, page 11 () set_c5_actions(cell action_list) impure asm "c5 POP"; -;; XCTOS doesn't automatically load exotic cells (unlike CTOS `begin_parse`). We use it in `verify_c5_actions` because during action phase processing exotic cells in c5 won't be unfolded too. ;;; transforms an ordinary or exotic cell into a Slice, as if it were an ordinary cell. A flag is returned indicating whether c is exotic. If that be the case, its type can later be deserialized from the first eight bits of s. (slice, int) begin_parse_raw(cell c) asm "XCTOS"; cell verify_c5_actions(cell c5, int is_external) inline { + ;; XCTOS doesn't automatically load exotic cells (unlike CTOS `begin_parse`). + ;; we use it in `verify_c5_actions` because during action phase processing exotic cells in c5 won't be unfolded too. + ;; exotic cell starts with 0x02, 0x03 or 0x04 so it will not pass action_send_msg prefix check (slice cs, _) = c5.begin_parse_raw(); while (~ cs.slice_empty?()) { From 816e4468a2a821c03f0fbc8ea9b588f37734a038 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Mon, 24 Jun 2024 11:00:13 +0400 Subject: [PATCH 077/121] add query_id to extension request so that the extension can recognise the error response --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 3 +++ wrappers/wallet-v5.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 482f3e6e..ee4b456f 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100027f000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae169f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08071d721d70bff80278d020c702dc01d0d60301c713dc01d72c232bc3a3748ea301fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd70db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54a956c8eb"} \ No newline at end of file +{"hex":"b5ee9c7241021201000283000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae169f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08071d721d70bff80280d020c702dc01d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed5475319bcc"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index a8162d3b..0bf08ad4 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -26,6 +26,7 @@ const int size::message_flags = 4; const int size::signature = 512; const int size::message_type_prefix = 32; const int size::address_hash_size = 256; +const int size::query_id = 64; const int prefix::signed_external = 0x7369676E; const int prefix::signed_internal = 0x73696E74; @@ -225,6 +226,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { (_, int extension_found) = extensions.udict_get?(size::address_hash_size, sender_address_hash); return_unless(extension_found); + in_msg_body~skip_bits(size::query_id); ;; skip query_id + process_actions(in_msg_body, false); return (); diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index aa6fece5..4f2a7d0f 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -159,6 +159,7 @@ export class WalletV5 implements Contract { sendMode: SendMode.PAY_GAS_SEPARATELY, body: beginCell() .storeUint(Opcodes.auth_extension, 32) + .storeUint(0, 64) // query id .storeSlice(opts.body.beginParse()) .endCell() }); From 43fabdf74fec537309eedb46903993e21f727d01 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Tue, 25 Jun 2024 18:23:53 +0400 Subject: [PATCH 078/121] action prefix 8bit --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 6 +++--- tests/actions.ts | 12 ++++++------ tests/wallet-v5-external.spec.ts | 2 +- tests/wallet-v5-internal.spec.ts | 2 +- wrappers/wallet-v5.ts | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index ee4b456f..4ccb7ef7 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021201000283000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae169f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08071d721d70bff80280d020c702dc01d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c09401d200018ed5d72c20e206dcfc2091709901d72c22f577a52412e25210b18e3530d72c21065dcad48e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed5475319bcc"} \ No newline at end of file +{"hex":"b5ee9c724102120100027a000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae169f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08071d721d70bff80280d020c702dc01d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02ae9401d200018eccd72c08142091709601d72c081c12e25210b18e3230d72c08248e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54edaac72e"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 0bf08ad4..a5561d51 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -38,9 +38,9 @@ const int prefix::signed_internal = 0x73696E74; (slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; -(slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{1c40db9f} SDBEGINSQ"; -(slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{5eaef4a4} SDBEGINSQ"; -(slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{20cbb95a} SDBEGINSQ"; +(slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{02} SDBEGINSQ"; +(slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{03} SDBEGINSQ"; +(slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{04} SDBEGINSQ"; ;;; returns the number of trailing zeroes in slice s. int count_trailing_zeroes(slice s) asm "SDCNTTRAIL0"; diff --git a/tests/actions.ts b/tests/actions.ts index 6212e603..9788e53b 100644 --- a/tests/actions.ts +++ b/tests/actions.ts @@ -18,31 +18,31 @@ export class ActionSendMsg { } export class ActionAddExtension { - public static readonly tag = 0x1c40db9f; + public static readonly tag = 0x02; public readonly tag = ActionAddExtension.tag; constructor(public readonly address: Address) {} public serialize(): Cell { - return beginCell().storeUint(this.tag, 32).storeAddress(this.address).endCell(); + return beginCell().storeUint(this.tag, 8).storeAddress(this.address).endCell(); } } export class ActionRemoveExtension { - public static readonly tag = 0x5eaef4a4; + public static readonly tag = 0x03; public readonly tag = ActionRemoveExtension.tag; constructor(public readonly address: Address) {} public serialize(): Cell { - return beginCell().storeUint(this.tag, 32).storeAddress(this.address).endCell(); + return beginCell().storeUint(this.tag, 8).storeAddress(this.address).endCell(); } } export class ActionSetSignatureAuthAllowed { - public static readonly tag = 0x20cbb95a; + public static readonly tag = 0x04; public readonly tag = ActionSetSignatureAuthAllowed.tag; @@ -50,7 +50,7 @@ export class ActionSetSignatureAuthAllowed { public serialize(): Cell { return beginCell() - .storeUint(this.tag, 32) + .storeUint(this.tag, 8) .storeUint(this.allowed ? 1 : 0, 1) .endCell(); } diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index a71ed3fb..dfba9f6a 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -190,7 +190,7 @@ describe('Wallet V5 sign auth external', () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const addExtensionAction = beginCell() - .storeUint(Opcodes.action_extended_add_extension, 32) + .storeUint(Opcodes.action_extended_add_extension, 8) .storeAddress(testExtension) .endCell(); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index e0eea790..50df21ad 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -186,7 +186,7 @@ describe('Wallet V5 sign auth internal', () => { const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const addExtensionAction = beginCell() - .storeUint(Opcodes.action_extended_add_extension, 32) + .storeUint(Opcodes.action_extended_add_extension, 8) .storeAddress(testExtension) .endCell(); diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 4f2a7d0f..932818c0 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -36,9 +36,9 @@ export const Opcodes = { action_send_msg: 0x0ec3c86d, action_set_code: 0xad4de08e, action_extended_set_data: 0x1ff8ea0b, - action_extended_add_extension: 0x1c40db9f, - action_extended_remove_extension: 0x5eaef4a4, - action_extended_set_signature_auth_allowed: 0x20cbb95a, + action_extended_add_extension: 0x02, + action_extended_remove_extension: 0x03, + action_extended_set_signature_auth_allowed: 0x04, auth_extension: 0x6578746e, auth_signed: 0x7369676e, auth_signed_internal: 0x73696e74 From 68a086618e4f1af99f986595a558329ebdd13b0c Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Tue, 25 Jun 2024 18:42:08 +0400 Subject: [PATCH 079/121] wallet_id 32bit --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- tests/wallet-v5-extensions.spec.ts | 2 +- tests/wallet-v5-external.spec.ts | 16 ++++---- tests/wallet-v5-get.spec.ts | 2 +- tests/wallet-v5-internal.spec.ts | 16 ++++---- wrappers/wallet-v5.ts | 62 +++++++++++++++--------------- 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 4ccb7ef7..1d9598ac 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100027a000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080b8eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae169f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08071d721d70bff80280d020c702dc01d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810171d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d7498102b1b9dc708ee3eda2edfb218308d722028308d723208020d721d34fd31fd31fed44d0d200d31f20d34fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02ae9401d200018eccd72c08142091709601d72c081c12e25210b18e3230d72c08248e27d200ed44d0d2005113baf26b54503091319b01810170d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810171d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54edaac72e"} \ No newline at end of file +{"hex":"b5ee9c724102120100027a000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080a0eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae163f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff80280d020c702dc01d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d749810281b9dc708ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02ae9401d200018eccd72c08142091709601d72c081c12e25210b18e3230d72c08248e27d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54a9269d25"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index a5561d51..9aa01b63 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -19,7 +19,7 @@ const int error::invalid_message_type = 47; const int size::bool = 1; const int size::seqno = 32; -const int size::wallet_id = 80; +const int size::wallet_id = 32; const int size::public_key = 256; const int size::valid_until = 32; const int size::message_flags = 4; diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index d4aa8e98..f70e186e 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -45,7 +45,7 @@ describe('Wallet V5 extensions auth', () => { function createBody(actionsList: Cell) { const payload = beginCell() .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno .storeSlice(actionsList.beginParse()) diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index dfba9f6a..1660055e 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -78,7 +78,7 @@ describe('Wallet V5 sign auth external', () => { function createBody(actionsList: Cell) { const payload = beginCell() .storeUint(Opcodes.auth_signed, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -475,7 +475,7 @@ describe('Wallet V5 sign auth external', () => { const vu = validUntil(); const payload = beginCell() - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(vu, 32) .storeUint(seqno, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -483,7 +483,7 @@ describe('Wallet V5 sign auth external', () => { const fakePayload = beginCell() .storeUint(Opcodes.auth_signed, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(vu, 32) .storeUint(seqno + 1, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -514,7 +514,7 @@ describe('Wallet V5 sign auth external', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -547,7 +547,7 @@ describe('Wallet V5 sign auth external', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno + 1, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -578,7 +578,7 @@ describe('Wallet V5 sign auth external', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(Math.round(Date.now() / 1000) - 600, 32) .storeUint(seqno, 32) .storeSlice(actionsList.beginParse()) @@ -609,7 +609,7 @@ describe('Wallet V5 sign auth external', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed, 32) - .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 80) + .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) .storeSlice(actionsList.beginParse()) @@ -640,7 +640,7 @@ describe('Wallet V5 sign auth external', () => { const payload = beginCell() // auth_signed_internal used instead of auth_signed .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) .storeSlice(actionsList.beginParse()) diff --git a/tests/wallet-v5-get.spec.ts b/tests/wallet-v5-get.spec.ts index d6f32517..86114070 100644 --- a/tests/wallet-v5-get.spec.ts +++ b/tests/wallet-v5-get.spec.ts @@ -79,7 +79,7 @@ describe('Wallet V5 get methods', () => { }); await deploy({ walletId: expectedWalletId.serialized }); const actualWalletId = await walletV5.getWalletId(); - expect(expectedWalletId.serialized).toEqual(actualWalletId.serialized); + expect(expectedWalletId.subwalletNumber).toEqual(actualWalletId.subwalletNumber); }); it('Get subwallet number', async () => { diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 50df21ad..60463a51 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -71,7 +71,7 @@ describe('Wallet V5 sign auth internal', () => { function createBody(actionsList: Cell) { const payload = beginCell() .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -471,14 +471,14 @@ describe('Wallet V5 sign auth internal', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(vu, 32) .storeUint(seqno, 32) // seqno .storeSlice(actionsList.beginParse()) .endCell(); const fakePayload = beginCell() - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(vu, 32) .storeUint(seqno + 1, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -523,7 +523,7 @@ describe('Wallet V5 sign auth internal', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -577,7 +577,7 @@ describe('Wallet V5 sign auth internal', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno + 1, 32) // seqno .storeSlice(actionsList.beginParse()) @@ -622,7 +622,7 @@ describe('Wallet V5 sign auth internal', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(Math.round(Date.now() / 1000) - 600, 32) .storeUint(seqno, 32) .storeSlice(actionsList.beginParse()) @@ -667,7 +667,7 @@ describe('Wallet V5 sign auth internal', () => { const payload = beginCell() .storeUint(Opcodes.auth_signed_internal, 32) - .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 80) + .storeUint(new WalletId({ ...WALLET_ID, subwalletNumber: 1 }).serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) .storeSlice(actionsList.beginParse()) @@ -712,7 +712,7 @@ describe('Wallet V5 sign auth internal', () => { const payload = beginCell() // auth_signed used instead of auth_signed_internal .storeUint(Opcodes.auth_signed, 32) - .storeUint(WALLET_ID.serialized, 80) + .storeUint(WALLET_ID.serialized, 32) .storeUint(validUntil(), 32) .storeUint(seqno, 32) .storeSlice(actionsList.beginParse()) diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 932818c0..9a90c8c9 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -26,7 +26,7 @@ export function walletV5ConfigToCell(config: WalletV5Config): Cell { return beginCell() .storeBit(config.signatureAllowed) .storeUint(config.seqno, 32) - .storeUint(config.walletId, 80) + .storeUint(config.walletId, 32) .storeBuffer(config.publicKey, 32) .storeDict(config.extensions, Dictionary.Keys.BigUint(256), Dictionary.Values.BigInt(1)) .endCell(); @@ -49,30 +49,30 @@ export class WalletId { v5: 0 }; - static deserialize(walletId: bigint | Buffer): WalletId { - const bitReader = new BitReader( - new BitString( - typeof walletId === 'bigint' ? Buffer.from(walletId.toString(16), 'hex') : walletId, - 0, - 80 - ) - ); - const networkGlobalId = bitReader.loadInt(32); - const workChain = bitReader.loadInt(8); - const walletVersionRaw = bitReader.loadUint(8); - const subwalletNumber = bitReader.loadUint(32); - - const walletVersion = Object.entries(this.versionsSerialisation).find( - ([_, value]) => value === walletVersionRaw - )?.[0] as WalletId['walletVersion'] | undefined; - - if (walletVersion === undefined) { - throw new Error( - `Can't deserialize walletId: unknown wallet version ${walletVersionRaw}` - ); - } - - return new WalletId({ networkGlobalId, workChain, walletVersion, subwalletNumber }); + static deserialize(walletId: bigint): WalletId { + // const bitReader = new BitReader( + // new BitString( + // typeof walletId === 'bigint' ? Buffer.from(walletId.toString(16), 'hex') : walletId, + // 0, + // 32 + // ) + // ); + // const networkGlobalId = bitReader.loadInt(32); + // const workChain = bitReader.loadInt(8); + // const walletVersionRaw = bitReader.loadUint(8); + const subwalletNumber = walletId; + // + // const walletVersion = Object.entries(this.versionsSerialisation).find( + // ([_, value]) => value === walletVersionRaw + // )?.[0] as WalletId['walletVersion'] | undefined; + // + // if (walletVersion === undefined) { + // throw new Error( + // `Can't deserialize walletId: unknown wallet version ${walletVersionRaw}` + // ); + // } + // + return new WalletId({ networkGlobalId: 0, workChain: 0, walletVersion: 'v5', subwalletNumber: Number(walletId) }); } readonly walletVersion: 'v5'; @@ -97,13 +97,13 @@ export class WalletId { this.subwalletNumber = args?.subwalletNumber ?? 0; this.walletVersion = args?.walletVersion ?? 'v5'; - const bitBuilder = new BitBuilder(80); - bitBuilder.writeInt(this.networkGlobalId, 32); - bitBuilder.writeInt(this.workChain, 8); - bitBuilder.writeUint(WalletId.versionsSerialisation[this.walletVersion], 8); - bitBuilder.writeUint(this.subwalletNumber, 32); + // const bitBuilder = new BitBuilder(32); + // bitBuilder.writeInt(this.networkGlobalId, 32); + // bitBuilder.writeInt(this.workChain, 8); + // bitBuilder.writeUint(WalletId.versionsSerialisation[this.walletVersion], 8); + // bitBuilder.writeUint(this.subwalletNumber, 32); - this.serialized = bufferToBigInt(bitBuilder.buffer()); + this.serialized = BigInt(this.subwalletNumber) // bufferToBigInt(bitBuilder.buffer()); } } From 21f229a000d2056e86fb60af1ccb1bbacffc3dd8 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Tue, 25 Jun 2024 18:49:39 +0400 Subject: [PATCH 080/121] change refs in message (tests not synced yet) --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 1d9598ac..f499330e 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100027a000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080a0eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae163f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff80280d020c702dc01d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d749810281b9dc708ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02ae9401d200018eccd72c08142091709601d72c081c12e25210b18e3230d72c08248e27d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2e30dd74cd001e8d74c011110005021d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54a9269d25"} \ No newline at end of file +{"hex":"b5ee9c724102120100028b000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080a0eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae163f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff80278d001d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d749810281b9dc708ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c4eda2edfb01f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b39130e0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac0009330db31e0d74cd01110006430d72c08248e27d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed547da1ee1f"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 9aa01b63..095a327e 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -89,8 +89,18 @@ cell verify_c5_actions(cell c5, int is_external) inline { } () process_actions(slice cs, int is_external) impure inline_ref { + cell c5_actions = cs~load_maybe_ref(); + if (~ cell_null?(c5_actions)) { + ;; Simply set the C5 register with all pre-computed actions after verification: + set_c5_actions(c5_actions.verify_c5_actions(is_external)); + } + int has_other_actions = cs~load_int(1); + if (~ has_other_actions) { + return (); + } + ;; Loop extended actions until we reach standard actions - while (cs~load_int(1)) { + while (true) { int is_add_extension = cs~check_and_remove_add_extension_prefix(); int is_remove_extension = is_add_extension ? 0 : cs~check_and_remove_remove_extension_prefix(); ;; Add/remove extensions @@ -140,10 +150,11 @@ cell verify_c5_actions(cell c5, int is_external) inline { } else { throw(error::unspported_action); } + if (cs.slice_refs() == 0) { + return (); + } cs = cs.preload_ref().begin_parse(); } - ;; Simply set the C5 register with all pre-computed actions after verification: - set_c5_actions(cs.preload_ref().verify_c5_actions(is_external)); } ;; ------------------------------------------------------------------------------------------------ @@ -202,7 +213,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ () recv_internal(cell in_msg_full, slice in_msg_body) impure inline { - return_if(in_msg_body.slice_refs_empty?()); ;; message with actions always have a ref + ;; return_if(in_msg_body.slice_refs_empty?()); ;; message with actions always have a ref todo slice in_msg_full_slice = in_msg_full.begin_parse(); slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool From 0a40931ab366ade923a7f7b42a40bb223e8ceda9 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Tue, 25 Jun 2024 18:51:57 +0400 Subject: [PATCH 081/121] only extension can change signature mode (tests not synced yet) --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index f499330e..cf68c06d 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102120100028b000114ff00f4a413f4bcf2c80b01020120030201eaf220d70b1f82107369676ebaf2af7f8ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f0201480d0402012006050019be5f0f6a2684080a0eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae163f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff80278d001d0d60301c713dc01d72c232bc3a3748ea701fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d72170db3ce30e0f0e01f83120d70b1f821073696e74badd20d749810281b9dc708ee3eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fdedb3cd80f02c4eda2edfb01f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b39130e0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac0009330db31e0d74cd01110006430d72c08248e27d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed547da1ee1f"} \ No newline at end of file +{"hex":"b5ee9c7241021201000295000114ff00f4a413f4bcf2c80b01020120030201f0f220d70b1f82107369676ebaf2af7f708ee5eda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd80f0201480d0402012006050019be5f0f6a2684080a0eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae163f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff8027ad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e0f0e01fe3120d70b1f821073696e74badd20d749810281b9dc70708ee5eda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd80f02c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01110006a30d72c08248e2a21f2b0d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed549235d7cc"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 095a327e..c8ca6371 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -16,6 +16,7 @@ const int error::invalid_seqno = 33; const int error::invalid_wallet_id = 34; const int error::expired = 36; const int error::invalid_message_type = 47; +const int error::only_extension_can_change_signature_mode = 48; const int size::bool = 1; const int size::seqno = 32; @@ -88,7 +89,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return c5; } -() process_actions(slice cs, int is_external) impure inline_ref { +() process_actions(slice cs, int is_external, int is_extension) impure inline_ref { cell c5_actions = cs~load_maybe_ref(); if (~ cell_null?(c5_actions)) { ;; Simply set the C5 register with all pre-computed actions after verification: @@ -131,6 +132,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { .end_cell()); } elseif (cs~check_and_remove_set_signature_allowed_prefix()) { + throw_unless(error::only_extension_can_change_signature_mode, is_extension); int allow_signature = cs~load_int(1); slice data_slice = get_data().begin_parse(); int is_signature_allowed = data_slice~load_int(size::bool); @@ -159,7 +161,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ -() process_signed_request(slice in_msg_body, int is_external) impure inline { +() process_signed_request(slice in_msg_body, int is_external, int is_extension) impure inline { slice signature = in_msg_body.get_last_bits(size::signature); slice signed_slice = in_msg_body.remove_last_bits(size::signature); @@ -202,12 +204,12 @@ cell verify_c5_actions(cell c5, int is_external) inline { commit(); } - process_actions(cs, is_external); + process_actions(cs, is_external, is_external); } () recv_external(slice in_msg_body) impure inline { throw_unless(error::invalid_message_type, in_msg_body.preload_uint(size::message_type_prefix) == prefix::signed_external); - process_signed_request(in_msg_body, true); + process_signed_request(in_msg_body, true, false); } ;; ------------------------------------------------------------------------------------------------ @@ -239,7 +241,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { in_msg_body~skip_bits(size::query_id); ;; skip query_id - process_actions(in_msg_body, false); + process_actions(in_msg_body, false, true); return (); } @@ -247,7 +249,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return_unless(in_msg_body.preload_uint(size::message_type_prefix) == prefix::signed_internal); ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); - process_signed_request(in_msg_body, false); + process_signed_request(in_msg_body, false, false); } ;; ------------------------------------------------------------------------------------------------ From 3d822d93154fe340aad31304d09f209c18859eb9 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Tue, 25 Jun 2024 18:53:38 +0400 Subject: [PATCH 082/121] allow work with (~ is_signature_allowed) and empty extensions --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index cf68c06d..1363e2ca 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021201000295000114ff00f4a413f4bcf2c80b01020120030201f0f220d70b1f82107369676ebaf2af7f708ee5eda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd80f0201480d0402012006050019be5f0f6a2684080a0eb90fa02c0201200c0702012009080019b45d1da89a10043ae43ae163f00201200b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff8027ad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e0f0e01fe3120d70b1f821073696e74badd20d749810281b9dc70708ee5eda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd70bff09f90140b9f9109927945f09db31e1f223df01f2a05122baf2a15036baf2a2f823bbf2642292f800dea47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd80f02c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01110006a30d72c08248e2a21f2b0d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed549235d7cc"} \ No newline at end of file +{"hex":"b5ee9c7241021301000238000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2af7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01de8eeceda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109928945f0adb31e1f223df02b35007b0f2605125baf2a15036baf2a2f823bbf2642292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd81002c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01211006a30d72c08248e2a21f2b0d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54b19a10ad"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index c8ca6371..cefafd29 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -173,7 +173,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { int stored_seqno = data_slice~load_uint(size::seqno); slice data_tail = data_slice; ;; wallet_id, public_key, extensions int stored_wallet_id = data_slice~load_uint(size::wallet_id); - int public_key = data_slice.preload_uint(size::public_key); + int public_key = data_slice~load_uint(size::public_key); + int is_extensions_not_empty = data_slice.preload_int(1); int is_signature_valid = check_signature(slice_hash(signed_slice), signature, public_key); ifnot (is_signature_valid) { @@ -183,7 +184,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return (); } } - throw_unless(error::signature_disabled, is_signature_allowed); + throw_if(error::signature_disabled, (~ is_signature_allowed) & is_extensions_not_empty); throw_unless(error::invalid_seqno, seqno == stored_seqno); throw_unless(error::invalid_wallet_id, wallet_id == stored_wallet_id); throw_if(error::expired, valid_until <= now()); From c5dc057f4b65f1481bf61e1086248ee6a45a109c Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 15:55:55 +0400 Subject: [PATCH 083/121] refactor errors - only only_extension_can_change_signature_mode number changed --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 1363e2ca..c2d1b30f 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021301000238000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2af7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01de8eeceda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109928945f0adb31e1f223df02b35007b0f2605125baf2a15036baf2a2f823bbf2642292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd81002c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01211006a30d72c08248e2a21f2b0d200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54b19a10ad"} \ No newline at end of file +{"hex":"b5ee9c7241021301000238000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2af7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01de8eeceda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109928945f0adb31e1f223df02b35007b0f2605125baf2a15036baf2a2f823bbf2642292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd81002c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01211006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed545d03e60a"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index cefafd29..d5aeba9c 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -2,21 +2,21 @@ #include "imports/stdlib.fc"; +const int error::signature_disabled = 32; +const int error::invalid_seqno = 33; +const int error::invalid_wallet_id = 34; +const int error::invalid_signature = 35; +const int error::expired = 36; const int error::invalid_c5_action = 37; -const int error::extension_wrong_workchain = 45; const int error::add_extension_error = 39; const int error::remove_extension_error = 40; +const int error::unspported_action = 41; const int error::disable_signature_when_extensions_is_empty = 42; const int error::this_signature_mode_already_set = 43; const int error::remove_last_extension_when_signature_disabled = 44; -const int error::unspported_action = 41; -const int error::signature_disabled = 32; -const int error::invalid_signature = 35; -const int error::invalid_seqno = 33; -const int error::invalid_wallet_id = 34; -const int error::expired = 36; +const int error::extension_wrong_workchain = 45; +const int error::only_extension_can_change_signature_mode = 46; const int error::invalid_message_type = 47; -const int error::only_extension_can_change_signature_mode = 48; const int size::bool = 1; const int size::seqno = 32; From c678610ea93170a1c5cbba4b8fbbcb2ae9f89003 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:02:21 +0400 Subject: [PATCH 084/121] refactor errors naming --- contracts/wallet_v5.fc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index d5aeba9c..9bcb20de 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -7,9 +7,9 @@ const int error::invalid_seqno = 33; const int error::invalid_wallet_id = 34; const int error::invalid_signature = 35; const int error::expired = 36; -const int error::invalid_c5_action = 37; -const int error::add_extension_error = 39; -const int error::remove_extension_error = 40; +const int error::external_send_message_must_have_ignore_errors_send_mode = 37; +const int error::add_extension = 39; +const int error::remove_extension = 40; const int error::unspported_action = 41; const int error::disable_signature_when_extensions_is_empty = 42; const int error::this_signature_mode_already_set = 43; @@ -81,7 +81,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; ;; https://github.com/ton-blockchain/ton/blob/5c392e0f2d946877bb79a09ed35068f7b0bd333a/crypto/block/block.tlb#L380 ;; load 7 bits and make sure that they end with 1 - throw_if(error::invalid_c5_action, is_external & (count_trailing_zeroes(cs.preload_bits(7)) > 0)); + throw_if(error::external_send_message_must_have_ignore_errors_send_mode, is_external & (count_trailing_zeroes(cs.preload_bits(7)) > 0)); (cs, _) = cs.preload_ref().begin_parse_raw(); } @@ -118,10 +118,10 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Add extension if (is_add_extension) { (extensions, int is_success) = extensions.udict_add_builder?(size::address_hash_size, address_hash, begin_cell().store_int(-1, 1)); - throw_unless( error::add_extension_error, is_success); + throw_unless( error::add_extension, is_success); } else { ;; Remove extension (extensions, int is_success) = extensions.udict_delete?(size::address_hash_size, address_hash); - throw_unless(error::remove_extension_error, is_success); + throw_unless(error::remove_extension, is_success); int is_signature_allowed = data_slice_before_extensions.preload_int(size::bool); throw_if(error::remove_last_extension_when_signature_disabled, null?(extensions) & (~ is_signature_allowed)); } From aff9c49123d8912c425692024f2d20730a594144 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:05:59 +0400 Subject: [PATCH 085/121] refactor errors --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index c2d1b30f..a7562726 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021301000238000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2af7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01de8eeceda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109928945f0adb31e1f223df02b35007b0f2605125baf2a15036baf2a2f823bbf2642292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd81002c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01211006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed545d03e60a"} \ No newline at end of file +{"hex":"b5ee9c7241021301000238000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2a67f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01de8eeceda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109928945f0adb31e1f223df02b35007b0f2605125baf2a15036baf2a2f823bbf2642292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd81002c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01211006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed5409c293b9"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 9bcb20de..6d209e04 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -8,6 +8,7 @@ const int error::invalid_wallet_id = 34; const int error::invalid_signature = 35; const int error::expired = 36; const int error::external_send_message_must_have_ignore_errors_send_mode = 37; +const int error::invalid_message_type = 38; const int error::add_extension = 39; const int error::remove_extension = 40; const int error::unspported_action = 41; @@ -16,7 +17,6 @@ const int error::this_signature_mode_already_set = 43; const int error::remove_last_extension_when_signature_disabled = 44; const int error::extension_wrong_workchain = 45; const int error::only_extension_can_change_signature_mode = 46; -const int error::invalid_message_type = 47; const int size::bool = 1; const int size::seqno = 32; From e4b8c5bca7002e79db772c22d5aaef4f895f29e2 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:13:16 +0400 Subject: [PATCH 086/121] cosmetic (bytecode not changed) --- contracts/wallet_v5.fc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 6d209e04..d6cdff2d 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -32,17 +32,17 @@ const int size::query_id = 64; const int prefix::signed_external = 0x7369676E; const int prefix::signed_internal = 0x73696E74; -;;; performs a RET, but only if integer f is non-zero. If f is a NaN, throws an integer overflow exception. -() return_if(int f) impure asm "IFRET"; -;;; performs a RET, but only if integer f is zero -() return_unless(int f) impure asm "IFNOTRET"; - (slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{02} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{03} SDBEGINSQ"; (slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{04} SDBEGINSQ"; +;;; performs a RET, but only if integer f is non-zero. If f is a NaN, throws an integer overflow exception. +() return_if(int f) impure asm "IFRET"; +;;; performs a RET, but only if integer f is zero +() return_unless(int f) impure asm "IFNOTRET"; + ;;; returns the number of trailing zeroes in slice s. int count_trailing_zeroes(slice s) asm "SDCNTTRAIL0"; ;;; returns the number of trailing ones in slice s. From dd9ff2899e74e3e130c58e4f65e72b327b4fa408 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:23:37 +0400 Subject: [PATCH 087/121] cosmetic comment (bytecode not changed) --- contracts/wallet_v5.fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index d6cdff2d..96a80b65 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -72,7 +72,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { (slice cs, _) = c5.begin_parse_raw(); while (~ cs.slice_empty?()) { - ;; only `action_send_msg` is allowed, `action_set_code` or `action_reserve_currency` are not + ;; only `action_send_msg` is allowed; `action_set_code`, `action_reserve_currency` or `action_change_library` are not. cs = cs.enforce_and_remove_action_send_msg_prefix(); ;; enforce that send_mode has +2 bit (ignore errors) set for external message. From b7b8fd89111d3e9c281e742dcbdce0f49d42a431 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:33:21 +0400 Subject: [PATCH 088/121] simplify --- contracts/wallet_v5.fc | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 96a80b65..c79fb4e8 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -91,14 +91,11 @@ cell verify_c5_actions(cell c5, int is_external) inline { () process_actions(slice cs, int is_external, int is_extension) impure inline_ref { cell c5_actions = cs~load_maybe_ref(); - if (~ cell_null?(c5_actions)) { + ifnot (cell_null?(c5_actions)) { ;; Simply set the C5 register with all pre-computed actions after verification: set_c5_actions(c5_actions.verify_c5_actions(is_external)); } - int has_other_actions = cs~load_int(1); - if (~ has_other_actions) { - return (); - } + return_unless(cs~load_int(1)); ;; has_other_actions ;; Loop extended actions until we reach standard actions while (true) { @@ -152,9 +149,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { } else { throw(error::unspported_action); } - if (cs.slice_refs() == 0) { - return (); - } + return_unless(cs.slice_refs()); cs = cs.preload_ref().begin_parse(); } } From 3dc030e79df3b6631449c6822276758bf8afab2c Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:33:35 +0400 Subject: [PATCH 089/121] fix mistake --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index a7562726..fabf5f39 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021301000238000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2a67f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01de8eeceda2edfb30218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109928945f0adb31e1f223df02b35007b0f2605125baf2a15036baf2a2f823bbf2642292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde20db3cd81002c4eda2edfb02f404216eb38e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55926c21e2d20001b3915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74ac000935bdb31e0d74cd01211006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed5409c293b9"} \ No newline at end of file +{"hex":"b5ee9c7241021301000228000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2a67f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01da8eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81002a802f404216e926c218e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74addd74cd01211006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed5487fb70d0"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index c79fb4e8..8a54766f 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -200,7 +200,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { commit(); } - process_actions(cs, is_external, is_external); + process_actions(cs, is_external, is_extension); } () recv_external(slice in_msg_body) impure inline { From e6ef9e4ee56a0e5b9f7f9fbaa13804ae86748bf6 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:46:38 +0400 Subject: [PATCH 090/121] receive tons --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index fabf5f39..02d71d8e 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021301000228000114ff00f4a413f4bcf2c80b0102012004020102f203011e20d70b1f82107369676ebaf2a67f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200d080201200a090019b45d1da89a10043ae43ae163f00201200c0b0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802aad001d0d60301c713dc01d72c232bc3a3748ea801fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3c8e973120d70b1f821073696e74badd20d749810281b9dc7070e2100f01da8eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81002a802f404216e926c218e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74addd74cd01211006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed5487fb70d0"} \ No newline at end of file +{"hex":"b5ee9c72410214010002ab000114ff00f4a413f4bcf2c80b0102012005020102f203011e20d70b1f82107369676ebaf2a67f700401da8eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd8110201480f0602012008070019be5f0f6a2684080a0eb90fa02c0201200e090201200b0a0019b45d1da89a10043ae43ae163f00201200d0c0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802bad020d749c120dc20d70b1f2082106578746eba21821073696e74bab1dd02d0d60301c713dc0282106578746eba8eac8020d72101fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e111001f03120d749810281b9dc70708eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81102a802f404216e926c218e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74addd74cd01312006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54b2d3ba0e"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 8a54766f..fbb4cef3 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -8,7 +8,7 @@ const int error::invalid_wallet_id = 34; const int error::invalid_signature = 35; const int error::expired = 36; const int error::external_send_message_must_have_ignore_errors_send_mode = 37; -const int error::invalid_message_type = 38; +const int error::invalid_message_operation = 38; const int error::add_extension = 39; const int error::remove_extension = 40; const int error::unspported_action = 41; @@ -25,14 +25,13 @@ const int size::public_key = 256; const int size::valid_until = 32; const int size::message_flags = 4; const int size::signature = 512; -const int size::message_type_prefix = 32; +const int size::message_operation_prefix = 32; const int size::address_hash_size = 256; const int size::query_id = 64; const int prefix::signed_external = 0x7369676E; const int prefix::signed_internal = 0x73696E74; - -(slice, int) check_and_remove_extension_action_prefix(slice body) impure asm "x{6578746E} SDBEGINSQ"; +const int prefix::extension_action = 0x6578746E; (slice, int) check_and_remove_add_extension_prefix(slice body) impure asm "x{02} SDBEGINSQ"; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{03} SDBEGINSQ"; @@ -160,7 +159,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { slice signature = in_msg_body.get_last_bits(size::signature); slice signed_slice = in_msg_body.remove_last_bits(size::signature); - slice cs = signed_slice.skip_bits(size::message_type_prefix); ;; skip signed_internal or signed_external prefix + slice cs = signed_slice.skip_bits(size::message_operation_prefix); ;; skip signed_internal or signed_external prefix (int wallet_id, int valid_until, int seqno) = (cs~load_uint(size::wallet_id), cs~load_uint(size::valid_until), cs~load_uint(size::seqno)); slice data_slice = get_data().begin_parse(); @@ -204,21 +203,24 @@ cell verify_c5_actions(cell c5, int is_external) inline { } () recv_external(slice in_msg_body) impure inline { - throw_unless(error::invalid_message_type, in_msg_body.preload_uint(size::message_type_prefix) == prefix::signed_external); + throw_unless(error::invalid_message_operation, in_msg_body.preload_uint(size::message_operation_prefix) == prefix::signed_external); process_signed_request(in_msg_body, true, false); } ;; ------------------------------------------------------------------------------------------------ () recv_internal(cell in_msg_full, slice in_msg_body) impure inline { - ;; return_if(in_msg_body.slice_refs_empty?()); ;; message with actions always have a ref todo + return_if(in_msg_body.slice_bits() < size::message_operation_prefix); + int op = in_msg_body.preload_uint(size::message_operation_prefix); + return_unless((op == prefix::extension_action) | (op == prefix::signed_internal)); slice in_msg_full_slice = in_msg_full.begin_parse(); slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. return_if(count_trailing_ones(message_flags_slice)); - if (in_msg_body~check_and_remove_extension_action_prefix()) { + if (op == prefix::extension_action) { + in_msg_body~skip_bits(size::message_operation_prefix); ;; Authenticate extension by its address. (int sender_address_wc, int sender_address_hash) = parse_std_addr(in_msg_full_slice~load_msg_addr()); @@ -242,9 +244,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { } - return_unless(in_msg_body.preload_uint(size::message_type_prefix) == prefix::signed_internal); ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(in_msg_body.slice_bits() < size::message_type_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); + return_if(in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); process_signed_request(in_msg_body, false, false); } From 39d0feb8d5dc59ef94c977c2762bee0ad1809ee5 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 16:47:40 +0400 Subject: [PATCH 091/121] get_wallet_id -> get_subwallet_id, same with v4 --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 02d71d8e..64d2d02e 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c72410214010002ab000114ff00f4a413f4bcf2c80b0102012005020102f203011e20d70b1f82107369676ebaf2a67f700401da8eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd8110201480f0602012008070019be5f0f6a2684080a0eb90fa02c0201200e090201200b0a0019b45d1da89a10043ae43ae163f00201200d0c0011b262fb513435c280200017b325fb51341c75c875c2c7e00019bb39ced44d08041d721d70bff802bad020d749c120dc20d70b1f2082106578746eba21821073696e74bab1dd02d0d60301c713dc0282106578746eba8eac8020d72101fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e111001f03120d749810281b9dc70708eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81102a802f404216e926c218e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74addd74cd01312006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed54b2d3ba0e"} \ No newline at end of file +{"hex":"b5ee9c72410214010002ab000114ff00f4a413f4bcf2c80b0102012005020102f203011e20d70b1f82107369676ebaf2a67f700401da8eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd8110201480f0602012008070019be5f0f6a2684080a0eb90fa02c0201200c090201480b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0e0d0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002bad020d749c120dc20d70b1f2082106578746eba21821073696e74bab1dd02d0d60301c713dc0282106578746eba8eac8020d72101fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e111001f03120d749810281b9dc70708eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81102a802f404216e926c218e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74addd74cd01312006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed543e0648b9"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index fbb4cef3..76ab150a 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -263,7 +263,7 @@ int seqno() method_id { .preload_uint(size::seqno); } -int get_wallet_id() method_id { +int get_subwallet_id() method_id { return get_data().begin_parse() .skip_bits(size::bool + size::seqno) .preload_uint(size::wallet_id); From 444dddea493a25db12ab10129e90a065d8db42bd Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 19:06:21 +0400 Subject: [PATCH 092/121] return_unless not work --- contracts/wallet_v5.fc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 76ab150a..7d7a9c69 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -148,7 +148,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { } else { throw(error::unspported_action); } - return_unless(cs.slice_refs()); + ifnot (cs.slice_refs()) { + return (); + } cs = cs.preload_ref().begin_parse(); } } From 3bd769e937e91816f5ea4799c3df1ad10b3b5d81 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 26 Jun 2024 20:59:08 +0400 Subject: [PATCH 093/121] update wrappers and tests --- tests/actions.ts | 52 +++- tests/wallet-v5-extensions.spec.ts | 18 +- tests/wallet-v5-external.spec.ts | 468 ++++++++++++++--------------- tests/wallet-v5-internal.spec.ts | 140 ++++++--- wrappers/wallet-v5.ts | 2 +- 5 files changed, 386 insertions(+), 294 deletions(-) diff --git a/tests/actions.ts b/tests/actions.ts index 9788e53b..e4ff0707 100644 --- a/tests/actions.ts +++ b/tests/actions.ts @@ -89,21 +89,47 @@ function packActionsListOut(actions: (OutAction | ExtendedAction)[]): Cell { .endCell(); } -function packActionsListExtended(actions: (OutAction | ExtendedAction)[]): Cell { - const [action, ...rest] = actions; - - if (!action || !isExtendedAction(action)) { - return beginCell() - .storeUint(0, 1) - .storeRef(packActionsListOut(actions.slice().reverse())) // tvm handles actions from c5 in reversed order - .endCell(); +function packExtendedActions(extendedActions: ExtendedAction[]): Cell { + const first = extendedActions[0]; + const rest = extendedActions.slice(1); + let builder = beginCell() + .storeSlice(first.serialize().beginParse()); + if (rest.length > 0) { + builder = builder.storeRef(packExtendedActions(extendedActions.slice(1))); } + return builder.endCell(); +} - return beginCell() - .storeUint(1, 1) - .storeSlice(action.serialize().beginParse()) - .storeRef(packActionsListExtended(rest)) - .endCell(); +function packActionsListExtended(actions: (OutAction | ExtendedAction)[]): Cell { + const extendedActions: ExtendedAction[] = []; + const outActions: OutAction[] = []; + actions.forEach(action => { + if (isExtendedAction(action)) { + extendedActions.push(action); + } else { + outActions.push(action); + } + }); + + let builder = beginCell(); + if (outActions.length === 0) { + builder = builder.storeUint(0, 1); + } else { + builder = builder.storeMaybeRef(packActionsListOut(outActions.slice().reverse())); + } + if (extendedActions.length === 0) { + builder = builder.storeUint(0, 1); + } else { + const first = extendedActions[0]; + const rest = extendedActions.slice(1); + builder = builder + .storeUint(1, 1) + .storeSlice(first.serialize().beginParse()); + if (rest.length > 0) { + builder = builder.storeRef(packExtendedActions(rest)); + } + } + return builder.endCell(); } export function packActionsList(actions: (OutAction | ExtendedAction)[]): Cell { diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index f70e186e..68929d71 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -326,11 +326,15 @@ describe('Wallet V5 extensions auth', () => { await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body: createBody(packActionsList([ - new ActionAddExtension(sender.address!), - new ActionSetSignatureAuthAllowed(false) + new ActionAddExtension(sender.address!) ])) }); + const receipt0 = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([new ActionSetSignatureAuthAllowed(false)]) + }); + const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const forwardValue = toNano(0.001); const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; @@ -362,11 +366,17 @@ describe('Wallet V5 extensions auth', () => { await walletV5.sendInternalSignedMessage(sender, { value: toNano(0.1), body: createBody(packActionsList([ - new ActionAddExtension(sender.address!), - new ActionSetSignatureAuthAllowed(false) + new ActionAddExtension(sender.address!) ])) }); + await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionSetSignatureAuthAllowed(false) + ]) + }); + const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); expect(isSignatureAuthAllowed).toEqual(0); diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 1660055e..fd693fe5 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -147,13 +147,13 @@ describe('Wallet V5 sign auth external', () => { .endCell(); const actionsList = beginCell() - .storeUint(0, 1) - .storeRef( + .storeMaybeRef( beginCell() - .storeRef(beginCell().endCell()) + .storeRef(beginCell().endCell()) // empty child - end of action list .storeSlice(sendTxactionAction.beginParse()) .endCell() ) + .storeUint(0, 1) // no other_actions .endCell(); if (config.microscope) @@ -195,8 +195,8 @@ describe('Wallet V5 sign auth external', () => { .endCell(); const actionsList = beginCell() - .storeUint(1, 1) - .storeRef(beginCell().storeUint(0, 1).storeRef(beginCell().endCell()).endCell()) + .storeUint(0, 1) // no c5 actions + .storeUint(1, 1) // have other actions .storeSlice(addExtensionAction.beginParse()) .endCell(); @@ -693,7 +693,7 @@ describe('Wallet V5 sign auth external', () => { expect(walletBalanceBefore).toEqual(walletBalanceAfter); }); - it('Should fail disallowing signature auth with no exts', async () => { + it('only_extension_can_change_signature_mode', async () => { const actionsList = packActionsList([ new ActionSetSignatureAuthAllowed(false) ]); @@ -704,237 +704,237 @@ describe('Wallet V5 sign auth external', () => { (receipt.transactions[0].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(42); + ).toEqual(46); // only_extension_can_change_signature_mode const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); expect(isSignatureAuthAllowed).toEqual(-1); }); - it('Should fail allowing signature auth when allowed', async () => { - const actionsList = packActionsList([ - new ActionSetSignatureAuthAllowed(true) - ]); - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - - expect( - ( - (receipt.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(43); - - const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed).toEqual(-1); - }); - - it('Should add ext and disallow signature auth', async () => { - const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - - const actionsList = packActionsList([ - new ActionAddExtension(testExtension), - new ActionSetSignatureAuthAllowed(false) - ]); - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - accountForGas(receipt.transactions); - - expect( - ( - (receipt.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(0); - - const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed).toEqual(0); - - const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno); - }); - - it('Should add ext and disallow signature auth in separate txs', async () => { - const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - - const actionsList = packActionsList([ - new ActionAddExtension(testExtension) - ]); - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - accountForGas(receipt.transactions); - - expect( - ( - (receipt.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(0); - - const extensionsDict = Dictionary.loadDirect( - Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(1), - await walletV5.getExtensions() - ); - - expect(extensionsDict.size).toEqual(1); - - expect(extensionsDict.get(packAddress(testExtension))).toEqual( - -1n - ); - - const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed).toEqual(-1); - - const actionsList2 = packActionsList([ - new ActionSetSignatureAuthAllowed(false) - ]); - const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); - accountForGas(receipt2.transactions); - - expect( - ( - (receipt2.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(0); - - const isSignatureAuthAllowed2 = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed2).toEqual(0); - - const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno); - }); - - it('Should add ext, disallow sign, allow sign, remove ext in one tx; send in other', async () => { - const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - - const actionsList = packActionsList([ - new ActionAddExtension(testExtension), - new ActionSetSignatureAuthAllowed(false), - new ActionSetSignatureAuthAllowed(true), - new ActionRemoveExtension(testExtension) - ]); - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - accountForGas(receipt.transactions); - - expect( - ( - (receipt.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(0); - - const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed).toEqual(-1); - - const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno); - - const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - const forwardValue = toNano(0.001); - - const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; - - const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); - - const actionsList2 = packActionsList([ - new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) - ]); - - const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); - - expect(receipt2.transactions.length).toEqual(2); - accountForGas(receipt2.transactions); - - expect(receipt2.transactions).toHaveTransaction({ - from: walletV5.address, - to: testReceiver, - value: forwardValue - }); - - const fee = receipt2.transactions[1].totalFees.coins; - const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; - expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); - }); - - it('Should fail removing last extension with signature auth disabled', async () => { - const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - - const actionsList = packActionsList([ - new ActionAddExtension(testExtension), - new ActionSetSignatureAuthAllowed(false), - new ActionRemoveExtension(testExtension) - ]); - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - accountForGas(receipt.transactions); - - expect( - ( - (receipt.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(44); - - const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed).toEqual(-1); - }); - - it('Should fail disallowing signature auth twice in tx', async () => { - const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - - const actionsList = packActionsList([ - new ActionAddExtension(testExtension), - new ActionSetSignatureAuthAllowed(false), - new ActionSetSignatureAuthAllowed(false) - ]); - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - accountForGas(receipt.transactions); - - expect( - ( - (receipt.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(43); - - const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed).toEqual(-1); // throw when handling, packet is dropped - }); - - it('Should add ext, disallow sig auth; fail different signed tx', async () => { - const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); - - const actionsList = packActionsList([ - new ActionAddExtension(testExtension), - new ActionSetSignatureAuthAllowed(false) - ]); - const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); - accountForGas(receipt.transactions); - - expect( - ( - (receipt.transactions[0].description as TransactionDescriptionGeneric) - .computePhase as TransactionComputeVm - ).exitCode - ).toEqual(0); - - const extensionsDict = Dictionary.loadDirect( - Dictionary.Keys.BigUint(256), - Dictionary.Values.BigInt(1), - await walletV5.getExtensions() - ); - - expect(extensionsDict.size).toEqual(1); - - expect(extensionsDict.get(packAddress(testExtension))).toEqual( - -1n - ); - - const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); - expect(isSignatureAuthAllowed).toEqual(0); - - const contract_seqno = await walletV5.getSeqno(); - expect(contract_seqno).toEqual(seqno); - - await disableConsoleError(() => - expect(walletV5.sendExternalSignedMessage(createBody(packActionsList([])))).rejects.toThrow() - ); - }); + // it('Should fail allowing signature auth when allowed', async () => { + // const actionsList = packActionsList([ + // new ActionSetSignatureAuthAllowed(true) + // ]); + // const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + // + // expect( + // ( + // (receipt.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(43); + // + // const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed).toEqual(-1); + // }); + + // it('Should add ext and disallow signature auth', async () => { + // const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + // + // const actionsList = packActionsList([ + // new ActionAddExtension(testExtension), + // new ActionSetSignatureAuthAllowed(false) + // ]); + // const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + // accountForGas(receipt.transactions); + // + // expect( + // ( + // (receipt.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(0); + // + // const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed).toEqual(0); + // + // const contract_seqno = await walletV5.getSeqno(); + // expect(contract_seqno).toEqual(seqno); + // }); + + // it('Should add ext and disallow signature auth in separate txs', async () => { + // const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + // + // const actionsList = packActionsList([ + // new ActionAddExtension(testExtension) + // ]); + // const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + // accountForGas(receipt.transactions); + // + // expect( + // ( + // (receipt.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(0); + // + // const extensionsDict = Dictionary.loadDirect( + // Dictionary.Keys.BigUint(256), + // Dictionary.Values.BigInt(1), + // await walletV5.getExtensions() + // ); + // + // expect(extensionsDict.size).toEqual(1); + // + // expect(extensionsDict.get(packAddress(testExtension))).toEqual( + // -1n + // ); + // + // const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed).toEqual(-1); + // + // const actionsList2 = packActionsList([ + // new ActionSetSignatureAuthAllowed(false) + // ]); + // const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); + // accountForGas(receipt2.transactions); + // + // expect( + // ( + // (receipt2.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(0); + // + // const isSignatureAuthAllowed2 = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed2).toEqual(0); + // + // const contract_seqno = await walletV5.getSeqno(); + // expect(contract_seqno).toEqual(seqno); + // }); + // + // it('Should add ext, disallow sign, allow sign, remove ext in one tx; send in other', async () => { + // const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + // + // const actionsList = packActionsList([ + // new ActionAddExtension(testExtension), + // new ActionSetSignatureAuthAllowed(false), + // new ActionSetSignatureAuthAllowed(true), + // new ActionRemoveExtension(testExtension) + // ]); + // const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + // accountForGas(receipt.transactions); + // + // expect( + // ( + // (receipt.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(0); + // + // const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed).toEqual(-1); + // + // const contract_seqno = await walletV5.getSeqno(); + // expect(contract_seqno).toEqual(seqno); + // + // const testReceiver = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + // const forwardValue = toNano(0.001); + // + // const receiverBalanceBefore = (await blockchain.getContract(testReceiver)).balance; + // + // const msg = createMsgInternal({ dest: testReceiver, value: forwardValue }); + // + // const actionsList2 = packActionsList([ + // new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg) + // ]); + // + // const receipt2 = await walletV5.sendExternalSignedMessage(createBody(actionsList2)); + // + // expect(receipt2.transactions.length).toEqual(2); + // accountForGas(receipt2.transactions); + // + // expect(receipt2.transactions).toHaveTransaction({ + // from: walletV5.address, + // to: testReceiver, + // value: forwardValue + // }); + // + // const fee = receipt2.transactions[1].totalFees.coins; + // const receiverBalanceAfter = (await blockchain.getContract(testReceiver)).balance; + // expect(receiverBalanceAfter).toEqual(receiverBalanceBefore + forwardValue - fee); + // }); + // + // it('Should fail removing last extension with signature auth disabled', async () => { + // const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + // + // const actionsList = packActionsList([ + // new ActionAddExtension(testExtension), + // new ActionSetSignatureAuthAllowed(false), + // new ActionRemoveExtension(testExtension) + // ]); + // const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + // accountForGas(receipt.transactions); + // + // expect( + // ( + // (receipt.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(44); + // + // const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed).toEqual(-1); + // }); + // + // it('Should fail disallowing signature auth twice in tx', async () => { + // const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + // + // const actionsList = packActionsList([ + // new ActionAddExtension(testExtension), + // new ActionSetSignatureAuthAllowed(false), + // new ActionSetSignatureAuthAllowed(false) + // ]); + // const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + // accountForGas(receipt.transactions); + // + // expect( + // ( + // (receipt.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(43); + // + // const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed).toEqual(-1); // throw when handling, packet is dropped + // }); + // + // it('Should add ext, disallow sig auth; fail different signed tx', async () => { + // const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); + // + // const actionsList = packActionsList([ + // new ActionAddExtension(testExtension), + // new ActionSetSignatureAuthAllowed(false) + // ]); + // const receipt = await walletV5.sendExternalSignedMessage(createBody(actionsList)); + // accountForGas(receipt.transactions); + // + // expect( + // ( + // (receipt.transactions[0].description as TransactionDescriptionGeneric) + // .computePhase as TransactionComputeVm + // ).exitCode + // ).toEqual(0); + // + // const extensionsDict = Dictionary.loadDirect( + // Dictionary.Keys.BigUint(256), + // Dictionary.Values.BigInt(1), + // await walletV5.getExtensions() + // ); + // + // expect(extensionsDict.size).toEqual(1); + // + // expect(extensionsDict.get(packAddress(testExtension))).toEqual( + // -1n + // ); + // + // const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); + // expect(isSignatureAuthAllowed).toEqual(0); + // + // const contract_seqno = await walletV5.getSeqno(); + // expect(contract_seqno).toEqual(seqno); + // + // await disableConsoleError(() => + // expect(walletV5.sendExternalSignedMessage(createBody(packActionsList([])))).rejects.toThrow() + // ); + // }); }); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 60463a51..0c9d35b9 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -140,13 +140,13 @@ describe('Wallet V5 sign auth internal', () => { .endCell(); const actionsList = beginCell() - .storeUint(0, 1) - .storeRef( + .storeMaybeRef( beginCell() .storeRef(beginCell().endCell()) .storeSlice(sendTxactionAction.beginParse()) .endCell() ) + .storeUint(0, 1) // no other actions .endCell(); if (config.microscope) @@ -191,8 +191,8 @@ describe('Wallet V5 sign auth internal', () => { .endCell(); const actionsList = beginCell() + .storeUint(0, 1) // no c5 actions .storeUint(1, 1) - .storeRef(beginCell().storeUint(0, 1).storeRef(beginCell().endCell()).endCell()) .storeSlice(addExtensionAction.beginParse()) .endCell(); @@ -856,15 +856,23 @@ describe('Wallet V5 sign auth internal', () => { it('Should fail disallowing signature auth with no exts', async () => { const actionsList = packActionsList([ - new ActionSetSignatureAuthAllowed(false) + new ActionAddExtension(sender.address!) ]); - const receipt = await walletV5.sendInternal(sender, { + await walletV5.sendInternal(sender, { sendMode: SendMode.PAY_GAS_SEPARATELY, value: toNano(0.1), body: createBody(actionsList) }); + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: packActionsList([ + new ActionRemoveExtension(sender.address!), + new ActionSetSignatureAuthAllowed(false) + ]) + }); + expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced expect( @@ -879,14 +887,21 @@ describe('Wallet V5 sign auth internal', () => { }); it('Should fail allowing signature auth when allowed', async () => { + await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + const actionsList = packActionsList([ new ActionSetSignatureAuthAllowed(true) ]); - const receipt = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList) + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList }); expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced @@ -903,6 +918,14 @@ describe('Wallet V5 sign auth internal', () => { }); it('Should add ext and disallow signature auth', async () => { + await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ @@ -910,10 +933,9 @@ describe('Wallet V5 sign auth internal', () => { new ActionSetSignatureAuthAllowed(false) ]); - const receipt = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList) + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList }); expect(receipt.transactions.length).toEqual(2); @@ -926,7 +948,7 @@ describe('Wallet V5 sign auth internal', () => { await walletV5.getExtensions() ); - expect(extensionsDict.size).toEqual(1); + expect(extensionsDict.size).toEqual(2); expect(extensionsDict.get(packAddress(testExtension))).toEqual( -1n @@ -947,16 +969,23 @@ describe('Wallet V5 sign auth internal', () => { }); it('Should add ext and disallow signature auth in separate txs', async () => { + await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ new ActionAddExtension(testExtension) ]); - const receipt = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList) + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList }); expect(receipt.transactions.length).toEqual(2); @@ -969,7 +998,7 @@ describe('Wallet V5 sign auth internal', () => { await walletV5.getExtensions() ); - expect(extensionsDict.size).toEqual(1); + expect(extensionsDict.size).toEqual(2); expect(extensionsDict.get(packAddress(testExtension))).toEqual( -1n @@ -989,10 +1018,9 @@ describe('Wallet V5 sign auth internal', () => { new ActionSetSignatureAuthAllowed(false) ]); - const receipt2 = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList2) + const receipt2 = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList2 }); expect(receipt2.transactions.length).toEqual(2); @@ -1014,6 +1042,14 @@ describe('Wallet V5 sign auth internal', () => { }); it('Should add ext, disallow sign, allow sign, remove ext in one tx; send in other', async () => { + await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ @@ -1022,11 +1058,9 @@ describe('Wallet V5 sign auth internal', () => { new ActionSetSignatureAuthAllowed(true), new ActionRemoveExtension(testExtension), ]); - - const receipt = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList) + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList }); expect(receipt.transactions.length).toEqual(2); @@ -1077,18 +1111,26 @@ describe('Wallet V5 sign auth internal', () => { }); it('Should fail removing last extension with signature auth disabled', async () => { + await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ new ActionAddExtension(testExtension), new ActionSetSignatureAuthAllowed(false), - new ActionRemoveExtension(testExtension) + new ActionRemoveExtension(testExtension), + new ActionRemoveExtension(sender.address!) ]); - const receipt = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList) + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList }); expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced @@ -1106,6 +1148,14 @@ describe('Wallet V5 sign auth internal', () => { }); it('Should fail disallowing signature auth twice in tx', async () => { + await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ @@ -1114,10 +1164,9 @@ describe('Wallet V5 sign auth internal', () => { new ActionSetSignatureAuthAllowed(false) ]); - const receipt = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList) + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList }); expect(receipt.transactions.length).toEqual(3); // sender_wallet -> wallet_v5 -> bounced @@ -1135,6 +1184,14 @@ describe('Wallet V5 sign auth internal', () => { }); it('Should add ext, disallow sig auth; fail different signed tx', async () => { + await walletV5.sendInternal(sender, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano(0.1), + body: createBody(packActionsList([ + new ActionAddExtension(sender.address!) + ])) + }); + const testExtension = Address.parse('EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y'); const actionsList = packActionsList([ @@ -1142,10 +1199,9 @@ describe('Wallet V5 sign auth internal', () => { new ActionSetSignatureAuthAllowed(false) ]); - const receipt = await walletV5.sendInternal(sender, { - sendMode: SendMode.PAY_GAS_SEPARATELY, - value: toNano(0.1), - body: createBody(actionsList) + const receipt = await walletV5.sendInternalMessageFromExtension(sender, { + value: toNano('0.1'), + body: actionsList }); expect(receipt.transactions.length).toEqual(2); @@ -1157,7 +1213,7 @@ describe('Wallet V5 sign auth internal', () => { await walletV5.getExtensions() ); - expect(extensionsDict.size).toEqual(1); + expect(extensionsDict.size).toEqual(2); expect(extensionsDict.get(packAddress(testExtension))).toEqual( -1n diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 9a90c8c9..5e59c57b 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -212,7 +212,7 @@ export class WalletV5 implements Contract { } async getWalletId(provider: ContractProvider) { - const result = await provider.get('get_wallet_id', []); + const result = await provider.get('get_subwallet_id', []); return WalletId.deserialize(result.stack.readBigNumber()); } From 38f7e3a6783fd69a9837b862af14b88a62a33902 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Thu, 27 Jun 2024 13:27:15 +0400 Subject: [PATCH 094/121] fix typo --- contracts/wallet_v5.fc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 7d7a9c69..af26d5e9 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -11,7 +11,7 @@ const int error::external_send_message_must_have_ignore_errors_send_mode = 37; const int error::invalid_message_operation = 38; const int error::add_extension = 39; const int error::remove_extension = 40; -const int error::unspported_action = 41; +const int error::unsupported_action = 41; const int error::disable_signature_when_extensions_is_empty = 42; const int error::this_signature_mode_already_set = 43; const int error::remove_last_extension_when_signature_disabled = 44; @@ -146,7 +146,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { .store_slice(data_tail) ;; seqno, wallet_id, public_key, extensions .end_cell()); } else { - throw(error::unspported_action); + throw(error::unsupported_action); } ifnot (cs.slice_refs()) { return (); From 64cff845f50f40d67c901b42237b3ac9051a3134 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 28 Jun 2024 12:43:24 +0400 Subject: [PATCH 095/121] change error codes to avoid collision with tvm-exit-codes --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 30 +++++++++++++++--------------- tests/wallet-v5-external.spec.ts | 8 ++++---- tests/wallet-v5-internal.spec.ts | 22 +++++++++++----------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 64d2d02e..89e1d965 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c72410214010002ab000114ff00f4a413f4bcf2c80b0102012005020102f203011e20d70b1f82107369676ebaf2a67f700401da8eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd8110201480f0602012008070019be5f0f6a2684080a0eb90fa02c0201200c090201480b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0e0d0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002bad020d749c120dc20d70b1f2082106578746eba21821073696e74bab1dd02d0d60301c713dc0282106578746eba8eac8020d72101fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e111001f03120d749810281b9dc70708eeaeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109929945f0bdb31e1f223df02b35007b0f2605125baf2a15037baf2a2f823bbf2642392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81102a802f404216e926c218e290221d739309420c700b38e19d72820761e436c20d71d06c712c2005220b0f265d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74addd74cd01312006a30d72c08248e2a21f2aed200ed44d0d2005113baf26b54503091319b01810140d721d70a00f2aae2c8ca0058cf16c9ed5492f229e2008e01fa4001fa44f828fa443058baf2aded44d0810141d718f405049c7fc8ca0040048307f453f2a78e12038307f45bf2a822d70a00216e01b3b0f26ce2c85003cf1612f400c9ed543e0648b9"} \ No newline at end of file +{"hex":"b5ee9c72410214010002c7000114ff00f4a413f4bcf2c80b0102012005020102f203012020d70b1f82107369676ebaf2e08a7f700401e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd8110201480f0602012008070019be5f0f6a2684080a0eb90fa02c0201200c090201480b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0e0d0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002bad020d749c120dc20d70b1f2082106578746eba21821073696e74bab1dd02d0d60301c713dc0282106578746eba8eac8020d72101fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e111001fa3120d749810281b9dc70708eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81102baeda2edfb02f404216e926c218e2a0221d739309420c700b38e1ad72820761e436c20d71d06c712c2005220b0f2d089d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74a935bdb31e1d74cd01312007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed5458c947bd"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index af26d5e9..8e6e6b0a 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -2,21 +2,21 @@ #include "imports/stdlib.fc"; -const int error::signature_disabled = 32; -const int error::invalid_seqno = 33; -const int error::invalid_wallet_id = 34; -const int error::invalid_signature = 35; -const int error::expired = 36; -const int error::external_send_message_must_have_ignore_errors_send_mode = 37; -const int error::invalid_message_operation = 38; -const int error::add_extension = 39; -const int error::remove_extension = 40; -const int error::unsupported_action = 41; -const int error::disable_signature_when_extensions_is_empty = 42; -const int error::this_signature_mode_already_set = 43; -const int error::remove_last_extension_when_signature_disabled = 44; -const int error::extension_wrong_workchain = 45; -const int error::only_extension_can_change_signature_mode = 46; +const int error::signature_disabled = 132; +const int error::invalid_seqno = 133; +const int error::invalid_wallet_id = 134; +const int error::invalid_signature = 135; +const int error::expired = 136; +const int error::external_send_message_must_have_ignore_errors_send_mode = 137; +const int error::invalid_message_operation = 138; +const int error::add_extension = 139; +const int error::remove_extension = 140; +const int error::unsupported_action = 141; +const int error::disable_signature_when_extensions_is_empty = 142; +const int error::this_signature_mode_already_set = 143; +const int error::remove_last_extension_when_signature_disabled = 144; +const int error::extension_wrong_workchain = 145; +const int error::only_extension_can_change_signature_mode = 146; const int size::bool = 1; const int size::seqno = 32; diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index fd693fe5..893b1793 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -386,7 +386,7 @@ describe('Wallet V5 sign auth external', () => { (receipt.transactions[0].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(41); + ).toEqual(141); }); it('Should fail SetCode action', async () => { @@ -429,7 +429,7 @@ describe('Wallet V5 sign auth external', () => { (receipt.transactions[0].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(39); + ).toEqual(139); const extensionsDict2 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), @@ -453,7 +453,7 @@ describe('Wallet V5 sign auth external', () => { (receipt.transactions[0].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(40); + ).toEqual(140); const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), @@ -704,7 +704,7 @@ describe('Wallet V5 sign auth external', () => { (receipt.transactions[0].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(46); // only_extension_can_change_signature_mode + ).toEqual(146); // only_extension_can_change_signature_mode const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); expect(isSignatureAuthAllowed).toEqual(-1); diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 0c9d35b9..597a1970 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -370,7 +370,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(41); + ).toEqual(141); }); it('Should fail SetCode action', async () => { @@ -421,7 +421,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(39); + ).toEqual(139); const extensionsDict2 = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), @@ -448,7 +448,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(40); + ).toEqual(140); const extensionsDict = Dictionary.loadDirect( Dictionary.Keys.BigUint(256), @@ -599,7 +599,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(33); + ).toEqual(133); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -644,7 +644,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(36); + ).toEqual(136); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -689,7 +689,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(34); + ).toEqual(134); expect(receipt.transactions).not.toHaveTransaction({ from: walletV5.address, @@ -880,7 +880,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(42); + ).toEqual(142); const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); expect(isSignatureAuthAllowed).toEqual(-1); @@ -911,7 +911,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(43); + ).toEqual(143); const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); expect(isSignatureAuthAllowed).toEqual(-1); @@ -1141,7 +1141,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(44); + ).toEqual(144); const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); expect(isSignatureAuthAllowed).toEqual(-1); @@ -1177,7 +1177,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(43); + ).toEqual(143); const isSignatureAuthAllowed = await walletV5.getIsSignatureAuthAllowed(); expect(isSignatureAuthAllowed).toEqual(-1); // throw when handling, packet is dropped @@ -1249,7 +1249,7 @@ describe('Wallet V5 sign auth internal', () => { (receipt2.transactions[1].description as TransactionDescriptionGeneric) .computePhase as TransactionComputeVm ).exitCode - ).toEqual(32); + ).toEqual(132); expect(receipt2.transactions).not.toHaveTransaction({ from: walletV5.address, From 6e81ad3d7dea3a9b18638aa9e201548ac1eb4720 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Fri, 28 Jun 2024 12:48:01 +0400 Subject: [PATCH 096/121] remove return_if, return_unless because its dangerous --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 33 +++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 89e1d965..9111636e 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c72410214010002c7000114ff00f4a413f4bcf2c80b0102012005020102f203012020d70b1f82107369676ebaf2e08a7f700401e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd8110201480f0602012008070019be5f0f6a2684080a0eb90fa02c0201200c090201480b0a0011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0e0d0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002bad020d749c120dc20d70b1f2082106578746eba21821073696e74bab1dd02d0d60301c713dc0282106578746eba8eac8020d72101fa4030fa44f828fa443058badded44d0810141d721f4058307f40e6fa131dd8040d721707fdb3ce30e111001fa3120d749810281b9dc70708eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81102baeda2edfb02f404216e926c218e2a0221d739309420c700b38e1ad72820761e436c20d71d06c712c2005220b0f2d089d74cd73930e85bed55e2d20001ddebd72c08142091709601d72c081c12e25210b1e30f20d74a935bdb31e1d74cd01312007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed5458c947bd"} \ No newline at end of file +{"hex":"b5ee9c7241021301000267000114ff00f4a413f4bcf2c80b0102012004020102f203012020d70b1f82107369676ebaf2e08a7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200b080201480a090011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0d0c0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810281b99130e07070e2100f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81002c2eda2edfb02f404216e926c218e2a0221d739309420c700b38e1ad72820761e436c20d71d06c712c2005220b0f2d089d74cd73930e85bed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a935bdb31e1d74cd01211007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54ad2777ad"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 8e6e6b0a..4879bed9 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -37,11 +37,6 @@ const int prefix::extension_action = 0x6578746E; (slice, int) check_and_remove_remove_extension_prefix(slice body) impure asm "x{03} SDBEGINSQ"; (slice, int) check_and_remove_set_signature_allowed_prefix(slice body) impure asm "x{04} SDBEGINSQ"; -;;; performs a RET, but only if integer f is non-zero. If f is a NaN, throws an integer overflow exception. -() return_if(int f) impure asm "IFRET"; -;;; performs a RET, but only if integer f is zero -() return_unless(int f) impure asm "IFNOTRET"; - ;;; returns the number of trailing zeroes in slice s. int count_trailing_zeroes(slice s) asm "SDCNTTRAIL0"; ;;; returns the number of trailing ones in slice s. @@ -94,7 +89,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Simply set the C5 register with all pre-computed actions after verification: set_c5_actions(c5_actions.verify_c5_actions(is_external)); } - return_unless(cs~load_int(1)); ;; has_other_actions + if (cs~load_int(1) == 0) { ;; has_other_actions + return (); + } ;; Loop extended actions until we reach standard actions while (true) { @@ -212,14 +209,20 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ () recv_internal(cell in_msg_full, slice in_msg_body) impure inline { - return_if(in_msg_body.slice_bits() < size::message_operation_prefix); + if (in_msg_body.slice_bits() < size::message_operation_prefix) { + return (); + } int op = in_msg_body.preload_uint(size::message_operation_prefix); - return_unless((op == prefix::extension_action) | (op == prefix::signed_internal)); + if ((op != prefix::extension_action) & (op != prefix::signed_internal)) { + return (); + } slice in_msg_full_slice = in_msg_full.begin_parse(); slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. - return_if(count_trailing_ones(message_flags_slice)); + if (count_trailing_ones(message_flags_slice) > 0) { + return (); + } if (op == prefix::extension_action) { in_msg_body~skip_bits(size::message_operation_prefix); @@ -228,7 +231,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { (int sender_address_wc, int sender_address_hash) = parse_std_addr(in_msg_full_slice~load_msg_addr()); (int my_address_wc, _) = parse_std_addr(my_address()); - return_unless(my_address_wc == sender_address_wc); + if (my_address_wc != sender_address_wc) { + return (); + } cell extensions = get_data().begin_parse() .skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key) @@ -237,7 +242,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; Note that some random contract may have deposited funds with this prefix, ;; so we accept the funds silently instead of throwing an error (wallet v4 does the same). (_, int extension_found) = extensions.udict_get?(size::address_hash_size, sender_address_hash); - return_unless(extension_found); + ifnot (extension_found) { + return (); + } in_msg_body~skip_bits(size::query_id); ;; skip query_id @@ -247,7 +254,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { } ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - return_if(in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature); + if (in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature) { + return (); + } process_signed_request(in_msg_body, false, false); } From facbfcf195e8b87ba3ee2985f94c9ff70dda41ed Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Mon, 1 Jul 2024 14:35:12 +0400 Subject: [PATCH 097/121] additional checks for c5 --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index 9111636e..f4f98556 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c7241021301000267000114ff00f4a413f4bcf2c80b0102012004020102f203012020d70b1f82107369676ebaf2e08a7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200b080201480a090011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0d0c0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810281b99130e07070e2100f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81002c2eda2edfb02f404216e926c218e2a0221d739309420c700b38e1ad72820761e436c20d71d06c712c2005220b0f2d089d74cd73930e85bed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a935bdb31e1d74cd01211007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54ad2777ad"} \ No newline at end of file +{"hex":"b5ee9c724102140100028d000114ff00f4a413f4bcf2c80b0102012004020102f203012020d70b1f82107369676ebaf2e08a7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200b080201480a090011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0d0c0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810281b99130e07070e2100f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f4eda2edfb02f404216e926c218e4d0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128100febbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f201312110014d74a935bdb31e1d74cd0007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed542304d79e"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 4879bed9..c668780b 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -17,6 +17,7 @@ const int error::this_signature_mode_already_set = 143; const int error::remove_last_extension_when_signature_disabled = 144; const int error::extension_wrong_workchain = 145; const int error::only_extension_can_change_signature_mode = 146; +const int error::invalid_c5 = 147; const int size::bool = 1; const int size::seqno = 32; @@ -65,10 +66,15 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; exotic cell starts with 0x02, 0x03 or 0x04 so it will not pass action_send_msg prefix check (slice cs, _) = c5.begin_parse_raw(); + int count = 0; + while (~ cs.slice_empty?()) { ;; only `action_send_msg` is allowed; `action_set_code`, `action_reserve_currency` or `action_change_library` are not. cs = cs.enforce_and_remove_action_send_msg_prefix(); + throw_unless(error::invalid_c5, cs.slice_bits() == 8); ;; send_mode + throw_unless(error::invalid_c5, cs.slice_refs() == 2); ;; next-action-ref and MessageRelaxed ref + ;; enforce that send_mode has +2 bit (ignore errors) set for external message. ;; if such send_mode is not set and sending fails at the action phase (for example due to insufficient balance) then the seqno will not be increased and the external message will be processed again and again. @@ -78,7 +84,10 @@ cell verify_c5_actions(cell c5, int is_external) inline { throw_if(error::external_send_message_must_have_ignore_errors_send_mode, is_external & (count_trailing_zeroes(cs.preload_bits(7)) > 0)); (cs, _) = cs.preload_ref().begin_parse_raw(); + count += 1; } + throw_unless(error::invalid_c5, count <= 254); + throw_unless(error::invalid_c5, cs.slice_refs() == 0); return c5; } From 84eaf15caf79b354507116964c283da08efbb564 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Mon, 1 Jul 2024 22:41:49 +0400 Subject: [PATCH 098/121] fix numbers --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index f4f98556..f59c6d42 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102140100028d000114ff00f4a413f4bcf2c80b0102012004020102f203012020d70b1f82107369676ebaf2e08a7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200b080201480a090011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0d0c0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810281b99130e07070e2100f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f4eda2edfb02f404216e926c218e4d0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128100febbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f201312110014d74a935bdb31e1d74cd0007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed542304d79e"} \ No newline at end of file +{"hex":"b5ee9c724102140100028c000114ff00f4a413f4bcf2c80b0102012004020102f203012020d70b1f82107369676ebaf2e08a7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200b080201480a090011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0d0c0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810282b99130e07070e2100f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a1312110010935bdb31e1d74cd0007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed545bfe7180"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index c668780b..91cf366a 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -86,7 +86,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { (cs, _) = cs.preload_ref().begin_parse_raw(); count += 1; } - throw_unless(error::invalid_c5, count <= 254); + throw_unless(error::invalid_c5, count <= 255); throw_unless(error::invalid_c5, cs.slice_refs() == 0); return c5; @@ -262,8 +262,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { } - ;; Additional check to make sure that there are enough bits for reading (+1 for actual actions flag) - if (in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + 1 + size::signature) { + ;; Additional check to make sure that there are enough bits for reading (+2 for maybe c5 and maybe other actions bits) + if (in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + 2 + size::signature) { return (); } process_signed_request(in_msg_body, false, false); From ee52524050be61051164d07e3a7a8816235aa9c9 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 2 Jul 2024 21:50:16 +0300 Subject: [PATCH 099/121] Upgrade packages --- package-lock.json | 1472 ++++++++++++++++++++++++--------------------- package.json | 24 +- 2 files changed, 805 insertions(+), 691 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ee2edc7..e27a735d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "WalletV5", "version": "0.0.1", "devDependencies": { - "@ton-community/blueprint": "^0.12.0", - "@ton-community/sandbox": "^0.11.0", - "@ton-community/test-utils": "^0.3.0", + "@ton/blueprint": "^0.21.0", + "@ton/core": "^0.56.3", + "@ton/crypto": "^3.2.0", + "@ton/sandbox": "^0.20.0", + "@ton/test-utils": "^0.4.2", "@types/jest": "^29.5.0", "@types/node": "^20.2.5", "@typescript-eslint/eslint-plugin": "^5.38.1", @@ -21,14 +23,12 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-unused-imports": "^2.0.0", - "jest": "^29.5.0", + "jest": "^29.7.0", "prettier": "^2.8.6", "ton": "~13.6.0", - "ton-core": "^0.51.0", - "ton-crypto": "^3.2.0", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "typescript": "^5.5.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -60,106 +60,44 @@ "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.10", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", - "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-compilation-targets": "^7.22.10", - "@babel/helper-module-transforms": "^7.22.9", - "@babel/helpers": "^7.22.11", - "@babel/parser": "^7.22.11", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.11", - "@babel/types": "^7.22.11", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", @@ -173,21 +111,15 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.10", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -195,14 +127,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -211,62 +143,66 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -285,79 +221,80 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", - "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.11", - "@babel/types": "^7.22.11" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -426,9 +363,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz", - "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -615,34 +552,34 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz", - "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.11", - "@babel/types": "^7.22.11", - "debug": "^4.1.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -650,13 +587,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -822,6 +759,12 @@ "multiformats": "^9.5.4" } }, + "node_modules/@ipld/dag-pb/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "dev": true + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -848,16 +791,16 @@ } }, "node_modules/@jest/console": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.4.tgz", - "integrity": "sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -865,15 +808,15 @@ } }, "node_modules/@jest/core": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.4.tgz", - "integrity": "sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "dependencies": { - "@jest/console": "^29.6.4", - "@jest/reporters": "^29.6.4", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", @@ -881,21 +824,21 @@ "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.6.3", - "jest-config": "^29.6.4", - "jest-haste-map": "^29.6.4", - "jest-message-util": "^29.6.3", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-resolve-dependencies": "^29.6.4", - "jest-runner": "^29.6.4", - "jest-runtime": "^29.6.4", - "jest-snapshot": "^29.6.4", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", - "jest-watcher": "^29.6.4", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -912,37 +855,37 @@ } }, "node_modules/@jest/environment": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.4.tgz", - "integrity": "sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.6.4", + "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.3" + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.4.tgz", - "integrity": "sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "dependencies": { - "expect": "^29.6.4", - "jest-snapshot": "^29.6.4" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", - "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { "jest-get-type": "^29.6.3" @@ -952,47 +895,47 @@ } }, "node_modules/@jest/fake-timers": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.4.tgz", - "integrity": "sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.6.3", - "jest-mock": "^29.6.3", - "jest-util": "^29.6.3" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.4.tgz", - "integrity": "sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/expect": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", - "jest-mock": "^29.6.3" + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.4.tgz", - "integrity": "sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.6.4", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", @@ -1006,9 +949,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3", - "jest-worker": "^29.6.4", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -1053,12 +996,12 @@ } }, "node_modules/@jest/test-result": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.4.tgz", - "integrity": "sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "dependencies": { - "@jest/console": "^29.6.4", + "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" @@ -1068,14 +1011,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.4.tgz", - "integrity": "sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.4", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -1083,9 +1026,9 @@ } }, "node_modules/@jest/transform": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", - "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -1096,9 +1039,9 @@ "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", + "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -1126,14 +1069,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1149,9 +1092,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1164,9 +1107,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1183,6 +1126,12 @@ "murmurhash3js-revisited": "^3.0.0" } }, + "node_modules/@multiformats/murmur3/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1292,9 +1241,9 @@ "dev": true }, "node_modules/@scarf/scarf": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.1.1.tgz", - "integrity": "sha512-VGbKDbk1RFIaSmdVb0cNjjWJoRWRI/Weo23AjRCC2nryO0iAS8pzsToJfPVPtVs74WHw4L1UTADNdIYRLkirZQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.3.0.tgz", + "integrity": "sha512-lHKK8M5CTcpFj2hZDB3wIjb0KAbEOgDmiJGDv1WBRfQgRm/a8/XMEkG/N1iM01xgbUDsPQwi42D+dFo1XPAKew==", "dev": true, "hasInstallScript": true }, @@ -1305,9 +1254,9 @@ "dev": true }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" @@ -1323,51 +1272,70 @@ } }, "node_modules/@tact-lang/compiler": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@tact-lang/compiler/-/compiler-1.1.3.tgz", - "integrity": "sha512-2UnHMW4S1+Rb5hUQAkNZWnQ8XJV4wRq+z3gAjchOzFiZEFNzE/46aIp7E1Jx+dFkfckdWXtGqHbhkRw6rc4CfQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@tact-lang/compiler/-/compiler-1.4.0.tgz", + "integrity": "sha512-MUZ8ulTrIs0sgs1tdNww7yan6ozMGNO7xR6S0yKZc57E0EN9o72vRqKdQW7k3iS3+MvltKPt6RVbNamtuVJ1tQ==", "dev": true, "dependencies": { "@ipld/dag-pb": "2.1.18", - "@tact-lang/opcode": "^0.0.13", - "arg": "^5.0.2", + "@tact-lang/opcode": "^0.0.14", + "@ton/core": "0.56.3", + "@ton/crypto": "^3.2.0", "blockstore-core": "1.0.5", "change-case": "^4.1.2", "ipfs-unixfs-importer": "9.0.10", + "meow": "^13.2.0", "mkdirp": "^2.1.3", - "multiformats": "9.9.0", - "ohm-js": "16.5.0", - "path-normalize": "^6.0.10", + "multiformats": "^13.1.0", + "ohm-js": "^17.1.0", + "path-normalize": "^6.0.13", "prando": "^6.0.1", - "qs": "^6.11.0", - "ton-core": ">=0.49.0", - "ton-crypto": "^3.2.0", - "zod": "^3.20.2" + "qs": "^6.12.1", + "zod": "^3.22.4" }, "bin": { "tact": "bin/tact" } }, "node_modules/@tact-lang/opcode": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@tact-lang/opcode/-/opcode-0.0.13.tgz", - "integrity": "sha512-4FGp1p3ahVrXr2QbyD2FqmnvXTMYapTlRJSPhj4O1L2yIq7dp0CkFL9EdKOCUfyirT0X5en78PHLfj0CuQGD7A==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@tact-lang/opcode/-/opcode-0.0.14.tgz", + "integrity": "sha512-8FKHK2jwvViRBReO2t40DCkHAP9KPTRWZof4kdsAUJFlyeWIC8SsRQSl9QkZxF+48WvjDduKNqN5Ltb80paufA==", "dev": true, "peerDependencies": { - "ton-core": ">=0.49.0", - "ton-crypto": "^3.2.0" + "@ton/core": ">=0.49.2", + "@ton/crypto": "^3.2.0" } }, - "node_modules/@ton-community/blueprint": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@ton-community/blueprint/-/blueprint-0.12.0.tgz", - "integrity": "sha512-QytcjOQCKtmaseEuEeuBGiKuQ649nwXGw4U3aGw7zz/SOd3SidMoTKAK3KE2VL84L8WRlw9HZx6wumJUkWPF7Q==", + "node_modules/@ton-community/func-js": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@ton-community/func-js/-/func-js-0.7.0.tgz", + "integrity": "sha512-VYJsv6Pqz6+qh3HlZWReBG5W9RXutAdIFYDqmblPSCXfjBhx/QjON/3WoppzUVrqQQdD0BVIh4PR+xRHRCBNhw==", + "dev": true, + "dependencies": { + "@ton-community/func-js-bin": "0.4.4-newops.1", + "arg": "^5.0.2" + }, + "bin": { + "func-js": "dist/cli.js" + } + }, + "node_modules/@ton-community/func-js-bin": { + "version": "0.4.4-newops.1", + "resolved": "https://registry.npmjs.org/@ton-community/func-js-bin/-/func-js-bin-0.4.4-newops.1.tgz", + "integrity": "sha512-TV4t6XhmItq4t+wv4pV30yEwb+YvdmsKo4Ig4B0zp4PLdI0r9iZHz4y5bBHcXmDQDRqulXzK6kTgfHvs2CIsaQ==", + "dev": true + }, + "node_modules/@ton/blueprint": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@ton/blueprint/-/blueprint-0.21.0.tgz", + "integrity": "sha512-87oHve+Xy+/IiyS1qNzs8JTGrxSI4kQrsNr7eERhZvC4qYmfAxpT0OBAJWx/qP+fcpzUOWR+vRiQqcJQQwwrLA==", "dev": true, "dependencies": { "@orbs-network/ton-access": "^2.3.3", - "@tact-lang/compiler": "^1.1.3", - "@ton-community/func-js": "^0.6.2", - "@tonconnect/sdk": "^2.1.3", + "@tact-lang/compiler": "^1.3.0", + "@ton-community/func-js": "^0.7.0", + "@tonconnect/sdk": "^2.2.0", "arg": "^5.0.2", "chalk": "^4.1.0", "dotenv": "^16.1.4", @@ -1380,52 +1348,65 @@ "blueprint": "dist/cli/cli.js" }, "peerDependencies": { - "ton": ">=13.4.1", - "ton-core": ">=0.48.0", - "ton-crypto": ">=3.2.0" + "@ton/core": ">=0.56.0", + "@ton/crypto": ">=3.2.0", + "@ton/ton": ">=13.11.0" } }, - "node_modules/@ton-community/func-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@ton-community/func-js/-/func-js-0.6.2.tgz", - "integrity": "sha512-5bewe8APG2TVgIPLUV5atQfAy+NtdjjGBfsWUeRdVUclzQ5H2wZ8aJsVNLiDBpKSNEKdOAP/1PownOFeodpQHg==", + "node_modules/@ton/core": { + "version": "0.56.3", + "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.56.3.tgz", + "integrity": "sha512-HVkalfqw8zqLLPehtq0CNhu5KjVzc7IrbDwDHPjGoOSXmnqSobiWj8a5F+YuWnZnEbQKtrnMGNOOjVw4LG37rg==", "dev": true, "dependencies": { - "@ton-community/func-js-bin": "0.4.4", - "arg": "^5.0.2" + "symbol.inspect": "1.0.1" }, - "bin": { - "func-js": "dist/cli.js" + "peerDependencies": { + "@ton/crypto": ">=3.2.0" } }, - "node_modules/@ton-community/func-js-bin": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@ton-community/func-js-bin/-/func-js-bin-0.4.4.tgz", - "integrity": "sha512-zCSVXmh+rFMgouzTbWkSVDIt1Z5i36u9rws/Kuqn89/a0vhA1aEoJJ3oJypz0TjWKJQveU4doJsPlqu7UT2zkw==", - "dev": true + "node_modules/@ton/crypto": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.2.0.tgz", + "integrity": "sha512-50RkwReEuV2FkxSZ8ht/x9+n0ZGtwRKGsJ0ay4I/HFhkYVG/awIIBQeH0W4j8d5lADdO5h01UtX8PJ8AjiejjA==", + "dev": true, + "dependencies": { + "@ton/crypto-primitives": "2.0.0", + "jssha": "3.2.0", + "tweetnacl": "1.0.3" + } + }, + "node_modules/@ton/crypto-primitives": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.0.0.tgz", + "integrity": "sha512-wttiNClmGbI6Dfy/8oyNnsIV0b/qYkCJz4Gn4eP62lJZzMtVQ94Ko7nikDX1EfYHkLI1xpOitWpW+8ZuG6XtDg==", + "dev": true, + "dependencies": { + "jssha": "3.2.0" + } }, - "node_modules/@ton-community/sandbox": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@ton-community/sandbox/-/sandbox-0.11.0.tgz", - "integrity": "sha512-3tlSprRBTSu9m0tJTC3cl4MXQep1vfNMPqk9+JAXSRJu9ToEvIUVpqO6MQNkbz9LkKDuOEBs5vyqT37DlKKcWw==", + "node_modules/@ton/sandbox": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@ton/sandbox/-/sandbox-0.20.0.tgz", + "integrity": "sha512-uci6DRDZGW1eu+hHgbVzf4lDTi29PV+5XKPC8ZyYUJaoOtulkHDtgyrfZ1H5QSOVOmUIjHDQhPwLsn1kU51yHw==", "dev": true, "peerDependencies": { - "ton-core": ">=0.48.0", - "ton-crypto": ">=3.2.0" + "@ton/core": ">=0.56.0", + "@ton/crypto": ">=3.2.0" } }, - "node_modules/@ton-community/test-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@ton-community/test-utils/-/test-utils-0.3.0.tgz", - "integrity": "sha512-eCw1c6a0TcKwiYEA4fmzPq+7dJtUx0UFYu+UEoRznIxEOcpEb8Ssjb9yMeiJEzbtUVMCkhEtpztdKt75ngDRWg==", + "node_modules/@ton/test-utils": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@ton/test-utils/-/test-utils-0.4.2.tgz", + "integrity": "sha512-fthY8Nrlmy8jnOl/vx6yjeKzzu62ZXMe7ej9Xg7rb4d3511V7dVQK+nw4YLSW5+dD/6WT03dFuNZXnuMYy5fHw==", "dev": true, "dependencies": { "node-inspect-extracted": "^2.0.0" }, "peerDependencies": { "@jest/globals": "*", - "chai": "*", - "ton-core": ">=0.36.1" + "@ton/core": ">=0.49.2", + "chai": "*" }, "peerDependenciesMeta": { "@jest/globals": { @@ -1436,6 +1417,36 @@ } } }, + "node_modules/@ton/ton": { + "version": "13.11.2", + "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-13.11.2.tgz", + "integrity": "sha512-EPqW+ZTe0MmfqguJEIGMuAqTAFRKMEce95HlDx8h6CGn2y3jiMgV1/oO+WpDIOiX+1wnTu+xtajk8JTWr8nKRQ==", + "dev": true, + "peer": true, + "dependencies": { + "axios": "^1.6.7", + "dataloader": "^2.0.0", + "symbol.inspect": "1.0.1", + "teslabot": "^1.3.0", + "zod": "^3.21.4" + }, + "peerDependencies": { + "@ton/core": ">=0.56.0", + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@ton/ton/node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dev": true, + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@tonconnect/isomorphic-eventsource": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.1.tgz", @@ -1455,9 +1466,9 @@ } }, "node_modules/@tonconnect/protocol": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.2.5.tgz", - "integrity": "sha512-kR0E+CWZl6JrE/30283v+sRiAvEu21t1xOLFx6f/BxlCNLY2wki39+L32+iicX8gn/Ig99L1flr9TAI9QW9bnQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.2.6.tgz", + "integrity": "sha512-kyoDz5EqgsycYP+A+JbVsAUYHNT059BCrK+m0pqxykMODwpziuSAXfwAZmHcg8v7NB9VKYbdFY55xKeXOuEd0w==", "dev": true, "dependencies": { "tweetnacl": "^1.0.3", @@ -1541,18 +1552,18 @@ } }, "node_modules/@types/bn.js": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", - "integrity": "sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -2188,12 +2199,12 @@ } }, "node_modules/babel-jest": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", - "integrity": "sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "dependencies": { - "@jest/transform": "^29.6.4", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", @@ -2347,6 +2358,12 @@ "multiformats": "^9.4.7" } }, + "node_modules/blockstore-core/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2370,9 +2387,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "dev": true, "funding": [ { @@ -2389,10 +2406,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -2453,13 +2470,19 @@ "dev": true }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2494,9 +2517,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001524", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", - "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", + "version": "1.0.30001638", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", + "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", "dev": true, "funding": [ { @@ -2591,9 +2614,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, "node_modules/cli-cursor": { @@ -2609,9 +2632,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", - "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -2744,6 +2767,27 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2788,9 +2832,9 @@ } }, "node_modules/dedent": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "dev": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -2828,6 +2872,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -2915,21 +2976,21 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/electron-to-chromium": { - "version": "1.4.503", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.503.tgz", - "integrity": "sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==", + "version": "1.4.812", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz", + "integrity": "sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg==", "dev": true }, "node_modules/emittery": { @@ -3018,6 +3079,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -3059,9 +3141,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -3659,16 +3741,16 @@ } }, "node_modules/expect": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", - "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.6.4", + "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3" + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3831,9 +3913,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -3874,9 +3956,9 @@ } }, "node_modules/fp-ts": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", - "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==", + "version": "2.16.6", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.6.tgz", + "integrity": "sha512-v7w209VPj4L6pPn/ftFRJu31Oa8QagwcVw7BZmLCUWU4AQoc954rX9ogSIahDf67Pg+GjPbkW/Kn9XWnlWJG0g==", "dev": true }, "node_modules/fs.realpath": { @@ -3900,10 +3982,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -3957,15 +4042,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4159,12 +4248,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4209,6 +4298,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/header-case": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", @@ -4380,6 +4481,12 @@ "multiformats": "^9.0.4" } }, + "node_modules/interface-blockstore/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "dev": true + }, "node_modules/interface-store": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/interface-store/-/interface-store-2.0.2.tgz", @@ -4401,9 +4508,9 @@ } }, "node_modules/io-ts": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz", - "integrity": "sha512-Rq2BsYmtwS5vVttie4rqrOCIfHCS9TgpRLFpKQCM1wZBBRY9nWVGmEvm2FnDbSE2un1UE39DvFpTR5UL47YDcA==", + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz", + "integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==", "dev": true, "peerDependencies": { "fp-ts": "^2.5.0" @@ -4463,6 +4570,12 @@ "npm": ">=7.0.0" } }, + "node_modules/ipfs-unixfs-importer/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "dev": true + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -4794,14 +4907,14 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", - "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", "dev": true, "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" }, @@ -4809,26 +4922,11 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -4836,12 +4934,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -4871,9 +4963,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -4929,15 +5021,15 @@ "dev": true }, "node_modules/jest": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.4.tgz", - "integrity": "sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "dependencies": { - "@jest/core": "^29.6.4", + "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.6.4" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" @@ -4955,13 +5047,13 @@ } }, "node_modules/jest-changed-files": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.6.3.tgz", - "integrity": "sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "dependencies": { "execa": "^5.0.0", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { @@ -4969,28 +5061,28 @@ } }, "node_modules/jest-circus": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.4.tgz", - "integrity": "sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/expect": "^29.6.4", - "@jest/test-result": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.6.3", - "jest-matcher-utils": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-runtime": "^29.6.4", - "jest-snapshot": "^29.6.4", - "jest-util": "^29.6.3", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -5000,22 +5092,21 @@ } }, "node_modules/jest-cli": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.4.tgz", - "integrity": "sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "dependencies": { - "@jest/core": "^29.6.4", - "@jest/test-result": "^29.6.4", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.6.4", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "bin": { @@ -5034,31 +5125,31 @@ } }, "node_modules/jest-config": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.4.tgz", - "integrity": "sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.6.4", + "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", - "babel-jest": "^29.6.4", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.6.4", - "jest-environment-node": "^29.6.4", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-runner": "^29.6.4", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -5079,24 +5170,24 @@ } }, "node_modules/jest-diff": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", - "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.6.3.tgz", - "integrity": "sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" @@ -5106,33 +5197,33 @@ } }, "node_modules/jest-each": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.3.tgz", - "integrity": "sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", - "jest-util": "^29.6.3", - "pretty-format": "^29.6.3" + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.4.tgz", - "integrity": "sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/fake-timers": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.3", - "jest-util": "^29.6.3" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -5148,9 +5239,9 @@ } }, "node_modules/jest-haste-map": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", - "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", @@ -5160,8 +5251,8 @@ "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", - "jest-util": "^29.6.3", - "jest-worker": "^29.6.4", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -5173,37 +5264,37 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz", - "integrity": "sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "dependencies": { "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", - "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.6.4", + "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-message-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", - "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", @@ -5212,7 +5303,7 @@ "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -5221,14 +5312,14 @@ } }, "node_modules/jest-mock": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.3.tgz", - "integrity": "sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.6.3" + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -5261,17 +5352,17 @@ } }, "node_modules/jest-resolve": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.4.tgz", - "integrity": "sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -5281,43 +5372,43 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.4.tgz", - "integrity": "sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "dependencies": { "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.6.4" + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.4.tgz", - "integrity": "sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "dependencies": { - "@jest/console": "^29.6.4", - "@jest/environment": "^29.6.4", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.6.3", - "jest-environment-node": "^29.6.4", - "jest-haste-map": "^29.6.4", - "jest-leak-detector": "^29.6.3", - "jest-message-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-runtime": "^29.6.4", - "jest-util": "^29.6.3", - "jest-watcher": "^29.6.4", - "jest-worker": "^29.6.4", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -5326,17 +5417,17 @@ } }, "node_modules/jest-runtime": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.4.tgz", - "integrity": "sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/fake-timers": "^29.6.4", - "@jest/globals": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", @@ -5344,13 +5435,13 @@ "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-mock": "^29.6.3", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-snapshot": "^29.6.4", - "jest-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -5359,9 +5450,9 @@ } }, "node_modules/jest-snapshot": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.4.tgz", - "integrity": "sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -5369,20 +5460,20 @@ "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.6.4", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.6.4", + "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "semver": "^7.5.3" }, "engines": { @@ -5423,9 +5514,9 @@ "dev": true }, "node_modules/jest-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", - "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", @@ -5440,9 +5531,9 @@ } }, "node_modules/jest-validate": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.3.tgz", - "integrity": "sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", @@ -5450,7 +5541,7 @@ "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -5469,18 +5560,18 @@ } }, "node_modules/jest-watcher": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.4.tgz", - "integrity": "sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.4", + "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { @@ -5488,13 +5579,13 @@ } }, "node_modules/jest-worker": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", - "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -5724,26 +5815,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -5751,12 +5827,6 @@ "node": ">=10" } }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -5772,6 +5842,18 @@ "tmpl": "1.0.5" } }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -5885,9 +5967,9 @@ "dev": true }, "node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.1.tgz", + "integrity": "sha512-JiptvwMmlxlzIlLLwhCi/srf/nk409UL0eUBr0kioRJq15hqqKyg68iftrBvhCRjR6Rw4fkNnSc4ZJXJDuta/Q==", "dev": true }, "node_modules/murmurhash3js-revisited": { @@ -5963,9 +6045,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/normalize-path": { @@ -6010,10 +6092,13 @@ "dev": true }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6106,9 +6191,9 @@ } }, "node_modules/ohm-js": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/ohm-js/-/ohm-js-16.5.0.tgz", - "integrity": "sha512-OXuB3g1cdP6vCyaGziLmihLkBrtfH9H9jmYp5nRqad093KVzkUSi062IVv5l+3SB/HIbMyDvBqhgR3Q3S+EEnw==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/ohm-js/-/ohm-js-17.1.0.tgz", + "integrity": "sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q==", "dev": true, "engines": { "node": ">=0.12.1" @@ -6361,9 +6446,9 @@ } }, "node_modules/path-normalize": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/path-normalize/-/path-normalize-6.0.12.tgz", - "integrity": "sha512-/lgDQDNQVtfOCKpQ9/Zr64/pT4OWmCiHDBtHCnJO/jkxXmnETI2Bes/E3y9yDbplYFMFvjRPSINfPvYxVrxEYA==", + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/path-normalize/-/path-normalize-6.0.13.tgz", + "integrity": "sha512-PfC1Pc+IEhI77UEN731pj2nMs9gHAV36IA6IW6VdXWjoQesf+jtO9hdMUqTRS6mwR0T5rmyUrQzd5vw0VwL1Lw==", "dev": true, "engines": { "node": ">=16.0.0" @@ -6385,9 +6470,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -6466,9 +6551,9 @@ } }, "node_modules/pretty-format": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", - "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", @@ -6530,6 +6615,13 @@ "pbts": "bin/pbts" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "peer": true + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -6540,9 +6632,9 @@ } }, "node_modules/pure-rand": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", - "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true, "funding": [ { @@ -6565,12 +6657,12 @@ } }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -6878,6 +6970,23 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6900,14 +7009,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7255,6 +7368,7 @@ "resolved": "https://registry.npmjs.org/ton-core/-/ton-core-0.51.0.tgz", "integrity": "sha512-FgejId/X1o7nNc5g8DBW1+9piwFolVU3e83Z8TudDCALik29YPhGOqLHSvcVYux3QV7SL0qCNaKgA3mV2nNfCA==", "dev": true, + "peer": true, "dependencies": { "symbol.inspect": "1.0.1" }, @@ -7518,9 +7632,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, "node_modules/tsutils": { @@ -7655,16 +7769,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", + "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uint8arrays": { @@ -7676,6 +7790,12 @@ "multiformats": "^9.4.2" } }, + "node_modules/uint8arrays/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7692,9 +7812,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "dev": true, "funding": [ { @@ -7711,8 +7831,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7767,25 +7887,19 @@ "dev": true }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7811,9 +7925,9 @@ "dev": true }, "node_modules/whatwg-fetch": { - "version": "3.6.17", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.17.tgz", - "integrity": "sha512-c4ghIvG6th0eudYwKZY5keb81wtFz9/WeAHAoy8+r18kcWlitUIrmGFQ2rWEl4UCKUilD3zCLHOIPheHx5ypRQ==", + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "dev": true }, "node_modules/whatwg-url": { @@ -7973,9 +8087,9 @@ } }, "node_modules/zod": { - "version": "3.22.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz", - "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index a6a89549..3488bceb 100644 --- a/package.json +++ b/package.json @@ -12,19 +12,13 @@ "scalpel": "bash ./scripts/scalpel.sh" }, "devDependencies": { - "@ton-community/blueprint": "^0.12.0", - "@ton-community/sandbox": "^0.11.0", - "@ton-community/test-utils": "^0.3.0", + "@ton/blueprint": "^0.21.0", + "@ton/core": "^0.56.3", + "@ton/crypto": "^3.2.0", + "@ton/sandbox": "^0.20.0", + "@ton/test-utils": "^0.4.2", "@types/jest": "^29.5.0", "@types/node": "^20.2.5", - "jest": "^29.5.0", - "prettier": "^2.8.6", - "ton": "~13.6.0", - "ton-core": "^0.51.0", - "ton-crypto": "^3.2.0", - "ts-jest": "^29.0.5", - "ts-node": "^10.9.1", - "typescript": "^4.9.5", "@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/parser": "^5.38.1", "eslint": "8.22.0", @@ -32,6 +26,12 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-unused-imports": "^2.0.0" + "eslint-plugin-unused-imports": "^2.0.0", + "jest": "^29.7.0", + "prettier": "^2.8.6", + "ton": "~13.6.0", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "typescript": "^5.5.2" } } From 047852ac96a45a8ed418d18960fa6d621a667ed3 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 2 Jul 2024 21:52:43 +0300 Subject: [PATCH 100/121] Migrate to @ton packages --- tests/actions.ts | 2 +- tests/test-only-actions.ts | 4 ++-- tests/wallet-v5-extensions.spec.ts | 12 ++++++------ tests/wallet-v5-external.spec.ts | 11 ++++++----- tests/wallet-v5-get.spec.ts | 8 ++++---- tests/wallet-v5-internal.spec.ts | 12 ++++++------ wrappers/library-deployer.ts | 4 ++-- wrappers/wallet-v5.ts | 12 ++++++++++-- wrappers/wallet_v5.compile.ts | 2 +- 9 files changed, 38 insertions(+), 29 deletions(-) diff --git a/tests/actions.ts b/tests/actions.ts index e4ff0707..cacba0b7 100644 --- a/tests/actions.ts +++ b/tests/actions.ts @@ -1,4 +1,4 @@ -import { Address, beginCell, Cell, MessageRelaxed, SendMode, storeMessageRelaxed } from 'ton-core'; +import { Address, beginCell, Cell, MessageRelaxed, SendMode, storeMessageRelaxed } from '@ton/core'; import { isTestOnlyExtendedAction, TestOnlyExtendedAction, TestOnlyOutAction } from './test-only-actions'; export class ActionSendMsg { diff --git a/tests/test-only-actions.ts b/tests/test-only-actions.ts index 75173517..00ca35df 100644 --- a/tests/test-only-actions.ts +++ b/tests/test-only-actions.ts @@ -7,7 +7,7 @@ import { SendMode, storeCurrencyCollection, storeMessageRelaxed -} from 'ton-core'; +} from '@ton/core'; import { ExtendedAction, OutAction @@ -79,4 +79,4 @@ export function isTestOnlyExtendedAction(action: OutAction | ExtendedAction): ac return ( action.tag === ActionSetData.tag ); -} \ No newline at end of file +} diff --git a/tests/wallet-v5-extensions.spec.ts b/tests/wallet-v5-extensions.spec.ts index 68929d71..68221053 100644 --- a/tests/wallet-v5-extensions.spec.ts +++ b/tests/wallet-v5-extensions.spec.ts @@ -1,8 +1,8 @@ -import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton-community/sandbox'; -import { Address, beginCell, Cell, Dictionary, Sender, SendMode, toNano } from 'ton-core'; +import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton/sandbox'; +import { Address, beginCell, Cell, Dictionary, Sender, SendMode, toNano } from '@ton/core'; import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; -import '@ton-community/test-utils'; -import { compile } from '@ton-community/blueprint'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from 'ton-crypto'; import { bufferToBigInt, createMsgInternal, packAddress, validUntil } from './utils'; import { @@ -11,8 +11,8 @@ import { ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; -import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; -import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; +import { TransactionDescriptionGeneric } from '@ton/core/src/types/TransactionDescription'; +import { TransactionComputeVm } from '@ton/core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; import { default as config } from './config'; diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 893b1793..93161235 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -1,9 +1,10 @@ -import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton-community/sandbox'; -import { Address, beginCell, Cell, Dictionary, internal, Sender, SendMode, toNano } from 'ton-core'; +import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton/sandbox'; +import { Address, beginCell, Cell, Dictionary, internal, Sender, SendMode, toNano } from '@ton/core'; import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; -import '@ton-community/test-utils'; -import { compile } from '@ton-community/blueprint'; -import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from 'ton-crypto'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; +import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from '@ton/crypto'; + import { bufferToBigInt, createMsgInternal, diff --git a/tests/wallet-v5-get.spec.ts b/tests/wallet-v5-get.spec.ts index 86114070..1283249e 100644 --- a/tests/wallet-v5-get.spec.ts +++ b/tests/wallet-v5-get.spec.ts @@ -1,8 +1,8 @@ -import { Blockchain, SandboxContract } from '@ton-community/sandbox'; -import { Address, beginCell, Cell, Dictionary, Sender, toNano } from 'ton-core'; +import { Blockchain, SandboxContract } from '@ton/sandbox'; +import { Address, beginCell, Cell, Dictionary, Sender, toNano } from '@ton/core'; import { WalletId, WalletV5 } from '../wrappers/wallet-v5'; -import '@ton-community/test-utils'; -import { compile } from '@ton-community/blueprint'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; import { getSecureRandomBytes, KeyPair, keyPairFromSeed } from 'ton-crypto'; import { bufferToBigInt, packAddress } from './utils'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; diff --git a/tests/wallet-v5-internal.spec.ts b/tests/wallet-v5-internal.spec.ts index 597a1970..963535f1 100644 --- a/tests/wallet-v5-internal.spec.ts +++ b/tests/wallet-v5-internal.spec.ts @@ -1,8 +1,8 @@ -import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton-community/sandbox'; -import { Address, beginCell, Cell, Dictionary, Sender, SendMode, toNano } from 'ton-core'; +import {Blockchain, BlockchainTransaction, SandboxContract} from '@ton/sandbox'; +import { Address, beginCell, Cell, Dictionary, Sender, SendMode, toNano } from '@ton/core'; import { Opcodes, WalletId, WalletV5 } from '../wrappers/wallet-v5'; -import '@ton-community/test-utils'; -import { compile } from '@ton-community/blueprint'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from 'ton-crypto'; import { bufferToBigInt, createMsgInternal, disableConsoleError, packAddress, validUntil } from './utils'; import { @@ -11,8 +11,8 @@ import { ActionSendMsg, ActionSetSignatureAuthAllowed, packActionsList } from './actions'; -import { TransactionDescriptionGeneric } from 'ton-core/src/types/TransactionDescription'; -import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase'; +import { TransactionDescriptionGeneric } from '@ton/core/src/types/TransactionDescription'; +import { TransactionComputeVm } from '@ton/core/src/types/TransactionComputePhase'; import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; import { default as config } from './config'; import { ActionSetCode, ActionSetData } from './test-only-actions'; diff --git a/wrappers/library-deployer.ts b/wrappers/library-deployer.ts index efec67e9..cb3c15c6 100644 --- a/wrappers/library-deployer.ts +++ b/wrappers/library-deployer.ts @@ -11,8 +11,8 @@ import { Sender, SendMode, SimpleLibrary -} from 'ton-core'; -import { SimpleLibraryValue } from 'ton-core/dist/types/SimpleLibrary'; +} from '@ton/core'; +import { SimpleLibraryValue } from '@ton/core/dist/types/SimpleLibrary'; export type LibraryDeployerConfig = { libraryCode: Cell; diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index 5e59c57b..d95c0b04 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -9,11 +9,19 @@ import { contractAddress, ContractProvider, Dictionary, + MessageRelaxed, + storeOutList, + OutAction, Sender, - SendMode -} from 'ton-core'; + SendMode, + Builder, + OutActionSendMsg, + toNano +} from '@ton/core'; import { bufferToBigInt } from '../tests/utils'; +import { sign } from '@ton/crypto'; + export type WalletV5Config = { signatureAllowed: boolean; seqno: number; diff --git a/wrappers/wallet_v5.compile.ts b/wrappers/wallet_v5.compile.ts index c4cb1261..e9b50f69 100644 --- a/wrappers/wallet_v5.compile.ts +++ b/wrappers/wallet_v5.compile.ts @@ -1,4 +1,4 @@ -import { CompilerConfig } from '@ton-community/blueprint'; +import { CompilerConfig } from '@ton/blueprint'; export const compile: CompilerConfig = { lang: 'func', From 982f30448b977978e09edc0f5f1b9ba0b4cc9f58 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 2 Jul 2024 21:54:57 +0300 Subject: [PATCH 101/121] Minor fix --- wrappers/wallet-v5.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/wrappers/wallet-v5.ts b/wrappers/wallet-v5.ts index d95c0b04..27963df1 100644 --- a/wrappers/wallet-v5.ts +++ b/wrappers/wallet-v5.ts @@ -182,12 +182,7 @@ export class WalletV5 implements Contract { } async sendExternalSignedMessage(provider: ContractProvider, body: Cell) { - await provider.external( - beginCell() - // .storeUint(Opcodes.auth_signed, 32) // Is signed inside message - .storeSlice(body.beginParse()) - .endCell() - ); + await provider.external(body); } async sendExternal(provider: ContractProvider, body: Cell) { @@ -242,7 +237,7 @@ export class WalletV5 implements Contract { ); return dict.keys().map(key => { - const wc = 0n; + const wc = this.address.workChain; const addressHex = key; return Address.parseRaw(`${wc}:${addressHex.toString(16).padStart(64, '0')}`); }); From ff8cd7498c3cd5c74aa8173151ab8c404082b624 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 2 Jul 2024 21:55:32 +0300 Subject: [PATCH 102/121] Extra moar utils for the utils god --- tests/gasUtils.ts | 415 ++++++++++++++++++++++++++++++++++++++++++++++ tests/utils.ts | 65 +++++++- 2 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 tests/gasUtils.ts diff --git a/tests/gasUtils.ts b/tests/gasUtils.ts new file mode 100644 index 00000000..f322cea4 --- /dev/null +++ b/tests/gasUtils.ts @@ -0,0 +1,415 @@ +import { Cell, Slice, toNano, beginCell, Address, Dictionary, Message, DictionaryValue, Transaction, BitString, SendMode, MessageRelaxed, CommonMessageInfoInternal, storeMessage, storeMessageRelaxed } from '@ton/core'; +import { internal } from '@ton/sandbox'; +import { randomAddress } from './utils'; + +export type GasPrices = { + flat_gas_limit: bigint, + flat_gas_price: bigint, + gas_price: bigint; +}; +export type StorageValue = { + utime_sice: number, + bit_price_ps: bigint, + cell_price_ps: bigint, + mc_bit_price_ps: bigint, + mc_cell_price_ps: bigint +}; + + +export type MsgPrices = ReturnType; +export type FullFees = ReturnType; + +export class StorageStats { + bits: bigint; + cells: bigint; + + constructor(bits?: number | bigint, cells?: number | bigint) { + this.bits = bits !== undefined ? BigInt(bits) : 0n; + this.cells = cells !== undefined ? BigInt(cells) : 0n; + } + add(...stats: StorageStats[]) { + let cells = this.cells, bits = this.bits; + for (let stat of stats) { + bits += stat.bits; + cells += stat.cells; + } + return new StorageStats(bits, cells); + } + sub(...stats: StorageStats[]) { + let cells = this.cells, bits = this.bits; + for (let stat of stats) { + bits -= stat.bits; + cells -= stat.cells; + } + return new StorageStats(bits, cells); + } + addBits(bits: number | bigint) { + return new StorageStats(this.bits + BigInt(bits), this.cells); + } + subBits(bits: number | bigint) { + return new StorageStats(this.bits - BigInt(bits), this.cells); + } + addCells(cells: number | bigint) { + return new StorageStats(this.bits, this.cells + BigInt(cells)); + } + subCells(cells: number | bigint) { + return new StorageStats(this.bits, this.cells - BigInt(cells)); + } + + toString() : string { + return JSON.stringify({ + bits: this.bits.toString(), + cells: this.cells.toString() + }); + } +} + +export function computedGeneric(transaction: T) { + if(transaction.description.type !== "generic") + throw("Expected generic transactionaction"); + if(transaction.description.computePhase.type !== "vm") + throw("Compute phase expected") + return transaction.description.computePhase; +} + +export function storageGeneric(transaction: T) { + if(transaction.description.type !== "generic") + throw("Expected generic transactionaction"); + const storagePhase = transaction.description.storagePhase; + if(storagePhase === null || storagePhase === undefined) + throw("Storage phase expected") + return storagePhase; +} + +function shr16ceil(src: bigint) { + let rem = src % BigInt(65536); + let res = src / 65536n; // >> BigInt(16); + if (rem != BigInt(0)) { + res += BigInt(1); + } + return res; +} + +export function collectCellStats(cell: Cell, visited:Array, skipRoot: boolean = false): StorageStats { + let bits = skipRoot ? 0n : BigInt(cell.bits.length); + let cells = skipRoot ? 0n : 1n; + let hash = cell.hash().toString(); + if (visited.includes(hash)) { + // We should not account for current cell data if visited + return new StorageStats(); + } + else { + visited.push(hash); + } + for (let ref of cell.refs) { + let r = collectCellStats(ref, visited); + cells += r.cells; + bits += r.bits; + } + return new StorageStats(bits, cells); +} + +export function getGasPrices(configRaw: Cell, workchain: 0 | -1): GasPrices { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + + const ds = config.get(21 + workchain)!.beginParse(); + if(ds.loadUint(8) !== 0xd1) { + throw new Error("Invalid flat gas prices tag!"); + } + + const flat_gas_limit = ds.loadUintBig(64); + const flat_gas_price = ds.loadUintBig(64); + + if(ds.loadUint(8) !== 0xde) { + throw new Error("Invalid gas prices tag!"); + } + return { + flat_gas_limit, + flat_gas_price, + gas_price: ds.preloadUintBig(64) + }; +} + +export function setGasPrice(configRaw: Cell, prices: GasPrices, workchain: 0 | -1) : Cell { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + const idx = 21 + workchain; + const ds = config.get(idx)!; + const tail = ds.beginParse().skip(8 + 64 + 64 + 8 + 64); + + const newPrices = beginCell().storeUint(0xd1, 8) + .storeUint(prices.flat_gas_limit, 64) + .storeUint(prices.flat_gas_price, 64) + .storeUint(0xde, 8) + .storeUint(prices.gas_price, 64) + .storeSlice(tail) + .endCell(); + config.set(idx, newPrices); + + return beginCell().storeDictDirect(config).endCell(); +} + +export const storageValue : DictionaryValue = { + serialize: (src, builder) => { + builder.storeUint(0xcc, 8) + .storeUint(src.utime_sice, 32) + .storeUint(src.bit_price_ps, 64) + .storeUint(src.cell_price_ps, 64) + .storeUint(src.mc_bit_price_ps, 64) + .storeUint(src.mc_cell_price_ps, 64) + }, + parse: (src) => { + return { + utime_sice: src.skip(8).loadUint(32), + bit_price_ps: src.loadUintBig(64), + cell_price_ps: src.loadUintBig(64), + mc_bit_price_ps: src.loadUintBig(64), + mc_cell_price_ps: src.loadUintBig(64) + }; + } + }; + +export function getStoragePrices(configRaw: Cell) { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32),storageValue, config.get(18)!); + const values = storageData.values(); + + return values[values.length - 1]; +} +export function calcStorageFee(prices: StorageValue, stats: StorageStats, duration: bigint) { + return shr16ceil((stats.bits * prices.bit_price_ps + stats.cells * prices.cell_price_ps) * duration) +} +export function setStoragePrices(configRaw: Cell, prices: StorageValue) { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32),storageValue, config.get(18)!); + storageData.set(storageData.values().length - 1, prices); + config.set(18, beginCell().storeDictDirect(storageData).endCell()); + return beginCell().storeDictDirect(config).endCell(); +} + +export function computeGasFee(prices: GasPrices, gas: bigint): bigint { + if(gas <= prices.flat_gas_limit) { + return prices.flat_gas_price; + } + return prices.flat_gas_price + prices.gas_price * (gas - prices.flat_gas_limit) / 65536n +} + +export function computeDefaultForwardFee(msgPrices: MsgPrices) { + return msgPrices.lumpPrice - ((msgPrices.lumpPrice * msgPrices.firstFrac) >> BigInt(16)); +} + +export function computeCellForwardFees(msgPrices: MsgPrices, msg: Cell) { + let storageStats = collectCellStats(msg, [], true); + return computeFwdFees(msgPrices, storageStats.cells, storageStats.bits); +} +export function computeMessageForwardFees(msgPrices: MsgPrices, msg: Message) { + // let msg = loadMessageRelaxed(cell.beginParse()); + let storageStats = new StorageStats(); + + if( msg.info.type !== "internal") { + throw Error("Helper intended for internal messages"); + } + const defaultFwd = computeDefaultForwardFee(msgPrices); + // If message forward fee matches default than msg cell is flat + if(msg.info.forwardFee == defaultFwd) { + return {fees: {total: msgPrices.lumpPrice, res : defaultFwd, remaining: defaultFwd}, stats: storageStats}; + } + let visited : Array = []; + // Init + if (msg.init) { + let addBits = 5n; // Minimal additional bits + let refCount = 0; + if(msg.init.splitDepth) { + addBits += 5n; + } + if(msg.init.libraries) { + refCount++; + storageStats = storageStats.add(collectCellStats(beginCell().storeDictDirect(msg.init.libraries).endCell(), visited, true)); + } + if(msg.init.code) { + refCount++; + storageStats = storageStats.add(collectCellStats(msg.init.code, visited)) + } + if(msg.init.data) { + refCount++; + storageStats = storageStats.add(collectCellStats(msg.init.data, visited)); + } + if(refCount >= 2) { //https://github.com/ton-blockchain/ton/blob/51baec48a02e5ba0106b0565410d2c2fd4665157/crypto/block/transaction.cpp#L2079 + storageStats.cells++; + storageStats.bits += addBits; + } + } + const lumpBits = BigInt(msg.body.bits.length); + const bodyStats = collectCellStats(msg.body,visited, true); + storageStats = storageStats.add(bodyStats); + + // NOTE: Extra currencies are ignored for now + let fees = computeFwdFeesVerbose(msgPrices, BigInt(storageStats.cells), BigInt(storageStats.bits)); + // Meeh + if(fees.remaining < msg.info.forwardFee) { + // console.log(`Remaining ${fees.remaining} < ${msg.info.forwardFee} lump bits:${lumpBits}`); + storageStats = storageStats.addCells(1).addBits(lumpBits); + fees = computeFwdFeesVerbose(msgPrices, storageStats.cells, storageStats.bits); + } + if(fees.remaining != msg.info.forwardFee) { + console.log("Result fees:", fees); + console.log(msg); + console.log(fees.remaining); + throw(new Error("Something went wrong in fee calcuation!")); + } + return {fees, stats: storageStats}; +} + +export const configParseMsgPrices = (sc: Slice) => { + + let magic = sc.loadUint(8); + + if(magic != 0xea) { + throw Error("Invalid message prices magic number!"); + } + return { + lumpPrice:sc.loadUintBig(64), + bitPrice: sc.loadUintBig(64), + cellPrice: sc.loadUintBig(64), + ihrPriceFactor: sc.loadUintBig(32), + firstFrac: sc.loadUintBig(16), + nextFrac: sc.loadUintBig(16) + }; +} + +export const setMsgPrices = (configRaw: Cell, prices: MsgPrices, workchain: 0 | -1) => { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + + const priceCell = beginCell().storeUint(0xea, 8) + .storeUint(prices.lumpPrice, 64) + .storeUint(prices.bitPrice, 64) + .storeUint(prices.cellPrice, 64) + .storeUint(prices.ihrPriceFactor, 32) + .storeUint(prices.firstFrac, 16) + .storeUint(prices.nextFrac, 16) + .endCell(); + config.set(25 + workchain, priceCell); + + return beginCell().storeDictDirect(config).endCell(); +} + +export const getMsgPrices = (configRaw: Cell, workchain: 0 | -1 ) => { + + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + + const prices = config.get(25 + workchain); + + if(prices === undefined) { + throw Error("No prices defined in config"); + } + + return configParseMsgPrices(prices.beginParse()); +} + +export function computeFwdFees(msgPrices: MsgPrices, cells: bigint, bits: bigint) { + return msgPrices.lumpPrice + (shr16ceil((msgPrices.bitPrice * bits) + + (msgPrices.cellPrice * cells)) + ); +} + +export function computeFwdFeesVerbose(msgPrices: MsgPrices, cells: bigint | number, bits: bigint | number) { + const fees = computeFwdFees(msgPrices, BigInt(cells), BigInt(bits)); + + const res = (fees * msgPrices.firstFrac) >> 16n; + return { + total: fees, + res, + remaining: fees - res + } +} + +export const setPrecompiledGas = (configRaw: Cell, code_hash: Buffer, gas_usage: number) => { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + + const entry = beginCell().storeUint(0xb0, 8) + .storeUint(gas_usage, 64) + .endCell().beginParse(); + let dict = Dictionary.empty(Dictionary.Keys.Buffer(32), Dictionary.Values.BitString(8 + 64)); + dict.set(code_hash, entry.loadBits(8 + 64)); + const param = beginCell().storeUint(0xc0, 8).storeBit(1).storeRef(beginCell().storeDictDirect(dict).endCell()).endCell(); + + config.set(45, param); + + return beginCell().storeDictDirect(config).endCell(); +}; + +export const estimateMessageImpact = (message: MessageRelaxed, sendTx: T, msgPrices: MsgPrices, balanceBefore: bigint, mode: SendMode, computed: boolean) => { + + if(message.info.type !== 'internal') { + throw new TypeError("External message is not supported!"); + } + + const computePhase = computedGeneric(sendTx); + + let inValue = 0n; + let inMessage = sendTx.inMessage; + let feesPaid = false; + + if(inMessage) { + if(inMessage.info.type == 'internal') { + inValue = inMessage.info.value.coins; + } + else if(inMessage.info.type == 'external-in') { + // Negative because of import cost + inValue -= computeCellForwardFees(msgPrices, beginCell().store(storeMessage(inMessage)).endCell()); + } + else { + throw new TypeError("external-out can't be incomming message!"); + } + } + + const msgPacked = beginCell().store(storeMessageRelaxed(message)).endCell(); + + const fees = computeCellForwardFees(msgPrices, msgPacked); + + let expOut = message.info.value.coins; + + let balanceAfter = balanceBefore - expOut; + // Usually means it's not the first action, so gas has already been deducted and credit added + if(!computed) { + balanceAfter += inValue - computePhase.gasFees; + } + + if(!(mode & SendMode.PAY_GAS_SEPARATELY)) { + expOut -= fees; + feesPaid = true; + } + else { + balanceAfter -= fees; + } + /* + else if(mode & SendMode.PAY_GAS_SEPARATELY) { + if(!(mode & SendMode.CARRY_ALL_REMAINING_BALANCE) || (mode & SendMode.CARRY_ALL_REMAINING_INCOMING_VALUE)) { + balanceAfter -= fees; + } + } + */ + if(mode & SendMode.CARRY_ALL_REMAINING_BALANCE) { + expOut = balanceAfter - fees + message.info.value.coins; + balanceAfter = 0n; + } + if(mode & SendMode.CARRY_ALL_REMAINING_INCOMING_VALUE) { + if(mode & SendMode.CARRY_ALL_REMAINING_BALANCE) { + throw new TypeError("Mode 64 and 128 is not compatible"); + } + if(!inMessage) { + throw new Error("Mode 64 doesn't work without incomming message"); + } + if(inMessage.info.type != 'internal') { + throw new Error("Mode 64 doesn't work with external incomming message"); + } + + expOut = inValue - computePhase.gasFees + message.info.value.coins - fees; + balanceAfter -= inValue - computePhase.gasFees; + /* + if(!feesPaid) { + expOut -= fees; + } + */ + } + return {expValue: expOut, balanceAfter}; +} diff --git a/tests/utils.ts b/tests/utils.ts index d309bf9e..8a326885 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,4 +1,4 @@ -import { Address, beginCell, Cell, CurrencyCollection, MessageRelaxed, StateInit } from 'ton-core'; +import { Address, beginCell, Cell, CurrencyCollection, MessageRelaxed, StateInit } from '@ton/core'; export function bufferToBigInt(buffer: Buffer): bigint { return BigInt('0x' + buffer.toString('hex')); @@ -37,6 +37,69 @@ export function createMsgInternal(params: { }; } +export const randomAddress = (wc: number = 0) => { + const buf = Buffer.alloc(32); + for (let i = 0; i < buf.length; i++) { + buf[i] = Math.floor(Math.random() * 256); + } + return new Address(wc, buf); +}; + +export const differentAddress = (old: Address) => { + let newAddr: Address; + do { + newAddr = randomAddress(old.workChain); + } while(newAddr.equals(old)); + + return newAddr; +} + +const getRandom = (min:number, max:number) => { + return Math.random() * (max - min) + min; +} + +export const getRandomInt = (min: number, max: number) => { + return Math.round(getRandom(min, max)); +} + +export const pickRandomN = (min: number, max: number, count: number): number[] => { + if(count > max - min) { + throw new Error("Element count can't be larger than range"); + } + + let uniqSet: Set = new Set(); + let foundCount = 0; + // I know it' inefficient + do { + const atempt = getRandomInt(min, max) + if(!uniqSet.has(atempt)) { + foundCount++; + uniqSet.add(atempt); + } + } while(foundCount < count); + + return [...uniqSet]; +} + +export const pickRandomNFrom = (count: number, from: T[]): T[] => { + let resultPick: T[] = new Array(count); + const pickIdxs = pickRandomN(0, from.length - 1, count); + + for(let i = 0; i < pickIdxs.length; i++) { + resultPick[i] = from[pickIdxs[i]]; + } + + return resultPick; +} +export const testArgs = (...args: unknown[]) => { + for(let arg of args) { + if(arg === undefined || arg === null) { + throw TypeError("Required argument is missing!"); + } + } +} + + export async function disableConsoleError(callback: () => Promise): Promise { const errorsHandler = console.error; console.error = () => {}; From d34988638170eb341fa7ffa516295ee02224b3bf Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 2 Jul 2024 21:55:54 +0300 Subject: [PATCH 103/121] Test wrapper and constants --- wrappers/Errors.ts | 18 ++++ wrappers/wallet-v5-test.ts | 190 +++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 wrappers/Errors.ts create mode 100644 wrappers/wallet-v5-test.ts diff --git a/wrappers/Errors.ts b/wrappers/Errors.ts new file mode 100644 index 00000000..b1c3b93e --- /dev/null +++ b/wrappers/Errors.ts @@ -0,0 +1,18 @@ +export abstract class ErrorsV5 { + static readonly signature_disabled = 132; + static readonly invalid_seqno = 133; + static readonly invalid_wallet_id = 134; + static readonly invalid_signature = 135; + static readonly expired = 136; + static readonly external_send_message_must_have_ignore_errors_send_mode = 137; + static readonly invalid_message_operation = 138; + static readonly add_extension = 139; + static readonly remove_extension = 140; + static readonly unsupported_action = 141; + static readonly disable_signature_when_extensions_is_empty = 142; + static readonly this_signature_mode_already_set = 143; + static readonly remove_last_extension_when_signature_disabled = 144; + static readonly extension_wrong_workchain = 145; + static readonly only_extension_can_change_signature_mode = 146; + static readonly invalid_c5 = 147; +} diff --git a/wrappers/wallet-v5-test.ts b/wrappers/wallet-v5-test.ts new file mode 100644 index 00000000..2d5c09ed --- /dev/null +++ b/wrappers/wallet-v5-test.ts @@ -0,0 +1,190 @@ +import { Cell, beginCell, Sender, ContractProvider, SendMode, MessageRelaxed, Address, toNano, contractAddress, OutAction, OutActionSendMsg, Builder, storeOutList } from '@ton/core'; +import { WalletV5, WalletV5Config, walletV5ConfigToCell, Opcodes } from './wallet-v5'; +import { sign } from '@ton/crypto'; + +export type WalletActions = { + wallet?: OutAction[] | Cell, + extended?: ExtendedAction[] | Cell +} + +export type ExtensionAdd = { + type: 'add_extension', + address: Address +} +export type ExtensionRemove = { + type: 'remove_extension', + address: Address +} + +export type SetSignatureAuth = { + type: 'sig_auth', + allowed: boolean +} + +export type ExtendedAction = ExtensionAdd | ExtensionRemove | SetSignatureAuth; + +export type MessageOut = { + message: MessageRelaxed, + mode: SendMode +}; + +function storeWalletActions(actions: WalletActions) { + // store compatable + return (builder: Builder) => { + let hasExtendedActions = false; + if(actions.wallet) { + let actionCell: Cell | null = null; + if(actions.wallet instanceof Cell) { + actionCell = actions.wallet; + } + else if(actions.wallet.length > 0) { + actionCell = beginCell().store(storeOutList(actions.wallet)).endCell(); + } + builder.storeMaybeRef(actionCell); + } + else { + builder.storeBit(false); + } + if(actions.extended) { + if(actions.extended instanceof Cell) { + builder.storeBit(true); + builder.storeSlice(actions.extended.asSlice()); + } + else if(actions.extended.length > 0) { + builder.storeBit(true); + builder.store(storeExtendedActions(actions.extended)); + } + else { + builder.storeBit(false); + } + } + else { + builder.storeBit(false); + } + } +} + +function storeExtensionAction(action: ExtendedAction) { + return (builder: Builder) => { + if(action.type == 'add_extension') { + builder.storeUint(2, 8).storeAddress(action.address); + } + else if(action.type == 'remove_extension') { + builder.storeUint(3, 8).storeAddress(action.address); + } + else { + builder.storeUint(4, 8).storeBit(action.allowed); + } + } +} + +export function storeExtendedActions(actions: ExtendedAction[]) { + const cell = actions.reverse().reduce((curCell, action) => { + const ds = beginCell().store(storeExtensionAction(action)); + if(curCell.bits.length > 0) { + ds.storeRef(curCell); + } + return ds.endCell(); + }, beginCell().endCell()); + + return (builder: Builder) => builder.storeSlice(cell.beginParse()); +} + +export function message2action(msg: MessageOut) : OutActionSendMsg { + return { + type: 'sendMsg', + mode: msg.mode, + outMsg: msg.message + } +} + + +export class WalletV5Test extends WalletV5 { + constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) { + super(address, init); + } + static createFromAddress(address: Address) { + return new WalletV5Test(address); + } + + static createFromConfig(config: WalletV5Config, code: Cell, workchain = 0) { + const data = walletV5ConfigToCell(config); + const init = { code, data }; + return new WalletV5Test(contractAddress(workchain, init), init); + } + static requestMessage(internal: boolean, wallet_id: bigint, valid_until: number, seqno: bigint | number, actions: WalletActions, key?: Buffer) { + const op = internal ? Opcodes.auth_signed_internal : Opcodes.auth_signed; + const msgBody = beginCell().storeUint(op, 32) + .storeUint(wallet_id, 32) + .storeUint(valid_until, 32) + .storeUint(seqno, 32) + .store(storeWalletActions(actions)) + .endCell(); + return key ? WalletV5Test.signRequestMessage(msgBody, key) : msgBody; + } + + static signRequestMessage(msg: Cell, key: Buffer) { + const signature = sign(msg.hash(), key); + + return beginCell().storeSlice(msg.asSlice()).storeBuffer(signature).endCell(); + } + async sendMessagesExternal(provider: ContractProvider, + wallet_id: bigint, + valid_until: number, + seqno: bigint | number, + key: Buffer, messages: MessageOut[]) { + const actions: OutActionSendMsg[] = messages.map(message2action); + + await provider.external( + WalletV5Test.requestMessage(false, wallet_id, valid_until, seqno, {wallet: actions}, key) + ); + } + + static extensionMessage(actions: WalletActions, query_id: bigint | number = 0) { + return beginCell() + .storeUint(Opcodes.auth_extension, 32) + .storeUint(query_id, 64) + .store(storeWalletActions(actions)) + .endCell(); + } + async sendExtensionActions(provider: ContractProvider, + via: Sender, + actions: WalletActions, + value: bigint = toNano('0.1'), + query_id: bigint | number = 0) { + + await provider.internal(via, { + value, + body: WalletV5Test.extensionMessage(actions, query_id), + sendMode: SendMode.PAY_GAS_SEPARATELY + }); + } + + async sendMessagesInternal(provider: ContractProvider, via: Sender, + wallet_id: bigint, + valid_until: number, + seqno: bigint | number, + key: Buffer, messages: MessageOut[], value: bigint = toNano('0.05')) { + + const actions: OutActionSendMsg[] = messages.map(message2action); + + await provider.internal(via, { + value, + body: WalletV5Test.requestMessage(true, wallet_id, valid_until, seqno, {wallet: actions}, key), + sendMode: SendMode.PAY_GAS_SEPARATELY + }); + } + + /* + async sendAddExtensionViaExternal(provider: ContractProvider, + wallet_id: bigint, + valid_until: number, + seqno: bigint | number, + key: Buffer, + extensions: Address[]) { + const reqMsg = WalletV5Test.requestMessage(false, wallet_id, valid_until, seqno, {extension: beginCell().endCell()}, key); + + await provider.external(reqMsg); + } + */ +} From 57e37aae304fd93119d04dcd1cecb332310a0a4d Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 2 Jul 2024 21:56:14 +0300 Subject: [PATCH 104/121] W5 wallet tests --- tests/WalletW5.spec.ts | 2870 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2870 insertions(+) create mode 100644 tests/WalletW5.spec.ts diff --git a/tests/WalletW5.spec.ts b/tests/WalletW5.spec.ts new file mode 100644 index 00000000..4307a981 --- /dev/null +++ b/tests/WalletW5.spec.ts @@ -0,0 +1,2870 @@ +import { compile } from '@ton/blueprint'; +import { + Address, + Cell, + Dictionary, + toNano, + Transaction, + internal as internal_relaxed, + beginCell, + SendMode, + Sender, + OutAction, + OutActionSendMsg, + contractAddress, + ExternalAddress, + storeOutAction +} from '@ton/core'; +import '@ton/test-utils'; +import { + Blockchain, + BlockchainSnapshot, + EmulationError, + SandboxContract, + SendMessageResult, + internal, + TreasuryContract +} from '@ton/sandbox'; +import { KeyPair, getSecureRandomBytes, keyPairFromSeed } from '@ton/crypto'; +import { Opcodes, walletV5ConfigToCell } from '../wrappers/wallet-v5'; +import { bufferToBigInt, getRandomInt, pickRandomNFrom } from './utils'; +import { findTransactionRequired, randomAddress } from '@ton/test-utils'; +import { estimateMessageImpact, getMsgPrices, MsgPrices, storageGeneric } from './gasUtils'; +import { ErrorsV5 } from '../wrappers/Errors'; +import { + WalletV5Test, + MessageOut, + WalletActions, + ExtendedAction, + message2action, + ExtensionAdd, + ExtensionRemove +} from '../wrappers/wallet-v5-test'; + +describe('Wallet v5 external tests', () => { + let blockchain: Blockchain; + let keys: KeyPair; + let wallet: SandboxContract; + let newWallet: SandboxContract; + let walletId: bigint; + const validOpCodes = [ + Opcodes.auth_signed, + Opcodes.auth_signed_internal, + Opcodes.auth_extension + ]; + const defaultExternalMode = SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS; + + let mockMessage: MessageOut; + let owner: SandboxContract; + let testWalletBc: SandboxContract; + let testWalletMc: SandboxContract; + let testExtensionBc: SandboxContract; + let testExtensionMc: SandboxContract; + + let initialState: BlockchainSnapshot; + let hasExtension: BlockchainSnapshot; + let hasMcWallet: BlockchainSnapshot; + + let msgPrices: MsgPrices; + let msgPricesMc: MsgPrices; + // let gasPrices: GasPrices; + + let code: Cell; + + let curTime: () => number; + let loadFrom: (snap: BlockchainSnapshot) => Promise; + let getWalletData: (from?: Address) => Promise; + let someMessages: (num: number) => OutActionSendMsg[]; + let someExtensions: ( + num: number, + action: 'add_extension' | 'remove_extension' + ) => ExtendedAction[]; + let assertMockMessage: (txs: Transaction[], from?: Address) => void; + let assertInternal: (txs: Transaction[], from: Address, exp: number) => void; + let shouldRejectWith: (p: Promise, code: number) => Promise; + let assertSendMessages: ( + exp: number, + wallet_id: bigint, + valid_until: number, + seqno: bigint | number, + messages: MessageOut[], + key: Buffer, + via?: Sender | ExtensionSender + ) => Promise; + + //type PartialBy = Omit & Partial>; + type TestArgs = { + walletId: bigint; + valid_until: number; + seqno: bigint | number; + actions: WalletActions; + key: Buffer; + prevState?: Cell; + extra?: any; + }; + type TestCase = (arg: TestArgs) => Promise; + type ExtensionSender = ( + arg: WalletActions + ) => Promise<{ op: number; res: SendMessageResult; is_inernal: boolean }>; + + /* Idea behind those wrappers is that we have common expectations of state + /* Everything common between Internal/External/Extension actions goes to wrapper. + /* Anything case specific goes to callbacks + */ + let testSendModes: ( + internal: boolean, + exp: number, + mask: SendMode, + modes: SendMode[], + customSender?: ExtensionSender + ) => Promise; + let extensionSender: ExtensionSender; + let testSendInit: (shouldSucceed: TestCase, validateNewWallet: TestCase) => Promise; + let testSetCode: (shouldSucceed: TestCase, validate: TestCase) => Promise; + let testAddExt: (shouldSucceed: TestCase, custom_addr?: Address) => Promise; + let testAddExtAlreadyIn: (shouldFail: TestCase) => Promise; + let testAddExtWrongChain: (shouldFail: TestCase, validateOnMc: TestCase) => Promise; + let testRemoveExt: (shouldSucceed: TestCase) => Promise; + let testAddRemoveSend: (shouldSucceed: TestCase) => Promise; + let testRemoveExtNonExistent: (shouldSucceed: TestCase) => Promise; + + beforeAll(async () => { + blockchain = await Blockchain.create(); + code = await compile('wallet_v5'); + keys = keyPairFromSeed(await getSecureRandomBytes(32)); + + owner = await blockchain.treasury('wallet_owner'); + testWalletBc = await blockchain.treasury('test_wallet', { workchain: 0 }); + testWalletMc = await blockchain.treasury('test_wallet', { workchain: -1 }); + + testExtensionBc = await blockchain.treasury('test_extension', { workchain: 0 }); + testExtensionMc = await blockchain.treasury('test_extension', { workchain: -1 }); + + mockMessage = { + message: internal_relaxed({ + to: testWalletBc.address, + value: toNano('1'), + body: beginCell().storeUint(0xdeadbeef, 32).endCell() + }), + mode: defaultExternalMode + }; + + msgPrices = getMsgPrices(blockchain.config, 0); + msgPricesMc = getMsgPrices(blockchain.config, -1); + + walletId = BigInt(getRandomInt(10, 1337)); + wallet = blockchain.openContract( + WalletV5Test.createFromConfig( + { + seqno: 0, + walletId, + signatureAllowed: true, + publicKey: keys.publicKey, + extensions: Dictionary.empty() + }, + code + ) + ); + + const deploy = await wallet.sendDeploy(owner.getSender(), toNano('100000')); + + expect(deploy.transactions).toHaveTransaction({ + on: wallet.address, + from: owner.address, + aborted: false, + deploy: true + }); + + initialState = blockchain.snapshot(); + + curTime = () => { + return blockchain.now ?? Math.floor(Date.now() / 1000); + }; + + loadFrom = async snap => { + if (snap == undefined) { + throw new Error("Snapshot doesn't exist yet. Check tests order"); + } + await blockchain.loadFrom(snap); + }; + getWalletData = async (address?: Address) => { + const contractAddress = address ?? wallet.address; + const smc = await blockchain.getContract(contractAddress); + if (!smc.account.account) throw 'Account not found'; + if (smc.account.account.storage.state.type != 'active') + throw 'Atempting to get data on inactive account'; + if (!smc.account.account.storage.state.state.data) throw 'Data is not present'; + return smc.account.account.storage.state.state.data; + }; + + someMessages = n => { + const messages: OutActionSendMsg[] = new Array(n); + for (let i = 0; i < n; i++) { + messages[i] = { + type: 'sendMsg', + mode: defaultExternalMode, + outMsg: internal_relaxed({ + to: testWalletBc.address, + value: toNano('1'), + body: beginCell().storeUint(i, 32).endCell() + }) + }; + } + return messages; + }; + someExtensions = (n, action) => { + const extensions: ExtendedAction[] = new Array(n); + + for (let i = 0; i < n; i++) { + extensions[i] = { + type: action, + address: randomAddress() + }; + } + + return extensions; + }; + + assertMockMessage = (txs, from) => { + const fromAddr = from ?? wallet.address; + expect(txs).toHaveTransaction({ + on: testWalletBc.address, + from: fromAddr, + value: toNano('1'), + body: beginCell().storeUint(0xdeadbeef, 32).endCell() + }); + }; + assertInternal = (txs, from, exp) => { + const expSuccess = exp == 0; + expect(txs).toHaveTransaction({ + on: wallet.address, + from, + success: expSuccess, + aborted: !expSuccess, + outMessagesCount: !expSuccess ? 1 : 0 + }); + }; + shouldRejectWith = async (p, code) => { + try { + const res = await p; + console.log((res as any).transactions[0].description); + throw new Error(`Should throw ${code}`); + } catch (e: unknown) { + if (e instanceof EmulationError) { + expect(e.exitCode !== undefined && e.exitCode == code).toBe(true); + } else { + throw e; + } + } + }; + + assertSendMessages = async (exp, wallet_id, valid_until, seqno, messages, key, via) => { + let res: SendMessageResult; + let op: number; + let isInternal: boolean; + + const smc = await blockchain.getContract(wallet.address); + let balanceBefore = BigInt(smc.balance); + + if (typeof via == 'function') { + const customRes = await via({ wallet: messages.map(message2action) }); + isInternal = customRes.is_inernal; + res = customRes.res; + op = customRes.op; + } else { + if (via) { + op = Opcodes.auth_signed_internal; + isInternal = true; + res = await wallet.sendMessagesInternal( + via, + wallet_id, + valid_until, + seqno, + key, + messages + ); + } else { + isInternal = false; + op = Opcodes.auth_signed; + res = await wallet.sendMessagesExternal( + wallet_id, + valid_until, + seqno, + key, + messages + ); + } + } + + if (exp == 0) { + const sendTx = findTransactionRequired(res.transactions, { + on: wallet.address, + op, + aborted: false, + outMessagesCount: messages.length + }); + // console.log(sendTx.description); + // console.log(sendTx.blockchainLogs); + + const storageFee = storageGeneric(sendTx).storageFeesCollected; + + balanceBefore -= storageFee; + + for (let i = 0; i < messages.length; i++) { + // console.log("Message:", i); + const msgOut = sendTx.outMessages.get(i)!; + if (msgOut.info.type == 'internal') { + const curPrices = msgOut.info.dest.workChain == 0 ? msgPrices : msgPricesMc; + const estMessage = estimateMessageImpact( + messages[i].message, + sendTx, + curPrices, + balanceBefore, + messages[i].mode, + i > 0 + ); + expect(res.transactions).toHaveTransaction({ + on: msgOut.info.dest, + from: wallet.address, + value: estMessage.expValue, + body: msgOut.body + }); + balanceBefore = estMessage.balanceAfter; + } + } + // console.log("Calculated balance:", balanceBefore); + // console.log("Real balance:", smc.balance); + expect(balanceBefore).toEqual(smc.balance); + } else { + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + outMessagesCount: isInternal ? 1 : 0, // On internal we should bounce + aborted: isInternal, + op, + exitCode: exp + }); + } + return res; + }; + testSendModes = async (internal, exp, mask, modes, sender) => { + let testMsgs: MessageOut[] = []; + let i = 0; + let seqNo = await wallet.getSeqno(); + const oldSeqno = seqNo; + const prevState = blockchain.snapshot(); + const testSender = sender ?? internal ? owner.getSender() : undefined; + /* + if(testSender == sender) { + console.log("Custom sender is working!"); + } + */ + try { + for (let mode of modes) { + // console.log("Testing mode:", mode); + const newMsg: MessageOut = { + message: internal_relaxed({ + to: testWalletBc.address, + value: toNano(1), + body: beginCell().storeUint(i++, 32).endCell() + }), + mode: mode | mask + }; + testMsgs.push(newMsg); + + // Test in single mode first + await assertSendMessages( + exp, + walletId, + curTime() + 1000, + seqNo, + [newMsg], + keys.secretKey, + testSender + ); + expect(await wallet.getSeqno()).toEqual(++seqNo); + } + + await blockchain.loadFrom(prevState); + + // Now all at once + await assertSendMessages( + exp, + walletId, + curTime() + 1000, + oldSeqno, + testMsgs, + keys.secretKey, + testSender + ); + expect(await wallet.getSeqno()).toEqual(oldSeqno + 1); + } finally { + await blockchain.loadFrom(prevState); + } + }; + extensionSender = async actions => { + const res = await wallet.sendExtensionActions(testExtensionBc.getSender(), actions); + return { + is_inernal: true, + res, + op: Opcodes.auth_extension + }; + }; + testSendInit = async (shouldWork, validateNewWallet) => { + let newWalletId: bigint; + let seqNo = await wallet.getSeqno(); + + do { + newWalletId = BigInt(getRandomInt(1000, 100000)); + } while (newWalletId == walletId); + + const newWalletData = walletV5ConfigToCell({ + walletId: newWalletId, + seqno: 0, + signatureAllowed: true, + publicKey: keys.publicKey, // Same key + extensions: Dictionary.empty() + }); + + // Deploying it to masterchain + const newAddress = contractAddress(-1, { code, data: newWalletData }); + + const testArgs: TestArgs = { + walletId, + valid_until: curTime() + 100, + seqno: seqNo, + actions: { + wallet: [ + { + type: 'sendMsg', + outMsg: internal_relaxed({ + to: newAddress, + value: toNano('100'), + init: { + code, + data: newWalletData + } + }), + mode: defaultExternalMode + } + ] + }, + key: keys.secretKey, + extra: { new_address: newAddress } + }; + + await shouldWork(testArgs); + + newWallet = blockchain.openContract(WalletV5Test.createFromAddress(newAddress)); + + // New wallet should be able to send message with current key + + await validateNewWallet({ + walletId: newWalletId, + valid_until: curTime() + 100, + seqno: 0, + actions: {}, // Won't be used by this handler anyway + key: keys.secretKey + }); + // res = await newWallet.sendMessagesExternal(newWalletId, curTime() + 100, 0, keys.secretKey, [mockMessage]); + + // Let's test getters while we can + expect((await newWallet.getWalletId()).subwalletNumber).toEqual(Number(newWalletId)); + expect(await newWallet.getPublicKey()).toEqual(bufferToBigInt(keys.publicKey)); + expect(await newWallet.getIsSignatureAuthAllowed()).toBe(-1); + + hasMcWallet = blockchain.snapshot(); + }; + testSetCode = async (testCb, validateCb) => { + let testMsgs: OutAction[] = new Array(254); + const newCode = beginCell().storeUint(getRandomInt(0, 1000), 32).endCell(); + let seqNo = await wallet.getSeqno(); + + const setCodeAction: OutAction = { + type: 'setCode', + newCode + }; + + testMsgs = someMessages(254); // Saving space for set_code + + const onlySetCode = [setCodeAction]; + const setCodeLast = [...testMsgs, setCodeAction]; + const setCodeFirst = [setCodeAction, ...testMsgs]; + const setCodeShuffle = [...testMsgs]; + + const setCodeIdx = getRandomInt(1, setCodeShuffle.length - 1); + // Just replace some random position with setCode + setCodeShuffle[setCodeIdx] = setCodeAction; + + const extraSetCode = [...setCodeShuffle]; + let newIdx = setCodeIdx; + + do { + newIdx = getRandomInt(1, setCodeShuffle.length - 1); + } while (newIdx == setCodeIdx); + // Insert another one, in case code removes first matched only + extraSetCode[newIdx] = setCodeAction; + + const prevState = await getWalletData(); + const defaultArgs = { + walletId, + seqno: seqNo, + valid_until: curTime() + 1000, + key: keys.secretKey, + prevState + }; + for (let actionSet of [ + onlySetCode, + setCodeLast, + setCodeFirst, + setCodeShuffle, + extraSetCode + ]) { + //const setCodeRequest = WalletV5Test.requestMessage(false, walletId, curTime() + 100, seqNo, {wallet: actionSet}, keys.secretKey); + const negTestArgs: TestArgs = { + ...defaultArgs, + seqno: seqNo, + actions: { wallet: actionSet } + }; + // const negTestArgs: TestArgs = {...defaultArgs, actions: {wallet: actionSet}}; + await testCb(negTestArgs); + seqNo = await wallet.getSeqno(); + } + + // Validate that it has nothing to do with message list + await validateCb({ ...defaultArgs, seqno: seqNo, actions: { wallet: testMsgs } }); + }; + testAddExt = async (checkTx, customAddr) => { + let seqNo = await wallet.getSeqno(); + + const extensionAddr = customAddr ?? testExtensionBc.address; + const testArgs: TestArgs = { + walletId, + valid_until: curTime() + 100, + seqno: seqNo, + actions: { + extended: [ + { + type: 'add_extension', + address: extensionAddr + } + ] + }, + key: keys.secretKey + }; + + await checkTx(testArgs); + + const installedExt = await wallet.getExtensionsArray(); + expect(installedExt.findIndex(a => a.equals(extensionAddr))).toBeGreaterThanOrEqual(0); + // expect(await wallet.getSeqno()).toEqual(seqNo + 1); + }; + testAddExtAlreadyIn = async checkTx => { + await loadFrom(hasExtension); + + const installedBefore = await wallet.getExtensionsArray(); + let seqNo = await wallet.getSeqno(); + + const testArgs: TestArgs = { + walletId, + valid_until: curTime() + 100, + seqno: seqNo, + actions: { + extended: [ + { + type: 'add_extension', + address: testExtensionBc.address + } + ] + }, + key: keys.secretKey + }; + + await checkTx(testArgs); + + const installedAfter = await wallet.getExtensionsArray(); + expect(installedBefore.length).toEqual(installedAfter.length); + + for (let i = 0; i < installedBefore.length; i++) { + expect(installedBefore[i].equals(installedAfter[i])).toBe(true); + } + }; + testAddExtWrongChain = async (shouldReject, validate) => { + const prevState = blockchain.snapshot(); + let seqNo = await wallet.getSeqno(); + const installedBefore = await wallet.getExtensionsArray(); + + let testArgs: TestArgs = { + walletId, + valid_until: curTime() + 100, + seqno: seqNo, + actions: { + extended: [ + { + type: 'add_extension', + address: testExtensionMc.address + } + ] + }, + key: keys.secretKey + }; + + await shouldReject(testArgs); + + const installedAfter = await wallet.getExtensionsArray(); + expect(installedBefore.length).toEqual(installedAfter.length); + + for (let i = 0; i < installedBefore.length; i++) { + expect(installedBefore[i].equals(installedAfter[i])).toBe(true); + } + // But it should work for the wallet in basechain + + const newSeqNo = await newWallet.getSeqno(); + const newId = BigInt((await newWallet.getWalletId()).subwalletNumber); + + testArgs = { + walletId: newId, + valid_until: curTime() + 100, + seqno: newSeqNo, + actions: { + extended: [ + { + type: 'add_extension', + address: testExtensionMc.address + } + ] + }, + key: keys.secretKey + }; + + let installedExt = await newWallet.getExtensionsArray(); + expect(installedExt.findIndex(a => a.equals(testExtensionMc.address))).toBe(-1); + + await validate(testArgs); + + installedExt = await newWallet.getExtensionsArray(); + expect( + installedExt.findIndex(a => a.equals(testExtensionMc.address)) + ).toBeGreaterThanOrEqual(0); + // expect(await wallet.getSeqno()).toEqual(seqNo + 1); + + await loadFrom(prevState); + }; + testRemoveExt = async shouldRemove => { + await loadFrom(hasExtension); + let seqNo = await wallet.getSeqno(); + let installedExt = await wallet.getExtensionsArray(); + expect(installedExt[0].equals(testExtensionBc.address)).toBe(true); + + const testArgs: TestArgs = { + walletId, + valid_until: curTime() + 100, + seqno: seqNo, + actions: { + extended: [ + { + type: 'remove_extension', + address: testExtensionBc.address + } + ] + }, + key: keys.secretKey + }; + + await shouldRemove(testArgs); + + installedExt = await wallet.getExtensionsArray(); + expect(installedExt.findIndex(a => a.equals(testExtensionBc.address))).toBe(-1); + }; + testRemoveExtNonExistent = async shouldFail => { + await loadFrom(hasExtension); + let seqNo = await wallet.getSeqno(); + const differentExt = await blockchain.treasury('totally different extension'); + const installedBefore = await wallet.getExtensionsArray(); + + expect(installedBefore.length).toBe(1); + expect(installedBefore[0].equals(testExtensionBc.address)).toBe(true); + + const testArgs: TestArgs = { + walletId, + valid_until: curTime() + 100, + seqno: seqNo, + actions: { + extended: [ + { + type: 'remove_extension', + address: differentExt.address + } + ] + }, + key: keys.secretKey + }; + await shouldFail(testArgs); + + const extAfter = await wallet.getExtensionsArray(); + expect(extAfter.length).toBe(1); + // expect(await wallet.getSeqno()).toEqual(seqNo + 1); + expect( + extAfter.findIndex(e => e.equals(testExtensionBc.address)) + ).toBeGreaterThanOrEqual(0); + }; + testAddRemoveSend = async shouldWork => { + const prevState = blockchain.snapshot(); + const seqNo = await wallet.getSeqno(); + + const extBefore = await wallet.getExtensionsArray(); + const testMessages = someMessages(255); // Full pack + const testExtensions = someExtensions(100, 'add_extension'); + + // Let's pick some of those for removal + const removeExt = pickRandomNFrom(5, testExtensions).map(e => { + const res: ExtensionRemove = { + type: 'remove_extension', + address: (e as ExtensionAdd).address + }; + return res; + }); + // console.log("Remove extensions:", removeExt); + const shouldStay = (testExtensions as ExtensionAdd[]) + .filter(e => removeExt.find(r => r.address.equals(e.address)) == undefined) + .map(e => e.address); + shouldStay.push(...extBefore); + testExtensions.push(...removeExt); + + const testArgs: TestArgs = { + walletId, + valid_until: curTime() + 100, + seqno: seqNo, + actions: { + wallet: testMessages, + extended: testExtensions + }, + key: keys.secretKey + }; + + await shouldWork(testArgs); + + const extAfter = await wallet.getExtensionsArray(); + + expect(extAfter.length).toEqual(shouldStay.length); + for (let i = 0; i < shouldStay.length; i++) { + const testAddr = shouldStay[i]; + expect(extAfter.findIndex(addr => addr.equals(testAddr))).toBeGreaterThanOrEqual(0); + expect(removeExt.findIndex(e => e.address.equals(testAddr))).toBe(-1); + } + // expect(await wallet.getSeqno()).toEqual(seqNo + 1); + await loadFrom(prevState); + }; + }); + describe('Basic', () => { + it('should deploy', async () => {}); + it('should be able to receive basic transfer', async () => { + const testWallet = await blockchain.treasury('test_wallet'); + const assertSimple = async (body?: Cell) => { + const res = await testWallet.send({ + to: wallet.address, + value: toNano(getRandomInt(1, 100)), + body, + sendMode: SendMode.PAY_GAS_SEPARATELY + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testWallet.address, + aborted: false, + outMessagesCount: 0 + }); + }; + + await assertSimple(); + await assertSimple( + beginCell().storeUint(0, 32).storeStringTail('Hey, bruh!').endCell() + ); + + const validSet = new Set(validOpCodes); + let testOp: number; + + do { + testOp = getRandomInt(1, (1 << 32) - 1); + } while (validSet.has(testOp)); + + await assertSimple( + beginCell().storeUint(testOp, 32).storeUint(curTime(), 64).endCell() + ); + }); + }); + describe('Actions', () => { + describe('External', () => { + it('should be able to send message to arbitrary address', async () => { + const msgValue = toNano(getRandomInt(1, 10)); + const randomBody = beginCell().storeUint(curTime(), 64).endCell(); + const seqNo = BigInt(await wallet.getSeqno()); + + await assertSendMessages( + 0, + walletId, + curTime() + 1000, + seqNo, + [ + { + message: internal_relaxed({ + to: testWalletBc.address, + value: msgValue, + body: randomBody + }), + mode: defaultExternalMode + }, + { + message: internal_relaxed({ + to: testWalletMc.address, + value: msgValue, + body: randomBody + }), + mode: defaultExternalMode + } + ], + keys.secretKey + ); + + const seqnoAfter = BigInt(await wallet.getSeqno()); + expect(seqnoAfter).toEqual(seqNo + 1n); + }); + it('should reject message with wrong signature', async () => { + const seqNo = await wallet.getSeqno(); + const badKeys = keyPairFromSeed(await getSecureRandomBytes(32)); + + await shouldRejectWith( + wallet.sendMessagesExternal( + walletId, + curTime() + 1000, + seqNo, + badKeys.secretKey, + [mockMessage] + ), + ErrorsV5.invalid_signature + ); + expect(await wallet.getSeqno()).toEqual(seqNo); + + const res = await wallet.sendMessagesExternal( + walletId, + curTime() + 1000, + seqNo, + keys.secretKey, + [mockMessage] + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + aborted: false, + outMessagesCount: 1 + }); + }); + it('should reject external message with prefix other than signed_external', async () => { + // All of the valid ops except acceptable, plus one random + const nonExternalOps = [ + ...validOpCodes.filter(op => op != Opcodes.auth_signed), + 0xdeadbeef + ]; + const seqNo = await wallet.getSeqno(); + const validMsg = WalletV5Test.requestMessage( + false, + walletId, + curTime() + 1000, + BigInt(seqNo), + {} + ); + const msgTail = validMsg.beginParse().skip(32); // skip op; + + for (let op of nonExternalOps) { + const newMsg = WalletV5Test.signRequestMessage( + beginCell().storeUint(op, 32).storeSlice(msgTail).endCell(), + keys.secretKey + ); + await shouldRejectWith( + wallet.sendExternalSignedMessage(newMsg), + ErrorsV5.invalid_message_operation + ); + // Should not change seqno + expect(await wallet.getSeqno()).toEqual(seqNo); + } + + // Validate that original message works + const res = await wallet.sendExternalSignedMessage( + WalletV5Test.signRequestMessage(validMsg, keys.secretKey) + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + aborted: false + }); + expect(await wallet.getSeqno()).toEqual(seqNo + 1); + }); + it('should be able to send up to 255 messages', async () => { + let testMsgs: MessageOut[] = new Array(255); + const seqNo = BigInt(await wallet.getSeqno()); + + for (let i = 0; i < 255; i++) { + testMsgs[i] = { + message: internal_relaxed({ + to: testWalletBc.address, + value: toNano('1'), + body: beginCell().storeUint(i, 32).endCell() + }), + mode: defaultExternalMode + }; + } + + await assertSendMessages( + 0, + walletId, + curTime() + 1000, + seqNo, + testMsgs, + keys.secretKey + ); + }); + it('should be able to send messages with different send modes', async () => { + await testSendModes(false, 0, SendMode.IGNORE_ERRORS, [ + SendMode.NONE, + SendMode.PAY_GAS_SEPARATELY, + SendMode.CARRY_ALL_REMAINING_BALANCE + ]); + }); + it('should reject send modes without IGNORE_ERRORS', async () => { + await testSendModes( + false, + ErrorsV5.external_send_message_must_have_ignore_errors_send_mode, + 0, + [ + SendMode.NONE, + SendMode.PAY_GAS_SEPARATELY, + SendMode.CARRY_ALL_REMAINING_BALANCE, + SendMode.CARRY_ALL_REMAINING_BALANCE | SendMode.DESTROY_ACCOUNT_IF_ZERO + ] + ); + }); + it('should be able to send message with init state', async () => { + await testSendInit( + async args => { + if (!Address.isAddress(args.extra.new_address)) { + throw new TypeError('Callback requires wallet address'); + } + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(reqMsg); + expect(res.transactions).toHaveTransaction({ + on: args.extra.new_address, + aborted: false, + deploy: true + }); + }, + async args => { + // New wallet should be able to send from new wallet via external + const res = await newWallet.sendMessagesExternal( + args.walletId, + args.valid_until, + args.seqno, + args.key, + [mockMessage] + ); + assertMockMessage(res.transactions, newWallet.address); + } + ); + }); + it('should be able to send external message', async () => { + const seqNo = await wallet.getSeqno(); + const testPayload = BigInt(getRandomInt(0, 100000)); + const testBody = beginCell().storeUint(testPayload, 32).endCell(); + + const res = await wallet.sendMessagesExternal( + walletId, + curTime() + 100, + seqNo, + keys.secretKey, + [ + { + message: { + info: { + type: 'external-out', + createdAt: 0, + createdLt: 0n, + dest: new ExternalAddress(testPayload, 32), + src: null + }, + body: testBody + }, + mode: defaultExternalMode + } + ] + ); + + const txSuccess = findTransactionRequired(res.transactions, { + on: wallet.address, + op: Opcodes.auth_signed, + aborted: false + }); + + expect(txSuccess.externals.length).toBe(1); + + const extOut = txSuccess.externals[0]; + + expect(extOut.info.dest!.value).toBe(testPayload); + expect(extOut.body).toEqualCell(testBody); + }); + it('should reject message with invalid seqno', async () => { + const seqNo = await wallet.getSeqno(); + expect(seqNo).toBeGreaterThan(2); // For better test + const testDelta = getRandomInt(2, seqNo); + + for (let testSeq of [seqNo - 1, seqNo + 1, seqNo + testDelta, seqNo - testDelta]) { + await shouldRejectWith( + wallet.sendMessagesExternal( + walletId, + curTime() + 100, + testSeq, + keys.secretKey, + [mockMessage] + ), + ErrorsV5.invalid_seqno + ); + expect(await wallet.getSeqno()).toEqual(seqNo); + } + }); + it('should reject message with invalid subwallet', async () => { + const seqNo = await wallet.getSeqno(); + const testDelta = BigInt(getRandomInt(2, Number(walletId))); + + for (let testId of [ + walletId - 1n, + walletId + 1n, + walletId - testDelta, + walletId + testDelta + ]) { + await shouldRejectWith( + wallet.sendMessagesExternal( + testId, + curTime() + 100, + seqNo, + keys.secretKey, + [mockMessage] + ), + ErrorsV5.invalid_wallet_id + ); + expect(await wallet.getSeqno()).toEqual(seqNo); + } + }); + it('should reject expired message', async () => { + blockchain.now = curTime(); // Stop ticking + const seqNo = await wallet.getSeqno(); + const testDelta = getRandomInt(1, 10000); + + // We're treating current time as expired. Should we? + for (let testUntil of [blockchain.now, blockchain.now - testDelta]) { + await shouldRejectWith( + wallet.sendMessagesExternal(walletId, testUntil, seqNo, keys.secretKey, [ + mockMessage + ]), + ErrorsV5.expired + ); + expect(await wallet.getSeqno()).toEqual(seqNo); + } + + const res = await wallet.sendMessagesExternal( + walletId, + blockchain.now + 1, + seqNo, + keys.secretKey, + [mockMessage] + ); + + assertMockMessage(res.transactions); + expect(await wallet.getSeqno()).toEqual(seqNo + 1); + }); + it('should reject set_code action', async () => { + await testSetCode( + async args => { + if (args.actions == undefined || args.key == undefined) { + throw new Error('Actions and keys are required'); + } + const setCodeRequest = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(setCodeRequest); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + success: true, // Because of commit call + exitCode: 9 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }, + async args => { + if (args.actions == undefined || args.key == undefined) { + throw new Error('Actions and keys are required'); + } + const sendJustMessages = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(sendJustMessages); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + aborted: false, + outMessagesCount: 254, + exitCode: 0 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + } + ); + }); + it('should be able to add extension', async () => { + await testAddExt(async args => { + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(reqMsg); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + exitCode: 0 // Because of commit we can't rely on compute phase status + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }); + + hasExtension = blockchain.snapshot(); + }); + it('should not be able to install already installed extendsion', async () => { + await testAddExtAlreadyIn(async args => { + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(reqMsg); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + exitCode: ErrorsV5.add_extension + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }); + }); + it('should not be able to install extension from different chain', async () => { + await testAddExtWrongChain( + async args => { + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(reqMsg); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + exitCode: ErrorsV5.extension_wrong_workchain + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }, + async args => { + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await newWallet.sendExternalSignedMessage(reqMsg); + expect(res.transactions).toHaveTransaction({ + on: newWallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + exitCode: 0 // We're good now + }); + expect(await newWallet.getSeqno()).toEqual(Number(args.seqno) + 1); + } + ); + }); + it('should be able to remove extension', async () => { + await testRemoveExt(async args => { + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(reqMsg); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + exitCode: 0 // Because of commit we can't rely on compute phase status + }); + }); + }); + it('should throw on removing non-existent extension', async () => { + await testRemoveExtNonExistent(async args => { + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(reqMsg); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + exitCode: ErrorsV5.remove_extension + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }); + }); + // Doesn't make much sense, since inderectly tested in too many places + it.skip('empty action list should increase seqno', async () => { + const seqNo = await wallet.getSeqno(); + const testMsg = WalletV5Test.requestMessage( + false, + walletId, + curTime() + 100, + seqNo, + {}, + keys.secretKey + ); + const res = await wallet.sendExternalSignedMessage(testMsg); + + expect(await wallet.getSeqno()).toEqual(seqNo + 1); + }); + it('should be able to add/remove extensions and send messages in one go', async () => { + await testAddRemoveSend(async args => { + const reqMsg = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(reqMsg); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + aborted: false, + outMessagesCount: 255 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }); + }); + }); + describe('Internal', () => { + it('should be able to send message to arbitrary address', async () => { + const msgValue = toNano(getRandomInt(1, 10)); + const randomBody = beginCell().storeUint(curTime(), 64).endCell(); + const seqNo = BigInt(await wallet.getSeqno()); + + const res = await assertSendMessages( + 0, + walletId, + curTime() + 1000, + seqNo, + [ + { + message: internal_relaxed({ + to: testWalletBc.address, + value: msgValue, + body: randomBody + }), + mode: SendMode.PAY_GAS_SEPARATELY + }, + { + message: internal_relaxed({ + to: testWalletMc.address, + value: msgValue, + body: randomBody + }), + mode: SendMode.PAY_GAS_SEPARATELY + } + ], + keys.secretKey, + owner.getSender() + ); + + const seqnoAfter = BigInt(await wallet.getSeqno()); + expect(seqnoAfter).toEqual(seqNo + 1n); + }); + it('should ignore message with wrong signature', async () => { + const seqNo = await wallet.getSeqno(); + const badKeys = keyPairFromSeed(await getSecureRandomBytes(32)); + const stateBefore = await getWalletData(); + + const msgActions = [message2action(mockMessage)]; + let testMsg = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 100, + seqNo, + { wallet: msgActions }, + badKeys.secretKey + ); + let res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: testMsg + }); + assertInternal(res.transactions, owner.address, 0); + expect(await getWalletData()).toEqualCell(stateBefore); + + testMsg = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 100, + seqNo, + { wallet: msgActions }, + keys.secretKey + ); + res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: testMsg + }); + assertMockMessage(res.transactions); + expect(await wallet.getSeqno()).toEqual(seqNo + 1); + }); + + it('should bounce message with invalid subwallet', async () => { + const seqNo = await wallet.getSeqno(); + const testDelta = BigInt(getRandomInt(2, Number(walletId))); + const stateBefore = await getWalletData(); + + for (let testId of [ + walletId - 1n, + walletId + 1n, + walletId - testDelta, + walletId + testDelta + ]) { + const res = await wallet.sendMessagesInternal( + owner.getSender(), + testId, + curTime() + 100, + seqNo, + keys.secretKey, + [mockMessage] + ); + assertInternal(res.transactions, owner.address, ErrorsV5.invalid_wallet_id); + expect(await getWalletData()).toEqualCell(stateBefore); + } + }); + it('should bounce expired message', async () => { + blockchain.now = curTime(); // Stop ticking + const seqNo = await wallet.getSeqno(); + const testDelta = getRandomInt(1, 10000); + const stateBefore = await getWalletData(); + + // We're treating current time as expired. Should we? + for (let testUntil of [blockchain.now, blockchain.now - testDelta]) { + const res = await wallet.sendMessagesInternal( + owner.getSender(), + walletId, + testUntil, + seqNo, + keys.secretKey, + [mockMessage] + ); + assertInternal(res.transactions, owner.address, ErrorsV5.expired); + expect(await getWalletData()).toEqualCell(stateBefore); + } + + const res = await wallet.sendMessagesExternal( + walletId, + blockchain.now + 1, + seqNo, + keys.secretKey, + [mockMessage] + ); + assertMockMessage(res.transactions); + expect(await wallet.getSeqno()).toEqual(seqNo + 1); + }); + it('should reject set_code action', async () => { + await testSetCode( + async args => { + const setCodeRequest = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: setCodeRequest + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed_internal, + outMessagesCount: 1, // bounce + aborted: true, + success: false, // No commit anymore + exitCode: 9 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno)); // On internal seqno is not commited + }, + async args => { + if (args.actions == undefined || args.key == undefined) { + throw new Error('Actions and keys are required'); + } + const sendJustMessages = WalletV5Test.requestMessage( + false, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExternalSignedMessage(sendJustMessages); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + aborted: false, + outMessagesCount: 254, + exitCode: 0 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + } + ); + }); + it('should bounce message with invalid seqno', async () => { + const seqNo = await wallet.getSeqno(); + expect(seqNo).toBeGreaterThan(2); // For better test + const testDelta = getRandomInt(2, seqNo); + const stateBefore = await getWalletData(); + + for (let testSeq of [seqNo - 1, seqNo + 1, seqNo + testDelta, seqNo - testDelta]) { + const res = await wallet.sendMessagesInternal( + owner.getSender(), + walletId, + curTime() + 100, + testSeq, + keys.secretKey, + [mockMessage] + ); + assertInternal(res.transactions, owner.address, ErrorsV5.invalid_seqno); + expect(await getWalletData()).toEqualCell(stateBefore); + } + }); + it('should ignore internal message with prefix other than signed_internal', async () => { + // All of the valid ops except acceptable, plus one random + const nonExternalOps = [ + ...validOpCodes.filter(op => op != Opcodes.auth_signed_internal), + 0xdeadbeef + ]; + const seqNo = await wallet.getSeqno(); + // Not yet signed + const validMsg = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 1000, + BigInt(seqNo), + {} + ); + const msgTail = validMsg.beginParse().skip(32); // skip op; + + for (let op of nonExternalOps) { + const newMsg = WalletV5Test.signRequestMessage( + beginCell().storeUint(op, 32).storeSlice(msgTail).endCell(), + keys.secretKey + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: newMsg + }); + assertInternal(res.transactions, owner.address, 0); // return no bounce + // Should not change seqno + expect(await wallet.getSeqno()).toEqual(seqNo); + } + + // Validate that original message works + const successMsg = WalletV5Test.signRequestMessage(validMsg, keys.secretKey); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: successMsg + }); + assertInternal(res.transactions, owner.address, 0); + expect(await wallet.getSeqno()).toEqual(seqNo + 1); + }); + it('should ignore internal message with correct prefix, but incorrect length', async () => { + const seqNo = await wallet.getSeqno(); + // So we have message with bad wallet id + const badMsg = WalletV5Test.requestMessage( + true, + walletId - 1n, + curTime() + 1000, + BigInt(seqNo), + {}, + keys.secretKey + ); + + // Now we have it's truncated version + const msgTrunc = beginCell() + .storeBits(badMsg.beginParse().loadBits(badMsg.bits.length - 10)) + .endCell(); // off by one + + let res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: msgTrunc + }); + // Now, because it's truncated it gets ignored + assertInternal(res.transactions, owner.address, 0); + + res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: badMsg + }); + assertInternal(res.transactions, owner.address, ErrorsV5.invalid_wallet_id); + // If we send it as is, the subwallet exception will trigger + }); + it('should be able to send up to 255 messages', async () => { + let testMsgs: MessageOut[] = new Array(255); + const seqNo = BigInt(await wallet.getSeqno()); + + for (let i = 0; i < 255; i++) { + testMsgs[i] = { + message: internal_relaxed({ + to: testWalletBc.address, + value: toNano('1'), + body: beginCell().storeUint(i, 32).endCell() + }), + mode: defaultExternalMode + }; + } + await assertSendMessages( + 0, + walletId, + curTime() + 1000, + seqNo, + testMsgs, + keys.secretKey + ); + }); + it('should be able to send message with init state', async () => { + await testSendInit( + async args => { + if (!Address.isAddress(args.extra.new_address)) { + throw new TypeError('Callback requires wallet address'); + } + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + + expect(res.transactions).toHaveTransaction({ + on: args.extra.new_address, + aborted: false, + deploy: true + }); + }, + async args => { + // New wallet should be able to send from new wallet via external + const res = await newWallet.sendMessagesInternal( + owner.getSender(), + args.walletId, + args.valid_until, + args.seqno, + args.key, + [mockMessage], + toNano('1') + ); + assertMockMessage(res.transactions, newWallet.address); + } + ); + }); + it('should be able to send external message', async () => { + const seqNo = await wallet.getSeqno(); + const testPayload = BigInt(getRandomInt(0, 100000)); + const testBody = beginCell().storeUint(testPayload, 32).endCell(); + + const res = await wallet.sendMessagesInternal( + owner.getSender(), + walletId, + curTime() + 100, + seqNo, + keys.secretKey, + [ + { + message: { + info: { + type: 'external-out', + createdAt: 0, + createdLt: 0n, + dest: new ExternalAddress(testPayload, 32), + src: null + }, + body: testBody + }, + mode: defaultExternalMode + } + ] + ); + + const txSuccess = findTransactionRequired(res.transactions, { + on: wallet.address, + from: owner.address, + op: Opcodes.auth_signed_internal, + aborted: false + }); + + expect(txSuccess.externals.length).toBe(1); + + const extOut = txSuccess.externals[0]; + + expect(extOut.info.dest!.value).toBe(testPayload); + expect(extOut.body).toEqualCell(testBody); + }); + + it('should be able to send messages with various send modes', async () => { + // Internal should work with + await testSendModes(true, 0, SendMode.IGNORE_ERRORS, [ + SendMode.NONE, + SendMode.PAY_GAS_SEPARATELY, + SendMode.CARRY_ALL_REMAINING_INCOMING_VALUE, + SendMode.CARRY_ALL_REMAINING_BALANCE + ]); + // And without IGNORE_ERRORS + await testSendModes(true, 0, 0, [ + SendMode.NONE, + SendMode.PAY_GAS_SEPARATELY, + SendMode.CARRY_ALL_REMAINING_INCOMING_VALUE, + SendMode.CARRY_ALL_REMAINING_BALANCE + ]); + }); + it('should bounce on set_code action', async () => { + await testSetCode( + async args => { + if ( + args.actions == undefined || + args.key == undefined || + args.prevState == undefined + ) { + throw new Error('Actions keys and state are required'); + } + const setCodeRequest = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: setCodeRequest + }); + assertInternal(res.transactions, owner.address, 9); + expect(await getWalletData()).toEqualCell(args.prevState); + }, + async args => { + if ( + args.actions == undefined || + args.key == undefined || + args.prevState == undefined + ) { + throw new Error('Actions keys and state are required'); + } + const sendJustMessages = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: sendJustMessages + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed_internal, + aborted: false, + outMessagesCount: 254, + exitCode: 0 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + } + ); + }); + it('should be able to add extension', async () => { + await loadFrom(initialState); + await testAddExt(async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: owner.address, + op: Opcodes.auth_signed_internal, + outMessagesCount: 0, + aborted: false, + exitCode: 0 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }); + hasExtension = blockchain.snapshot(); + }); + it('should not be able to install already installed extendsion', async () => { + await testAddExtAlreadyIn(async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: owner.address, + op: Opcodes.auth_signed_internal, + aborted: true, + outMessagesCount: 1, + exitCode: ErrorsV5.add_extension + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno)); + }); + }); + it('should not be able to install extension from different chain', async () => { + await loadFrom(hasMcWallet); + await testAddExtWrongChain( + async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: owner.address, + op: Opcodes.auth_signed_internal, + outMessagesCount: 1, + aborted: true, + exitCode: ErrorsV5.extension_wrong_workchain + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno)); + }, + async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await newWallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + expect(res.transactions).toHaveTransaction({ + on: newWallet.address, + from: owner.address, + op: Opcodes.auth_signed_internal, + outMessagesCount: 0, + aborted: false + }); + expect(await newWallet.getSeqno()).toEqual(Number(args.seqno) + 1); + } + ); + }); + it('should be able to remove extension', async () => { + await testRemoveExt(async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: owner.address, + op: Opcodes.auth_signed_internal, + outMessagesCount: 0, + aborted: false + }); + }); + }); + it('should throw on removing non-existent extension', async () => { + await testRemoveExtNonExistent(async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed_internal, + outMessagesCount: 1, + aborted: true, + exitCode: ErrorsV5.remove_extension + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno)); + }); + }); + it('should be able to add/remove extensions and send messages in one go', async () => { + await testAddRemoveSend(async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: reqMsg + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed_internal, + aborted: false, + outMessagesCount: 255 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); + }); + }); + describe('Bounce', () => { + it('should ignore bounced mesages', async () => { + await loadFrom(hasExtension); + const seqNo = await wallet.getSeqno(); + const mockActions: WalletActions = { wallet: [message2action(mockMessage)] }; + + // Note that in reality bounce gets prefixed by 0xFFFFFFFF + // With current code, that would mean message would be ignored + // due to op check + // However we still could test as if TVM doesn't add prefix to bounce somehow + const intReq = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 1000, + seqNo, + mockActions, + keys.secretKey + ); + const extReq = WalletV5Test.extensionMessage(mockActions); + // ihr_disable and bounce flags combinations + let flagFuzz = [ + [false, false], + [true, false], + [false, true], + [true, true] + ]; + + const stateBefore = await getWalletData(); + + for (let flags of flagFuzz) { + let res = await blockchain.sendMessage( + internal({ + from: owner.address, + to: wallet.address, + body: intReq, + value: toNano('1'), + bounced: true, + ihrDisabled: flags[0], + bounce: flags[1] + }) + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed_internal, + aborted: false, + outMessagesCount: 0 + }); + expect(await getWalletData()).toEqualCell(stateBefore); + + res = await blockchain.sendMessage( + internal({ + from: testExtensionBc.address, + to: wallet.address, + body: extReq, + value: toNano('1'), + bounced: true, + ihrDisabled: flags[0], + bounce: flags[1] + }) + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_extension, + aborted: false, + outMessagesCount: 0 + }); + expect(await getWalletData()).toEqualCell(stateBefore); + } + + // Let's proove that bounce flag is the reason + const resInt = await blockchain.sendMessage( + internal({ + from: owner.address, + to: wallet.address, + body: intReq, + value: toNano('1'), + ihrDisabled: true, + bounce: true + }) + ); + assertMockMessage(resInt.transactions); + + const resExt = await blockchain.sendMessage( + internal({ + from: testExtensionBc.address, + to: wallet.address, + body: extReq, + value: toNano('1'), + ihrDisabled: false, + bounce: false + }) + ); + assertMockMessage(resExt.transactions); + }); + }); + }); + describe('Extension', () => { + let actionFuzz: WalletActions[]; + beforeAll(async () => { + actionFuzz = [ + { wallet: [message2action(mockMessage)] }, + { wallet: someMessages(10) }, + { extended: someExtensions(5, 'add_extension') }, + { wallet: someMessages(5), extended: someExtensions(5, 'add_extension') }, + { extended: [{ type: 'remove_extension', address: testExtensionBc.address }] }, + { + wallet: someMessages(5), + extended: [{ type: 'remove_extension', address: testExtensionBc.address }] + }, + { extended: [{ type: 'sig_auth', allowed: false }] }, + { wallet: someMessages(5), extended: [{ type: 'sig_auth', allowed: false }] } + ]; + + await loadFrom(hasExtension); + }); + + it('should be able to send message to arbitrary address', async () => { + const msgValue = toNano(getRandomInt(1, 10)); + const randomBody = beginCell().storeUint(curTime(), 64).endCell(); + + await assertSendMessages( + 0, + walletId, + curTime() + 1000, + 0, + [ + { + message: internal_relaxed({ + to: testWalletBc.address, + value: msgValue, + body: randomBody + }), + mode: SendMode.PAY_GAS_SEPARATELY + }, + { + message: internal_relaxed({ + to: testWalletMc.address, + value: msgValue, + body: randomBody + }), + mode: SendMode.PAY_GAS_SEPARATELY + } + ], + keys.secretKey, + extensionSender + ); + }); + it('extension action is only allowed from installed extension address', async () => { + const differentExt = await blockchain.treasury('Not installed'); + + const stateBefore = await getWalletData(); + + for (let testSender of [owner, differentExt]) { + for (let actions of actionFuzz) { + const res = await wallet.sendExtensionActions( + testSender.getSender(), + actions + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testSender.address, + op: Opcodes.auth_extension, + aborted: false, + outMessagesCount: 0 + }); + expect(await getWalletData()).toEqualCell(stateBefore); + } + } + + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + actionFuzz[0] + ); + + assertMockMessage(res.transactions); + expect(await getWalletData()).toEqualCell(stateBefore); + }); + it('extension request with same hash from different workchain should be ignored', async () => { + // Those should completely equal by hash + expect(testExtensionBc.address.hash.equals(testExtensionMc.address.hash)).toBe( + true + ); + + // Extension with such has is installed + const curExt = await wallet.getExtensionsArray(); + expect( + curExt.findIndex(e => e.hash.equals(testExtensionMc.address.hash)) + ).toBeGreaterThanOrEqual(0); + + const stateBefore = await getWalletData(); + + for (let actions of actionFuzz) { + const res = await wallet.sendExtensionActions( + testExtensionMc.getSender(), + actions + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionMc.address, + op: Opcodes.auth_extension, + aborted: false, + outMessagesCount: 0 + }); + expect(await getWalletData()).toEqualCell(stateBefore); + } + }); + it('should be able to send up to 255 messages', async () => { + let testMsgs: MessageOut[] = new Array(255); + const seqNo = BigInt(await wallet.getSeqno()); + + for (let i = 0; i < 255; i++) { + testMsgs[i] = { + message: internal_relaxed({ + to: testWalletBc.address, + value: toNano('1'), + body: beginCell().storeUint(i, 32).endCell() + }), + mode: defaultExternalMode + }; + } + await assertSendMessages( + 0, + walletId, + curTime() + 1000, + seqNo, + testMsgs, + keys.secretKey, + extensionSender + ); + }); + it('should be able to send messages with various send modes', async () => { + let modeSet = [ + SendMode.NONE, + SendMode.PAY_GAS_SEPARATELY, + SendMode.CARRY_ALL_REMAINING_INCOMING_VALUE, + SendMode.CARRY_ALL_REMAINING_BALANCE + ]; + await testSendModes(true, 0, SendMode.IGNORE_ERRORS, modeSet, extensionSender); + // And without IGNORE_ERRORS + await testSendModes(true, 0, 0, modeSet, extensionSender); + }); + it('should be able to send message with init state', async () => { + await testSendInit( + async args => { + if (!Address.isAddress(args.extra.new_address)) { + throw new TypeError('Callback requires wallet address'); + } + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions + ); + + expect(res.transactions).toHaveTransaction({ + on: args.extra.new_address, + aborted: false, + deploy: true + }); + }, + async args => { + // Couldn't think of any better + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + { + wallet: [message2action(mockMessage)] + }, + args.key + ); + + const testMsg = internal_relaxed({ + to: newWallet.address, + value: toNano('2'), + body: reqMsg + }); + + const res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + wallet: [ + { + type: 'sendMsg', + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: testMsg + } + ] + }); + + // So tx chain ext->basechain wallet->mc wallet->mock message + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false, + outMessagesCount: 1 + }); + expect(res.transactions).toHaveTransaction({ + on: newWallet.address, + from: wallet.address, + op: Opcodes.auth_signed_internal, + aborted: false, + outMessagesCount: 1 + }); + // Finally mock messages goes live + assertMockMessage(res.transactions, newWallet.address); + } + ); + }); + it('should be able to send external message', async () => { + const testPayload = BigInt(getRandomInt(0, 100000)); + const testBody = beginCell().storeUint(testPayload, 32).endCell(); + + const res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + wallet: [ + { + type: 'sendMsg', + mode: SendMode.NONE, + outMsg: { + info: { + type: 'external-out', + createdAt: 0, + createdLt: 0n, + dest: new ExternalAddress(testPayload, 32), + src: null + }, + body: testBody + } + } + ] + }); + + const txSuccess = findTransactionRequired(res.transactions, { + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false + }); + + expect(txSuccess.externals.length).toBe(1); + + const extOut = txSuccess.externals[0]; + + expect(extOut.info.dest!.value).toBe(testPayload); + expect(extOut.body).toEqualCell(testBody); + }); + it('should bounce set_code action', async () => { + await testSetCode( + async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_extension, + outMessagesCount: 1, // bounce + aborted: true, + success: false, // No commit anymore + exitCode: 9 + }); + expect(await wallet.getSeqno()).toEqual(Number(args.seqno)); // On internal seqno is not commited + }, + async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false, + outMessagesCount: 254, + exitCode: 0 + }); + } + ); + }); + it('should be able to add extension', async () => { + const randomExtAddres = randomAddress(); + await testAddExt(async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + outMessagesCount: 0, + aborted: false, + exitCode: 0 + }); + }, randomExtAddres); + }); + it('should not be able to install already installed extendsion', async () => { + await testAddExtAlreadyIn(async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: true, + outMessagesCount: 1, + exitCode: ErrorsV5.add_extension + }); + }); + }); + it('should not be able to install extension from different chain', async () => { + await loadFrom(hasMcWallet); + await testAddExtWrongChain( + async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions, + toNano('1') + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + outMessagesCount: 1, + aborted: true, + exitCode: ErrorsV5.extension_wrong_workchain + }); + }, + async args => { + const reqMsg = WalletV5Test.requestMessage( + true, + args.walletId, + args.valid_until, + args.seqno, + args.actions, + args.key + ); + const testMsg = internal_relaxed({ + to: newWallet.address, + value: toNano('2'), + body: reqMsg + }); + + const res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + wallet: [ + { + type: 'sendMsg', + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: testMsg + } + ] + }); + // So via extension we've sent signed add_extension message + // through our wallet to the masterchain wallet + // And it should end up being installed + expect(res.transactions).toHaveTransaction({ + on: newWallet.address, + from: wallet.address, + op: Opcodes.auth_signed_internal, + outMessagesCount: 0, + aborted: false + }); + } + ); + }); + it('should be able to remove extension', async () => { + await testRemoveExt(async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions, + toNano('1') + ); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + outMessagesCount: 0, + aborted: false + }); + }); + }); + it('should throw on removing non-existent extension', async () => { + await testRemoveExtNonExistent(async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + outMessagesCount: 1, + aborted: true, + exitCode: ErrorsV5.remove_extension + }); + }); + }); + it('should be able to add/remove extensions and send messages in one go', async () => { + await testAddRemoveSend(async args => { + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + args.actions, + toNano('1') + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false, + outMessagesCount: 255 + }); + }); + }); + }); + describe('Malformed action list', () => { + it('action list exceeding 255 elements should be rejected', async () => { + await loadFrom(hasExtension); + let seqNo = await wallet.getSeqno(); + let tooMuch = someMessages(256); + const extReq = WalletV5Test.requestMessage( + false, + walletId, + curTime() + 100, + seqNo, + { wallet: tooMuch }, + keys.secretKey + ); + + let res = await wallet.sendExternalSignedMessage(extReq); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + outMessagesCount: 0, + exitCode: ErrorsV5.invalid_c5 + }); + expect(await wallet.getSeqno()).toEqual(++seqNo); + + const intReq = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 100, + seqNo, + { wallet: tooMuch }, + keys.secretKey + ); + res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: intReq + }); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed_internal, + aborted: true, + outMessagesCount: 1, + exitCode: ErrorsV5.invalid_c5 + }); + + res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + { wallet: tooMuch }, + toNano('1') + ); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_extension, + aborted: true, + outMessagesCount: 1, + exitCode: ErrorsV5.invalid_c5 + }); + }); + it('should reject action list with extra data/refs', async () => { + let seqNo = await wallet.getSeqno(); + const testActionRaw = beginCell().store( + storeOutAction({ + type: 'sendMsg', + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: mockMessage.message + }) + ); + + const ds = testActionRaw.asSlice(); + ds.loadRef(); // Drop one + const noRef = beginCell().storeSlice(ds).endCell(); + const excessiveData = beginCell() + .storeSlice(testActionRaw.asSlice()) + .storeBit(true) + .endCell(); + const truncated = beginCell() + .storeBits(testActionRaw.asSlice().loadBits(testActionRaw.bits - 1)) + .endCell(); + const extraRef = beginCell() + .storeSlice(testActionRaw.asSlice()) + .storeRef(beginCell().storeUint(0x0ec3c86d, 32).endCell()) + .endCell(); + + const origActions = beginCell() + .storeRef(beginCell().endCell()) + .storeSlice(testActionRaw.asSlice()) + .endCell(); + + for (let payload of [excessiveData, truncated, extraRef, noRef]) { + const actionList = beginCell() + .storeRef(beginCell().endCell()) + .storeSlice(payload.asSlice()) + .endCell(); + + const intReq = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 100, + seqNo, + { wallet: actionList }, + keys.secretKey + ); + let res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: intReq + }); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed_internal, + aborted: true, + outMessagesCount: 1, + exitCode: ErrorsV5.invalid_c5 + }); + + const extReq = WalletV5Test.requestMessage( + false, + walletId, + curTime() + 100, + seqNo, + { wallet: actionList }, + keys.secretKey + ); + res = await wallet.sendExternalSignedMessage(extReq); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_signed, + aborted: false, + outMessagesCount: 0, + exitCode: ErrorsV5.invalid_c5 + }); + expect(await wallet.getSeqno()).toEqual(++seqNo); + + res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + { wallet: actionList }, + toNano('1') + ); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + op: Opcodes.auth_extension, + aborted: true, + outMessagesCount: 1, + exitCode: ErrorsV5.invalid_c5 + }); + } + + const intReq = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 100, + seqNo, + { wallet: origActions }, + keys.secretKey + ); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: intReq + }); + assertMockMessage(res.transactions); + }); + }); + }); + describe('Signature auth', () => { + type OwnerArguments = { walletId: bigint; seqno: number | bigint; key: Buffer }; + let signatureDisabled: BlockchainSnapshot; + let signatureEnabled: BlockchainSnapshot; + let multipleExtensions: BlockchainSnapshot; + /* + let testRemoveExtension: (exp: number, + reqType:RequestType, + extension: Address, + via: Sender, commonArgs: OwnerArguments) => Promise; + */ + beforeAll(async () => { + await loadFrom(hasExtension); + }); + + it('extension should be able to set signature mode', async () => { + const seqNo = await wallet.getSeqno(); + const allowedBefore = await wallet.getIsSignatureAuthAllowed(); + expect(allowedBefore).toBe(-1); + signatureEnabled = blockchain.snapshot(); + + let res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + extended: [ + { + type: 'sig_auth', + allowed: false + } + ] + }); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false + }); + expect(await wallet.getIsSignatureAuthAllowed()).toBe(0); + expect(await wallet.getSeqno()).toEqual(seqNo); + signatureDisabled = blockchain.snapshot(); + + res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + extended: [ + { + type: 'sig_auth', + allowed: true + } + ] + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false + }); + expect(await wallet.getIsSignatureAuthAllowed()).toBe(-1); + expect(await wallet.getSeqno()).toEqual(seqNo); + signatureEnabled = blockchain.snapshot(); // Usefull? + }); + it('should reject atempt to change sig auth via internal/external request', async () => { + let seqNo = await wallet.getSeqno(); + + const disableSigAuth: ExtendedAction = { + type: 'sig_auth', + allowed: false + }; + const enableSigAuth: ExtendedAction = { + type: 'sig_auth', + allowed: true + }; + + const mockExtensions = someExtensions(100, 'add_extension'); + const randIdx = getRandomInt(0, mockExtensions.length - 2); + + /* + const msgInt = WalletV5Test.requestMessage(true, walletId, curTime() + 100, seqNo, testWalletAction, keys.secretKey); + const msgExt = WalletV5Test.requestMessage(false, walletId, curTime() + 100, seqNo, testWalletAction, keys.secretKey); + */ + + //const res = await wallet.sendExtensionActions(testExtensionBc.getSender(), testWalletActions); + + // const fromInt = async (message) => await wallet.sendInternalSignedMessage(owner.getSender(), {value: toNano('1'), body: message}); + // const fromExt = async (message) => await wallet.sendExternalSignedMessage(message); + + for (let action of [disableSigAuth, enableSigAuth]) { + const actionSingle = [action]; + const actionFirst = [action, ...mockExtensions]; + const actionLast = [...mockExtensions, action]; + const actionRandom = [...mockExtensions]; + actionRandom[randIdx] = action; + + for (let actionSet of [actionSingle, actionFirst, actionLast, actionRandom]) { + const msgInt = WalletV5Test.requestMessage( + true, + walletId, + curTime() + 100, + seqNo, + { extended: actionSet }, + keys.secretKey + ); + const msgExt = WalletV5Test.requestMessage( + false, + walletId, + curTime() + 100, + seqNo, + { extended: actionSet }, + keys.secretKey + ); + // Meh, kinda much + for (let testMsg of [msgInt, msgExt]) { + if (testMsg == msgInt) { + const stateBefore = await getWalletData(); + const res = await wallet.sendInternalSignedMessage(owner.getSender(), { + value: toNano('1'), + body: msgInt + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + aborted: true, + exitCode: ErrorsV5.only_extension_can_change_signature_mode + }); + expect(await getWalletData()).toEqualCell(stateBefore); + } else { + const res = await wallet.sendExternalSignedMessage(msgExt); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + aborted: false, + exitCode: ErrorsV5.only_extension_can_change_signature_mode + }); + expect(await wallet.getSeqno()).toEqual(++seqNo); + } + } + } + } + }); + it('should reject sig auth if mode is already set', async () => { + let i = 0; + for (let testState of [signatureDisabled, signatureEnabled]) { + await loadFrom(testState); + let stateBefore = await getWalletData(); + let res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + extended: [ + { + type: 'sig_auth', + allowed: Boolean(i++) + } + ] + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: true, + exitCode: ErrorsV5.this_signature_mode_already_set + }); + expect(await getWalletData()).toEqualCell(stateBefore); + } + }); + it('should not accept signed external when signature auth is disabled and extension present', async () => { + await loadFrom(signatureDisabled); + const seqNo = await wallet.getSeqno(); + await shouldRejectWith( + wallet.sendMessagesExternal(walletId, curTime() + 100, seqNo, keys.secretKey, [ + mockMessage + ]), + ErrorsV5.signature_disabled + ); + expect(await wallet.getSeqno()).toEqual(seqNo); + }); + it('should not accept signed internal when signature auth is disabled and exension is present', async () => { + await loadFrom(signatureDisabled); + const seqNo = await wallet.getSeqno(); + await assertSendMessages( + ErrorsV5.signature_disabled, + walletId, + curTime() + 100, + seqNo, + [mockMessage], + keys.secretKey, + owner.getSender() + ); + }); + it('extension should be able to add another extension when sig auth is disabled', async () => { + await loadFrom(signatureDisabled); + const testExtAddr = randomAddress(); + const stateBefore = await getWalletData(); + const extBefore = await wallet.getExtensionsArray(); + expect(extBefore.length).toBe(1); + + const res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + extended: [{ type: 'add_extension', address: testExtAddr }] + }); + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false + }); + + const extAfter = await wallet.getExtensionsArray(); + expect(extAfter.length).toBe(2); + expect(extAfter.findIndex(a => a.equals(testExtAddr))).toBeGreaterThanOrEqual(0); + + multipleExtensions = blockchain.snapshot(); + }); + it('should not allow to remove last extension when sig auth is disabled', async () => { + await loadFrom(signatureDisabled); + + const stateBefore = await getWalletData(); + const extBefore = await wallet.getExtensionsArray(); + expect(extBefore.length).toBe(1); + + let res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + extended: [ + { + type: 'remove_extension', + address: extBefore[0] + } + ] + }); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: true, + exitCode: ErrorsV5.remove_last_extension_when_signature_disabled + }); + expect(await getWalletData()).toEqualCell(stateBefore); + }); + it('should remove extension if sig auth disabled and at lease one left', async () => { + await loadFrom(multipleExtensions); + + const extBefore = await wallet.getExtensionsArray(); + expect(extBefore.length).toBeGreaterThan(1); + + const pickExt = extBefore[getRandomInt(0, extBefore.length - 1)]; + + const res = await wallet.sendExtensionActions(testExtensionBc.getSender(), { + extended: [{ type: 'remove_extension', address: pickExt }] + }); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + from: testExtensionBc.address, + op: Opcodes.auth_extension, + aborted: false + }); + + const extAfter = await wallet.getExtensionsArray(); + expect(extAfter.length).toBe(extBefore.length - 1); + expect(extAfter.findIndex(a => a.equals(pickExt))).toBe(-1); + }); + it('should not allow to remove last extension and then disable sig auth', async () => { + await loadFrom(signatureEnabled); + const stateBefore = await getWalletData(); + const testWalletActions: WalletActions = { + extended: [ + { type: 'remove_extension', address: testExtensionBc.address }, + { type: 'sig_auth', allowed: false } + ] + }; + + const res = await wallet.sendExtensionActions( + testExtensionBc.getSender(), + testWalletActions + ); + + expect(res.transactions).toHaveTransaction({ + on: wallet.address, + aborted: true, + exitCode: ErrorsV5.disable_signature_when_extensions_is_empty + }); + + expect(await getWalletData()).toEqualCell(stateBefore); + }); + }); +}); From 6220bd65a5dc370f6daaebc33ad6262541fcf076 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 13:45:10 +0400 Subject: [PATCH 105/121] rebuild --- build/wallet_v5.compiled.json | 2 +- jest.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index f59c6d42..da9c2225 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c724102140100028c000114ff00f4a413f4bcf2c80b0102012004020102f203012020d70b1f82107369676ebaf2e08a7f700f0201480e0502012007060019be5f0f6a2684080a0eb90fa02c0201200b080201480a090011b262fb513435c280200017b325fb51341c75c875c2c7e002016e0d0c0019af1df6a2684010eb90eb858fc00019adce76a2684020eb90eb85ffc002f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810282b99130e07070e2100f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a1312110010935bdb31e1d74cd0007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de2009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed545bfe7180"} \ No newline at end of file +{"hash":"8122d374ce05988e668315db78ea4f97973d5b2147f2883c555420f79de71f64","hashBase64":"gSLTdM4FmI5mgxXbeOpPl5c9WyFH8og8VVQg953nH2Q=","hex":"b5ee9c724102140100028c000114ff00f4a413f4bcf2c80b01020120020d020148030402f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810282b99130e07070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e012020d70b1f82107369676ebaf2e08a7f700f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd0eae2b2de"} \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index 884cddc3..9075ecd7 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,6 +4,7 @@ const config: Config = { preset: 'ts-jest', testEnvironment: 'node', testPathIgnorePatterns: ['/node_modules/', '/dist/'], + testTimeout: 15000 }; export default config; From 2f962c360c2f7fc7eb4a53d691c64280a1cc7dfc Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 13:54:15 +0400 Subject: [PATCH 106/121] optimize bounced check --- contracts/wallet_v5.fc | 9 +- tests/WalletW5.spec.ts | 190 ++++++++++++++++++++--------------------- 2 files changed, 98 insertions(+), 101 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 91cf366a..f5ede4d5 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -226,16 +226,13 @@ cell verify_c5_actions(cell c5, int is_external) inline { return (); } - slice in_msg_full_slice = in_msg_full.begin_parse(); - slice message_flags_slice = in_msg_full_slice~load_bits(size::message_flags); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool - ;; skip bounced messages - if bounced flag (last bit) is set amount of trailing ones will be non-zero, else it will be zero. - if (count_trailing_ones(message_flags_slice) > 0) { - return (); - } + ;; bounded messages has 0xffffff prefix and skipped by op check if (op == prefix::extension_action) { in_msg_body~skip_bits(size::message_operation_prefix); + slice in_msg_full_slice = in_msg_full.begin_parse(); + in_msg_full_slice~skip_bits(size::message_flags); ;; Authenticate extension by its address. (int sender_address_wc, int sender_address_hash) = parse_std_addr(in_msg_full_slice~load_msg_addr()); (int my_address_wc, _) = parse_std_addr(my_address()); diff --git a/tests/WalletW5.spec.ts b/tests/WalletW5.spec.ts index 4307a981..263fd85e 100644 --- a/tests/WalletW5.spec.ts +++ b/tests/WalletW5.spec.ts @@ -1905,101 +1905,101 @@ describe('Wallet v5 external tests', () => { expect(await wallet.getSeqno()).toEqual(Number(args.seqno) + 1); }); }); - describe('Bounce', () => { - it('should ignore bounced mesages', async () => { - await loadFrom(hasExtension); - const seqNo = await wallet.getSeqno(); - const mockActions: WalletActions = { wallet: [message2action(mockMessage)] }; - - // Note that in reality bounce gets prefixed by 0xFFFFFFFF - // With current code, that would mean message would be ignored - // due to op check - // However we still could test as if TVM doesn't add prefix to bounce somehow - const intReq = WalletV5Test.requestMessage( - true, - walletId, - curTime() + 1000, - seqNo, - mockActions, - keys.secretKey - ); - const extReq = WalletV5Test.extensionMessage(mockActions); - // ihr_disable and bounce flags combinations - let flagFuzz = [ - [false, false], - [true, false], - [false, true], - [true, true] - ]; - - const stateBefore = await getWalletData(); - - for (let flags of flagFuzz) { - let res = await blockchain.sendMessage( - internal({ - from: owner.address, - to: wallet.address, - body: intReq, - value: toNano('1'), - bounced: true, - ihrDisabled: flags[0], - bounce: flags[1] - }) - ); - expect(res.transactions).toHaveTransaction({ - on: wallet.address, - op: Opcodes.auth_signed_internal, - aborted: false, - outMessagesCount: 0 - }); - expect(await getWalletData()).toEqualCell(stateBefore); - - res = await blockchain.sendMessage( - internal({ - from: testExtensionBc.address, - to: wallet.address, - body: extReq, - value: toNano('1'), - bounced: true, - ihrDisabled: flags[0], - bounce: flags[1] - }) - ); - expect(res.transactions).toHaveTransaction({ - on: wallet.address, - op: Opcodes.auth_extension, - aborted: false, - outMessagesCount: 0 - }); - expect(await getWalletData()).toEqualCell(stateBefore); - } - - // Let's proove that bounce flag is the reason - const resInt = await blockchain.sendMessage( - internal({ - from: owner.address, - to: wallet.address, - body: intReq, - value: toNano('1'), - ihrDisabled: true, - bounce: true - }) - ); - assertMockMessage(resInt.transactions); - - const resExt = await blockchain.sendMessage( - internal({ - from: testExtensionBc.address, - to: wallet.address, - body: extReq, - value: toNano('1'), - ihrDisabled: false, - bounce: false - }) - ); - assertMockMessage(resExt.transactions); - }); - }); + // describe('Bounce', () => { + // it('should ignore bounced mesages', async () => { + // await loadFrom(hasExtension); + // const seqNo = await wallet.getSeqno(); + // const mockActions: WalletActions = { wallet: [message2action(mockMessage)] }; + // + // // Note that in reality bounce gets prefixed by 0xFFFFFFFF + // // With current code, that would mean message would be ignored + // // due to op check + // // However we still could test as if TVM doesn't add prefix to bounce somehow + // const intReq = WalletV5Test.requestMessage( + // true, + // walletId, + // curTime() + 1000, + // seqNo, + // mockActions, + // keys.secretKey + // ); + // const extReq = WalletV5Test.extensionMessage(mockActions); + // // ihr_disable and bounce flags combinations + // let flagFuzz = [ + // [false, false], + // [true, false], + // [false, true], + // [true, true] + // ]; + // + // const stateBefore = await getWalletData(); + // + // for (let flags of flagFuzz) { + // let res = await blockchain.sendMessage( + // internal({ + // from: owner.address, + // to: wallet.address, + // body: intReq, + // value: toNano('1'), + // bounced: true, + // ihrDisabled: flags[0], + // bounce: flags[1] + // }) + // ); + // expect(res.transactions).toHaveTransaction({ + // on: wallet.address, + // op: Opcodes.auth_signed_internal, + // aborted: false, + // outMessagesCount: 0 + // }); + // expect(await getWalletData()).toEqualCell(stateBefore); + // + // res = await blockchain.sendMessage( + // internal({ + // from: testExtensionBc.address, + // to: wallet.address, + // body: extReq, + // value: toNano('1'), + // bounced: true, + // ihrDisabled: flags[0], + // bounce: flags[1] + // }) + // ); + // expect(res.transactions).toHaveTransaction({ + // on: wallet.address, + // op: Opcodes.auth_extension, + // aborted: false, + // outMessagesCount: 0 + // }); + // expect(await getWalletData()).toEqualCell(stateBefore); + // } + // + // // Let's proove that bounce flag is the reason + // const resInt = await blockchain.sendMessage( + // internal({ + // from: owner.address, + // to: wallet.address, + // body: intReq, + // value: toNano('1'), + // ihrDisabled: true, + // bounce: true + // }) + // ); + // assertMockMessage(resInt.transactions); + // + // const resExt = await blockchain.sendMessage( + // internal({ + // from: testExtensionBc.address, + // to: wallet.address, + // body: extReq, + // value: toNano('1'), + // ihrDisabled: false, + // bounce: false + // }) + // ); + // assertMockMessage(resExt.transactions); + // }); + // }); }); describe('Extension', () => { let actionFuzz: WalletActions[]; From b66b1031e0977872d245b2ae697e9f6c4efb8cd4 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 13:56:53 +0400 Subject: [PATCH 107/121] more accurate check --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index da9c2225..d76e9c07 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hash":"8122d374ce05988e668315db78ea4f97973d5b2147f2883c555420f79de71f64","hashBase64":"gSLTdM4FmI5mgxXbeOpPl5c9WyFH8og8VVQg953nH2Q=","hex":"b5ee9c724102140100028c000114ff00f4a413f4bcf2c80b01020120020d020148030402f2d020d749c120915b8f6e20d70b1f2082106578746ebd21821073696e74bdb0925f03e002d0d60301c713c200925f03e00282106578746eba8eb08020d72101fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810282b99130e07070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e012020d70b1f82107369676ebaf2e08a7f700f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd0eae2b2de"} \ No newline at end of file +{"hash":"8b59a99853b0a3c983a6a4a3a695805892813736027e7a765bf6ce5b17b0b0d7","hashBase64":"i1mpmFOwo8mDpqSjppWAWJKBNzYCfnp2W/bOWxewsNc=","hex":"b5ee9c7241021401000282000114ff00f4a413f4bcf2c80b01020120020d020148030402ded020d749c120915b8f6420d70b1f2082106578746ebd21821073696e74bdb0925f03e082106578746eba8eb48020d72101d074d721fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810280b99130e07070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e012020d70b1f82107369676ebaf2e08a7f700f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd09084a9e7"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index f5ede4d5..650f3eff 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -259,8 +259,8 @@ cell verify_c5_actions(cell c5, int is_external) inline { } - ;; Additional check to make sure that there are enough bits for reading (+2 for maybe c5 and maybe other actions bits) - if (in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + 2 + size::signature) { + ;; Additional check to make sure that there are enough bits for reading before signature check + if (in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + size::signature) { return (); } process_signed_request(in_msg_body, false, false); From 62bfd99f1cd2a709053641a470d44ab1157bb620 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 14:03:03 +0400 Subject: [PATCH 108/121] delete unused code --- contracts/wallet_v5.fc | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 650f3eff..28cf3071 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -40,8 +40,6 @@ const int prefix::extension_action = 0x6578746E; ;;; returns the number of trailing zeroes in slice s. int count_trailing_zeroes(slice s) asm "SDCNTTRAIL0"; -;;; returns the number of trailing ones in slice s. -int count_trailing_ones(slice s) asm "SDCNTTRAIL1"; ;;; returns the last 0 ≤ l ≤ 1023 bits of s. slice get_last_bits(slice s, int l) asm "SDCUTLAST"; From 0bfb383e6b327e54f11bd19d53220b640b583fec Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 14:38:49 +0400 Subject: [PATCH 109/121] update tlb --- types.tlb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/types.tlb b/types.tlb index 6390684b..5f15c88d 100644 --- a/types.tlb +++ b/types.tlb @@ -12,10 +12,10 @@ action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; signed_request$_ // 32 (opcode from outer) - wallet_id: WalletID // 80 + wallet_id: # // 32 valid_until: # // 32 msg_seqno: # // 32 - inner: InnerRequest // 1 .. (1 + 32 + 256) + ^Cell + inner: InnerRequest // signature: bits512 // 512 = SignedRequest; // Total: 688 .. 976 + ^Cell @@ -23,8 +23,7 @@ internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; internal_extension#6578746e inner:InnerRequest = InternalMsgBody; external_signed#7369676e signed:SignedRequest = ExternalMsgBody; -actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; +actions$_ out_actions:(Maybe OutList) has_other_actions:(## 1) {m:#} {n:#} other_actions:(ActionList n m) = InnerRequest; // Contract state -wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; -contract_state$_ signature_auth_disabled:(## 1) seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; +contract_state$_ is_signature_allowed:(## 1) seqno:# wallet_id:(## 32) public_key:(## 256) extensions_dict:(HashmapE 256 int1) = ContractState; From 2c1b96403b47b2f4e0ce635616500ebdb92a0e56 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 15:24:50 +0400 Subject: [PATCH 110/121] update tlb --- types.tlb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/types.tlb b/types.tlb index 5f15c88d..c70ad6f5 100644 --- a/types.tlb +++ b/types.tlb @@ -4,12 +4,12 @@ out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; // Extended actions in W5: -action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0; -action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); +action_list_basic$_ {n:#} actions:^(OutList n) = ActionList n 0; +action_list_extended$_ {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); -action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; -action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; -action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; +action_add_ext#02 addr:MsgAddressInt = ExtendedAction; +action_delete_ext#03 addr:MsgAddressInt = ExtendedAction; +action_set_signature_auth_allowed#04 allowed:(## 1) = ExtendedAction; signed_request$_ // 32 (opcode from outer) wallet_id: # // 32 From e02e43c65ecd35a5421b503449b167aab51a0fe6 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 15:31:49 +0400 Subject: [PATCH 111/121] delete v4 --- build/library-deployer.compiled.json | 2 +- contracts/wallet_v4.fc | 200 --------------------------- tests/wallet-v5-external.spec.ts | 1 - wrappers/library-deployer.compile.ts | 2 +- wrappers/wallet-v4.ts | 33 ----- wrappers/wallet_v4.compile.ts | 6 - 6 files changed, 2 insertions(+), 242 deletions(-) delete mode 100644 contracts/wallet_v4.fc delete mode 100644 wrappers/wallet-v4.ts delete mode 100644 wrappers/wallet_v4.compile.ts diff --git a/build/library-deployer.compiled.json b/build/library-deployer.compiled.json index 3acdc095..c68d6dbe 100644 --- a/build/library-deployer.compiled.json +++ b/build/library-deployer.compiled.json @@ -1 +1 @@ -{"hex":"b5ee9c72410106010030000114ff00f4a413f4bcf2c80b0102012003020006f2f0010202d1050400193b511cbec1b232483ec13b552000053c00601cfc59c2"} \ No newline at end of file +{"hash":"7b886902938fda7f8ee72fe31ee250744bf82489579c0b8e502105a49cb72e2a","hashBase64":"e4hpApOP2n+O5y/jHuJQdEv4JIlXnAuOUCEFpJy3Lio=","hex":"b5ee9c72410106010030000114ff00f4a413f4bcf2c80b0102012002050202d1030400053c006000193b511cbec1b232483ec13b55200006f2f0014136d496"} \ No newline at end of file diff --git a/contracts/wallet_v4.fc b/contracts/wallet_v4.fc deleted file mode 100644 index 85240089..00000000 --- a/contracts/wallet_v4.fc +++ /dev/null @@ -1,200 +0,0 @@ -#pragma version =0.4.4; -#include "imports/stdlib.fc"; - -;; Wallet smart contract with plugins - -(slice, int) dict_get?(cell dict, int key_len, slice index) asm(index dict key_len) "DICTGET" "NULLSWAPIFNOT"; -(cell, int) dict_add_builder?(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTADDB"; -(cell, int) dict_delete?(cell dict, int key_len, slice index) asm(index dict key_len) "DICTDEL"; - -() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure { - var cs = in_msg_cell.begin_parse(); - var flags = cs~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool - if (flags & 1) { - ;; ignore all bounced messages - return (); - } - if (in_msg.slice_bits() < 32) { - ;; ignore simple transfers - return (); - } - int op = in_msg~load_uint(32); - if (op != 0x706c7567) & (op != 0x64737472) { ;; "plug" & "dstr" - ;; ignore all messages not related to plugins - return (); - } - slice s_addr = cs~load_msg_addr(); - (int wc, int addr_hash) = parse_std_addr(s_addr); - slice wc_n_address = begin_cell().store_int(wc, 8).store_uint(addr_hash, 256).end_cell().begin_parse(); - var ds = get_data().begin_parse().skip_bits(32 + 32 + 256); - var plugins = ds~load_dict(); - var (_, success?) = plugins.dict_get?(8 + 256, wc_n_address); - if ~(success?) { - ;; it may be a transfer - return (); - } - int query_id = in_msg~load_uint(64); - var msg = begin_cell(); - if (op == 0x706c7567) { ;; request funds - - (int r_toncoins, cell r_extra) = (in_msg~load_grams(), in_msg~load_dict()); - - [int my_balance, _] = get_balance(); - throw_unless(80, my_balance - msg_value >= r_toncoins); - - msg = msg.store_uint(0x18, 6) - .store_slice(s_addr) - .store_grams(r_toncoins) - .store_dict(r_extra) - .store_uint(0, 4 + 4 + 64 + 32 + 1 + 1) - .store_uint(0x706c7567 | 0x80000000, 32) - .store_uint(query_id, 64); - send_raw_message(msg.end_cell(), 64); - - } - - if (op == 0x64737472) { ;; remove plugin by its request - - plugins~dict_delete?(8 + 256, wc_n_address); - var ds = get_data().begin_parse().first_bits(32 + 32 + 256); - set_data(begin_cell().store_slice(ds).store_dict(plugins).end_cell()); - ;; return coins only if bounce expected - if (flags & 2) { - msg = msg.store_uint(0x18, 6) - .store_slice(s_addr) - .store_grams(0) - .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .store_uint(0x64737472 | 0x80000000, 32) - .store_uint(query_id, 64); - send_raw_message(msg.end_cell(), 64); - } - } -} - -() recv_external(slice in_msg) impure { - var signature = in_msg~load_bits(512); - var cs = in_msg; - var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32)); - throw_if(36, valid_until <= now()); - var ds = get_data().begin_parse(); - var (stored_seqno, stored_subwallet, public_key, plugins) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256), ds~load_dict()); - ds.end_parse(); - throw_unless(33, msg_seqno == stored_seqno); - throw_unless(34, subwallet_id == stored_subwallet); - throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); - accept_message(); - set_data(begin_cell() - .store_uint(stored_seqno + 1, 32) - .store_uint(stored_subwallet, 32) - .store_uint(public_key, 256) - .store_dict(plugins) - .end_cell()); - commit(); - cs~touch(); - int op = cs~load_uint(8); - - if (op == 0) { ;; simple send - while (cs.slice_refs()) { - var mode = cs~load_uint(8); - send_raw_message(cs~load_ref(), mode); - } - return (); ;; have already saved the storage - } - - if (op == 1) { ;; deploy and install plugin - int plugin_workchain = cs~load_int(8); - int plugin_balance = cs~load_grams(); - (cell state_init, cell body) = (cs~load_ref(), cs~load_ref()); - int plugin_address = cell_hash(state_init); - slice wc_n_address = begin_cell().store_int(plugin_workchain, 8).store_uint(plugin_address, 256).end_cell().begin_parse(); - var msg = begin_cell() - .store_uint(0x18, 6) - .store_uint(4, 3).store_slice(wc_n_address) - .store_grams(plugin_balance) - .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) - .store_ref(state_init) - .store_ref(body); - send_raw_message(msg.end_cell(), 3); - (plugins, int success?) = plugins.dict_add_builder?(8 + 256, wc_n_address, begin_cell()); - throw_unless(39, success?); - } - - if (op == 2) { ;; install plugin - slice wc_n_address = cs~load_bits(8 + 256); - int amount = cs~load_grams(); - int query_id = cs~load_uint(64); - - (plugins, int success?) = plugins.dict_add_builder?(8 + 256, wc_n_address, begin_cell()); - throw_unless(39, success?); - - builder msg = begin_cell() - .store_uint(0x18, 6) - .store_uint(4, 3).store_slice(wc_n_address) - .store_grams(amount) - .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .store_uint(0x6e6f7465, 32) ;; op - .store_uint(query_id, 64); - send_raw_message(msg.end_cell(), 3); - } - - if (op == 3) { ;; remove plugin - slice wc_n_address = cs~load_bits(8 + 256); - int amount = cs~load_grams(); - int query_id = cs~load_uint(64); - - (plugins, int success?) = plugins.dict_delete?(8 + 256, wc_n_address); - throw_unless(39, success?); - - builder msg = begin_cell() - .store_uint(0x18, 6) - .store_uint(4, 3).store_slice(wc_n_address) - .store_grams(amount) - .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .store_uint(0x64737472, 32) ;; op - .store_uint(query_id, 64); - send_raw_message(msg.end_cell(), 3); - } - - set_data(begin_cell() - .store_uint(stored_seqno + 1, 32) - .store_uint(stored_subwallet, 32) - .store_uint(public_key, 256) - .store_dict(plugins) - .end_cell()); -} - -;; Get methods - -int seqno() method_id { - return get_data().begin_parse().preload_uint(32); -} - -int get_subwallet_id() method_id { - return get_data().begin_parse().skip_bits(32).preload_uint(32); -} - -int get_public_key() method_id { - var cs = get_data().begin_parse().skip_bits(64); - return cs.preload_uint(256); -} - -int is_plugin_installed(int wc, int addr_hash) method_id { - var ds = get_data().begin_parse().skip_bits(32 + 32 + 256); - var plugins = ds~load_dict(); - var (_, success?) = plugins.dict_get?(8 + 256, begin_cell().store_int(wc, 8).store_uint(addr_hash, 256).end_cell().begin_parse()); - return success?; -} - -tuple get_plugin_list() method_id { - var list = null(); - var ds = get_data().begin_parse().skip_bits(32 + 32 + 256); - var plugins = ds~load_dict(); - do { - var (wc_n_address, _, f) = plugins~dict::delete_get_min(8 + 256); - if (f) { - (int wc, int addr) = (wc_n_address~load_int(8), wc_n_address~load_uint(256)); - list = cons(pair(wc, addr), list); - } - } until (~ f); - return list; -} diff --git a/tests/wallet-v5-external.spec.ts b/tests/wallet-v5-external.spec.ts index 93161235..8f4a9150 100644 --- a/tests/wallet-v5-external.spec.ts +++ b/tests/wallet-v5-external.spec.ts @@ -23,7 +23,6 @@ import { TransactionComputeVm } from 'ton-core/src/types/TransactionComputePhase import { buildBlockchainLibraries, LibraryDeployer } from '../wrappers/library-deployer'; import { default as config } from './config'; import { ActionSetCode, ActionSetData } from './test-only-actions'; -import { WalletV4 } from '../wrappers/wallet-v4'; const WALLET_ID = new WalletId({ networkGlobalId: -239, workChain: -1, subwalletNumber: 0 }); diff --git a/wrappers/library-deployer.compile.ts b/wrappers/library-deployer.compile.ts index 77534c83..5963a5ad 100644 --- a/wrappers/library-deployer.compile.ts +++ b/wrappers/library-deployer.compile.ts @@ -1,4 +1,4 @@ -import { CompilerConfig } from '@ton-community/blueprint'; +import { CompilerConfig } from '@ton/blueprint'; export const compile: CompilerConfig = { lang: 'func', diff --git a/wrappers/wallet-v4.ts b/wrappers/wallet-v4.ts deleted file mode 100644 index 113a677a..00000000 --- a/wrappers/wallet-v4.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Address, Cell, Contract, ContractProvider } from 'ton-core'; - -export class WalletV4 implements Contract { - constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {} - - static createFromAddress(address: Address) { - return new WalletV4(address); - } - - async sendExternalSignedMessage(provider: ContractProvider, body: Cell) { - await provider.external(body); - } - - async getPublicKey(provider: ContractProvider) { - const result = await provider.get('get_public_key', []); - return result.stack.readBigNumber(); - } - - async getSeqno(provider: ContractProvider) { - const result = await provider.get('seqno', []); - return result.stack.readNumber(); - } - - async getSubWalletID(provider: ContractProvider) { - const result = await provider.get('get_subwallet_id', []); - return result.stack.readNumber(); - } - - async getExtensions(provider: ContractProvider) { - const result = await provider.get('get_plugin_list', []); - return result.stack.readCellOpt(); - } -} diff --git a/wrappers/wallet_v4.compile.ts b/wrappers/wallet_v4.compile.ts deleted file mode 100644 index 7bcb1e6a..00000000 --- a/wrappers/wallet_v4.compile.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CompilerConfig } from '@ton-community/blueprint'; - -export const compile: CompilerConfig = { - lang: 'func', - targets: ['contracts/wallet_v4.fc'] -}; From 26ab2292839d3a0624ee1c98bf80fd3a570dd089 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 15:35:34 +0400 Subject: [PATCH 112/121] update Specification.md --- Specification.md | 72 ++---------------------------------------------- 1 file changed, 2 insertions(+), 70 deletions(-) diff --git a/Specification.md b/Specification.md index fdfef5d3..8533d90e 100644 --- a/Specification.md +++ b/Specification.md @@ -42,7 +42,7 @@ Authentication: Operations: * standard "send message" action (up to 255 messages at once), -* enable/disable signature authentication, +* enable/disable signature authentication (can be invoked only by extension), * install/remove extension. Signed messages can be delivered both by external and internal messages. @@ -111,77 +111,9 @@ When wallet contract is being deployed, original code hash is being used as the Library contract itself data and code are empty cells. That leads to the inability to change the library code, delete the contract, or withdraw funds from it. Therefore, any Wallet V5 user can top up the library contract balance if they are afraid that the library code of their wallet will be frozen. -## Wallet ID - -Wallet ID disambiguates requests signed with the same public key to different wallet versions (V3/V4/V5) or wallets deployed on different chains. - -For Wallet V5 we suggest using the following wallet ID: - -``` -wallet_id$_ global_id:int32 wc:int8 version:(## 8) subwallet_number:(## 32) = WalletID; -``` - -- `global_id` is a TON chain identifier. TON Mainnet `global_id = -239` and TON Testnet `global_id = -3`. -- `wc` is a Workchain. -1 for Masterchain and 0 for Basechain. -- `version`: current version of wallet v5 is `0`. -- `subwallet_number` can be used to get multiple wallet contracts bound to the single keypair. - -## Packed address - -To make authorize extensions efficiently we compress 260-bit address (workchain + sha256 of stateinit) into a 256-bit integer: - -``` -int addr = addr_hash ^ (wc + 1) -``` - -Previously deployed wallet v4 was packing the address into a cell which costs ≈500 gas, while access to dictionary costs approximately `120*lg2(N)` in gas, that is serialization occupies more than half of the access cost for wallets with up to 16 extensions. This design makes packing cost around 50 gas and allows cutting the authentication cost 2-3x for reasonably sized wallets. - -As of 2023 TON network consists of two workchains: -1 (master) and 0 (base). This means that the proposed address packing reduces second-preimage resistance of sha256 by 1 bit which we consider negligible. Even if the network is expanded with 254 more workchains in a distant future, our scheme would reduce security of extension authentication by only 8 bits down to 248 bits. Note that birthday attack is irrelevant in our setting as the user agent is not installing random extensions, although the security margin is plenty anyway (124 bits). - - - ## TL-B definitions -Action types: - -```tl-b -// Standard actions from block.tlb: -out_list_empty$_ = OutList 0; -out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); -action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; - -// Extended actions in W5: -action_list_basic$0 {n:#} actions:^(OutList n) = ActionList n 0; -action_list_extended$1 {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); - -action_add_ext#1c40db9f addr:MsgAddressInt = ExtendedAction; -action_delete_ext#5eaef4a4 addr:MsgAddressInt = ExtendedAction; -action_set_signature_auth_allowed#20cbb95a allowed:(## 1) = ExtendedAction; -``` - -Authentication modes: - -```tl-b -signed_request$_ // 32 (opcode from outer) - wallet_id: WalletID // 80 - valid_until: # // 32 - msg_seqno: # // 32 - inner: InnerRequest // 1 .. (1 + 32 + 256) + ^Cell - signature: bits512 // 512 -= SignedRequest; // Total: 688 .. 976 + ^Cell - -internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; -internal_extension#6578746e inner:InnerRequest = InternalMsgBody; -external_signed#7369676e signed:SignedRequest = ExternalMsgBody; - -actions$_ {m:#} {n:#} actions:(ActionList n m) = InnerRequest; -``` - -Contract state: -```tl-b -wallet_id$_ global_id:# wc:int8 version:(## 8) subwallet_number:# = WalletID; -contract_state$_ signature_auth_disabled:(## 1) seqno:# wallet_id:WalletID public_key:(## 256) extensions_dict:(HashmapE 256 int8) = ContractState; -``` +See `types.tlb`. ## Source code From 230e48bf6ca4ca286a4293354c0ab699553c7891 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 15:39:24 +0400 Subject: [PATCH 113/121] simplify --- build/wallet_v5.compiled.json | 2 +- contracts/wallet_v5.fc | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/wallet_v5.compiled.json b/build/wallet_v5.compiled.json index d76e9c07..d276e8b3 100644 --- a/build/wallet_v5.compiled.json +++ b/build/wallet_v5.compiled.json @@ -1 +1 @@ -{"hash":"8b59a99853b0a3c983a6a4a3a695805892813736027e7a765bf6ce5b17b0b0d7","hashBase64":"i1mpmFOwo8mDpqSjppWAWJKBNzYCfnp2W/bOWxewsNc=","hex":"b5ee9c7241021401000282000114ff00f4a413f4bcf2c80b01020120020d020148030402ded020d749c120915b8f6420d70b1f2082106578746ebd21821073696e74bdb0925f03e082106578746eba8eb48020d72101d074d721fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810280b99130e07070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e012020d70b1f82107369676ebaf2e08a7f700f01e48eefeda2edfb228308d722038308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ddf9109a29945f0bdb31e1f2c087df02b35007b0f2d0845125baf2e0855037baf2e086f823bbf2d0882392f800de01a47fc8ca00cb1f01cf16c9ed542192f80fdedb3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd09084a9e7"} \ No newline at end of file +{"hash":"20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f","hashBase64":"IINLe3KxEhR+Gy+0V7hOdNGjDwT3N9T2KmaOlVLSty8=","hex":"b5ee9c7241021401000281000114ff00f4a413f4bcf2c80b01020120020d020148030402dcd020d749c120915b8f6320d70b1f2082106578746ebd21821073696e74bdb0925f03e082106578746eba8eb48020d72101d074d721fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810280b99130e070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e011e20d70b1f82107369676ebaf2e08a7f0f01e68ef0eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109a28945f0adb31e1f2c087df02b35007b0f2d0845125baf2e0855036baf2e086f823bbf2d0882292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde70db3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd0b4d6c35e"} \ No newline at end of file diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 28cf3071..0835c9a0 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -161,7 +161,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; ------------------------------------------------------------------------------------------------ -() process_signed_request(slice in_msg_body, int is_external, int is_extension) impure inline { +() process_signed_request(slice in_msg_body, int is_external) impure inline { slice signature = in_msg_body.get_last_bits(size::signature); slice signed_slice = in_msg_body.remove_last_bits(size::signature); @@ -205,12 +205,12 @@ cell verify_c5_actions(cell c5, int is_external) inline { commit(); } - process_actions(cs, is_external, is_extension); + process_actions(cs, is_external, false); } () recv_external(slice in_msg_body) impure inline { throw_unless(error::invalid_message_operation, in_msg_body.preload_uint(size::message_operation_prefix) == prefix::signed_external); - process_signed_request(in_msg_body, true, false); + process_signed_request(in_msg_body, true); } ;; ------------------------------------------------------------------------------------------------ @@ -261,7 +261,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { if (in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + size::signature) { return (); } - process_signed_request(in_msg_body, false, false); + process_signed_request(in_msg_body, false); } ;; ------------------------------------------------------------------------------------------------ From 5e711b2b67c170f64b4c02a2c24fd7b3742a3b33 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 15:52:06 +0400 Subject: [PATCH 114/121] strict func version --- package-lock.json | 6 +++--- package.json | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e27a735d..de638f91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1321,9 +1321,9 @@ } }, "node_modules/@ton-community/func-js-bin": { - "version": "0.4.4-newops.1", - "resolved": "https://registry.npmjs.org/@ton-community/func-js-bin/-/func-js-bin-0.4.4-newops.1.tgz", - "integrity": "sha512-TV4t6XhmItq4t+wv4pV30yEwb+YvdmsKo4Ig4B0zp4PLdI0r9iZHz4y5bBHcXmDQDRqulXzK6kTgfHvs2CIsaQ==", + "version": "0.4.5-tvmbeta.3", + "resolved": "https://registry.npmjs.org/@ton-community/func-js-bin/-/func-js-bin-0.4.5-tvmbeta.3.tgz", + "integrity": "sha512-bPeXO1dhjvJuMtiP20eePF1UI536ygot9FIBj8lPwWKBLEphBkz0uPp1vhNg2vvPZACGw6vzw840rAGpCtnFRg==", "dev": true }, "node_modules/@ton/blueprint": { diff --git a/package.json b/package.json index 3488bceb..fee70664 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,9 @@ "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "typescript": "^5.5.2" + }, + "overrides": { + "@ton-community/func-js-bin": "0.4.5-tvmbeta.3", + "@ton-community/func-js": "0.7.0" } } From 59621033f8dc57f84835827fd59d53fb1a423fd8 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Wed, 3 Jul 2024 17:08:23 +0400 Subject: [PATCH 115/121] update tlb --- types.tlb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types.tlb b/types.tlb index c70ad6f5..45eebd0b 100644 --- a/types.tlb +++ b/types.tlb @@ -20,7 +20,7 @@ signed_request$_ // 32 (opcode from outer) = SignedRequest; // Total: 688 .. 976 + ^Cell internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; -internal_extension#6578746e inner:InnerRequest = InternalMsgBody; +internal_extension#6578746e query_id:(## 64) inner:InnerRequest = InternalMsgBody; external_signed#7369676e signed:SignedRequest = ExternalMsgBody; actions$_ out_actions:(Maybe OutList) has_other_actions:(## 1) {m:#} {n:#} other_actions:(ActionList n m) = InnerRequest; From d5f0a0da21cdf22d648954b2674a03400bc7f5cd Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Thu, 4 Jul 2024 13:12:30 +0400 Subject: [PATCH 116/121] add additional comments to the smart contract code --- contracts/wallet_v5.fc | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 0835c9a0..65e213ab 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -70,7 +70,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { ;; only `action_send_msg` is allowed; `action_set_code`, `action_reserve_currency` or `action_change_library` are not. cs = cs.enforce_and_remove_action_send_msg_prefix(); - throw_unless(error::invalid_c5, cs.slice_bits() == 8); ;; send_mode + throw_unless(error::invalid_c5, cs.slice_bits() == 8); ;; send_mode uint8 throw_unless(error::invalid_c5, cs.slice_refs() == 2); ;; next-action-ref and MessageRelaxed ref ;; enforce that send_mode has +2 bit (ignore errors) set for external message. @@ -100,7 +100,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return (); } - ;; Loop extended actions until we reach standard actions + ;; Loop extended actions while (true) { int is_add_extension = cs~check_and_remove_add_extension_prefix(); int is_remove_extension = is_add_extension ? 0 : cs~check_and_remove_remove_extension_prefix(); @@ -109,7 +109,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { (int address_wc, int address_hash) = parse_std_addr(cs~load_msg_addr()); (int my_address_wc, _) = parse_std_addr(my_address()); - throw_unless(error::extension_wrong_workchain, my_address_wc == address_wc); + throw_unless(error::extension_wrong_workchain, my_address_wc == address_wc); ;; the extension must be in the same workchain as the wallet. slice data_slice = get_data().begin_parse(); slice data_slice_before_extensions = data_slice~load_bits(size::bool + size::seqno + size::wallet_id + size::public_key); @@ -131,7 +131,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { .store_dict(extensions) .end_cell()); - } elseif (cs~check_and_remove_set_signature_allowed_prefix()) { + } elseif (cs~check_and_remove_set_signature_allowed_prefix()) { ;; allow/disallow signature throw_unless(error::only_extension_can_change_signature_mode, is_extension); int allow_signature = cs~load_int(1); slice data_slice = get_data().begin_parse(); @@ -184,6 +184,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return (); } } + ;; In case the wallet application has initially, by mistake, deployed a contract with the wrong bit (signature is forbidden and extensions are empty) - we allow such a contract to work. throw_if(error::signature_disabled, (~ is_signature_allowed) & is_extensions_not_empty); throw_unless(error::invalid_seqno, seqno == stored_seqno); throw_unless(error::invalid_wallet_id, wallet_id == stored_wallet_id); @@ -193,7 +194,6 @@ cell verify_c5_actions(cell c5, int is_external) inline { accept_message(); } - ;; Store and commit the seqno increment to prevent replays even if the subsequent requests fail. stored_seqno = stored_seqno + 1; set_data(begin_cell() .store_int(true, size::bool) ;; is_signature_allowed @@ -202,6 +202,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { .end_cell()); if (is_external) { + ;; For external messages we commit seqno changes, so that even if an exception occurs further on, the reply-protection will still work. commit(); } @@ -217,14 +218,14 @@ cell verify_c5_actions(cell c5, int is_external) inline { () recv_internal(cell in_msg_full, slice in_msg_body) impure inline { if (in_msg_body.slice_bits() < size::message_operation_prefix) { - return (); + return (); ;; just receive Toncoins } int op = in_msg_body.preload_uint(size::message_operation_prefix); if ((op != prefix::extension_action) & (op != prefix::signed_internal)) { - return (); + return (); ;; just receive Toncoins } - ;; bounded messages has 0xffffff prefix and skipped by op check + ;; bounced messages has 0xffffff prefix and skipped by op check if (op == prefix::extension_action) { in_msg_body~skip_bits(size::message_operation_prefix); @@ -257,7 +258,9 @@ cell verify_c5_actions(cell c5, int is_external) inline { } - ;; Additional check to make sure that there are enough bits for reading before signature check + ;; Before signature checking we handle errors silently (return), after signature checking we throw exceptions. + + ;; Check to make sure that there are enough bits for reading before signature check if (in_msg_body.slice_bits() < size::message_operation_prefix + size::wallet_id + size::valid_until + size::seqno + size::signature) { return (); } @@ -290,7 +293,7 @@ int get_public_key() method_id { .preload_uint(size::public_key); } -;; Returns raw dictionary (or null if empty) where keys are address hashes. Workchains of extensions are same with wallet smart contract workchain +;; Returns raw dictionary (or null if empty) where keys are address hashes. Workchains of extensions are same with wallet smart contract workchain. cell get_extensions() method_id { return get_data().begin_parse() .skip_bits(size::bool + size::seqno + size::wallet_id + size::public_key) From badcbfb5a65ee5c97c7c5161c368b296cb39600a Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Thu, 4 Jul 2024 13:16:43 +0400 Subject: [PATCH 117/121] delete docs about optimization contest --- Contest.md | 26 -------------------------- contest.png | Bin 445101 -> 0 bytes 2 files changed, 26 deletions(-) delete mode 100644 Contest.md delete mode 100644 contest.png diff --git a/Contest.md b/Contest.md deleted file mode 100644 index c3be22f3..00000000 --- a/Contest.md +++ /dev/null @@ -1,26 +0,0 @@ -## Contest note! - -
-Contest logo -
- -**Because of the extreme amount of optimizations, developer's discretion is advised!** *Evil laugh* - -The build system is the same as in the original Wallet V5, **no security features have been sacrificed** -for performance improvements, that is - there are **practically no tradeoffs or compromises**. - -Message and storage layouts were **not changed**, although some rearragement might squeeze a little more gas, -but that may break existing optimizations due to stack reordering. - -Also, **tests were improved** - a **Global Gas Counter** mechanism was added that accounts for gas in all transactions -of all test suites and cases (except for negative and getter ones). This allows to keep an eye on other non-contest -cases to track how bad is tradeoff when performing optimizations here and there. - -Another utility that was developed for contest is ***scalpel script***, that allows for a detailed, *really* detailed optimizations -of the code by comparing lines of code function by function, printing out diffs, and providing detailed TVM files with -stack comments and rewrites. This utility allowed to make some latter optimizations, since with each optimization -next one becomes exponentionally harder to make. While result is not entirely precise and is needed to be verified -by tests, this allows to instantly estimate whether there is some progress or not, since scalpel is executed immediately, -while tests take approximately 10 seconds to execute. - -### Details of optimizations, their rationale and explanations, comparison of consumed gas both in test cases and not in test cases (global gas counter) are provided on a dedicated page: [Gas improvements](Improvements.rst). diff --git a/contest.png b/contest.png deleted file mode 100644 index ab8354391b5d2b87a7c2db3440305e88d85d011c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445101 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�oyNfE$v&GU%;H<`g8{KYT+_)EXz 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

vKU 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%0A09<@lA*7pJ@Mv@S@d~|0U%#+0Cp9d|XCK3A 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≀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 H$uQ`D!9?zT From 0ed97d4959665678cfc0b495d2d1bd8a3803ff8a Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Thu, 4 Jul 2024 13:17:11 +0400 Subject: [PATCH 118/121] delete docs about optimization contest --- Improvements.rst | 447 ----------------------------------------------- 1 file changed, 447 deletions(-) delete mode 100644 Improvements.rst diff --git a/Improvements.rst b/Improvements.rst deleted file mode 100644 index 9560ab9d..00000000 --- a/Improvements.rst +++ /dev/null @@ -1,447 +0,0 @@ -Improvements log -================ - -In this section a table is presented with optimization results in several projections. - -Contest test cases display how much gas is used by contest-like methods of test suite, total saved and percentage -as compared to the original commit gas use. - -Global gas counters were introduced after commit ``Keep your functions close and vars even closer`` to make a measurable -metric of "tradeoff" between contest test cases and all other cases, that are totalled by their corresponding test suite. - -Only positive (no fail) tests without getters (since there is no point to optimize getters) are included in the table. - -This metric proven to be useful, because an ``Localize extensions in loop and short-circ simple`` commit resulted in very -big jump in savings on the test cases, meanwhile it severly impaired all other cases (GGC increased a lot on it). As a -result, the very next commit ``Refactored internal message flows, good GGC value`` managed to bring the GGC below the initial -total level, with further commits do a ``stable development`` of contest test cases with improving GGC as well. - -+----------------------------------------------------------------+-------------------------------------------+--------------------------------+ -| Commit | Contest test cases | Global gas counters | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| .____________________________________________________________. | Ext | Int | Extn | Total | Save | Perc% | Exter | Inter | Exten | Total | -+================================================================+======+======+======+=======+======+=======+=======+=======+=======+========+ -| *Origin point: INITIAL* | 3235 | 4210 | 2760 | 10205 | 0 | 0.00% | 64038 | 71163 | 38866 | 174067 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Optimized unneccessary cell loads and operations | 3185 | 4014 | 2744 | 9943 | 262 | 2.56% | 65556 | 70764 | 40304 | 176624 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Removed unneccessary always true check | 3185 | 3823 | 2501 | 9509 | 696 | 6.82% | 65504 | 68993 | 38998 | 173495 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Unrolled the common internal handler code | 3185 | 3700 | 2373 | 9258 | 947 | 9.28% | 65504 | 67886 | 38204 | 171594 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Implicitly return from the external handler | 3165 | 3700 | 2373 | 9238 | 967 | 9.48% | 65264 | 67886 | 38204 | 171354 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Reaped benefits of separated internal loaders | 3165 | 3700 | 2295 | 9160 | 1045 | 10.2% | 65264 | 67886 | 37736 | 170886 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Discarded unneccessary slice remains in dispatcher | 3155 | 3690 | 2285 | 9130 | 1075 | 10.5% | 65034 | 67716 | 37646 | 170396 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Loaded auth_kind optionally using LDUQ instruction | 3155 | 3654 | 2249 | 9058 | 1147 | 11.2% | 65050 | 67408 | 37430 | 169888 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Is ifnot a joke for you? (emits less instructions) | 3155 | 3654 | 2231 | 9040 | 1165 | 11.4% | 65050 | 67408 | 37322 | 169780 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Localize extensions in loop and short-circ simple | 3045 | 3644 | 2121 | 8810 | 1395 | 13.7% | 69697 | 71316 | 39314 | 180327 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Reordering int msg handlers somehow saves 10 gas | 3045 | 3567 | 2188 | 8800 | 1405 | 13.8% | 69697 | 70623 | 39716 | 180036 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Moving signature check higher saves some gas | 3027 | 3549 | 2188 | 8764 | 1441 | 14.1% | 69481 | 70461 | 39716 | 179658 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Reordering checks somehow sames some more gas | 3009 | 3531 | 2188 | 8728 | 1477 | 14.5% | 69265 | 70299 | 39716 | 179280 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Removing end_parse is -gas and +reliability | 2983 | 3505 | 2188 | 8676 | 1529 | 15.0% | 68953 | 70065 | 39716 | 178734 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Keep your functions close and vars even closer | 2957 | 3505 | 2188 | 8650 | 1555 | 15.2% | 68641 | 70065 | 39716 | 178422 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < restore extensions var in loop > | 3067 | 3533 | 2288 | 8888 | | | 65669 | 67568 | 38456 | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < move complex logic to inline_ref > | 2957 | 3423 | 2316 | 8696 | | | 65528 | 67495 | 39148 | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < optimize tail loading of extensions > | 2957 | 3423 | 2298 | 8678 | | | 65528 | 67495 | 39040 | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < optimize preference for simple ext ops > | 2957 | 3423 | 2248 | 8628 | | | 65528 | 67495 | 39324 | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Refactored internal message flows, good GGC value | 2957 | 3423 | 2248 | 8628 | 1577 | 15.5% | 65528 | 67495 | 39324 | 172347 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| ^^^ This commit finally shows TOTAL GGC less then initial one WHILE providing 15.5% gas save on contest test cases! ^^^ | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Reorganized inlining point for extension message flow | 2957 | 3423 | 2230 | 8610 | 1595 | 15.6% | 65528 | 67495 | 38782 | 171805 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Do not carry around params not needed (ext opt) | 2957 | 3423 | 2176 | 8556 | 1649 | 16.2% | 65176 | 67275 | 38586 | 171037 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Optimize argument order to match stack | 2939 | 3405 | 2148 | 8492 | 1713 | 16.8% | 64960 | 67113 | 38346 | 170419 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Swapping extn and sign order back saves some net gas | 2939 | 3464 | 2063 | 8466 | 1739 | 17.0% | 65004 | 67676 | 37876 | 170556 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Short-circuit optimization of LDUQ with IFNOTRET | 2939 | 3420 | 2019 | 8378 | 1827 | 17.9% | 64929 | 67205 | 37612 | 169746 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < short-circuit flags check with asm > | 2939 | 3402 | 2001 | 8342 | | | | | | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < short-circuit int msg sign last check with asm > | 2939 | 3376 | 2001 | 8316 | | | | | | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < short-circuit ext msg sign last check with asm > | 2913 | 3376 | 2001 | 8290 | | | | | | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| < short-circuit extension dictionary check with asm > | 2913 | 3376 | 1983 | 8272 | | | | | | | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Short-circuited some returns with asm | 2913 | 3376 | 1983 | 8272 | 1933 | 18.9% | 64599 | 66791 | 37373 | 168763 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| ASM-optimized simple action cases | 2885 | 3348 | 1981 | 8214 | 1991 | 19.5% | 64470 | 66700 | 37351 | 168521 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Optimized out more unneeded instructions if may RET | 2885 | 3338 | 1955 | 8178 | 2027 | 19.9% | 64470 | 66610 | 37177 | 168257 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Removed another unneccessary DROP with preload uint | 2875 | 3338 | 1955 | 8168 | 2037 | 20.0% | 64350 | 66610 | 37177 | 168137 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| ^^^ Finally got **20%** optimization in test cases! ^^^ | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Reordered argument order to optimize stack operations | 2857 | 3320 | 1955 | 8132 | 2073 | 20.3% | 64134 | 66448 | 37137 | 167719 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Rewritten RETALT to IFNOTJMP - less gas, more reliable | 2836 | 3299 | 1934 | 8069 | 2136 | 20.9% | 64071 | 66406 | 37220 | 167697 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Another argument stack optimization (psr -> dr call) | 2818 | 3281 | 1934 | 8033 | 2172 | 21.3% | 64017 | 66370 | 37220 | 167607 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| ^^^ Finally this solution is better then original in **EVERY** test suite branch! The last hurdle - Exter GGC is now less!!! ^^^ | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Black magic route optimization (drop some result later) | 2818 | 3281 | 1916 | 8015 | 2190 | 21.5% | 64017 | 66370 | 37130 | 167517 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Another black magic optimization (drop auth_kind later) | 2818 | 3281 | 1906 | 8005 | 2200 | 21.6% | 64017 | 66370 | 37102 | 167489 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Backported FunC optimizations from entrypoint branch | 2782 | 3373 | 1824 | 7979 | 2226 | 21.8% | 60011 | 64810 | 34138 | 158959 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Use SDBEGINS to enforce prefix in external message | 2710 | 3373 | 1824 | 7907 | 2298 | 22.5% | 59147 | 64810 | 34138 | 158095 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Use SDBEGINSQ to check internal message prefixes | 2710 | 3283 | 1736 | 7729 | 2476 | 24.3% | 59237 | 64090 | 33578 | 156905 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Backport some optimizations from EP and coalesce code | 2699 | 3165 | 1828 | 7692 | 2513 | 24.6% | 59108 | 63031 | 34141 | 156280 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| Optimized instructions order for extension and fix args | 2699 | 3165 | 1810 | 7674 | 2531 | 24.8% | 59108 | 63031 | 34033 | 156172 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ -| *Reminder and origin point: INITIAL* | 3235 | 4210 | 2760 | 10205 | 0 | 0.00% | 64038 | 71163 | 38866 | 174067 | -+----------------------------------------------------------------+------+------+------+-------+------+-------+-------+-------+-------+--------+ - -*It seems that backporting optimization wiggles around values here and there.* To get the maximum possible gas savings please consider taking -a look at ``entrypoint`` ("radical") branch. Since optimizations are carefully made there, they decrease used gas by all cases without compromises. - -As an example, here is a comparison of used gas in main ("conservative") and entrypoint ("radical") branches: - -+-----------------+----------+----------------+ -| Test case | main gas | entrypoint gas | -+=================+==========+================+ -| External | 2699 | **2707** | -+-----------------+----------+----------------+ -| Internal | 3165 | **2963** | -+-----------------+----------+----------------+ -| Extension | 1810 | **1608** | -+-----------------+----------+----------------+ -| External GGC | 59108 | **58893** | -+-----------------+----------+----------------+ -| Internal GGC | 63031 | **61011** | -+-----------------+----------+----------------+ -| Extension GGC | 34033 | **32821** | -+-----------------+----------+----------------+ - -The external test case uses a tiny miniscule more gas due to if ordering, making it way around messes up cell slicing completely. -Nevertheless, external global counter is still less, therefore the overall result is not that bad. - -N.B. Contest multiplier: 9905/10205 = 0.9706 (approximate) -> place multipliers ~ 0.3235342, 0.0970602, 0.0485301 - -Details and rationale -===================== - -In this section, details of optimization in each commit are pointed out, sometimes with detailed rationale and reasoning, -when neccessary (in some relatively controversial optimizations). - -Origin point: INITIAL ---------------------- -The origin point, the state of the contract when the contest was started. It is used as a basis point to measure further improvements. - -Optimized unneccessary cell loads and operations ------------------------------------------------- -There is some data that is not needed to be loaded right away, since most likely it won't be used, so that data loading is deferred -until the moment it is actually needed. First of all, that is ``extensions`` dictionary, since loading dict (consequently, a cell) -is a pretty expensive operation. - -Also, reading ``stored_subwallet, public_key, extensions`` and writing them back just to increase ``stored_seqno`` is completely -unneccessary, so I took a snapshot of slice immediately after ``stored_seqno``, and write it as a slice, instead of 3 write operations -when increasing the ``stored_seqno``. - -Instead of ``extensions`` now ``immutable_tail`` is being passed around, and ``extensions`` are extracted from it, when needed. - -Removed unneccessary always true check --------------------------------------- -Adding return to the if condition decreased amount of gas (due to turning ``IF`` into ``IFJMP``), and, consequently, -second check of opcode is not required, since it is allowed to be only one of two options, one of which was already checked. - -Unrolled the common internal handler code ------------------------------------------ -Copying the common data load code to separate execution paths in internal message handler somehow saves considerable amount -of gas, but, most importantly, allows to optimize the data loading in future (since it is now different code). - -Implicitly return from the external handler -------------------------------------------- -*Explicity* (commit name has logic mistake) returning from the external handler saves some gas due to some TVM optimizations. - -Reaped benefits of separated internal loaders ---------------------------------------------- -Because data loading is now handled separately for signed and extension messages, it is possible to optimize data loading -so as not to waste unneccessary gas to load data that is not required for a specific execution path. - -More precisely, extensions are now loaded from immutable tail, that allows to streamline stack manipulations that decrease -amount of used gas, also, this logic will be even more simplified in future to save even more gas. - -Discarded unneccessary slice remains in dispatcher --------------------------------------------------- -Using ``preload_ref`` instead of ``load_ref`` on a varible that is not used anymore saves considerable amount of gas, since -it is not required anymore to do stack manipulations and dropping the unneccessary result. - -Loaded auth_kind optionally using LDUQ instruction --------------------------------------------------- -An ``LDUQ`` TVM instruction was used to construct a ``try_load_uint32`` that attempts to load an ``uint32`` from a slice, -and returns the success indicator alongside with result, that allows to compact checking of availability of bits in slice -and reading the integer itself into one instruction - less branching, instructions, checks and gas. - -Is ifnot a joke for you? (emits less instructions) --------------------------------------------------- -Using ``ifnot`` instead of ``if ~...`` saves gas, since ``NOT`` instruction is not needed anymore. ``ifnot`` has same price -and bit length as the ``if``, therefore it is **always** advised to use ``ifnot`` for negative conditions. - -Localize extensions in loop and short-circ simple -------------------------------------------------- -In this commit, there are two different changes. First one is localizing ``extensions`` inside loop, that allowed to save -some gas in case ``extensions`` are not needed to be changed. - -**The second one is one of the most important optimizations**, that opens the door for many more further gas optimizations -in the code. The idea is that if the message is simple, that it, has no extended actions (the first bit is right away 0), -it is possible to immediately do the ``set_actions`` and ``return``. - -While the first idea has a noticeable tradeoff, that will be eliminated in future by optimizations all around the code, -the second one does not make other execution paths more pricey, while making the main ones much better in terms of gas. - -Reordering int msg handlers somehow saves 10 gas ------------------------------------------------- -Moving ``sign`` above ``extn`` one in internal message handler somehow saved 10 gas. - -Moving signature check higher saves some gas --------------------------------------------- -In ``process_signed_request`` moving signature check to the top of the function saves some gas. - -Reordering checks somehow sames some more gas ---------------------------------------------- -In ``process_signed_request`` changing order of parameter checks decreased amount of stack manipulations and saved some gas. - -Removing end_parse is -gas and +reliability -------------------------------------------- -In this commit, ``end_parse`` (and coincidentally now unneeded ``skip_dict``) was removed from this code. This leads to -increased reliability, less gas usage, and opens road to some more optimizations (like tail preloading). - -**While decreasing gas usage and opening road to more optimizations is pretty obvious, let's me explain on the increased reliabilty.** - -The idea behind it is, that usually, ``end_parse`` is used to force structure of user messages. Therefore, mostly, using -it to enforce structure of internal data of the contract is quite excessive, since the contract itself is the one, who -only can write it's own data, and therefore if it cannot be corrupted by the code, then there is no way extra data appears -after the expected end. Therefore, using ``end_parse`` is unneccessary, and just wastes gas. - -However, in this contract the user can directly do ``set_data`` using extended actions on the contract. And here is the point -why reliability of the contract is actually **increased** by removing the ``end_parse``. It is possible in future, that the -user might accidentally append extra data to the end of the contract. This may happen if the user would like to upgrade the -contract, it will have some more extra data, but for some reason failed or forgot to do the code upgrade action, or it failed -one or another way. In this situation the user will end up with **the old contract with the new data**. And in this situation, -all the TONs, tokens and NFTs on this wallet will be locked **forever!!!** just because of that ``end_parse``. Therefore, -removing the ``end_parse`` also helps against such kind of mistakes, and there are no any kind of implications on removing it. - -The only place where it should **really** be used is checking close-structured (without open ends, like in our case, where -the list can be of any length) input user data, in order to make sure, that a specific request can have only one single -implementation in order to prevent some playing with signatures, but that is completely not an our case. - -Keep your functions close and vars even closer ----------------------------------------------- -This refactoring of external message handler streamlines data flows in it, therefore avoiding unneccessary stack manipulations -and saving some gas as a result. More precisely, the ``auth_kind`` is loaded right away from ``body`` (since it is the last -parameter of the function, it is at the top of the stack at that moment), and data is being loaded later after the check. - -Refactored internal message flows, good GGC value -------------------------------------------------- -This commit, and several other technical commits before it (not described here, since they are technical ones and do not -affect the code) lays beginning for calculation and optimizations of **GGC** (global gas counter). While not being a direct -target of the contest, the **GGC** is important metric, that allows to measure the tradeoff, of how optimizing contest paths -inadversely affects all other logic of the code that is not measured. Therefore, keeping an eye on **GGC** is important for -**sustained development** of contest paths, where optimizing them does not severely impair all other code logic. - -This commit, while increasing extension gas usage a little (this problem will be addressed to and solved in later commits), -immensely decreases usage of gas in GGC, and brings it down below the GGC in initial commit. Therefore, starting at this point, -I can strongly assert, that the optimizations of the main contest paths do not impair the other code paths and logic. - -restore extensions var in loop -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -First of all, ``extensions`` variable in complex handling loop was reinstated, because saving exts in cell and popping them -off each time required a lot of gas due to recreation of cell each time. - -move complex logic to inline_ref -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Next, the complex dispatch request handling logic was moved off to a separate function, that is called with ``inline_ref`` -modifier. This allows to save some gas on simple cases, and **is actually a very important optimization for future**, because -at some point in future, the *cell breaking point* where TVM Assembler decides to break cell into pieces because a critical -point for further optimization. - -optimize tail loading of extensions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The way how extensions are loaded in internal message handler is optimized so as not to load the unneccessary at that moment data. - -optimize preference for simple ext ops -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Simple operations initiated by extensions now do not require to load the unneccessary data from the contract. - -Reorganized inlining point for extension message flow ------------------------------------------------------ -Some optimizations were made to tell the compiler to break the cell at exact place by using ``inline`` and ``inline_ref`` accurately. - -Do not carry around params not needed (ext opt) ------------------------------------------------ -Getting the data of the contract in place, even accounting for the ``begin_slice`` is more efficient than carrying it around -in many parameters, that forces stack shaping when crossing the function boundary, and constraints on how efficient stack -manipulations may be, therefore all the unneccessary parameters were removed and data is extracted closer to the point -where it is actually needed. - -Optimize argument order to match stack --------------------------------------- -Some parameters were reordered to match how they are ordered in stack, so that to decrease amount of unneccessary stack operations. - -Swapping extn and sign order back saves some net gas ----------------------------------------------------- -In internal message handler ``sign`` and ``extn`` message handlers were swapped back once again, since somehow, after all the -optimizations carried out above, that order is now more efficient in terms of gas. - -Short-circuit optimization of LDUQ with IFNOTRET ------------------------------------------------- -Instead of pretty complex in terms of instructions and gas FunC construct, a single ``IFNOTRET`` is used to quickly end -execution when there are not enough bits in the slice to obtain the opcode from the internal message. - -Short-circuited some returns with asm -------------------------------------- -Following the idea of the previous commit, some more operations now use ``IF(NOT)RET`` instead of conditionals to save more gas. - -short-circuit flags check with asm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Flags of internal message (bounced, to be more precise) are now checked by a concise ASM function that does ``IFRET`` to -end the execution in case a bounced message is detected. - -short-circuit int msg sign last check with asm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The check of second operation can be made shorter by comparing two numbers equality and performing ``IFNOTRET`` in ASM. - -short-circuit ext msg sign last check with asm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The same applies for opcode check in internal message handler. - -short-circuit extension dictionary check with asm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -... and the ``success?`` result of locating the sender in the ``extensions`` dictionary. - -ASM-optimized simple action cases ---------------------------------- -An optimized code construct was built to replace the not-so-efficient FunC code for simple function cases. This one uses -a specific ordering of result on the stack after executing the neccessary instructions. - -Optimized out more unneeded instructions if may RET ---------------------------------------------------- -An ``udict_get_or_return`` instruction was introduced that instead of returning ``success?`` alongside with the result -returns immediately if the entry is not found in the dictionary. - -Also, I have noticed, that ``public_key`` is read from ``cs`` using ``~load_uint``, but that ``cs`` is not used anymore -in the code, so saved an unneccessary ``DROP`` by using ``.preload_uint`` instead. - -Removed another unneccessary DROP with preload uint ---------------------------------------------------- -The same optimization for ``public_key`` loading was done in the external message handler in this commit. - -Reordered argument order to optimize stack operations ------------------------------------------------------ -Some arguments were reordered to save gas on stack manipulations. Also, another ``public_key`` loading was optimized (the -last one, in the extension handler execution path). - -Rewritten RETALT to IFNOTJMP - less gas, more reliable ------------------------------------------------------- -The simple actions handler was rewritten from ``IFNOT:<{ ... RETALT }>`` to ``IFNOTJMP:<{ ... }>``. This saves some gas -(since implicit returns are cheaper), and makes the code more reliable (since we cannot be 100% sure that ``RETALT`` will -end the execution as expected if the code will be modified in future, therefore using ``IFNOTJMP`` eliminates this uncertainity). - -Another argument stack optimization (psr -> dr call) ----------------------------------------------------- -Some another reordering of function arguments was done to eliminate unneccessary stack operations. - -Black magic route optimization (drop some result later) -------------------------------------------------------- -An unused result of extension dictionary checking is now carried around inside the called function in order to be dropped -later after the simple actions checker. Surprisingly, this does not impair non-test code paths at all, since the ``DROP`` -at the end of simple actions checker is merged with drop of the carried result into ``2DROP``, thus having no drawbacks. - -Another black magic optimization (drop auth_kind later) -------------------------------------------------------- -Another variable is now called around for delayed drop, this time ``auth_kind``, which turns ``2DROP`` into ``3 BLKDROP``, -that is still not bad, increases gas efficiency on primary paths, and does not impair it on other ones. - -Backported FunC optimizations from entrypoint branch ----------------------------------------------------- -Backported some FunC optimizations done in entrypoint branch (although, they may be not as efficient): - -Rearranged entrypoint conditions flow, compiler fix -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -External and internal message processing conditions order are swapped that result in less gas usage overall. Also, some -mistakes in TVM Assembler are fixed and functions were renamed so as not to accidentally compile it using an ordinary compiler. - -some commits not affecting the main test branches -""""""""""""""""""""""""""""""""""""""""""""""""" -Some additional improvements to the complex dispatch case were made to decrease the global gas counters. This did not affect -the gas usage in the main test cases, but made my optimizations for friendly to the natur... to the other code branches. - -Removed unneccessary exploded data parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Moved data (``ds``) variables closer to their actual usage. Therefore it is not required to pass lots of those variables -in the arguments anymore saving some gas on stack reorganizations. - -Moreover, this allows to move data variable code inbetween other code in ``process_signed_request`` function, saving even -more code by optimizing order of operations. - -Use SDBEGINS to enforce prefix in external message --------------------------------------------------- -I have found out a super useful ``SDBEGINS(Q)`` TVM instruction that allows to verify the prefix of a slice against another -one (in this version of function the prefix is even conveniently embedded into the instruction code itself), and even has -a very convenient behaviour of throwing if prefix does not match (that is very convenient for external message, since -returning from it without accepting message is effectively the same as throwing an exception), and returns the slice without -that prefix is correct, that perfectly matches the previous behaviour. - -As such, replacing compare and return with this instruction saves considerable amount of gas with no implications. - -Use SDBEGINSQ to check internal message prefixes ------------------------------------------------- -The quiet version of aforementioned instruction, ``SDBEGINSQ`` exhibits even more convenient behaviour for multi-case checking -and pipelining: on the top of the stack it puts whether the prefix matched or not, that can be consumed for any kind of condition -checks, and always returns a slice after it. The great behaviour is that if the prefix matched the returned slice is stripped of -it, and if the prefix did not match, the original slice is returned. This allows to use this instruction, branch into processing -code if it matched, or use it again if did not, and keep doing that (something like a switch-case). - -Therefore, I have used this instruction to check for opcode prefix in internal message processing. - -Backport some optimizations from EP and coalesce code ------------------------------------------------------ -Backported some more optimizations from entrypoint branch - -Use SDFIRST instead of PLDU to check first bit -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -It is possible to use shorter ``SDFIRST`` instruction to check if first bit of slice is set, that saves some gas. - -I have used it in checking whether to use simple action processing code, that saves some gas in each execution branch. - -Check bounced flag using slices and trail bits -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -It is more efficient to get a 4-bit slice and check trailing bits with ``SDCNTTRAIL1`` (it will always be non-zero -if last bit (bounced) is non-zero, and it always will be zero if it is zero - a perfect instruction to check the last bit). -Therefore by such approach checking bounced flag bit is much more effective than loading 4-bit number from slice, pushing 1 -to stack, and performing the or operation. - -Using SDBEGINSQ to check for starting zero -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Like with internal message prefixes, it is more efficient to use a single ``SDBEGINSQ`` instruction to check that prefix -starts with zero and is a simple action even than preload a single uint1. - -Optimized instructions order for extension and fix args -------------------------------------------------------- -Adjusting order of instructions in extension branch allows to save some gas. Also fixed arguments because TON Plugin -was complaining (no gas or instructions change whatsoever). \ No newline at end of file From 2f2eaca439d2a171be6fcac49dabd006612143b5 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Thu, 4 Jul 2024 13:48:14 +0400 Subject: [PATCH 119/121] update readme and deploy script --- README.md | 32 +++++++++++++++++++------------- Specification.md | 4 +--- scripts/deployLibrary.ts | 2 +- scripts/deployWalletV5.ts | 6 +++--- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c695619c..4ceeb7df 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,22 @@ -# 🔥W5: wallet v5 standard +# W5: wallet smart contract v5 -This is an extensible wallet specification aimed at replacing V4 and allowing arbitrary extensions. +New version of wallet smart contract, the previous one was [v4r2](https://github.com/ton-blockchain/wallet-contract). -W5 has **25% lower fees**, supports **gasless transactions** (via third party relayers) and implements a **flexible extension mechanism**. +The entire concept is proposed by the [Tonkeeper team](https://tonkeeper.com/). + +New Features: + +- Send up to 255 messages at once; + +- Signed actions can be sent not only by external message, but also by internal messages (can be used for gasless transactions); + +- Unlimited extensions; + +- Extension can prohibit signed actions in the wallet (can be used for 2fa or key recovery); + +- Optimizations to reduce network fees; + +- Better foolproofing safety - reply-protection for external messages, wallet id rethinking; ## Project structure @@ -13,25 +27,17 @@ W5 has **25% lower fees**, supports **gasless transactions** (via third party re - `scripts` - scripts used by the project, mainly the deployment scripts, additionally contains utilities for gas optimisation. - `fift` - contains standard Fift v0.4.4 library including the assembler and disassembler for gas optimisation utilities. -### Additional documentation - -- [Gas improvements](Improvements.rst) - a log of improvements, detailed by primary code paths, global gas counters per commit. -- [Contest](Contest.md) - a note showing some information about interesting improvements during the optimisation contest. - ## How to use ### Build -`npm run build:v5` +`npm run build` ### Test `npm run test` ### Deployment -1. Deploy library: `npm run deploy-library` -2. Deploy wallet: `npm run deploy-wallet` -### Get wallet compiled code +Deploy wallet: `npm run deploy-wallet` -`npm run print-wallet-code` diff --git a/Specification.md b/Specification.md index 8533d90e..9896b0b6 100644 --- a/Specification.md +++ b/Specification.md @@ -7,8 +7,6 @@ This is an extensible wallet specification aimed at replacing V4 and allowing ar * [Features](#features) * [Overview](#overview) * [Discussion](#discussion) -* [Wallet ID](#wallet-id) -* [Packed address](#packed-address) * [TL-B definitions](#tl-b-definitions) * [Source code](#source-code) @@ -30,7 +28,7 @@ Thanks to [Skydev](https://github.com/Skydev0h) for optimization and preparing t * Extensions can perform the same operations as the signer: emit arbitrary messages on behalf of the owner, add and remove extensions. * Signed requests can be delivered via internal message to allow 3rd party pay for gas. * For consistency and ease of indexing, external messages also receive a 32-bit opcode. -* To lay foundation for support of scenarios like 2FA or access recovery it is possible to disable signature authentication. +* To lay foundation for support of scenarios like 2FA or access recovery it is possible to disable signature authentication by extension. ## Overview diff --git a/scripts/deployLibrary.ts b/scripts/deployLibrary.ts index 171241be..94059f39 100644 --- a/scripts/deployLibrary.ts +++ b/scripts/deployLibrary.ts @@ -1,5 +1,5 @@ import { toNano } from 'ton-core'; -import { compile, NetworkProvider } from '@ton-community/blueprint'; +import { compile, NetworkProvider } from '@ton/blueprint'; import 'dotenv/config'; import { LibraryDeployer } from '../wrappers/library-deployer'; diff --git a/scripts/deployWalletV5.ts b/scripts/deployWalletV5.ts index ce66fbbe..47625220 100644 --- a/scripts/deployWalletV5.ts +++ b/scripts/deployWalletV5.ts @@ -1,6 +1,6 @@ import { Dictionary, toNano } from 'ton-core'; import { WalletId, WalletV5 } from '../wrappers/wallet-v5'; -import { compile, NetworkProvider } from '@ton-community/blueprint'; +import { compile, NetworkProvider } from '@ton/blueprint'; import { LibraryDeployer } from '../wrappers/library-deployer'; import { getSecureRandomBytes, keyPairFromSeed } from 'ton-crypto'; @@ -15,11 +15,11 @@ export async function run(provider: NetworkProvider) { const walletV5 = provider.open( WalletV5.createFromConfig( { - signature_auth_disabled: false, + signatureAllowed: true, seqno: 0, walletId: new WalletId({ networkGlobalId: -3 }).serialized, // testnet publicKey: keypair.publicKey, - extensions: Dictionary.empty() + extensions: Dictionary.empty() as any }, LibraryDeployer.exportLibCode(await compile('wallet_v5')) ) From 4fab977f4fae3a37c1aac216ed2b7e611a9bc2af Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Thu, 4 Jul 2024 14:22:02 +0400 Subject: [PATCH 120/121] add more info to README --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index 4ceeb7df..96f866cf 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,49 @@ New Features: Deploy wallet: `npm run deploy-wallet` +### Known issues + +1) Since the `valid_until` is uint32 it will not work after 2106 year. We believe new versions of wallet smart contract will be available by then. + +2) If the `action_send_msg` content is invalid and the sendmode has +2, the error will not be ignored. An update of the node is planned where this behaviour will be changed (with +2 sendmode and `action_send_msg` invalid content the error will be ignored). + +3) It would be good to do `end_parse()` for messages and contract data. But this is not done within optimisations. + +### Gasless flow + +1. When sending an USDt (or other Jetton) the user signs one message containing two outgoing USDt transfers: + + * USDt transfer to the recipient's address. + + * Transfer of a small amount of USDt in favor of the Service. + +2. This signed message is sent offchain by HTTPS to the Service backend. The Service backend checks message and sends it to the TON blockchain paying Toncoins for network fees. + +### Gasless known issues + +1) By requesting a gasless service, a user can have time to increase the seqno on his own, or via another service. + + In this case, the gasless service will incur gas costs. + + However, this is a non-scalable scenario, as it requires the user to incur gas costs as well. + + A blacklist on the service backend side solves the problem. + +2) The user can request a gasless service and by means of a specialised extension have time to withdraw the entire balance of Jettons without change seqno. + + In this case, the Jetton transfer message from the service will encounter a balance shortage and the Toncoins attached to message will return to the user's wallet. + + However, this is a non-scalable scenario, as it requires the user to incur gas costs as well. + + A blacklist on the service backend side solves the problem. + +### Suggested extensions + +1) Decentralised subscriptions. The extension can withdraw a given number of Toncoins or Jettons once in a given period. + +2) 2FA: Multisig extension is added, extension prohibits wallet signature; + +3) Key recovery: 2FA, but in multisig extension there is an option to change the control keys. Possible cooldown period when the other party can cancel the key change. + +4) Key compromise: An extension with a new key is added, extension prohibits wallet signature; + From 88557ebc33047a95207f6e47ac8aadb102dff744 Mon Sep 17 00:00:00 2001 From: tolya-yanot Date: Sun, 21 Jul 2024 14:24:43 +0400 Subject: [PATCH 121/121] fix comment --- contracts/wallet_v5.fc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/wallet_v5.fc b/contracts/wallet_v5.fc index 65e213ab..83b5199c 100644 --- a/contracts/wallet_v5.fc +++ b/contracts/wallet_v5.fc @@ -225,7 +225,7 @@ cell verify_c5_actions(cell c5, int is_external) inline { return (); ;; just receive Toncoins } - ;; bounced messages has 0xffffff prefix and skipped by op check + ;; bounced messages has 0xffffffff prefix and skipped by op check if (op == prefix::extension_action) { in_msg_body~skip_bits(size::message_operation_prefix);

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�h;r=NY?GFn*-sVrmX{ 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