Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 27 additions & 7 deletions modules/sdk-coin-avaxc/src/avaxc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,31 @@ export class AvaxC extends AbstractEthLikeNewCoins {
await this.validateHopPrebuild(wallet, txPrebuild.hopTransaction, { recipients });
} else if (txParams.recipients.length > 1) {
// Check total amount for batch transaction
let expectedTotalAmount = new BigNumber(0);
for (let i = 0; i < txParams.recipients.length; i++) {
expectedTotalAmount = expectedTotalAmount.plus(txParams.recipients[i].amount);
if (txParams.tokenName) {
const expectedTotalAmount = new BigNumber(0);
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
throw new Error('batch token transaction amount in txPrebuild should be zero for token transfers');
}
} else {
let expectedTotalAmount = new BigNumber(0);
for (let i = 0; i < txParams.recipients.length; i++) {
expectedTotalAmount = expectedTotalAmount.plus(txParams.recipients[i].amount);
}
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
throw new Error(
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
);
}
}
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
throw new Error(
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
);

// Check batch transaction is sent to the batcher contract address for the chain
const network = this.getNetwork();
const batcherContractAddress = network?.batcherContractAddress as string;
if (
!batcherContractAddress ||
batcherContractAddress.toLowerCase() !== txPrebuild.recipients[0].address.toLowerCase()
) {
throw new Error('recipient address of txPrebuild does not match batcher address');
}
} else {
// Check recipient address and amount for normal transaction
Expand Down Expand Up @@ -1045,6 +1062,9 @@ export class AvaxC extends AbstractEthLikeNewCoins {
expireTime: params.txPrebuild.expireTime,
hopTransaction: params.txPrebuild.hopTransaction,
custodianTransactionId: params.custodianTransactionId,
contractSequenceId: params.txPrebuild.nextContractSequenceId as number,
sequenceId: params.sequenceId,
...(params.txPrebuild.isBatch ? { isBatch: params.txPrebuild.isBatch } : {}),
};

return { halfSigned: txParams };
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-coin-avaxc/src/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface AvaxcTransactionParams extends TransactionParams {
gasLimit?: number;
hopParams?: HopParams;
hop?: boolean;
tokenName?: string;
}

export interface VerifyAvaxcTransactionOptions extends VerifyTransactionOptions {
Expand Down Expand Up @@ -150,6 +151,7 @@ export interface TxPreBuild extends BaseTransactionPrebuild {
expireTime?: number;
hopTransaction?: string;
eip1559?: EIP1559;
isBatch?: boolean;
recipients?: Recipient[];
txPrebuild?: {
halfSigned: {
Expand Down
200 changes: 200 additions & 0 deletions modules/sdk-coin-avaxc/test/unit/avaxc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TavaxP } from '@bitgo/sdk-coin-avaxp';
import { decodeTransaction, parseTransaction, walletSimpleABI } from './helpers';
import * as sinon from 'sinon';
import { BN } from 'ethereumjs-util';
import { EthereumNetwork } from '@bitgo/statics';

nock.enableNetConnect();

Expand Down Expand Up @@ -375,6 +376,35 @@ describe('Avalanche C-Chain', function () {
halfSignedRawTx.halfSigned.recipients[0].amount.should.equals(customRecipients[0].amount);
halfSignedRawTx.halfSigned.recipients[0].data.should.equals(customRecipients[0].data);
});

it('should include isBatch, contractSequenceId, and sequenceId in half-signed txParams for batch transactions', async function () {
const builder = getBuilder('tavaxc') as TransactionBuilder;
builder.fee({
fee: '280000000000',
gasLimit: '7000000',
});
builder.counter(1);
builder.type(TransactionType.Send);
builder.contract(account_1.address);
builder.transfer().amount('1').to(account_2.address).expirationTime(10000).contractSequenceId(1);

const unsignedTx = await builder.build();
const unsignedTxForBroadcasting = unsignedTx.toBroadcastFormat();

const halfSignedRawTx = await tavaxCoin.signTransaction({
txPrebuild: {
txHex: unsignedTxForBroadcasting,
isBatch: true,
nextContractSequenceId: 42,
},
prv: account_1.owner_2,
sequenceId: '7',
});

halfSignedRawTx.halfSigned.isBatch.should.equal(true);
halfSignedRawTx.halfSigned.contractSequenceId.should.equal(42);
halfSignedRawTx.halfSigned.sequenceId.should.equal('7');
});
});

describe('Transaction Verification', () => {
Expand Down Expand Up @@ -783,6 +813,176 @@ describe('Avalanche C-Chain', function () {
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
.should.be.rejectedWith('coin in txPrebuild did not match that in txParams supplied by client');
});

describe('Batch transaction verification', () => {
let batcherContractAddress: string;

beforeEach(function () {
batcherContractAddress = (tavaxCoin.staticsCoin?.network as EthereumNetwork)?.batcherContractAddress as string;
});
it('should verify a native coin batch transaction with matching total amount', async function () {
const wallet = new Wallet(bitgo, tavaxCoin, {});

const txParams = {
recipients: [
{ amount: '1000000000000', address: address1 },
{ amount: '2500000000000', address: address2 },
],
wallet: wallet,
walletPassphrase: 'fakeWalletPassphrase',
};

const txPrebuild = {
recipients: [{ amount: '3500000000000', address: batcherContractAddress }],
nextContractSequenceId: 0,
gasPrice: 20000000000,
gasLimit: 500000,
isBatch: true,
coin: 'tavaxc',
walletId: 'fakeWalletId',
walletContractAddress: 'fakeWalletContractAddress',
};

const verification = {};

const isTransactionVerified = await tavaxCoin.verifyTransaction({
txParams,
txPrebuild,
wallet,
verification,
});
isTransactionVerified.should.equal(true);
});

it('should verify a token batch transaction with zero native amount', async function () {
const wallet = new Wallet(bitgo, tavaxCoin, {});

const txParams = {
recipients: [
{ amount: '1000000000000', address: address1 },
{ amount: '2500000000000', address: address2 },
],
wallet: wallet,
walletPassphrase: 'fakeWalletPassphrase',
tokenName: 'tavaxc:USDC',
};

const txPrebuild = {
recipients: [{ amount: '0', address: batcherContractAddress }],
nextContractSequenceId: 0,
gasPrice: 20000000000,
gasLimit: 500000,
isBatch: true,
coin: 'tavaxc',
walletId: 'fakeWalletId',
walletContractAddress: 'fakeWalletContractAddress',
};

const verification = {};

const isTransactionVerified = await tavaxCoin.verifyTransaction({
txParams,
txPrebuild,
wallet,
verification,
});
isTransactionVerified.should.equal(true);
});

it('should reject a token batch transaction with non-zero native amount', async function () {
const wallet = new Wallet(bitgo, tavaxCoin, {});

const txParams = {
recipients: [
{ amount: '1000000000000', address: address1 },
{ amount: '2500000000000', address: address2 },
],
wallet: wallet,
walletPassphrase: 'fakeWalletPassphrase',
tokenName: 'tavaxc:USDC',
};

const txPrebuild = {
recipients: [{ amount: '1000000000000', address: batcherContractAddress }],
nextContractSequenceId: 0,
gasPrice: 20000000000,
gasLimit: 500000,
isBatch: true,
coin: 'tavaxc',
walletId: 'fakeWalletId',
walletContractAddress: 'fakeWalletContractAddress',
};

const verification = {};

await tavaxCoin
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
.should.be.rejectedWith('batch token transaction amount in txPrebuild should be zero for token transfers');
});

it('should reject a native coin batch transaction with mismatched total amount', async function () {
const wallet = new Wallet(bitgo, tavaxCoin, {});

const txParams = {
recipients: [
{ amount: '1000000000000', address: address1 },
{ amount: '2500000000000', address: address2 },
],
wallet: wallet,
walletPassphrase: 'fakeWalletPassphrase',
};

const txPrebuild = {
recipients: [{ amount: '9999999999999', address: batcherContractAddress }],
nextContractSequenceId: 0,
gasPrice: 20000000000,
gasLimit: 500000,
isBatch: true,
coin: 'tavaxc',
walletId: 'fakeWalletId',
walletContractAddress: 'fakeWalletContractAddress',
};

const verification = {};

await tavaxCoin
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
.should.be.rejectedWith(
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
);
});

it('should reject a batch transaction sent to wrong batcher contract address', async function () {
const wallet = new Wallet(bitgo, tavaxCoin, {});
const wrongBatcherAddress = '0x0000000000000000000000000000000000000001';

const txParams = {
recipients: [
{ amount: '1000000000000', address: address1 },
{ amount: '2500000000000', address: address2 },
],
wallet: wallet,
walletPassphrase: 'fakeWalletPassphrase',
};

const txPrebuild = {
recipients: [{ amount: '3500000000000', address: wrongBatcherAddress }],
nextContractSequenceId: 0,
gasPrice: 20000000000,
gasLimit: 500000,
isBatch: true,
coin: 'tavaxc',
walletId: 'fakeWalletId',
walletContractAddress: 'fakeWalletContractAddress',
};

const verification = {};

await tavaxCoin
.verifyTransaction({ txParams, txPrebuild, wallet, verification })
.should.be.rejectedWith('recipient address of txPrebuild does not match batcher address');
});
});
});

describe('Hop Transaction Parameters', () => {
Expand Down
Loading