Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat!: prevent implicit asset burn #3540

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8b58f28
feat: added test for feature
petertonysmith94 Jan 3, 2025
0ea3d8f
feat: added guards against illicit asset burns
petertonysmith94 Jan 3, 2025
a33ec72
chore: docs
petertonysmith94 Jan 3, 2025
69e480f
chore: changeset
petertonysmith94 Jan 3, 2025
5e053b3
chore: removed test.only
petertonysmith94 Jan 6, 2025
61da932
chore: finalize the PR
petertonysmith94 Jan 6, 2025
987221f
Merge branch 'master' into ps/feat/prevent-illicit-burn
petertonysmith94 Jan 6, 2025
1caa00c
Merge branch 'master' of github.com:FuelLabs/fuels-ts into ps/feat/pr…
petertonysmith94 Jan 7, 2025
ac03f64
chore: refactored the burnable assets to a helper
petertonysmith94 Jan 7, 2025
9c4080c
chore: use `autoCost`
petertonysmith94 Jan 7, 2025
16a6b07
chore: breaking change
petertonysmith94 Jan 7, 2025
262bba4
docs: added docs on asset burn
petertonysmith94 Jan 7, 2025
4ff6361
speeling
petertonysmith94 Jan 7, 2025
ad96a1a
chore: changeset
petertonysmith94 Jan 7, 2025
c408eda
lintfix
petertonysmith94 Jan 7, 2025
ad30912
Merge branch 'master' of github.com:FuelLabs/fuels-ts into ps/feat/pr…
petertonysmith94 Jan 7, 2025
4004a0a
chore: allow asset burn via `sendTransaction` options
petertonysmith94 Jan 7, 2025
c5a5b7e
petertonysmith94 Jan 7, 2025
a572ca4
Merge branch 'master' of github.com:FuelLabs/fuels-ts into ps/feat/pr…
petertonysmith94 Jan 7, 2025
6f6a156
Update apps/docs/src/guide/transactions/transaction-request.md
petertonysmith94 Jan 7, 2025
82f472d
chore: added asset burn validation to wallet
petertonysmith94 Jan 8, 2025
3eef294
chore: update wallet-unlocked tx example to be valid
petertonysmith94 Jan 8, 2025
8b2c598
Merge branch 'ps/feat/prevent-illicit-burn' of github.com:FuelLabs/fu…
petertonysmith94 Jan 8, 2025
280db16
Merge branch 'master' of github.com:FuelLabs/fuels-ts into ps/feat/pr…
petertonysmith94 Jan 8, 2025
68e3a6a
chore: update test assertions
petertonysmith94 Jan 8, 2025
4343f71
Merge branch 'master' into ps/feat/prevent-illicit-burn
petertonysmith94 Jan 8, 2025
16985a3
Merge branch 'master' into ps/feat/prevent-illicit-burn
petertonysmith94 Jan 8, 2025
a2e6bc3
Merge branch 'master' into ps/feat/prevent-illicit-burn
nedsalk Jan 8, 2025
43cba99
Merge branch 'master' into ps/feat/prevent-illicit-burn
arboleya Jan 9, 2025
b321ec8
chore: added asset burn validation for `MessageCoin`
petertonysmith94 Jan 9, 2025
0179db4
Merge branch 'ps/feat/prevent-illicit-burn' of github.com:FuelLabs/fu…
petertonysmith94 Jan 9, 2025
62dc663
chore: update validation check
petertonysmith94 Jan 9, 2025
33e5ea9
chore: use transactionRequest for helpers
petertonysmith94 Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/wild-avocados-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/account": patch
"@fuel-ts/errors": patch
---

