Skip to content

Commit 70292a3

Browse files
committed
fix: contract abi for vechain staking
Ticket: SC-2596
1 parent 8f7ddc0 commit 70292a3

File tree

4 files changed

+101
-74
lines changed

4 files changed

+101
-74
lines changed

modules/sdk-coin-vet/src/lib/iface.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,6 @@ import {
44
TransactionRecipient,
55
} from '@bitgo/sdk-core';
66

7-
/**
8-
* Interface for ABI input parameter
9-
*/
10-
export interface AbiInput {
11-
internalType: string;
12-
name: string;
13-
type: string;
14-
}
15-
16-
/**
17-
* Interface for ABI output parameter
18-
*/
19-
export interface AbiOutput {
20-
internalType?: string;
21-
name?: string;
22-
type: string;
23-
}
24-
25-
/**
26-
* Interface for ABI function definition
27-
*/
28-
export interface AbiFunction {
29-
inputs: AbiInput[];
30-
name: string;
31-
outputs: AbiOutput[];
32-
stateMutability: string;
33-
type: string;
34-
}
35-
36-
/**
37-
* Type for contract ABI
38-
*/
39-
export type ContractAbi = AbiFunction[];
40-
417
/**
428
* The transaction data returned from the toJson() function of a transaction
439
*/

modules/sdk-coin-vet/src/lib/transaction/stakingTransaction.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { Transaction as VetTransaction, Secp256k1 } from '@vechain/sdk-core';
44
import { Transaction } from './transaction';
5-
import { VetTransactionData, ContractAbi } from '../iface';
5+
import { VetTransactionData } from '../iface';
6+
import EthereumAbi from 'ethereumjs-abi';
67
import utils from '../utils';
78

89
export class StakingTransaction extends Transaction {
910
private _stakingContractAddress: string;
1011
private _amountToStake: string;
11-
private _stakingContractABI: ContractAbi;
12+
private _stakingContractABI: EthereumAbi;
1213

1314
constructor(_coinConfig: Readonly<CoinConfig>) {
1415
super(_coinConfig);
@@ -31,11 +32,11 @@ export class StakingTransaction extends Transaction {
3132
this._amountToStake = amount;
3233
}
3334

34-
get stakingContractABI(): ContractAbi {
35+
get stakingContractABI(): EthereumAbi {
3536
return this._stakingContractABI;
3637
}
3738

38-
set stakingContractABI(abi: ContractAbi) {
39+
set stakingContractABI(abi: EthereumAbi) {
3940
this._stakingContractABI = abi;
4041
}
4142

modules/sdk-coin-vet/src/lib/transactionBuilder/stakingBuilder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { TransactionClause } from '@vechain/sdk-core';
66
import { TransactionBuilder } from './transactionBuilder';
77
import { Transaction } from '../transaction/transaction';
88
import { StakingTransaction } from '../transaction/stakingTransaction';
9-
import { ContractAbi } from '../iface';
9+
import EthereumAbi from 'ethereumjs-abi';
1010
import utils from '../utils';
1111

1212
export class StakingBuilder extends TransactionBuilder {
@@ -101,10 +101,10 @@ export class StakingBuilder extends TransactionBuilder {
101101
/**
102102
* Sets the staking contract ABI for this staking tx.
103103
*
104-
* @param {ContractAbi} abi - The staking contract ABI
104+
* @param {EthereumAbi} abi - The staking contract ABI
105105
* @returns {StakingBuilder} This transaction builder
106106
*/
107-
stakingContractABI(abi: ContractAbi): this {
107+
stakingContractABI(abi: EthereumAbi): this {
108108
this.stakingTransaction.stakingContractABI = abi;
109109
return this;
110110
}
@@ -127,7 +127,7 @@ export class StakingBuilder extends TransactionBuilder {
127127
}
128128
assert(transaction.stakingContractAddress, 'Staking contract address is required');
129129
assert(transaction.amountToStake, 'Amount to stake is required');
130-
130+
assert(transaction.stakingContractABI, 'Staking contract ABI is required');
131131
this.validateAddress({ address: transaction.stakingContractAddress });
132132
}
133133

modules/sdk-coin-vet/test/transactionBuilder/stakingTransaction.ts

Lines changed: 92 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,6 @@ describe('VET Staking Transaction', function () {
99
const factory = new TransactionBuilderFactory(coins.get('tvet'));
1010
const stakingContractAddress = '0x1EC1D168574603ec35b9d229843B7C2b44bCB770';
1111
const amountToStake = '1000000000000000000'; // 1 VET in wei
12-
const stakingContractABI = [
13-
{
14-
inputs: [
15-
{
16-
internalType: 'uint256',
17-
name: 'amount',
18-
type: 'uint256',
19-
},
20-
],
21-
name: 'stake',
22-
outputs: [],
23-
stateMutability: 'payable',
24-
type: 'function',
25-
},
26-
];
2712

2813
// Helper function to create a basic transaction builder with common properties
2914
const createBasicTxBuilder = () => {
@@ -42,7 +27,7 @@ describe('VET Staking Transaction', function () {
4227
const txBuilder = factory.getStakingBuilder();
4328
txBuilder.stakingContractAddress(stakingContractAddress);
4429
txBuilder.amountToStake(amountToStake);
45-
txBuilder.stakingContractABI(stakingContractABI);
30+
txBuilder.stakingContractABI(EthereumAbi);
4631
txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9');
4732
txBuilder.chainTag(0x27); // Testnet chain tag
4833
txBuilder.blockRef('0x0000000000000000');
@@ -59,7 +44,7 @@ describe('VET Staking Transaction', function () {
5944
const stakingTx = tx as StakingTransaction;
6045
stakingTx.stakingContractAddress.should.equal(stakingContractAddress);
6146
stakingTx.amountToStake.should.equal(amountToStake);
62-
stakingTx.stakingContractABI.should.deepEqual(stakingContractABI);
47+
stakingTx.stakingContractABI.should.deepEqual(EthereumAbi);
6348

6449
// Verify clauses
6550
stakingTx.clauses.length.should.equal(1);
@@ -93,15 +78,15 @@ describe('VET Staking Transaction', function () {
9378
it('should throw error when stakingContractAddress is missing', async function () {
9479
const txBuilder = createBasicTxBuilder();
9580
txBuilder.amountToStake(amountToStake);
96-
txBuilder.stakingContractABI(stakingContractABI);
81+
txBuilder.stakingContractABI(EthereumAbi);
9782

9883
await txBuilder.build().should.be.rejectedWith('Staking contract address is required');
9984
});
10085

10186
it('should throw error when amountToStake is missing', async function () {
10287
const txBuilder = createBasicTxBuilder();
10388
txBuilder.stakingContractAddress(stakingContractAddress);
104-
txBuilder.stakingContractABI(stakingContractABI);
89+
txBuilder.stakingContractABI(EthereumAbi);
10590

10691
await txBuilder.build().should.be.rejectedWith('Amount to stake is required');
10792
});
@@ -115,10 +100,44 @@ describe('VET Staking Transaction', function () {
115100
}).throw(/Invalid address/);
116101
});
117102

103+
it('should throw error when amountToStake is not a valid number string', async function () {
104+
const txBuilder = createBasicTxBuilder();
105+
txBuilder.stakingContractAddress(stakingContractAddress);
106+
txBuilder.stakingContractABI(EthereumAbi);
107+
108+
// Invalid amount (not a number)
109+
should(() => {
110+
txBuilder.amountToStake('not-a-number');
111+
}).not.throw(); // The setter doesn't validate
112+
// But it should fail when building the transaction
113+
await txBuilder.build().should.be.rejectedWith(/Invalid character/);
114+
});
115+
116+
it('should pass validation with any ABI object but may fail during build', async function () {
117+
const txBuilder = createBasicTxBuilder();
118+
txBuilder.stakingContractAddress(stakingContractAddress);
119+
txBuilder.amountToStake(amountToStake);
120+
121+
// Set an invalid ABI object
122+
const invalidAbi = {};
123+
txBuilder.stakingContractABI(invalidAbi as EthereumAbi);
124+
125+
// The validation will pass because it only checks if the ABI property exists
126+
// But the build might fail if the ABI is actually used in the build process
127+
// Since the actual encoding is done by utils.getStakingData() which doesn't use
128+
// the ABI set on the transaction, this might still succeed
129+
try {
130+
await txBuilder.build();
131+
} catch (e) {
132+
// If it fails, it should be because of an invalid ABI
133+
e.message.should.match(/methodID|rawEncode/);
134+
}
135+
});
136+
118137
it('should allow zero amountToStake but encode it properly', async function () {
119138
const txBuilder = createBasicTxBuilder();
120139
txBuilder.stakingContractAddress(stakingContractAddress);
121-
txBuilder.stakingContractABI(stakingContractABI);
140+
txBuilder.stakingContractABI(EthereumAbi);
122141
txBuilder.amountToStake('0');
123142

124143
const tx = await txBuilder.build();
@@ -133,28 +152,21 @@ describe('VET Staking Transaction', function () {
133152
stakingTx.clauses[0].data.should.equal(expectedData);
134153
});
135154

136-
it('should generate correct transaction data even without explicitly setting stakingContractABI', async function () {
155+
it('should throw error when stakingContractABI is missing', async function () {
137156
const txBuilder = createBasicTxBuilder();
138157
txBuilder.stakingContractAddress(stakingContractAddress);
139158
txBuilder.amountToStake(amountToStake);
140159
// Not setting stakingContractABI
141160

142-
const tx = await txBuilder.build();
143-
tx.should.be.instanceof(StakingTransaction);
144-
145-
const stakingTx = tx as StakingTransaction;
146-
// Verify the transaction data is correctly generated using the default staking method ID
147-
stakingTx.clauses[0].data.should.startWith(STAKING_METHOD_ID);
148-
// Verify the encoded amount matches what we expect
149-
const expectedData = '0xa694fc3a0000000000000000000000000000000000000000000000000de0b6b3a7640000';
150-
stakingTx.clauses[0].data.should.equal(expectedData);
161+
// Should fail when trying to build without ABI
162+
await txBuilder.build().should.be.rejectedWith('Staking contract ABI is required');
151163
});
152164

153165
it('should build transaction with undefined sender but include it in inputs', async function () {
154166
const txBuilder = factory.getStakingBuilder();
155167
txBuilder.stakingContractAddress(stakingContractAddress);
156168
txBuilder.amountToStake(amountToStake);
157-
txBuilder.stakingContractABI(stakingContractABI);
169+
txBuilder.stakingContractABI(EthereumAbi);
158170
txBuilder.chainTag(0x27);
159171
txBuilder.blockRef('0x0000000000000000');
160172
txBuilder.expiration(64);
@@ -181,7 +193,7 @@ describe('VET Staking Transaction', function () {
181193
const txBuilder = factory.getStakingBuilder();
182194
txBuilder.stakingContractAddress(stakingContractAddress);
183195
txBuilder.amountToStake(amountToStake);
184-
txBuilder.stakingContractABI(stakingContractABI);
196+
txBuilder.stakingContractABI(EthereumAbi);
185197
// Not setting chainTag
186198
txBuilder.blockRef('0x0000000000000000');
187199
txBuilder.expiration(64);
@@ -197,5 +209,53 @@ describe('VET Staking Transaction', function () {
197209
// Verify the chainTag is set to the testnet default (39)
198210
stakingTx.chainTag.should.equal(39);
199211
});
212+
213+
it('should verify ABI encoding matches expected output for different amounts', async function () {
214+
const txBuilder = createBasicTxBuilder();
215+
txBuilder.stakingContractAddress(stakingContractAddress);
216+
txBuilder.stakingContractABI(EthereumAbi);
217+
218+
// Test with a different amount
219+
const differentAmount = '500000000000000000'; // 0.5 VET
220+
txBuilder.amountToStake(differentAmount);
221+
222+
const tx = await txBuilder.build();
223+
const stakingTx = tx as StakingTransaction;
224+
225+
// Manually encode the expected data
226+
const methodName = 'stake';
227+
const types = ['uint256'];
228+
const params = [new BN(differentAmount)];
229+
230+
const method = EthereumAbi.methodID(methodName, types);
231+
const args = EthereumAbi.rawEncode(types, params);
232+
const expectedData = '0x' + Buffer.concat([method, args]).toString('hex');
233+
234+
// Verify the transaction data matches our manual encoding
235+
stakingTx.clauses[0].data.should.equal(expectedData);
236+
stakingTx.clauses[0].data.should.startWith(STAKING_METHOD_ID);
237+
});
238+
239+
it('should handle extremely large stake amounts correctly', async function () {
240+
const txBuilder = createBasicTxBuilder();
241+
txBuilder.stakingContractAddress(stakingContractAddress);
242+
txBuilder.stakingContractABI(EthereumAbi);
243+
244+
// Test with a very large amount (near uint256 max)
245+
const largeAmount = '115792089237316195423570985008687907853269984665640564039457584007913129639935'; // 2^256 - 1
246+
txBuilder.amountToStake(largeAmount);
247+
248+
const tx = await txBuilder.build();
249+
const stakingTx = tx as StakingTransaction;
250+
251+
// Verify the amount is stored correctly
252+
stakingTx.amountToStake.should.equal(largeAmount);
253+
254+
// The data should still be properly encoded
255+
stakingTx.clauses[0].data.should.startWith(STAKING_METHOD_ID);
256+
257+
// Verify recipients
258+
stakingTx.recipients[0].amount.should.equal(largeAmount);
259+
});
200260
});
201261
});

0 commit comments

Comments
 (0)