feat: prevent implicit asset burn
petertonysmith94 marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 6 additions & 0 deletions apps/docs/src/guide/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ When an [`Account`](https://fuels-ts-docs-api.vercel.app/classes/_fuel_ts_accoun

It could be caused during the deployments of contracts when an account is required to sign the transaction. This can be resolved by following the deployment guide [here](../contracts/deploying-contracts.md).

### `ASSET_BURN_DETECTED`

When you are trying to send a transaction that will result in an asset burn.

Add relevant coin change outputs to the transaction, or enable asset burn in the transaction request.

### `CONFIG_FILE_NOT_FOUND`

When a configuration file is not found. This could either be a `fuels.config.[ts,js,mjs,cjs]` file or a TOML file.
Expand Down
20 changes: 16 additions & 4 deletions packages/account/src/providers/provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Address } from '@fuel-ts/address';
import { Address, getRandomB256 } from '@fuel-ts/address';
import { ZeroBytes32 } from '@fuel-ts/address/configs';
import { randomBytes, randomUUID } from '@fuel-ts/crypto';
import { FuelError, ErrorCode } from '@fuel-ts/errors';
import { expectToThrowFuelError, safeExec } from '@fuel-ts/errors/test-utils';
import { BN, bn } from '@fuel-ts/math';
import type { Receipt } from '@fuel-ts/transactions';
import { InputType, ReceiptType } from '@fuel-ts/transactions';
import { InputType, OutputType, ReceiptType } from '@fuel-ts/transactions';
import { DateTime, arrayify, sleep } from '@fuel-ts/utils';
import { ASSET_A, ASSET_B } from '@fuel-ts/utils/test-utils';
import { versions } from '@fuel-ts/versions';
Expand Down Expand Up @@ -34,7 +34,10 @@ import Provider, {
} from './provider';
import type { ExcludeResourcesOption } from './resource';
import { isCoin } from './resource';
import type { CoinTransactionRequestInput } from './transaction-request';
import type {
ChangeTransactionRequestOutput,
CoinTransactionRequestInput,
} from './transaction-request';
import { CreateTransactionRequest, ScriptTransactionRequest } from './transaction-request';
import { TransactionResponse } from './transaction-response';
import type { SubmittedStatus } from './transaction-summary/types';
Expand Down Expand Up @@ -325,19 +328,27 @@ describe('Provider', () => {
it('can call()', async () => {
using launched = await setupTestProviderAndWallets();
const { provider } = launched;
const owner = getRandomB256();
const baseAssetId = provider.getBaseAssetId();

const CoinInputs: CoinTransactionRequestInput[] = [
{
type: InputType.Coin,
id: '0xbc90ada45d89ec6648f8304eaf8fa2b03384d3c0efabc192b849658f4689b9c500',
owner: baseAssetId,
owner,
assetId: baseAssetId,
txPointer: '0x00000000000000000000000000000000',
amount: 500_000,
witnessIndex: 0,
},
];
const ChangeOutputs: ChangeTransactionRequestOutput[] = [
{
type: OutputType.Change,
assetId: baseAssetId,
to: owner,
},
];
const transactionRequest = new ScriptTransactionRequest({
tip: 0,
gasLimit: 100_000,
Expand All @@ -352,6 +363,7 @@ describe('Provider', () => {
arrayify('0x504000ca504400ba3341100024040000'),
scriptData: randomBytes(32),
inputs: CoinInputs,
outputs: ChangeOutputs,
witnesses: ['0x'],
});

Expand Down
7 changes: 7 additions & 0 deletions packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,13 @@ Supported fuel-core version: ${supportedVersion}.`
`The transaction exceeds the maximum allowed number of outputs. Tx outputs: ${tx.outputs.length}, max outputs: ${maxOutputs}`
);
}

if (tx.hasBurnableAssets()) {
throw new FuelError(
ErrorCode.ASSET_BURN_DETECTED,
'Asset burn detected.\nAdd relevant coin change outputs to the transaction, or enable asset burn in the transaction request (`request.enableBurn()`).'
);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { TransactionType, UpgradePurposeTypeEnum } from '@fuel-ts/transactions';
import { concat, hexlify } from '@fuel-ts/utils';
import { ASSET_A, ASSET_B } from '@fuel-ts/utils/test-utils';

import {
SCRIPT_TX_COIN_REQUEST_INPUT,
SCRIPT_TX_COIN_REQUEST_OUTPUT_CHANGE,
} from '../../../test/fixtures/transaction-request';
import { WalletUnlocked } from '../../wallet';
import type { Coin } from '../coin';
import type { CoinQuantity } from '../coin-quantity';
Expand Down Expand Up @@ -291,4 +295,33 @@ describe('transactionRequestify', () => {
expect(txRequest.witnessIndex).toEqual(txRequestLike.witnessIndex);
expect(txRequest.type).toEqual(txRequestLike.type);
});

it('should have burnable assets [single input, no change]', () => {
const txRequest = new ScriptTransactionRequest();
const hasBurnableAssets = true;

txRequest.inputs.push(SCRIPT_TX_COIN_REQUEST_INPUT);

expect(txRequest.hasBurnableAssets()).toEqual(hasBurnableAssets);
});

it('should not have burnable assets [single input, single change]', () => {
const txRequest = new ScriptTransactionRequest();
const hasBurnableAssets = false;

txRequest.inputs.push(SCRIPT_TX_COIN_REQUEST_INPUT);
txRequest.outputs.push(SCRIPT_TX_COIN_REQUEST_OUTPUT_CHANGE);

expect(txRequest.hasBurnableAssets()).toEqual(hasBurnableAssets);
});

it('should not have burnable assets [single input, burn asset enabled]', () => {
const txRequest = new ScriptTransactionRequest();
const hasBurnableAssets = false;

txRequest.enableBurn(true);
txRequest.inputs.push(SCRIPT_TX_COIN_REQUEST_INPUT);

expect(txRequest.hasBurnableAssets()).toEqual(hasBurnableAssets);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
outputs: TransactionRequestOutput[] = [];
/** List of witnesses */
witnesses: TransactionRequestWitness[] = [];
/** Whether the transaction request should enable asset burn */
burnEnabled: boolean = false;

/**
* Constructor for initializing a base transaction request.
Expand Down Expand Up @@ -709,4 +711,30 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
byteLength(): number {
return this.toTransactionBytes().byteLength;
}

/**
* Enables asset burn for the transaction request.
*
* @param burnEnabled - Whether the transaction request should enable asset burn.
* @returns This transaction request.
*/
enableBurn(burnEnabled: boolean = true): this {
this.burnEnabled = burnEnabled;
return this;
}

hasBurnableAssets(): boolean {
if (this.burnEnabled) {
petertonysmith94 marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

const coinInputs = new Set(this.getCoinInputs().map((input) => input.assetId));
const changeOutputs = new Set(
this.outputs
.filter((output) => output.type === OutputType.Change)
.map((output) => output.assetId)
);
const difference = new Set([...coinInputs].filter((x) => !changeOutputs.has(x)));
return difference.size > 0;
}
}
55 changes: 33 additions & 22 deletions packages/account/test/fixtures/transaction-request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import type {
ChangeTransactionRequestOutput,
CoinTransactionRequestInput,
CoinTransactionRequestOutput,
} from '../../src';
import { ScriptTransactionRequest } from '../../src/providers/transaction-request/script-transaction-request';

export const SCRIPT_TX_COIN_REQUEST_INPUT: CoinTransactionRequestInput = {
type: 0,
id: '0x000000000000000000000000000000000000000000000000000000000000000000',
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
amount: '0x989680',
owner: '0xf1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e',
txPointer: '0x00000000000000000000000000000000',
witnessIndex: 0,
predicate: '0x',
predicateData: '0x',
predicateGasUsed: '0x20',
};

export const SCRIPT_TX_COIN_REQUEST_OUTPUT_COIN: CoinTransactionRequestOutput = {
type: 0,
to: '0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077',
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
amount: 1,
};

export const SCRIPT_TX_COIN_REQUEST_OUTPUT_CHANGE: ChangeTransactionRequestOutput = {
type: 2,
to: '0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077',
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
};

export const SCRIPT_TX_REQUEST = new ScriptTransactionRequest({
gasLimit: 10_000,
script: '0x24400000',
Expand All @@ -8,27 +39,7 @@ export const SCRIPT_TX_REQUEST = new ScriptTransactionRequest({
maxFee: 90000,
maturity: 0,
witnessLimit: 3000,
inputs: [
{
type: 0,
id: '0x000000000000000000000000000000000000000000000000000000000000000000',
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
amount: '0x989680',
owner: '0xf1e92c42b90934aa6372e30bc568a326f6e66a1a0288595e6e3fbd392a4f3e6e',
txPointer: '0x00000000000000000000000000000000',
witnessIndex: 0,
predicate: '0x',
predicateData: '0x',
predicateGasUsed: '0x20',
},
],
outputs: [
{
type: 0,
to: '0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077',
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
amount: 1,
},
],
inputs: [SCRIPT_TX_COIN_REQUEST_INPUT],
outputs: [SCRIPT_TX_COIN_REQUEST_OUTPUT_COIN],
witnesses: ['0x'],
});
2 changes: 2 additions & 0 deletions packages/errors/src/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export enum ErrorCode {
FUNDS_TOO_LOW = 'funds-too-low',
MAX_OUTPUTS_EXCEEDED = 'max-outputs-exceeded',
MAX_COINS_REACHED = 'max-coins-reached',
ASSET_BURN_DETECTED = 'asset-burn-detected',

// receipt
INVALID_RECEIPT_TYPE = 'invalid-receipt-type',

Expand Down
71 changes: 69 additions & 2 deletions packages/fuel-gauge/src/transaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { hexlify, InputMessageCoder, sleep, TransactionType } from 'fuels';
import { launchTestNode, TestMessage } from 'fuels/test-utils';
import type { CoinTransactionRequestInput } from 'fuels';
import {
FuelError,
hexlify,
InputMessageCoder,
InputType,
ScriptTransactionRequest,
sleep,
TransactionType,
} from 'fuels';
import { ASSET_A, expectToThrowFuelError, launchTestNode, TestMessage } from 'fuels/test-utils';

import { CallTestContractFactory } from '../test/typegen';

Expand Down Expand Up @@ -139,4 +148,62 @@ describe('Transaction', () => {
expect(isStatusSuccess).toBeTruthy();
expect(status.state).toBe('SPENT');
});

it('should allow an asset burn when enabled', async () => {
const {
wallets: [sender],
} = await launchTestNode();

const request = new ScriptTransactionRequest();

// Set the asset burn flag
request.enableBurn(true);

// Add a coin input, which adds the relevant coin change output
const { coins } = await sender.getCoins(ASSET_A);
const [coin] = coins;
request.addCoinInput(coin);

petertonysmith94 marked this conversation as resolved.
Show resolved Hide resolved
const cost = await sender.getTransactionCost(request);
request.gasLimit = cost.gasUsed;
request.maxFee = cost.maxFee;
await sender.fund(request, cost);

const tx = await sender.sendTransaction(request);
const { isStatusSuccess } = await tx.waitForResult();
expect(isStatusSuccess).toEqual(true);
});

it.only('should throw an error when an asset burn is detected', async () => {
const {
petertonysmith94 marked this conversation as resolved.
Show resolved Hide resolved
wallets: [sender],
} = await launchTestNode();

const request = new ScriptTransactionRequest();

// Add a coin input, without any output change
const { coins } = await sender.getCoins(ASSET_A);
const [coin] = coins;
const { id, owner, amount, assetId, predicate, predicateData } = coin;
const coinInput: CoinTransactionRequestInput = {
id,
type: InputType.Coin,
owner: owner.toB256(),
amount,
assetId,
txPointer: '0x00000000000000000000000000000000',
witnessIndex: request.getCoinInputWitnessIndexByOwner(owner) ?? request.addEmptyWitness(),
predicate,
predicateData,
};
request.inputs.push(coinInput);

await expectToThrowFuelError(
() => sender.sendTransaction(request),
new FuelError(
FuelError.CODES.ASSET_BURN_DETECTED,
'Asset burn detected.\nAdd relevant coin change outputs to the transaction, or enable asset burn in the transaction request (`request.enableBurn()`).'
)
);
});
});
Loading